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