codex/tbank-broker-portfolios-design #15
@ -14,6 +14,13 @@ export default registerAs('app', () => ({
|
|||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
tbank: {
|
||||||
|
token: process.env.T_BANK_TOKEN || '',
|
||||||
|
baseUrl: process.env.T_BANK_BASE_URL || 'invest-public-api.tbank.ru:443',
|
||||||
|
appName: process.env.T_BANK_APP_NAME || 'ksv741.moex-vibe',
|
||||||
|
rateLimitPerSecond: parseInt(process.env.T_BANK_RATE_LIMIT_PER_SECOND || '5', 10),
|
||||||
|
requestTimeoutMs: parseInt(process.env.T_BANK_REQUEST_TIMEOUT_MS || '10000', 10),
|
||||||
|
},
|
||||||
cache: {
|
cache: {
|
||||||
marketDataTtl: parseInt(process.env.CACHE_MARKET_DATA_TTL || '900', 10),
|
marketDataTtl: parseInt(process.env.CACHE_MARKET_DATA_TTL || '900', 10),
|
||||||
historyTtl: parseInt(process.env.CACHE_HISTORY_TTL || '3600', 10),
|
historyTtl: parseInt(process.env.CACHE_HISTORY_TTL || '3600', 10),
|
||||||
@ -21,6 +28,10 @@ export default registerAs('app', () => ({
|
|||||||
securityTtl: parseInt(process.env.CACHE_SECURITY_TTL || '86400', 10),
|
securityTtl: parseInt(process.env.CACHE_SECURITY_TTL || '86400', 10),
|
||||||
searchTtl: parseInt(process.env.CACHE_SEARCH_TTL || '3600', 10),
|
searchTtl: parseInt(process.env.CACHE_SEARCH_TTL || '3600', 10),
|
||||||
dividendsTtl: parseInt(process.env.CACHE_DIVIDENDS_TTL || '86400', 10),
|
dividendsTtl: parseInt(process.env.CACHE_DIVIDENDS_TTL || '86400', 10),
|
||||||
|
tbankAccountsTtl: parseInt(process.env.CACHE_TBANK_ACCOUNTS_TTL || '3600', 10),
|
||||||
|
tbankPortfolioTtl: parseInt(process.env.CACHE_TBANK_PORTFOLIO_TTL || '60', 10),
|
||||||
|
tbankOperationsTtl: parseInt(process.env.CACHE_TBANK_OPERATIONS_TTL || '300', 10),
|
||||||
|
tbankInstrumentTtl: parseInt(process.env.CACHE_TBANK_INSTRUMENT_TTL || '86400', 10),
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production',
|
jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production',
|
||||||
|
|||||||
39
apps/backend/src/modules/tbank/tbank.config.spec.ts
Normal file
39
apps/backend/src/modules/tbank/tbank.config.spec.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import configuration from '../../config/configuration';
|
||||||
|
|
||||||
|
describe('T-Bank configuration', () => {
|
||||||
|
const originalEnv = process.env;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses conservative defaults for T-Bank integration', () => {
|
||||||
|
delete process.env.T_BANK_BASE_URL;
|
||||||
|
delete process.env.T_BANK_RATE_LIMIT_PER_SECOND;
|
||||||
|
delete process.env.CACHE_TBANK_PORTFOLIO_TTL;
|
||||||
|
|
||||||
|
const config = configuration();
|
||||||
|
|
||||||
|
expect(config.tbank.baseUrl).toBe('invest-public-api.tbank.ru:443');
|
||||||
|
expect(config.tbank.rateLimitPerSecond).toBe(5);
|
||||||
|
expect(config.cache.tbankPortfolioTtl).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads T-Bank token and TTL overrides from environment', () => {
|
||||||
|
process.env.T_BANK_TOKEN = 'secret-token';
|
||||||
|
process.env.T_BANK_BASE_URL = 'sandbox-invest-public-api.tbank.ru:443';
|
||||||
|
process.env.T_BANK_RATE_LIMIT_PER_SECOND = '2';
|
||||||
|
process.env.CACHE_TBANK_ACCOUNTS_TTL = '120';
|
||||||
|
|
||||||
|
const config = configuration();
|
||||||
|
|
||||||
|
expect(config.tbank.token).toBe('secret-token');
|
||||||
|
expect(config.tbank.baseUrl).toBe('sandbox-invest-public-api.tbank.ru:443');
|
||||||
|
expect(config.tbank.rateLimitPerSecond).toBe(2);
|
||||||
|
expect(config.cache.tbankAccountsTtl).toBe(120);
|
||||||
|
});
|
||||||
|
});
|
||||||
22
apps/backend/src/modules/tbank/tbank.config.ts
Normal file
22
apps/backend/src/modules/tbank/tbank.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export const TBANK_PROTO_PACKAGE = 'tinkoff.public.invest.api.contract.v1';
|
||||||
|
|
||||||
|
export const TBANK_PROTO_FILES = {
|
||||||
|
users: 'users.proto',
|
||||||
|
operations: 'operations.proto',
|
||||||
|
instruments: 'instruments.proto',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const TBANK_ACCOUNT_TYPES = {
|
||||||
|
brokerage: 'ACCOUNT_TYPE_TINKOFF',
|
||||||
|
iis: 'ACCOUNT_TYPE_TINKOFF_IIS',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const TBANK_OPEN_ACCOUNT_STATUS = 'ACCOUNT_STATUS_OPEN';
|
||||||
|
|
||||||
|
export const TBANK_CACHE_KEYS = {
|
||||||
|
accounts: 'tbank:accounts',
|
||||||
|
portfolio: 'tbank:portfolio',
|
||||||
|
positions: 'tbank:positions',
|
||||||
|
operations: 'tbank:operations',
|
||||||
|
instrument: 'tbank:instrument',
|
||||||
|
} as const;
|
||||||
90
apps/backend/src/modules/tbank/types/broker.types.ts
Normal file
90
apps/backend/src/modules/tbank/types/broker.types.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
export type BrokerMoney = {
|
||||||
|
currency: string;
|
||||||
|
units: string;
|
||||||
|
nano: number;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerAccount = {
|
||||||
|
id: string;
|
||||||
|
type: 'brokerage' | 'iis';
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
openedAt: string | null;
|
||||||
|
accessLevel: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerPosition = {
|
||||||
|
figi: string | null;
|
||||||
|
instrumentUid: string | null;
|
||||||
|
positionUid: string | null;
|
||||||
|
ticker: string | null;
|
||||||
|
classCode: string | null;
|
||||||
|
instrumentType: string | null;
|
||||||
|
name: string | null;
|
||||||
|
quantity: number | null;
|
||||||
|
blockedLots: number | null;
|
||||||
|
currentPrice: BrokerMoney | null;
|
||||||
|
currentValue: BrokerMoney | null;
|
||||||
|
averagePositionPrice: BrokerMoney | null;
|
||||||
|
expectedYieldPercent: number | null;
|
||||||
|
dailyYield: BrokerMoney | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerPortfolio = {
|
||||||
|
account: BrokerAccount;
|
||||||
|
totals: {
|
||||||
|
shares: BrokerMoney | null;
|
||||||
|
bonds: BrokerMoney | null;
|
||||||
|
etf: BrokerMoney | null;
|
||||||
|
currencies: BrokerMoney | null;
|
||||||
|
futures: BrokerMoney | null;
|
||||||
|
options: BrokerMoney | null;
|
||||||
|
structuredProducts: BrokerMoney | null;
|
||||||
|
dfa: BrokerMoney | null;
|
||||||
|
portfolio: BrokerMoney | null;
|
||||||
|
};
|
||||||
|
yields: {
|
||||||
|
expectedPercent: number | null;
|
||||||
|
daily: BrokerMoney | null;
|
||||||
|
dailyPercent: number | null;
|
||||||
|
};
|
||||||
|
cash: BrokerMoney[];
|
||||||
|
blockedCash: BrokerMoney[];
|
||||||
|
positions: BrokerPosition[];
|
||||||
|
asOf: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerOperationCategory = 'trade' | 'income' | 'tax' | 'fee' | 'transfer' | 'other';
|
||||||
|
|
||||||
|
export type BrokerOperation = {
|
||||||
|
cursor: string | null;
|
||||||
|
accountId: string;
|
||||||
|
id: string | null;
|
||||||
|
parentOperationId: string | null;
|
||||||
|
date: string | null;
|
||||||
|
type: string;
|
||||||
|
category: BrokerOperationCategory;
|
||||||
|
description: string | null;
|
||||||
|
state: string | null;
|
||||||
|
instrumentUid: string | null;
|
||||||
|
figi: string | null;
|
||||||
|
ticker: string | null;
|
||||||
|
classCode: string | null;
|
||||||
|
instrumentType: string | null;
|
||||||
|
payment: BrokerMoney | null;
|
||||||
|
price: BrokerMoney | null;
|
||||||
|
commission: BrokerMoney | null;
|
||||||
|
yield: BrokerMoney | null;
|
||||||
|
accruedInt: BrokerMoney | null;
|
||||||
|
quantity: number | null;
|
||||||
|
quantityDone: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrokerOperationsPage = {
|
||||||
|
accountId: string;
|
||||||
|
items: BrokerOperation[];
|
||||||
|
nextCursor: string | null;
|
||||||
|
hasNext: boolean;
|
||||||
|
asOf: string;
|
||||||
|
};
|
||||||
148
apps/backend/src/modules/tbank/types/tbank-proto.types.ts
Normal file
148
apps/backend/src/modules/tbank/types/tbank-proto.types.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
export type TBankTimestamp = {
|
||||||
|
seconds?: number | string;
|
||||||
|
nanos?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankMoneyValue = {
|
||||||
|
currency?: string;
|
||||||
|
units?: number | string;
|
||||||
|
nano?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankQuotation = {
|
||||||
|
units?: number | string;
|
||||||
|
nano?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankAccount = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
name?: string;
|
||||||
|
status: string;
|
||||||
|
openedDate?: TBankTimestamp;
|
||||||
|
closedDate?: TBankTimestamp;
|
||||||
|
accessLevel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankAccountsResponse = {
|
||||||
|
accounts?: TBankAccount[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankPortfolioPosition = {
|
||||||
|
figi?: string;
|
||||||
|
instrumentType?: string;
|
||||||
|
quantity?: TBankQuotation;
|
||||||
|
averagePositionPrice?: TBankMoneyValue;
|
||||||
|
expectedYield?: TBankQuotation;
|
||||||
|
currentNkd?: TBankMoneyValue;
|
||||||
|
currentPrice?: TBankMoneyValue;
|
||||||
|
averagePositionPriceFifo?: TBankMoneyValue;
|
||||||
|
blocked?: boolean;
|
||||||
|
blockedLots?: TBankQuotation;
|
||||||
|
positionUid?: string;
|
||||||
|
instrumentUid?: string;
|
||||||
|
expectedYieldFifo?: TBankQuotation;
|
||||||
|
dailyYield?: TBankMoneyValue;
|
||||||
|
ticker?: string;
|
||||||
|
classCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankPortfolioResponse = {
|
||||||
|
accountId?: string;
|
||||||
|
totalAmountShares?: TBankMoneyValue;
|
||||||
|
totalAmountBonds?: TBankMoneyValue;
|
||||||
|
totalAmountEtf?: TBankMoneyValue;
|
||||||
|
totalAmountCurrencies?: TBankMoneyValue;
|
||||||
|
totalAmountFutures?: TBankMoneyValue;
|
||||||
|
expectedYield?: TBankQuotation;
|
||||||
|
positions?: TBankPortfolioPosition[];
|
||||||
|
totalAmountOptions?: TBankMoneyValue;
|
||||||
|
totalAmountSp?: TBankMoneyValue;
|
||||||
|
totalAmountPortfolio?: TBankMoneyValue;
|
||||||
|
dailyYield?: TBankMoneyValue;
|
||||||
|
dailyYieldRelative?: TBankQuotation;
|
||||||
|
totalAmountDfa?: TBankMoneyValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankPositionsSecurity = {
|
||||||
|
figi?: string;
|
||||||
|
blocked?: string | number;
|
||||||
|
balance?: string | number;
|
||||||
|
positionUid?: string;
|
||||||
|
instrumentUid?: string;
|
||||||
|
ticker?: string;
|
||||||
|
classCode?: string;
|
||||||
|
exchangeBlocked?: boolean;
|
||||||
|
instrumentType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankPositionsResponse = {
|
||||||
|
accountId?: string;
|
||||||
|
money?: TBankMoneyValue[];
|
||||||
|
blocked?: TBankMoneyValue[];
|
||||||
|
securities?: TBankPositionsSecurity[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankOperationTrade = {
|
||||||
|
num?: string;
|
||||||
|
date?: TBankTimestamp;
|
||||||
|
quantity?: string | number;
|
||||||
|
price?: TBankMoneyValue;
|
||||||
|
yield?: TBankMoneyValue;
|
||||||
|
yieldRelative?: TBankQuotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankOperationItem = {
|
||||||
|
cursor?: string;
|
||||||
|
brokerAccountId?: string;
|
||||||
|
id?: string;
|
||||||
|
parentOperationId?: string;
|
||||||
|
name?: string;
|
||||||
|
date?: TBankTimestamp;
|
||||||
|
type?: string;
|
||||||
|
description?: string;
|
||||||
|
state?: string;
|
||||||
|
instrumentUid?: string;
|
||||||
|
figi?: string;
|
||||||
|
instrumentType?: string;
|
||||||
|
instrumentKind?: string;
|
||||||
|
positionUid?: string;
|
||||||
|
ticker?: string;
|
||||||
|
classCode?: string;
|
||||||
|
payment?: TBankMoneyValue;
|
||||||
|
price?: TBankMoneyValue;
|
||||||
|
commission?: TBankMoneyValue;
|
||||||
|
yield?: TBankMoneyValue;
|
||||||
|
yieldRelative?: TBankQuotation;
|
||||||
|
accruedInt?: TBankMoneyValue;
|
||||||
|
quantity?: string | number;
|
||||||
|
quantityRest?: string | number;
|
||||||
|
quantityDone?: string | number;
|
||||||
|
tradesInfo?: { trades?: TBankOperationTrade[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankOperationsByCursorResponse = {
|
||||||
|
hasNext?: boolean;
|
||||||
|
nextCursor?: string;
|
||||||
|
items?: TBankOperationItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankInstrument = {
|
||||||
|
figi?: string;
|
||||||
|
ticker?: string;
|
||||||
|
classCode?: string;
|
||||||
|
isin?: string;
|
||||||
|
lot?: number;
|
||||||
|
currency?: string;
|
||||||
|
name?: string;
|
||||||
|
exchange?: string;
|
||||||
|
instrumentType?: string;
|
||||||
|
uid?: string;
|
||||||
|
positionUid?: string;
|
||||||
|
assetUid?: string;
|
||||||
|
instrumentKind?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBankInstrumentResponse = {
|
||||||
|
instrument?: TBankInstrument;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user