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, meta?: LiveBarMeta) => void, opts?: BarSubscriptionOptions): () => void;
subscribeDepth?(req: BarsRequest, onDepth: (depth: MarketDepth) => void, opts?: { depthLevel?: number }): () => void;
// optional: instruments matching what the user typed, closest first
searchSymbols?(request: SymbolSearchRequest): Promise<SymbolMatch[]>;
}
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;
}
interface LiveBarMeta {
provisional?: boolean;
}DataFeed, BarsRequest, BarSubscriptionOptions, LiveBarMeta, MarketDepth,
SymbolSearchRequest and SymbolMatch 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.
Pass meta.provisional when a live builder opened a bucket after missing its
earlier ticks. DataLoadingController uses that flag to retain an authoritative
history open during repair. Depth is separately optional; the requested level
is broker-dependent.
Bar.oi carries an optional open-interest observation. Preserve zero, omit a
missing reading, and declare instrument support separately through
ChartDataContext.hasOpenInterest. See Open interest.
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);
}
}Symbol search
From 2.6.0 a feed can also answer symbol lookups. The widget uses
searchSymbols for its symbol box, typed entry, watchlist and study symbol
inputs whenever the host passes no symbolSearch, so a feed that searches needs
no host code for it. A host symbolSearch still wins.
interface SymbolSearchRequest {
query: string;
signal?: AbortSignal; // aborted once a newer query replaces this one
}
interface SymbolMatch {
symbol: string;
exchange?: string;
name?: string;
assetClass?: string; // shown as category filters
iconUrl?: string;
contractGroup?: { label?: string; contracts: readonly SymbolMatch[] };
}Both OpenAlgo feeds implement it through POST /api/v1/search, with the API key
in the body as history sends it. They keep the platform’s ranking, show at most
50 rows, file each row under Index, Equity, Futures or Options, and gather
the options of one underlying and expiry into one row that opens onto its
contracts, so a search for an index lists expiries rather than every strike.
withBarCache passes searchSymbols through when the wrapped feed has it.
import { OpenAlgoDataFeed } from 'openalgo-charts';
import { createWidget } from 'openalgo-charts/widget';
// Keep the key out of source control; a backend proxy is better still.
const feed = new OpenAlgoDataFeed({ baseUrl: 'http://127.0.0.1:5000', apiKey: 'YOUR_KEY' });
createWidget(container, { feed, symbol: 'RELIANCE', exchange: 'NSE' });
// The symbol box now searches OpenAlgo's instruments.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.
OrderFeed and TradeFeed
OrderFeed (from openalgo-charts/trade) is what the engine’s write path needs, and the
only one you 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 deprecated and goes in 3.0.0. Nothing in the library
takes or implements it, and the trading layer never calls one:
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 OrderFeed against your broker for the engine’s writes, and hand the orders and
positions you read back to the chart with chart.trading.syncState: that is what the on-chart
trading layer draws from. 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.