Untitled
Anonymous
plain_text
10/03/2026 4:24 PM
24.5 KB
2
Indexable
D:\Vela_TradingCharts\Vela>type src\data\providers\ibkr\IBKRProvider.ts
import {
BarSizeSetting,
EventName,
IBApi,
SecType,
WhatToShow,
} from '@stoqey/ib';
import type { Contract } from '@stoqey/ib';
import type {
DataProvider,
ProviderCapabilities,
ProviderInfo,
SymbolDescriptor,
} from '../../../core/ports/DataProvider';
import type { OHLCV } from '../../../core/model/ohlcv';
import type {
BarRange,
SymbolInfo,
} from '../../../core/ports/MarketDataFeed';
type ContractConfig = {
conId: number;
localSymbol: string;
description: string;
multiplier: number;
minTick: number;
expiration: string;
tradingClass: string;
};
const CONTRACTS: Record<string, ContractConfig> = {
ES: {
conId: 515416632,
localSymbol: 'ESZ6',
description: 'E-mini S&P 500',
multiplier: 50,
minTick: 0.25,
expiration: '20261218',
tradingClass: 'ES',
},
NQ: {
conId: 563947726,
localSymbol: 'NQZ6',
description: 'E-mini Nasdaq-100',
multiplier: 20,
minTick: 0.25,
expiration: '20261218',
tradingClass: 'NQ',
},
MES: {
conId: 815824257,
localSymbol: 'MESZ6',
description: 'Micro E-mini S&P 500',
multiplier: 5,
minTick: 0.25,
expiration: '20261218',
tradingClass: 'MES',
},
MNQ: {
conId: 815824267,
localSymbol: 'MNQZ6',
description: 'Micro E-mini Nasdaq-100',
multiplier: 2,
minTick: 0.25,
expiration: '20261218',
tradingClass: 'MNQ',
},
};
export class IBKRProvider implements DataProvider {
private ib: IBApi | undefined;
private connected = false;
private connecting: Promise<void> | undefined;
private requestId = 1000;
private readonly host = '127.0.0.1';
private readonly port = 4002;
private readonly clientId = 321;
private readonly capabilities: ProviderCapabilities = {
enumerate: true,
stream: false,
symbolInfo: true,
};
info(): ProviderInfo {
return {
name: 'ibkr',
displayName: 'Interactive Brokers',
requiresApiKey: false,
supportedTimeframes: [
'1',
'2',
'3',
'5',
'10',
'15',
'20',
'30',
'60',
'120',
'180',
'240',
'D',
'W',
'M',
],
capabilities: this.capabilities,
};
}
capabilitiesFor(
_ticker: string,
): ProviderCapabilities {
return this.capabilities;
}
async listSymbols(): Promise<SymbolDescriptor[]> {
return [
{
ticker: 'ES',
description: 'E-mini S&P 500',
type: 'futures',
group: 'CME',
default: true,
market: 'CME',
provider: 'ibkr',
},
{
ticker: 'NQ',
description: 'E-mini Nasdaq-100',
type: 'futures',
group: 'CME',
market: 'CME',
provider: 'ibkr',
},
{
ticker: 'MES',
description: 'Micro E-mini S&P 500',
type: 'futures',
group: 'CME',
market: 'CME',
provider: 'ibkr',
},
{
ticker: 'MNQ',
description: 'Micro E-mini Nasdaq-100',
type: 'futures',
group: 'CME',
market: 'CME',
provider: 'ibkr',
},
];
}
async getSymbolInfo(
ticker: string,
): Promise<SymbolInfo | undefined> {
const root = ticker.trim().toUpperCase();
const config = CONTRACTS[root];
if (!config) {
return undefined;
}
return {
ticker: root,
name: config.localSymbol,
description: config.description,
type: 'futures',
exchange: 'CME',
currency: 'USD',
multiplier: config.multiplier,
minTick: config.minTick,
conId: config.conId,
expiration: config.expiration,
tradingClass: config.tradingClass,
} as SymbolInfo;
}
async getBars(
ticker: string,
timeframe: string,
range: BarRange,
): Promise<OHLCV[]> {
const root = ticker.trim().toUpperCase();
const config = CONTRACTS[root];
if (!config) {
throw new Error(
`IBKR contract not found for symbol: ${root}`,
);
}
const barSize = this.toBarSize(timeframe);
if (!barSize) {
throw new Error(
`IBKR unsupported timeframe: ${timeframe}`,
);
}
const duration = this.toDuration(range);
const contract: Contract = {
conId: config.conId,
symbol: root,
secType: SecType.FUT,
exchange: 'CME',
currency: 'USD',
} as Contract;
console.log('[IBKR DEBUG] getBars', {
ticker: root,
timeframe,
duration,
barSize,
contract,
});
const bars = await this.getHistoricalData(
contract,
duration,
barSize,
undefined,
);
return bars
.filter(
(bar) =>
bar &&
typeof bar !== 'string' &&
bar.date !== undefined,
)
.map((bar) => ({
time: this.toTimestamp(bar.date),
open: Number(bar.open),
high: Number(bar.high),
low: Number(bar.low),
close: Number(bar.close),
volume: Number(bar.volume ?? 0),
}))
.filter(
(bar) =>
Number.isFinite(bar.time) &&
Number.isFinite(bar.open) &&
Number.isFinite(bar.high) &&
Number.isFinite(bar.low) &&
Number.isFinite(bar.close),
);
}
private getHistoricalData(
contract: Contract,
duration: string,
barSize: BarSizeSetting,
session?: string,
): Promise<any[]> {
return new Promise(
async (resolve, reject) => {
await this.ensureConnected();
const reqId = this.nextRequestId();
const ib = this.ib;
if (!ib) {
reject(
new Error(
'IBKR connection unavailable',
),
);
return;
}
const bars: any[] = [];
let settled = false;
const cleanup = () => {
(ib as any).off(
EventName.historicalData,
onBar,
);
(ib as any).off(
EventName.historicalDataUpdate,
onUpdate,
);
(ib as any).off(
EventName.error,
onError,
);
};
const finish = () => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(bars);
};
const onBar = (
id: number,
time: string | number,
open: number,
high: number,
low: number,
close: number,
volume: number,
count: number,
WAP: number,
hasGaps: boolean,
) => {
if (id !== reqId) {
return;
}
console.log(
'[IBKR DEBUG] historicalData:',
id,
time,
open,
high,
low,
close,
volume,
);
if (
typeof time === 'string' &&
time.startsWith('finished-')
) {
finish();
return;
}
bars.push({
date: time,
open,
high,
low,
close,
volume,
count,
WAP,
hasGaps,
});
};
const onUpdate = (
id: number,
time: string | number,
open: number,
high: number,
low: number,
close: number,
volume: number,
count: number,
WAP: number,
hasGaps: boolean,
) => {
if (id !== reqId) {
return;
}
if (
typeof time === 'string' &&
time.startsWith('finished-')
) {
finish();
return;
}
if (
time !== undefined &&
open !== undefined &&
high !== undefined &&
low !== undefined &&
close !== undefined
) {
bars.push({
date: time,
open,
high,
low,
close,
volume,
count,
WAP,
hasGaps,
});
}
};
const onError = (
id: number,
errorCode: number,
errorMessage: string,
) => {
if (
id !== reqId ||
settled
) {
return;
}
settled = true;
cleanup();
reject(
new Error(
`IBKR historical data error ${errorCode}: ${errorMessage}`,
),
);
};
(ib as any).on(
EventName.historicalData,
onBar,
);
(ib as any).on(
EventName.historicalDataUpdate,
onUpdate,
);
(ib as any).on(
EventName.error,
onError,
);
console.log(
'[IBKR DEBUG] ABOUT TO REQUEST',
{
reqId,
contract,
duration,
barSize,
},
);
try {
ib.reqHistoricalData(
reqId,
contract,
undefined,
duration,
barSize,
WhatToShow.TRADES,
session === 'regular'
? 1
: 0,
2,
false,
);
console.log(
'[IBKR DEBUG] REQUEST SENT',
);
} catch (error) {
console.error(
'[IBKR DEBUG] REQUEST THREW',
error,
);
settled = true;
cleanup();
reject(error);
}
},
);
}
private async ensureConnected(): Promise<void> {
if (
this.connected &&
this.ib
) {
return;
}
if (this.connecting) {
return this.connecting;
}
this.connecting = new Promise(
(resolve, reject) => {
const ib = new IBApi({
host: this.host,
port: this.port,
clientId: this.clientId,
});
this.ib = ib;
let settled = false;
const onConnected = () => {
if (settled) {
return;
}
settled = true;
this.connected = true;
ib.reqMarketDataType(3);
console.log(
'[IBKR DEBUG] CONNECTED',
);
resolve();
};
const onError = (
id: number,
errorCode: number,
errorMessage: string,
) => {
if (
settled &&
this.connected
) {
return;
}
if (
errorCode === 2104 ||
errorCode === 2106 ||
errorCode === 2158
) {
return;
}
if (!settled) {
settled = true;
reject(
new Error(
`IBKR connection error ${errorCode}: ${errorMessage}`,
),
);
}
};
ib.on(
EventName.connected,
onConnected,
);
ib.on(
EventName.error,
onError,
);
ib.on(
EventName.disconnected,
() => {
this.connected = false;
},
);
ib.connect();
},
);
try {
await this.connecting;
} finally {
this.connecting = undefined;
}
}
private nextRequestId(): number {
this.requestId += 1;
return this.requestId;
}
private toBarSize(
timeframe: string,
): BarSizeSetting | undefined {
switch (timeframe) {
case '1':
return BarSizeSetting.MINUTES_ONE;
case '2':
return BarSizeSetting.MINUTES_TWO;
case '3':
return BarSizeSetting.MINUTES_THREE;
case '5':
return BarSizeSetting.MINUTES_FIVE;
case '10':
return BarSizeSetting.MINUTES_TEN;
case '15':
return BarSizeSetting.MINUTES_FIFTEEN;
case '20':
return BarSizeSetting.MINUTES_TWENTY;
case '30':
return BarSizeSetting.MINUTES_THIRTY;
case '60':
return BarSizeSetting.HOURS_ONE;
case '120':
return BarSizeSetting.HOURS_TWO;
case '180':
return BarSizeSetting.HOURS_THREE;
case '240':
return BarSizeSetting.HOURS_FOUR;
case 'D':
return BarSizeSetting.DAYS_ONE;
case 'W':
return BarSizeSetting.WEEKS_ONE;
case 'M':
return BarSizeSetting.MONTHS_ONE;
default:
return undefined;
}
}
private toDuration(
range: BarRange,
): string {
const limit =
typeof range?.limit === 'number'
? range.limit
: 500;
if (limit <= 100) {
return '1 D';
}
if (limit <= 500) {
return '1 W';
}
if (limit <= 2000) {
return '1 M';
}
if (limit <= 10000) {
return '1 Y';
}
return '2 Y';
}
private toTimestamp(
value: unknown,
): number {
if (typeof value === 'number') {
return value > 10_000_000_000
? value
: value * 1000;
}
if (typeof value === 'string') {
const numeric = Number(value);
if (Number.isFinite(numeric)) {
return numeric > 10_000_000_000
? numeric
: numeric * 1000;
}
const parsed = Date.parse(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return NaN;
}
}
D:\Vela_TradingCharts\Vela>Editor is loading...
Leave a Comment