- MoexHttpClient: infrastructure (axios, rate limiter, circuit breaker) - MoexSecuritiesClient: search and security descriptions - MoexMarketDataClient: share/bond market data and batch queries - MoexCandlesClient: candle data - MoexHistoryClient: share/bond history - MoexDividendsClient: dividend data - Removed @Global() from MoexClientModule - Updated all 7 consumers with explicit DI - All 141 tests passing
220 lines
9.8 KiB
TypeScript
220 lines
9.8 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { MoexHttpClient } from './moex-http.client';
|
|
import {
|
|
MoexShareMarketData,
|
|
MoexBondData,
|
|
MoexBondMarketData,
|
|
MoexBondPositionData,
|
|
} from './moex-client.types';
|
|
|
|
@Injectable()
|
|
export class MoexMarketDataClient {
|
|
constructor(private readonly http: MoexHttpClient) {}
|
|
|
|
async getShareMarketData(secid: string, boardId = 'TQBR'): Promise<MoexShareMarketData | null> {
|
|
const data = await this.http.request<Record<string, unknown>>(
|
|
`/engines/stock/markets/shares/securities/${secid}`,
|
|
{ boards: boardId },
|
|
);
|
|
const rows = this.http.extractTable(data, 'securities');
|
|
const share = rows.find((r) => r.BOARDID === boardId);
|
|
if (!share) return null;
|
|
|
|
const mktRows = this.http.extractTable(data, 'marketdata');
|
|
const mkt = mktRows.find((r) => r.BOARDID === boardId);
|
|
|
|
return {
|
|
secid,
|
|
boardid: boardId,
|
|
shortName: (share?.SHORTNAME as string) || '',
|
|
bid: mkt ? parseFloat((mkt.BID as string) || '') : null,
|
|
offer: mkt ? parseFloat((mkt.OFFER as string) || '') : null,
|
|
open: mkt ? parseFloat((mkt.OPEN as string) || '') : null,
|
|
low: mkt ? parseFloat((mkt.LOW as string) || '') : null,
|
|
high: mkt ? parseFloat((mkt.HIGH as string) || '') : null,
|
|
last: mkt
|
|
? parseFloat((mkt.LAST as string) || '')
|
|
: parseFloat((share.PREVPRICE as string) || ''),
|
|
lastChange: mkt ? parseFloat((mkt.LASTCHANGE as string) || '') : null,
|
|
lastChangePrcnt: mkt ? parseFloat((mkt.LASTCHANGEPRCNT as string) || '') : null,
|
|
volume: mkt ? parseInt((mkt.VOLTODAY as string) || '0', 10) : 0,
|
|
value: mkt ? parseFloat((mkt.VALTODAY as string) || '0') : 0,
|
|
waprice: mkt ? parseFloat((mkt.WAPRICE as string) || '') : null,
|
|
numtrades: mkt ? parseInt((mkt.NUMTRADES as string) || '0', 10) : 0,
|
|
issueCapitalization: mkt ? parseFloat((mkt.ISSUECAPITALIZATION as string) || '') : null,
|
|
tradingStatus: (mkt?.TRADINGSTATUS as string) || '',
|
|
updateTime: (mkt?.UPDATETIME as string) || '',
|
|
};
|
|
}
|
|
|
|
async getShareMarketDataBatch(
|
|
secids: string[],
|
|
boardId = 'TQBR',
|
|
): Promise<MoexShareMarketData[]> {
|
|
const params: Record<string, string> = { boards: boardId };
|
|
if (secids.length > 0) {
|
|
params.securities = secids.join(',');
|
|
}
|
|
const data = await this.http.request<Record<string, unknown>>(
|
|
`/engines/stock/markets/shares/securities`,
|
|
params,
|
|
);
|
|
const securities = this.http.extractTable(data, 'securities');
|
|
const marketdata = this.http.extractTable(data, 'marketdata');
|
|
|
|
const secidSet = secids.length > 0 ? new Set(secids) : null;
|
|
const filteredSecurities = secidSet
|
|
? securities.filter((r) => secidSet.has(r.SECID as string))
|
|
: securities;
|
|
|
|
return filteredSecurities.map((sec) => {
|
|
const secid = sec.SECID as string;
|
|
const mkt =
|
|
marketdata.find((r) => r.SECID === secid && r.BOARDID === boardId) ||
|
|
marketdata.find((r) => r.SECID === secid);
|
|
|
|
return {
|
|
secid,
|
|
boardid: boardId,
|
|
shortName: (sec?.SHORTNAME as string) || '',
|
|
bid: mkt ? parseFloat((mkt.BID as string) || '') : null,
|
|
offer: mkt ? parseFloat((mkt.OFFER as string) || '') : null,
|
|
open: mkt ? parseFloat((mkt.OPEN as string) || '') : null,
|
|
low: mkt ? parseFloat((mkt.LOW as string) || '') : null,
|
|
high: mkt ? parseFloat((mkt.HIGH as string) || '') : null,
|
|
last: mkt
|
|
? parseFloat((mkt.LAST as string) || '')
|
|
: parseFloat((sec?.PREVPRICE as string) || ''),
|
|
lastChange: mkt ? parseFloat((mkt.LASTCHANGE as string) || '') : null,
|
|
lastChangePrcnt: mkt ? parseFloat((mkt.LASTCHANGEPRCNT as string) || '') : null,
|
|
volume: mkt ? parseInt((mkt.VOLTODAY as string) || '0', 10) : 0,
|
|
value: mkt ? parseFloat((mkt.VALTODAY as string) || '0') : 0,
|
|
waprice: mkt ? parseFloat((mkt.WAPRICE as string) || '') : null,
|
|
numtrades: mkt ? parseInt((mkt.NUMTRADES as string) || '0', 10) : 0,
|
|
issueCapitalization: mkt ? parseFloat((mkt.ISSUECAPITALIZATION as string) || '') : null,
|
|
tradingStatus: (mkt?.TRADINGSTATUS as string) || '',
|
|
updateTime: (mkt?.UPDATETIME as string) || '',
|
|
};
|
|
});
|
|
}
|
|
|
|
async getBondData(secid: string, boardId = 'TQCB'): Promise<MoexBondData | null> {
|
|
const data = await this.http.request<Record<string, unknown>>(
|
|
`/engines/stock/markets/bonds/securities/${secid}`,
|
|
{ boards: boardId },
|
|
);
|
|
const rows = this.http.extractTable(data, 'securities');
|
|
const bond =
|
|
rows.find((r) => r.BOARDID === boardId && r.PREVWAPRICE != null) ||
|
|
rows.find((r) => r.PREVWAPRICE != null) ||
|
|
rows[0];
|
|
if (!bond) return null;
|
|
|
|
return {
|
|
secid,
|
|
boardid: boardId,
|
|
shortName: (bond.SHORTNAME as string) || '',
|
|
prevWaprice: parseFloat((bond.PREVWAPRICE as string) || '') || null,
|
|
yieldAtPrevWaprice: parseFloat((bond.YIELDATPREVWAPRICE as string) || '') || null,
|
|
couponValue: bond.COUPONVALUE != null ? parseFloat(bond.COUPONVALUE as string) : null,
|
|
nextCoupon: (bond.NEXTCOUPON as string) || null,
|
|
accruedInt: bond.ACCRUEDINT != null ? parseFloat(bond.ACCRUEDINT as string) : null,
|
|
prevPrice: parseFloat((bond.PREVPRICE as string) || '') || null,
|
|
lotSize: parseInt((bond.LOTSIZE as string) || '1', 10),
|
|
faceValue: parseFloat((bond.FACEVALUE as string) || '1000'),
|
|
matDate: (bond.MATDATE as string) || '',
|
|
couponPeriod: parseInt((bond.COUPONPERIOD as string) || '0', 10),
|
|
issueSize: parseInt((bond.ISSUESIZE as string) || '0', 10),
|
|
isin: (bond.ISIN as string) || '',
|
|
couponPercent: bond.COUPONPERCENT != null ? parseFloat(bond.COUPONPERCENT as string) : null,
|
|
offerDate: (bond.OFFERDATE as string) || null,
|
|
buybackDate: (bond.BUYBACKDATE as string) || null,
|
|
bondType: (bond.BONDTYPE as string) || '',
|
|
bondSubType: (bond.BONDSUBTYPE as string) || '',
|
|
listLevel: parseInt((bond.LISTLEVEL as string) || '0', 10),
|
|
};
|
|
}
|
|
|
|
async getBondMarketData(secid: string, boardId = 'TQCB'): Promise<MoexBondMarketData | null> {
|
|
const data = await this.http.request<Record<string, unknown>>(
|
|
`/engines/stock/markets/bonds/securities/${secid}`,
|
|
{ boards: boardId },
|
|
);
|
|
const mktRows = this.http.extractTable(data, 'marketdata');
|
|
const mkt =
|
|
mktRows.find((r) => r.BOARDID === boardId && r.LAST != null) ||
|
|
mktRows.find((r) => r.LAST != null) ||
|
|
mktRows.find((r) => r.SECID === secid);
|
|
if (!mkt) return null;
|
|
|
|
return {
|
|
secid,
|
|
bid: mkt.BID != null ? parseFloat(mkt.BID as string) : null,
|
|
offer: mkt.OFFER != null ? parseFloat(mkt.OFFER as string) : null,
|
|
open: mkt.OPEN != null ? parseFloat(mkt.OPEN as string) : null,
|
|
low: mkt.LOW != null ? parseFloat(mkt.LOW as string) : null,
|
|
high: mkt.HIGH != null ? parseFloat(mkt.HIGH as string) : null,
|
|
last: mkt.LAST != null ? parseFloat(mkt.LAST as string) : null,
|
|
yield: mkt.YIELD != null ? parseFloat(mkt.YIELD as string) : null,
|
|
waprice: mkt.WAPRICE != null ? parseFloat(mkt.WAPRICE as string) : null,
|
|
yieldAtWaprice: mkt.YIELDATWAPRICE != null ? parseFloat(mkt.YIELDATWAPRICE as string) : null,
|
|
duration: mkt.DURATION != null ? parseFloat(mkt.DURATION as string) : null,
|
|
volume: parseInt((mkt.VOLTODAY as string) || '0', 10),
|
|
value: parseFloat((mkt.VALTODAY as string) || '0'),
|
|
numtrades: parseInt((mkt.NUMTRADES as string) || '0', 10),
|
|
tradingStatus: (mkt.TRADINGSTATUS as string) || '',
|
|
updateTime: (mkt.UPDATETIME as string) || '',
|
|
};
|
|
}
|
|
|
|
async getBondPositionDataBatch(
|
|
secids: string[],
|
|
boardId = 'TQCB',
|
|
): Promise<MoexBondPositionData[]> {
|
|
const params: Record<string, string> = { boards: boardId };
|
|
if (secids.length > 0) {
|
|
params.securities = secids.join(',');
|
|
}
|
|
const data = await this.http.request<Record<string, unknown>>(
|
|
`/engines/stock/markets/bonds/securities`,
|
|
params,
|
|
);
|
|
const securities = this.http.extractTable(data, 'securities');
|
|
const marketdata = this.http.extractTable(data, 'marketdata');
|
|
|
|
const secidSet = secids.length > 0 ? new Set(secids) : null;
|
|
const filteredSecurities = secidSet
|
|
? securities.filter((r) => secidSet.has(r.SECID as string))
|
|
: securities;
|
|
|
|
return filteredSecurities.map((bond) => {
|
|
const secid = bond.SECID as string;
|
|
const mkt =
|
|
marketdata.find((r) => r.SECID === secid && r.BOARDID === boardId && r.LAST != null) ||
|
|
marketdata.find((r) => r.SECID === secid && r.LAST != null) ||
|
|
marketdata.find((r) => r.SECID === secid);
|
|
|
|
return {
|
|
secid,
|
|
boardid: (bond.BOARDID as string) || boardId,
|
|
shortName: (bond?.SHORTNAME as string) || '',
|
|
price: mkt?.LAST != null ? parseFloat(mkt.LAST as string) : null,
|
|
yieldToMaturity: mkt?.YIELD != null ? parseFloat(mkt.YIELD as string) : null,
|
|
duration: mkt?.DURATION != null ? parseFloat(mkt.DURATION as string) : null,
|
|
couponValue: bond?.COUPONVALUE != null ? parseFloat(bond.COUPONVALUE as string) : null,
|
|
couponPercent:
|
|
bond?.COUPONPERCENT != null ? parseFloat(bond.COUPONPERCENT as string) : null,
|
|
nextCouponDate: (bond?.NEXTCOUPON as string) || null,
|
|
matDate: (bond?.MATDATE as string) || null,
|
|
accruedInt: bond?.ACCRUEDINT != null ? parseFloat(bond.ACCRUEDINT as string) : null,
|
|
faceValue: parseFloat((bond?.FACEVALUE as string) || '1000'),
|
|
bid: mkt?.BID != null ? parseFloat(mkt.BID as string) : null,
|
|
offer: mkt?.OFFER != null ? parseFloat(mkt.OFFER as string) : null,
|
|
couponPeriod: parseInt((bond?.COUPONPERIOD as string) || '0', 10),
|
|
bondType: (bond?.BONDTYPE as string) || null,
|
|
offerDate: (bond?.OFFERDATE as string) || null,
|
|
};
|
|
});
|
|
}
|
|
}
|