@ -7,12 +7,16 @@ import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable , Logger } from '@nestjs/common' ;
import { Injectable , Logger } from '@nestjs/common' ;
import { SymbolProfile } from '@prisma/client' ;
import { SymbolProfile } from '@prisma/client' ;
import { countries } from 'countries-list' ;
import { countries } from 'countries-list' ;
import { Browser , impersonate } from 'node-libcurl-ja3' ;
import { launch } from 'puppeteer' ;
@Injectable ( )
@Injectable ( )
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://www.trackinsight.com/data-api' ;
private static baseUrl = 'https://www.trackinsight.com/data-api' ;
private static countriesMapping = {
private static countriesMapping = {
'Russian Federation' : 'Russia'
'Russian Federation' : 'Russia' ,
USA : 'United States' ,
'Republic of Korea' : 'South Korea'
} ;
} ;
private static holdingsWeightTreshold = 0.85 ;
private static holdingsWeightTreshold = 0.85 ;
private static sectorsMapping = {
private static sectorsMapping = {
@ -21,11 +25,54 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
'Health Care' : 'Healthcare' ,
'Health Care' : 'Healthcare' ,
'Information Technology' : 'Technology'
'Information Technology' : 'Technology'
} ;
} ;
private static curly = impersonate ( Browser . Chrome ) ;
public constructor (
public constructor (
private readonly configurationService : ConfigurationService
private readonly configurationService : ConfigurationService
) { }
) { }
private async fetchFromTrackInsight ( {
url ,
requestTimeout
} : {
url : string ;
requestTimeout? : number ;
} ) : Promise < any > {
const useImpersonate = this . configurationService . get (
'TRACK_INSIGHT_TRY_CURL_IMPERSONATE'
) ;
const usePuppeteer = this . configurationService . get (
'TRACK_INSIGHT_TRY_PUPPETEER'
) ;
if ( usePuppeteer ) {
const browserPath = this . configurationService . get (
'TRACK_INSIGHT_CHROMIUM_PATH'
) ;
const browser = await launch ( {
args : [ '--no-sandbox' , '--disable-setuid-sandbox' ] ,
executablePath : browserPath
} ) ;
const page = await browser . newPage ( ) ;
await page . setJavaScriptEnabled ( true ) ;
await page . goto ( url , {
waitUntil : 'networkidle0'
} ) ;
const data = await page . evaluate ( ( ) = > {
return document . body . innerText ;
} ) ;
await browser . close ( ) ;
return JSON . parse ( data ) ;
}
if ( useImpersonate ) {
return TrackinsightDataEnhancerService . curly
. get ( url )
. then ( ( res ) = > res . data ) ;
}
return fetch ( url , { signal : AbortSignal.timeout ( requestTimeout ) } ) . then (
( res ) = > res . json ( )
) ;
}
public async enhance ( {
public async enhance ( {
requestTimeout = this . configurationService . get ( 'REQUEST_TIMEOUT' ) ,
requestTimeout = this . configurationService . get ( 'REQUEST_TIMEOUT' ) ,
response ,
response ,
@ -60,16 +107,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response ;
return response ;
}
}
const profile = await fetch (
const profile = await this . fetchFromTrackInsight ( {
` ${ TrackinsightDataEnhancerService . baseUrl } /funds/ ${ trackinsightSymbol } .json ` ,
url : ` ${ TrackinsightDataEnhancerService . baseUrl } /funds/ ${ trackinsightSymbol } .json `
{
} ) . catch ( ( ) = > {
signal : AbortSignal.timeout ( requestTimeout )
return { } ;
}
} ) ;
)
. then ( ( res ) = > res . json ( ) )
. catch ( ( ) = > {
return { } ;
} ) ;
const cusip = profile ? . cusip ;
const cusip = profile ? . cusip ;
@ -83,16 +125,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response . isin = isin ;
response . isin = isin ;
}
}
const holdings = await fetch (
const holdings = await this . fetchFromTrackInsight ( {
` ${ TrackinsightDataEnhancerService . baseUrl } /holdings/ ${ trackinsightSymbol } .json ` ,
url : ` ${ TrackinsightDataEnhancerService . baseUrl } /holdings/ ${ trackinsightSymbol } .json `
{
} ) . catch ( ( ) = > {
signal : AbortSignal.timeout ( requestTimeout )
return { } ;
}
} ) ;
)
. then ( ( res ) = > res . json ( ) )
. catch ( ( ) = > {
return { } ;
} ) ;
if (
if (
holdings ? . weight < TrackinsightDataEnhancerService . holdingsWeightTreshold
holdings ? . weight < TrackinsightDataEnhancerService . holdingsWeightTreshold
@ -182,19 +219,14 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
requestTimeout : number ;
requestTimeout : number ;
symbol : string ;
symbol : string ;
} ) {
} ) {
return fetch (
return await this . fetchFromTrackInsight ( {
` https://www.trackinsight.com/search-api/search_v2/ ${ symbol } /_/ticker/default/0/3 ` ,
url : ` https://www.trackinsight.com/search-api/search_v2/ ${ symbol } /_/ticker/default/0/3 ` ,
{
requestTimeout
signal : AbortSignal.timeout ( requestTimeout )
} )
}
)
. then ( ( res ) = > res . json ( ) )
. then ( ( jsonRes ) = > {
. then ( ( jsonRes ) = > {
if (
if (
jsonRes [ 'results' ] ? . [ 'count' ] === 1 ||
jsonRes [ 'results' ] ? . [ 'count' ] === 1 ||
// Allow exact match
jsonRes [ 'results' ] ? . [ 'docs' ] ? . [ 0 ] ? . [ 'ticker' ] === symbol ||
jsonRes [ 'results' ] ? . [ 'docs' ] ? . [ 0 ] ? . [ 'ticker' ] === symbol ||
// Allow EXCHANGE:SYMBOL
jsonRes [ 'results' ] ? . [ 'docs' ] ? . [ 0 ] ? . [ 'ticker' ] ? . endsWith ( ` : ${ symbol } ` )
jsonRes [ 'results' ] ? . [ 'docs' ] ? . [ 0 ] ? . [ 'ticker' ] ? . endsWith ( ` : ${ symbol } ` )
) {
) {
return jsonRes [ 'results' ] [ 'docs' ] [ 0 ] [ 'ticker' ] ;
return jsonRes [ 'results' ] [ 'docs' ] [ 0 ] [ 'ticker' ] ;
@ -207,7 +239,6 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
` Failed to search Trackinsight symbol for ${ symbol } ( ${ message } ) ` ,
` Failed to search Trackinsight symbol for ${ symbol } ( ${ message } ) ` ,
'TrackinsightDataEnhancerService'
'TrackinsightDataEnhancerService'
) ;
) ;
return undefined ;
return undefined ;
} ) ;
} ) ;
}
}