DocumentationCustom Data Feeds

Custom data feeds

The engine never imports a broker SDK. It depends on two small interfaces, so wiring a new source is a focused adapter - not a fork.

DataFeed

interface DataFeed {
  getBars(req: BarsRequest): Promise<Bar[]>;
  getBarsPage?(req: BarsPageRequest): Promise<BarsPage>;
  getCachedBars?(req: BarsRequest): Promise<Bar[] | undefined>;
  // optional: live updates to the last bar, or a new last bar
  subscribeBars?(req: BarsRequest, onBar: (bar: Bar) => void, opts?: BarSubscriptionOptions): () => void;
}
 
interface BarsRequest {
  symbol: string;
  exchange: string;
  interval: string;       // '5s' | '1m' | '1h' | 'D' | ...
  signal?: AbortSignal;
  timeoutMs?: number;
  countBack?: number;
  from?: number;          // UTC seconds
  to?: number;            // UTC seconds
  noCache?: boolean;      // bypass cached history for an authoritative refresh
}
 
interface BarSubscriptionOptions {
  seedFrom?: Bar;
  cumDayVolumeSoFar?: number;
  onResync?: () => void;
}

DataFeed, BarsRequest, and BarSubscriptionOptions are exported from openalgo-charts. The third subscription argument is optional; existing feeds with two-argument subscribeBars implementations remain compatible. A history-only feed omits subscribeBars so hosts can detect whether live data is available.

interval is a code your feed and the engine both have to agree on. The built-in tokens (5s, 1m, 1h, D, W) resolve on their own; anything else, including a monthly bar or a 500-tick bar, is registered by the host. See Custom Intervals, and note that an unrecognised code now throws rather than quietly meaning one minute.

A minimal adapter just maps your API’s rows to Bar (UTC seconds + OHLCV):

class MyFeed implements DataFeed {
  async getBars(req: BarsRequest): Promise<Bar[]> {
    const rows = await fetchFromMyApi(req);
    return rows.map((r) => ({
      time: Math.floor(r.epochMs / 1000),
      open: r.o, high: r.h, low: r.l, close: r.c, volume: r.v,
    })).sort((a, b) => a.time - b.time);
  }
}

Continuing history and recovering a stream

After loading history, pass its last bar to subscribeBars through seedFrom so a time-bucketed live builder continues the current candle’s open, high, low and volume. Count-driven bars cannot resume historical trade counts. The optional cumDayVolumeSoFar is the cumulative day volume at that same snapshot, for feeds using day-volume deltas; it is not the last bar’s volume.

A feed calls onResync when a restored connection may have missed bars. A host using a bare chart should mark its display stale, keep receiving and buffering live bars, and fetch the current history window with noCache: true. Merge the buffered observations into that history before setData, then seed the replacement subscription from the merged last bar before releasing the old subscription. Keep resync monitoring active so another reconnect can supersede the pending request. Preserve the viewport, and ignore results for a symbol or interval the user has since left.

Live candle volumes and history candle volumes are snapshots; adding them together double-counts overlapping trades. A repair needs a defined merge policy for overlapping timestamps. The widget retains the history open, combines high/low extremes, uses the latest buffered close, and takes the maximum volume. This does not reconstruct unseen trades. A buffered whole candle can retain a seed extreme that history corrected; exact reconciliation needs snapshot/tick ordering the bar contract does not provide. Bars without buffered observations keep their authoritative history values. If recovery fails, keep the previous display marked stale and offer a retry while continuing to collect live observations.

onBar is for tail updates. Do not send older gap-fill bars through series.update(); they need the history replacement path. The widget handles this recovery sequence automatically for feeds that signal onResync.

Warm loads for any feed

withBarCache is a DataFeed in and a DataFeed out, so an adapter of your own gets the same warm-load caching the bundled one does, with nothing to implement:

import { withBarCache } from 'openalgo-charts';
 
const feed = withBarCache(new MyFeed(), { ttlMs: 60_000 });
await feed.getBars({ symbol, exchange, interval, from, to, noCache: true });

noCache: true skips a cached history result and refreshes it from the wrapped source. Custom feeds and cache wrappers should honor or forward this optional BarsRequest field. withBarCache also forwards the subscription options, including onResync.

Entries are keyed symbol | exchange | interval with the range deliberately left out, and the forming bar is never stored, because a chart that paints a frozen candle from cache is confidently wrong with no spinner to warn anyone. See Bar Cache for the freshness rules, the memory bounds, and how to plug in your own persistent store.

The cache is separate from the bars already retained by a chart. For a live adapter that stays open for hours, keep a bounded source window and release every subscription during teardown. See Performance & Operations.

TradeFeed

There are two interfaces here and they are not interchangeable. OrderEngine takes the smaller OrderFeed; TradeFeed is the higher-level broker abstraction with its own subscriptions.

OrderFeed (from openalgo-charts/trade) is what the engine’s write path needs, and the only one you must implement to place orders. Note that place receives the engine’s mode, so an adapter can route analyzer traffic differently from live:

interface OrderFeed {
  place(req: PlaceRequest & { mode: 'live' | 'analyzer' }): Promise<{ orderId: string }>;
  modify(orderId: string, patch: { price?: number; triggerPrice?: number; qty?: number }): Promise<void>;
  cancel(orderId: string): Promise<void>;
}

TradeFeed (from the base package) is the broader broker surface, with push subscriptions rather than snapshot getters:

interface TradeFeed {
  placeOrder(o: PlaceOrder): Promise<{ orderId: string }>;
  modifyOrder(orderId: string, patch: Partial<PlaceOrder>): Promise<void>;
  cancelOrder(orderId: string): Promise<void>;
  subscribeOrders(cb: (orders: unknown[]) => void): UnsubscribeFn;
  subscribePositions(cb: (positions: unknown[]) => void): UnsubscribeFn;
}

Implement against your broker and the entire on-chart trading layer works unchanged. The bundled OpenAlgoTradeFeed implements OrderFeed and is the reference implementation; FakeBroker is an in-memory OrderFeed for tests and demos.

Time conversion helpers

The library exports the conversions it uses internally, so your adapter can reuse them:

import {
  epochMsToUtcSeconds, istStringToUtcSeconds, utcSecondsToIstDateString,
  formatIstTime, formatIstTimeSeconds, IST_OFFSET_SECONDS,
} from 'openalgo-charts';

Managed history capabilities (2.1.6)

BarsPageRequest extends BarsRequest with exclusive before: UTCSeconds and required countBack: number. Return BarsPage { bars: Bar[]; hasMore?: boolean; nextBefore?: UTCSeconds }. Omit hasMore when the provider cannot establish exhaustion; one empty date window is not enough evidence. The next cursor must move backward. Existing feeds need only getBars and continue to work.

Forward req.signal to fetch and honor noCache for authoritative repairs. HistoryRequestPool shares identical pending requests without cancelling other consumers when one chart changes symbol. The pool and DataLoadingController are in the base tier. See data loading for options, cache snapshots, deadlines, replay fences and widget integration.