mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
10 changed files with 663 additions and 12 deletions
@ -0,0 +1,606 @@ |
|||||
|
import { |
||||
|
DataProviderInterface, |
||||
|
GetDividendsParams, |
||||
|
GetHistoricalParams, |
||||
|
GetQuotesParams, |
||||
|
GetSearchParams |
||||
|
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; |
||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; |
||||
|
import { |
||||
|
IDataProviderHistoricalResponse, |
||||
|
IDataProviderResponse |
||||
|
} from '@ghostfolio/api/services/interfaces/interfaces'; |
||||
|
import { |
||||
|
DataProviderInfo, |
||||
|
LookupResponse |
||||
|
} from '@ghostfolio/common/interfaces'; |
||||
|
|
||||
|
import { Injectable, Logger } from '@nestjs/common'; |
||||
|
import { $Enums, SymbolProfile } from '@prisma/client'; |
||||
|
import { isISO4217CurrencyCode } from 'class-validator'; |
||||
|
import { |
||||
|
subYears, |
||||
|
format, |
||||
|
startOfYesterday, |
||||
|
subDays, |
||||
|
differenceInDays, |
||||
|
addYears, |
||||
|
differenceInMilliseconds |
||||
|
} from 'date-fns'; |
||||
|
import { createMoexCLient } from 'moex-iss-api-client'; |
||||
|
import { |
||||
|
IGetSecuritiesParams, |
||||
|
TGetSecuritiesParamsGroupBy |
||||
|
} from 'moex-iss-api-client/dist/client/security/requestTypes'; |
||||
|
import { ISecuritiesResponse } from 'moex-iss-api-client/dist/client/security/responseTypes'; |
||||
|
|
||||
|
const moexClient = createMoexCLient(); |
||||
|
|
||||
|
declare interface ResponseData { |
||||
|
columns: string[]; |
||||
|
data: (string | number | null)[][]; |
||||
|
} |
||||
|
|
||||
|
declare interface Response<T> { |
||||
|
data: T; |
||||
|
issError: string; |
||||
|
} |
||||
|
|
||||
|
function response_data_to_map( |
||||
|
response: ResponseData, |
||||
|
keyColumnName: string |
||||
|
): Map<string | number, Map<string, string | number>> { |
||||
|
const result = new Map<string | number, Map<string, string | number>>(); |
||||
|
|
||||
|
response.data.forEach((x) => { |
||||
|
const item = new Map<string, string | number>(); |
||||
|
response.columns.forEach((c, i) => item.set(c, x[i])); |
||||
|
result.set(item.get(keyColumnName), item); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
function getCurrency(currency: string): string { |
||||
|
if (currency === 'SUR' || currency === 'RUR') return 'RUB'; |
||||
|
|
||||
|
return currency; |
||||
|
} |
||||
|
|
||||
|
/// So, we try to guess sectors of security by looking into indexes,
|
||||
|
/// in which this security was put by MOEX
|
||||
|
const indexToSectorMapping = new Map<string, string[]>([ |
||||
|
['MOEXOG', ['Energy']], // MOEX Oil and Gas Index
|
||||
|
['MOEXEU', ['Utilities']], // MOEX Electric Utilities Index
|
||||
|
['MOEXTL', ['Communication Services']], // MOEX Telecommunication Index
|
||||
|
['MOEXMM', ['Basic Materials', 'Industrial']], // MOEX Metals and Mining Index
|
||||
|
['MOEXFN', ['Financial Services']], // MOEX Financials Index
|
||||
|
['MOEXCN', ['Consumer Defensive', 'Consumer Cyclical', 'Healthcare']], // MOEX Consumer Index
|
||||
|
['MOEXCH', ['Basic Materials']], // MOEX Chemicals Index
|
||||
|
['MOEXIT', ['Technology']], // MOEX Information Technologies Index
|
||||
|
['MOEXRE', ['Real Estate']], // MOEX Real Estate Index
|
||||
|
['MOEXTN', ['Consumer Cyclical', 'Industrial']] // MOEX Transportation Index
|
||||
|
]); |
||||
|
|
||||
|
async function getSectors( |
||||
|
symbol: string |
||||
|
): Promise<{ name: string; weight: number }[]> { |
||||
|
const indicesResponse: Response<{ indices: ResponseData }> = |
||||
|
await moexClient.security.getSecurityIndexes({ security: symbol }); |
||||
|
const errorMessage = indicesResponse.issError; |
||||
|
if (errorMessage) { |
||||
|
Logger.warn(errorMessage, 'MoexService.getSectors'); |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
const indices = response_data_to_map(indicesResponse.data.indices, 'SECID'); |
||||
|
|
||||
|
const sectorIncluded = new Set<string>(); |
||||
|
const sectors = new Array<{ name: string; weight: number }>(); |
||||
|
|
||||
|
for (const [indexCode, indexSectors] of indexToSectorMapping.entries()) { |
||||
|
const index = indices.get(indexCode); |
||||
|
if (!index) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (sectorIncluded.has(indexCode)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
sectorIncluded.add(indexCode); |
||||
|
indexSectors.forEach((x) => sectors.push({ name: x, weight: 1.0 })); |
||||
|
} |
||||
|
|
||||
|
return sectors; |
||||
|
} |
||||
|
|
||||
|
async function getDividendsFromMoex({ |
||||
|
from, |
||||
|
symbol, |
||||
|
to |
||||
|
}: GetDividendsParams): Promise<{ |
||||
|
[date: string]: IDataProviderHistoricalResponse; |
||||
|
}> { |
||||
|
const response: Response<{ dividends: ResponseData }> = |
||||
|
await moexClient.request(`securities/${symbol}/dividends`); |
||||
|
if (response.issError) { |
||||
|
Logger.warn(response.issError, 'MoexService.getDividends'); |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
const dividends = response_data_to_map( |
||||
|
response.data.dividends, |
||||
|
'registryclosedate' |
||||
|
); |
||||
|
const result: { |
||||
|
[date: string]: IDataProviderHistoricalResponse; |
||||
|
} = {}; |
||||
|
|
||||
|
for (const [key, value] of dividends.entries()) { |
||||
|
const date = new Date(key); |
||||
|
|
||||
|
if (date < from || date > to) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
const price = value.get('value'); |
||||
|
if (typeof price === 'number') result[key] = { marketPrice: price }; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
async function readBatchedResponse<T>( |
||||
|
getNextBatch: (start: number) => Promise<Response<T>>, |
||||
|
extractor: (batch: Response<T>) => ResponseData, |
||||
|
max_items?: number |
||||
|
): Promise<ResponseData> { |
||||
|
let batch: ResponseData; |
||||
|
const wholeResponse: ResponseData = { columns: [], data: [] }; |
||||
|
|
||||
|
do { |
||||
|
const response: Response<T> = await getNextBatch(wholeResponse.data.length); |
||||
|
if (response === undefined) { |
||||
|
break; |
||||
|
} |
||||
|
if (response.issError) { |
||||
|
Logger.warn(response.issError, 'MoexService.readBatchedResponse'); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
batch = extractor(response); |
||||
|
if (wholeResponse.columns.length === 0) { |
||||
|
wholeResponse.columns = batch.columns; |
||||
|
} |
||||
|
|
||||
|
wholeResponse.data = [...wholeResponse.data, ...batch.data]; |
||||
|
} while ( |
||||
|
batch.data.length > 0 && |
||||
|
(!max_items || wholeResponse.data.length <= max_items) |
||||
|
); |
||||
|
|
||||
|
return wholeResponse; |
||||
|
} |
||||
|
|
||||
|
function getYahooSymbolFromMoex(symbol: string): string { |
||||
|
return `${symbol}.ME`; |
||||
|
} |
||||
|
|
||||
|
async function getAssetUrlFromYahoo( |
||||
|
yahooFinanceService: YahooFinanceService, |
||||
|
symbol: string |
||||
|
): Promise<string> { |
||||
|
try { |
||||
|
const profile = await yahooFinanceService.getAssetProfile({ |
||||
|
symbol: getYahooSymbolFromMoex(symbol) |
||||
|
}); |
||||
|
return profile?.url; |
||||
|
} catch (e) { |
||||
|
Logger.warn(`Can't get url for symbol ${symbol} from YAHOO, error is ${e}`); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function getSecuritySpecification( |
||||
|
symbol: string, |
||||
|
dataSectionName: string, |
||||
|
key: string |
||||
|
): Promise<Map<string | number, Map<string, string | number>>> { |
||||
|
const securitySpecificationResponse = |
||||
|
await moexClient.security.getSecuritySpecification({ security: symbol }); |
||||
|
const errorMessage = securitySpecificationResponse.issError; |
||||
|
if (errorMessage) { |
||||
|
Logger.warn(errorMessage, 'MoexService.getAssetProfile'); |
||||
|
return new Map<string | number, Map<string, string | number>>(); |
||||
|
} |
||||
|
|
||||
|
return response_data_to_map( |
||||
|
securitySpecificationResponse.data[dataSectionName], |
||||
|
key |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
interface SecurityTypeMap { |
||||
|
[key: string]: [$Enums.AssetClass?, $Enums.AssetSubClass?]; |
||||
|
} |
||||
|
|
||||
|
/// MOEX security types were obtained here: https://iss.moex.com/iss/index.json (add `?lang=en` for english)
|
||||
|
/// | id | security_type_name | ghostfolio_assetclass | ghostfolio_assetsubclass |
|
||||
|
/// | ---- | --------------------- | --------------------- | ------------------------ |
|
||||
|
/// | 1 | preferred_share | EQUITY | STOCK |
|
||||
|
/// | 2 | corporate_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 3 | common_share | EQUITY | STOCK |
|
||||
|
/// | 4 | cb_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 5 | currency | LIQUIDITY | CASH |
|
||||
|
/// | 6 | futures | COMMODITY | |
|
||||
|
/// | 7 | public_ppif | EQUITY | MUTUALFUND |
|
||||
|
/// | 8 | interval_ppif | EQUITY | MUTUALFUND |
|
||||
|
/// | 9 | private_ppif | EQUITY | MUTUALFUND |
|
||||
|
/// | 10 | state_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 41 | subfederal_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 42 | ifi_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 43 | exchange_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 44 | stock_index | | |
|
||||
|
/// | 45 | municipal_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 51 | depositary_receipt | EQUITY | STOCK |
|
||||
|
/// | 52 | option | COMMODITY | |
|
||||
|
/// | 53 | rts_index | | |
|
||||
|
/// | 54 | ofz_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 55 | etf_ppif | EQUITY | ETF |
|
||||
|
/// | 57 | stock_mortgage | REAL_ESTATE | |
|
||||
|
/// | 58 | gold_metal | LIQUIDITY | PRECIOUS_METAL |
|
||||
|
/// | 59 | silver_metal | LIQUIDITY | PRECIOUS_METAL |
|
||||
|
/// | 60 | euro_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 62 | currency_futures | LIQUIDITY | CASH |
|
||||
|
/// | 63 | stock_deposit | LIQUIDITY | CASH |
|
||||
|
/// | 73 | currency_fixing | LIQUIDITY | CASH |
|
||||
|
/// | 74 | exchange_ppif | EQUITY | ETF |
|
||||
|
/// | 75 | currency_index | LIQUIDITY | CASH |
|
||||
|
/// | 76 | currency_wap | LIQUIDITY | CASH |
|
||||
|
/// | 78 | non_exchange_bond | FIXED_INCOME | BOND |
|
||||
|
/// | 84 | stock_index_eq | | |
|
||||
|
/// | 85 | stock_index_fi | | |
|
||||
|
/// | 86 | stock_index_mx | | |
|
||||
|
/// | 87 | stock_index_ie | | |
|
||||
|
/// | 88 | stock_index_if | | |
|
||||
|
/// | 89 | stock_index_ci | | |
|
||||
|
/// | 90 | stock_index_im | | |
|
||||
|
/// | 1030 | stock_index_namex | | |
|
||||
|
/// | 1031 | option_on_shares | EQUITY | STOCK |
|
||||
|
/// | 1034 | stock_index_rusfar | | |
|
||||
|
/// | 1155 | stock_index_pf | | |
|
||||
|
/// | 1291 | option_on_currency | | |
|
||||
|
/// | 1293 | option_on_indices | | |
|
||||
|
/// | 1295 | option_on_commodities | COMMODITY | |
|
||||
|
/// | 1337 | futures_spread | COMMODITY | |
|
||||
|
/// | 1338 | futures_collateral | COMMODITY | |
|
||||
|
/// | 1347 | currency_otcindices | LIQUIDITY | CASH |
|
||||
|
/// | 1352 | other_metal | COMMODITY | PRECIOUS_METAL |
|
||||
|
/// | 1403 | stock_index_ri | | |
|
||||
|
const securityTypeMap: SecurityTypeMap = { |
||||
|
preferred_share: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], |
||||
|
corporate_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
common_share: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], |
||||
|
cb_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
currency: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
futures: [$Enums.AssetClass.COMMODITY], |
||||
|
public_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.MUTUALFUND], |
||||
|
interval_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.MUTUALFUND], |
||||
|
private_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.MUTUALFUND], |
||||
|
state_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
subfederal_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
ifi_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
exchange_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
municipal_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], |
||||
|
depositary_receipt: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], |
||||
|
option: [$Enums.AssetClass.COMMODITY], |
||||
|
etf_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.ETF], |
||||
|
stock_mortgage: [$Enums.AssetClass.REAL_ESTATE], |
||||
|
gold_metal: [ |
||||
|
$Enums.AssetClass.LIQUIDITY, |
||||
|
$Enums.AssetSubClass.PRECIOUS_METAL |
||||
|
], |
||||
|
silver_metal: [ |
||||
|
$Enums.AssetClass.LIQUIDITY, |
||||
|
$Enums.AssetSubClass.PRECIOUS_METAL |
||||
|
], |
||||
|
currency_futures: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
stock_deposit: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
currency_fixing: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
exchange_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.ETF], |
||||
|
currency_index: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
currency_wap: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
non_exchange_bond: [ |
||||
|
$Enums.AssetClass.FIXED_INCOME, |
||||
|
$Enums.AssetSubClass.BOND |
||||
|
], |
||||
|
option_on_shares: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], |
||||
|
option_on_commodities: [$Enums.AssetClass.COMMODITY], |
||||
|
futures_spread: [$Enums.AssetClass.COMMODITY], |
||||
|
futures_collateral: [$Enums.AssetClass.COMMODITY], |
||||
|
currency_otcindices: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], |
||||
|
other_metal: [ |
||||
|
$Enums.AssetClass.COMMODITY, |
||||
|
$Enums.AssetSubClass.PRECIOUS_METAL |
||||
|
] |
||||
|
}; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class MoexService implements DataProviderInterface { |
||||
|
public constructor( |
||||
|
private readonly yahooFinanceService: YahooFinanceService |
||||
|
) {} |
||||
|
|
||||
|
canHandle(): boolean { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
async getAssetProfile({ |
||||
|
symbol |
||||
|
}: { |
||||
|
symbol: string; |
||||
|
}): Promise<Partial<SymbolProfile>> { |
||||
|
const securitySpecification = await getSecuritySpecification( |
||||
|
symbol, |
||||
|
'description', |
||||
|
'name' |
||||
|
); |
||||
|
|
||||
|
const issueDate = securitySpecification.get('ISSUEDATE'); |
||||
|
const faceunit = securitySpecification.get('FACEUNIT'); |
||||
|
const isin = securitySpecification.get('ISIN'); |
||||
|
const latname = securitySpecification.get('LATNAME'); |
||||
|
const shortname = securitySpecification.get('SHORTNAME'); |
||||
|
const name = securitySpecification.get('NAME'); |
||||
|
const secid = securitySpecification.get('SECID'); |
||||
|
const type = securitySpecification.get('TYPE'); |
||||
|
const [assetClass, assetSubClass] = |
||||
|
securityTypeMap[type.get('value').toString()] ?? []; |
||||
|
|
||||
|
let currency = faceunit |
||||
|
? getCurrency(faceunit.get('value').toString().toUpperCase()) |
||||
|
: 'RUB'; |
||||
|
|
||||
|
if (!isISO4217CurrencyCode(currency)) { |
||||
|
currency = 'RUB'; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
assetClass: assetClass, |
||||
|
assetSubClass: assetSubClass, |
||||
|
createdAt: issueDate ? new Date(issueDate.get('value')) : new Date(), |
||||
|
currency, |
||||
|
dataSource: this.getName(), |
||||
|
id: symbol, |
||||
|
isin: isin ? isin.get('value').toString() : null, |
||||
|
name: (latname ?? shortname ?? name ?? secid).get('value').toString(), |
||||
|
sectors: await getSectors(symbol), |
||||
|
symbol: symbol, |
||||
|
countries: isin |
||||
|
? [ |
||||
|
{ |
||||
|
code: isin.get('value').toString().substring(0, 2), |
||||
|
weight: 1 |
||||
|
} |
||||
|
] |
||||
|
: null, |
||||
|
url: await getAssetUrlFromYahoo(this.yahooFinanceService, symbol) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
getDataProviderInfo(): DataProviderInfo { |
||||
|
return { |
||||
|
isPremium: false |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// MOEX endpoint for dividends isn't documented and sometimes doesn't return newer dividends.
|
||||
|
// YAHOO endpoint for dividends sometimes doesn't respect date filters.
|
||||
|
// So, we'll requests dividends for 2 years more from both providers and merge data.
|
||||
|
// If dividends date from MOEX and YAHOO differs for 2 days or less, we'll assume it's the same payout given amount is the same.
|
||||
|
// Payouts considered the same if they differ less that 1/100 of the currency.
|
||||
|
async getDividends({ from, symbol, to }: GetDividendsParams): Promise<{ |
||||
|
[date: string]: IDataProviderHistoricalResponse; |
||||
|
}> { |
||||
|
const twoYearsAgo = subYears(from, 2); |
||||
|
const twoYearsAhead = addYears(to, 2); |
||||
|
const [dividends, dividendsFromYahoo] = await Promise.all([ |
||||
|
getDividendsFromMoex({ from: twoYearsAgo, symbol, to: twoYearsAhead }), |
||||
|
this.yahooFinanceService.getDividends({ |
||||
|
from: twoYearsAgo, |
||||
|
symbol: getYahooSymbolFromMoex(symbol), |
||||
|
to: twoYearsAhead |
||||
|
}) |
||||
|
]); |
||||
|
|
||||
|
const dateAlmostTheSame = (x: Date, y: Date) => |
||||
|
Math.abs(differenceInDays(x, y)) <= 2; |
||||
|
const payoutsAlmostTheSame = (x: number, y: number) => |
||||
|
100 * Math.abs(x - y) < 1; |
||||
|
|
||||
|
for (const [yahooDateStr, yahooDividends] of Object.entries( |
||||
|
dividendsFromYahoo |
||||
|
)) { |
||||
|
const yahooDate = new Date(yahooDateStr); |
||||
|
const sameDividendIndex = Object.entries(dividends).findIndex( |
||||
|
(x) => |
||||
|
dateAlmostTheSame(new Date(x[0]), yahooDate) && |
||||
|
payoutsAlmostTheSame(x[1].marketPrice, yahooDividends.marketPrice) |
||||
|
); |
||||
|
if (sameDividendIndex === -1) { |
||||
|
dividends[yahooDateStr] = yahooDividends; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const result: { [date: string]: IDataProviderHistoricalResponse } = {}; |
||||
|
|
||||
|
Object.entries(dividends) |
||||
|
.map( |
||||
|
([dateStr, dividends]) => |
||||
|
[dateStr, dividends, new Date(dateStr)] as [ |
||||
|
string, |
||||
|
IDataProviderHistoricalResponse, |
||||
|
Date |
||||
|
] |
||||
|
) |
||||
|
.filter(([, , date]) => date >= from) |
||||
|
.filter(([, , date]) => date <= to) |
||||
|
.sort(([, , a], [, , b]) => differenceInMilliseconds(a, b)) |
||||
|
.forEach(([dateStr, dividends]) => (result[dateStr] = dividends)); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
async getHistorical({ from, symbol, to }: GetHistoricalParams): Promise<{ |
||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; |
||||
|
}> { |
||||
|
const securitySpecification = await getSecuritySpecification( |
||||
|
symbol, |
||||
|
'boards', |
||||
|
'is_primary' |
||||
|
); |
||||
|
const primaryBoard = securitySpecification.get(1); |
||||
|
|
||||
|
const board_id = primaryBoard.get('boardid'); |
||||
|
const market = primaryBoard.get('market'); |
||||
|
const engine = primaryBoard.get('engine'); |
||||
|
|
||||
|
const params: Record<string, any> = { |
||||
|
sort_order: 'desc', |
||||
|
marketprice_board: '1', |
||||
|
from: format(from, 'yyyy-MM-dd'), |
||||
|
to: format(to, 'yyyy-MM-dd') |
||||
|
}; |
||||
|
|
||||
|
const historyResponse = await readBatchedResponse<{ |
||||
|
history: ResponseData; |
||||
|
}>( |
||||
|
async (x) => { |
||||
|
params['start'] = x; |
||||
|
return await moexClient.request( |
||||
|
`history/engines/${engine}/markets/${market}/securities/${symbol}`, |
||||
|
params |
||||
|
); |
||||
|
}, |
||||
|
(x) => { |
||||
|
return { |
||||
|
columns: x.data.history.columns, |
||||
|
data: x.data.history.data.filter((x) => x[0] === board_id) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const history = response_data_to_map(historyResponse, 'TRADEDATE'); |
||||
|
|
||||
|
const result: { |
||||
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; |
||||
|
} = {}; |
||||
|
result[symbol] = {}; |
||||
|
|
||||
|
for (const [key, value] of history.entries()) { |
||||
|
const price = value.get('LEGALCLOSEPRICE') ?? value.get('CLOSE'); |
||||
|
if (typeof price === 'number') |
||||
|
result[symbol][key] = { marketPrice: price }; |
||||
|
else |
||||
|
Logger.error( |
||||
|
`We have quote, but can't get price. Symbol ${symbol}, columns are [${historyResponse.columns.join(', ')}]` |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
getMaxNumberOfSymbolsPerRequest?(): number { |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
getName(): $Enums.DataSource { |
||||
|
return $Enums.DataSource.MOEX; |
||||
|
} |
||||
|
|
||||
|
async getQuotes({ |
||||
|
symbols |
||||
|
}: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { |
||||
|
const result: { [symbol: string]: IDataProviderResponse } = {}; |
||||
|
|
||||
|
for (const symbol of symbols) { |
||||
|
const profile = await this.getAssetProfile({ symbol: symbol }); |
||||
|
|
||||
|
for ( |
||||
|
let date = startOfYesterday(); |
||||
|
!result[symbol]; |
||||
|
date = subDays(date, 1) |
||||
|
) { |
||||
|
const history = await this.getHistorical({ |
||||
|
from: date, |
||||
|
to: date, |
||||
|
symbol: symbol |
||||
|
}); |
||||
|
|
||||
|
for (const [, v] of Object.entries(history[symbol])) { |
||||
|
result[symbol] = { |
||||
|
currency: profile.currency, |
||||
|
dataSource: profile.dataSource, |
||||
|
marketPrice: v.marketPrice, |
||||
|
marketState: 'closed' |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
getTestSymbol(): string { |
||||
|
return 'SBER'; |
||||
|
} |
||||
|
|
||||
|
async search({ query }: GetSearchParams): Promise<LookupResponse> { |
||||
|
const MAX_SEARCH_ITEMS: number = 50; |
||||
|
// MOEX doesn't support search for queries less than 3 symbols
|
||||
|
if (query.length < 3) { |
||||
|
return { items: [] }; |
||||
|
} |
||||
|
|
||||
|
const params: IGetSecuritiesParams & TGetSecuritiesParamsGroupBy = { |
||||
|
q: query |
||||
|
}; |
||||
|
|
||||
|
const searchResponse = await readBatchedResponse<ISecuritiesResponse>( |
||||
|
async (x) => { |
||||
|
params['start'] = x; |
||||
|
return await moexClient.security.getSecurities(params); |
||||
|
}, |
||||
|
(x) => x.data.securities, |
||||
|
MAX_SEARCH_ITEMS |
||||
|
); |
||||
|
|
||||
|
const search = response_data_to_map(searchResponse, 'secid'); |
||||
|
|
||||
|
const result: LookupResponse = { items: [] }; |
||||
|
for (const k of search.keys()) { |
||||
|
if (typeof k != 'string') { |
||||
|
continue; |
||||
|
} |
||||
|
const profile = await this.getAssetProfile({ symbol: k }); |
||||
|
const lookedup_profile = { |
||||
|
assetClass: profile.assetClass, |
||||
|
assetSubClass: profile.assetSubClass, |
||||
|
currency: profile.currency, |
||||
|
dataProviderInfo: this.getDataProviderInfo(), |
||||
|
dataSource: this.getName(), |
||||
|
name: profile.name, |
||||
|
symbol: k |
||||
|
}; |
||||
|
|
||||
|
if (k === query) { |
||||
|
result.items.unshift(lookedup_profile); |
||||
|
} else { |
||||
|
result.items.push(lookedup_profile); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
@ -0,0 +1,2 @@ |
|||||
|
-- AlterEnum |
||||
|
ALTER TYPE "DataSource" ADD VALUE 'MOEX'; |
Loading…
Reference in new issue