@christtrade/depth 0.12.25 → 0.12.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -95,7 +95,7 @@ export interface ChartEvents {
95
95
  'timeframe:change': {
96
96
  tf: Timeframe;
97
97
  };
98
- /** Runtime entitlement change (see `chart.allowTimeframes`). The UI re-reads
98
+ /** Runtime entitlement change. The UI re-reads
99
99
  * `features` so locks and gates reflect what is allowed now. */
100
100
  'features:change': void;
101
101
  'timeframe:add-failed': {
@@ -565,11 +565,66 @@ export interface ChartEvents {
565
565
  * ParamDef already carries min, max and step.
566
566
  */
567
567
  paramDefs: Record<string, unknown>;
568
+ /** Bars this run actually covered - not always what was asked for. `clipped` is false for a run over everything loaded. */
569
+ range: {
570
+ fromNs: bigint | null;
571
+ toNs: bigint | null;
572
+ bars: number;
573
+ totalBars: number;
574
+ clipped: boolean;
575
+ /** Span the loaded data covers, so a picker can bound itself. */
576
+ dataFromNs: bigint | null;
577
+ dataToNs: bigint | null;
578
+ };
568
579
  };
569
580
  'plugin:apply-params': {
570
581
  id: string;
571
582
  params: Record<string, unknown>;
572
583
  };
584
+ 'plugin:strategy-range': {
585
+ id: string;
586
+ range: {
587
+ fromNs?: bigint;
588
+ toNs?: bigint;
589
+ } | null;
590
+ /**
591
+ * Fetch the span instead of clipping to what the chart holds. Opt-in -
592
+ * clipping is free and covers almost every range, fetching walks the
593
+ * network a chunk at a time. Requires `fromNs`; no streaming backwards
594
+ * from an open-ended start.
595
+ */
596
+ fetch?: boolean;
597
+ };
598
+ 'plugin:strategy-run': {
599
+ id: string;
600
+ /** `null` clears any bound and runs over everything loaded; omitted keeps the stored bound. */
601
+ range?: {
602
+ fromNs?: bigint;
603
+ toNs?: bigint;
604
+ } | null;
605
+ /** As on 'plugin:strategy-range' - go get the span, don't clip. */
606
+ fetch?: boolean;
607
+ };
608
+ 'plugin:strategy-mode': {
609
+ id: string;
610
+ manual: boolean;
611
+ };
612
+ 'plugin:strategy-stale': {
613
+ id: string;
614
+ name: string;
615
+ /** Bars arrived since the last run, or 0. */
616
+ newBars: number;
617
+ /** Params edited since the last run. Empty when none. */
618
+ params: Record<string, unknown>;
619
+ };
620
+ 'plugin:strategy-progress': {
621
+ id: string;
622
+ name: string;
623
+ phase: 'fetching' | 'running' | 'analysing' | 'done' | 'failed';
624
+ done: number;
625
+ total: number;
626
+ error?: string;
627
+ };
573
628
  'plugin:strategy-sweep': {
574
629
  id: string;
575
630
  grid: Array<Record<string, unknown>>;
@@ -25,9 +25,11 @@ export type { ChartModelInit, ChartPluginRef, ChartPaneState } from './ChartMode
25
25
  export { DrawingStore } from './DrawingStore';
26
26
  export type { ChartPlugin, PluginType, Permission, PluginManifest, PluginContext, PluginDataSnapshot, DrawingPlugin, ChartTypePlugin, ChartTypeRenderContext, DataSourcePlugin, IndicatorPlugin, IndicatorRenderContext, } from '../interfaces/plugins';
27
27
  export { createScriptedPlugin } from './ScriptedPlugin';
28
- export { StrategyEngine, DEFAULT_STRATEGY_CONFIG } from './strategy-runtime';
28
+ export { StrategyEngine, DEFAULT_STRATEGY_CONFIG, reconcileIntrabar } from './strategy-runtime';
29
29
  export { axisValues, checkSweepBudget, expandGrid, splitIndex, MAX_SWEEP_COMBOS, MAX_SWEEP_BAR_ITERATIONS, } from './strategy-sweep';
30
30
  export type { SweepAxis, SweepSpec, SweepResult, SweepBudget } from './strategy-sweep';
31
+ export { clipRange, hasRange, emptyRangeReason } from './strategy-range';
32
+ export type { StrategyRange, ClippedRange } from './strategy-range';
31
33
  export { planWalkForward, walkForwardEfficiency, parameterStability, pickBest, } from './strategy-walkforward';
32
34
  export type { WalkForwardSpec, WalkForwardWindow, WalkForwardWindowResult, ParameterStability, } from './strategy-walkforward';
33
35
  export type { BrokerApi, StrategyEquityPoint, ExitReason, OrderOpts, Side, StrategyBar, StrategyConfig, StrategyOrder, StrategyPosition, StrategyResult, StrategyStats, StrategyTrade, } from './strategy-runtime';
@@ -2,8 +2,10 @@ import { STDLIB } from '../lib/indicator-stdlib';
2
2
  import { PluginType, DataLevel, Layout, ExitReason, SCRIPT_DSL } from './script-dsl';
3
3
  export { STDLIB, SCRIPT_DSL, PluginType, DataLevel, Layout, ExitReason };
4
4
  export type { OhlcvBar, DrawCommand } from '../lib/indicator-stdlib';
5
- export { StrategyEngine, DEFAULT_STRATEGY_CONFIG } from './strategy-runtime';
5
+ export { StrategyEngine, DEFAULT_STRATEGY_CONFIG, reconcileIntrabar } from './strategy-runtime';
6
6
  export { axisValues, checkSweepBudget, expandGrid, splitIndex } from './strategy-sweep';
7
+ export { clipRange, hasRange, emptyRangeReason } from './strategy-range';
8
+ export type { StrategyRange, ClippedRange } from './strategy-range';
7
9
  export type { SweepAxis, SweepSpec, SweepResult, SweepBudget } from './strategy-sweep';
8
10
  export { planWalkForward, walkForwardEfficiency, parameterStability, pickBest, } from './strategy-walkforward';
9
11
  export type { WalkForwardSpec, WalkForwardWindow, WalkForwardWindowResult, ParameterStability, } from './strategy-walkforward';
@@ -0,0 +1,41 @@
1
+ import type { StrategyBar } from './strategy-runtime';
2
+ /** Inclusive both ends, nanoseconds. An omitted end means "as far as the data goes". */
3
+ export interface StrategyRange {
4
+ fromNs?: bigint;
5
+ toNs?: bigint;
6
+ }
7
+ /** Where a range lands in a bar array, plus what it cost. */
8
+ export interface ClippedRange {
9
+ /** First bar in range. */
10
+ from: number;
11
+ /** One past the last bar in range, so `bars.slice(from, to)` is the run. */
12
+ to: number;
13
+ /** How many bars the run will see. */
14
+ count: number;
15
+ /** How many bars were available before clipping. */
16
+ total: number;
17
+ /** Timestamps of the first and last bar actually in range. */
18
+ firstTs: bigint | null;
19
+ lastTs: bigint | null;
20
+ /** Span the whole data set covers, ignoring the range - lets a picker bound itself to what exists. */
21
+ dataFromNs: bigint | null;
22
+ dataToNs: bigint | null;
23
+ }
24
+ /**
25
+ * Resolve a range against a sorted bar array. `toNs` includes the bar that opens
26
+ * exactly on it. Binary search, not a filter - this runs once per sweep
27
+ * combination, and a linear scan over a million bars twenty thousand times
28
+ * turns a sweep into a hang.
29
+ *
30
+ * No warmup before `from`: feeding a run bars it isn't allowed to trade is how
31
+ * an out-of-sample window quietly stops being out of sample.
32
+ */
33
+ export declare function clipRange(bars: readonly StrategyBar[], range?: StrategyRange): ClippedRange;
34
+ /** True when the range asks for something, rather than being absent or empty. */
35
+ export declare function hasRange(range?: StrategyRange): boolean;
36
+ /**
37
+ * Why a clipped run has nothing to run over. Spelled out rather than an empty
38
+ * result, since a strategy with no trades and one given no bars look identical
39
+ * in a results panel, and only one of those is the user's fault.
40
+ */
41
+ export declare function emptyRangeReason(clipped: ClippedRange, range?: StrategyRange): string;
@@ -28,13 +28,9 @@ export interface StrategyPosition {
28
28
  tp?: number;
29
29
  tag?: string;
30
30
  /**
31
- * Best and worst price reached while this position has been open.
32
- *
33
- * Tracked from the bar the position opened, over that bar's full range. A
34
- * market order fills at the open so the whole range is fair; a limit or stop
35
- * filling mid-bar makes the entry bar's contribution an upper bound. Every
36
- * later bar is exact. Worth knowing before reading MAE as gospel on
37
- * one-bar trades.
31
+ * Best/worst price while open, from the entry bar's full range. Exact for
32
+ * a market fill; an upper bound for a limit/stop filling mid-bar - don't
33
+ * read MAE as gospel on a one-bar trade.
38
34
  */
39
35
  highWatermark: number;
40
36
  lowWatermark: number;
@@ -52,10 +48,9 @@ export interface StrategyPosition {
52
48
  /** Fills that have gone into this position. Checked against `pyramiding`. */
53
49
  entries: number;
54
50
  /**
55
- * Commission already paid to open the quantity still held. Carried on the
56
- * position so the trade log can report a round-trip fee: equity is charged
57
- * at entry, but nothing is logged until the exit, and a trade whose pnl
58
- * omitted the entry side would not sum to the equity curve.
51
+ * Commission already paid to open the quantity still held. Carried so the
52
+ * exit's trade record can report the round-trip fee - otherwise pnl
53
+ * wouldn't sum to the equity curve.
59
54
  */
60
55
  entryFees: number;
61
56
  }
@@ -84,6 +79,12 @@ export interface StrategyTrade {
84
79
  durationNs: bigint;
85
80
  tag?: string;
86
81
  reason: ExitReason;
82
+ /**
83
+ * Stop and target both sat inside the resolving bar, so which came first
84
+ * was assumed (stop wins) rather than observed. Intrabar data shrinks how
85
+ * often this happens but never zeroes it.
86
+ */
87
+ ambiguousExit?: boolean;
87
88
  /** Worst the price went against this position before it closed. */
88
89
  maeAbs: number;
89
90
  /** Best the price went in favour of it. */
@@ -197,16 +198,26 @@ export interface StrategyStats {
197
198
  totalFees: number;
198
199
  totalCommission: number;
199
200
  /**
200
- * What slippage cost, in account currency.
201
- *
202
- * Not a separate deduction - slippage is already inside every fill price and
203
- * so already inside netPnl. This reports what that was worth, so a strategy
204
- * that only loses to slippage is distinguishable from one that is simply
205
- * wrong. Subtracting it again would double-count it.
201
+ * What slippage cost, in account currency - already inside netPnl via the
202
+ * fill prices, reported here rather than deducted again.
206
203
  */
207
204
  totalSlippage: number;
208
205
  /** What the run would have made with no commission at all. */
209
206
  grossPnlBeforeCosts: number;
207
+ /** Chart bars whose fills were resolved against finer intrabar data. */
208
+ intrabarBars: number;
209
+ /**
210
+ * Bars given intrabar data that didn't reconcile with the aggregate and
211
+ * fell back to it. Non-zero means the finer feed has holes - the run is
212
+ * still valid, just not the run the intrabar toggle implies.
213
+ */
214
+ intrabarFallbacks: number;
215
+ /**
216
+ * Exits where stop and target shared a resolving bar and the stop was
217
+ * assumed first. Compare against `totalTrades` for how much of the result
218
+ * rests on that assumption.
219
+ */
220
+ ambiguousExits: number;
210
221
  }
211
222
  export interface StrategyConfig {
212
223
  initialCapital: number;
@@ -220,19 +231,15 @@ export interface StrategyConfig {
220
231
  /** How many entries may stack in the same direction. */
221
232
  pyramiding: number;
222
233
  /**
223
- * Whether an opposite-side order flips an open position or merely closes it.
224
- *
225
- * True matches how most scripts read: sell() while long means "get out and go
226
- * short". False means sell() can only ever flatten, and opening the other way
227
- * takes a second, separate order.
234
+ * Whether an opposite-side order flips a position or just closes it. True
235
+ * matches how most scripts read: sell() while long means "get out and go
236
+ * short".
228
237
  */
229
238
  allowReverse: boolean;
230
239
  /**
231
- * Smallest tradable quantity increment. Order sizes floor to it.
232
- *
233
- * 1 for a listed future, where a third of a contract does not exist; spot
234
- * venues are the other case entirely, so this comes from the instrument
235
- * rather than being assumed.
240
+ * Smallest tradable quantity increment; order sizes floor to it. 1 for a
241
+ * listed future, fractional for spot - comes from the instrument, not
242
+ * assumed.
236
243
  */
237
244
  qtyStep: number;
238
245
  }
@@ -253,6 +260,18 @@ export interface StrategyBar {
253
260
  close: number;
254
261
  volume: number;
255
262
  }
263
+ /**
264
+ * Do these finer bars actually subdivide this one?
265
+ *
266
+ * A feed with a hole in it is worse than none at all - it'd resolve a stop
267
+ * against a range missing the second price actually traded through. Rejected
268
+ * whole rather than repaired, since fixing it would mean inventing the
269
+ * ordering inside the gap.
270
+ *
271
+ * @param tolerance Half a tick - two aggregation paths over the same trades
272
+ * agree to the tick, not the float.
273
+ */
274
+ export declare function reconcileIntrabar(bar: StrategyBar, sub: readonly StrategyBar[] | undefined, tolerance: number): boolean;
256
275
  export interface OrderOpts {
257
276
  /** Place a limit instead of a market order. */
258
277
  limit?: number;
@@ -293,10 +312,33 @@ export declare class StrategyEngine {
293
312
  private closeRequested;
294
313
  private barNs;
295
314
  private slippagePaid;
315
+ private usedIntrabar;
316
+ private intrabarBars;
317
+ private intrabarFallbacks;
318
+ private ambiguousExits;
319
+ private pendingEquity;
320
+ private equityCount;
321
+ private firstTs;
322
+ private lastTs;
323
+ private lastEquity;
324
+ private prevEquity;
325
+ private maxDdAbs;
326
+ private maxDdPct;
327
+ private ulcerSum;
328
+ private retN;
329
+ private retMean;
330
+ private retM2;
331
+ private downSum;
332
+ private downCount;
333
+ private curvePhase;
334
+ private curveStride;
335
+ private lastFolded;
296
336
  constructor(cfg?: Partial<StrategyConfig>);
297
337
  setBarNs(ns: bigint): void;
298
- beginBar(bar: StrategyBar, index: number): void;
338
+ beginBar(bar: StrategyBar, index: number, sub?: readonly StrategyBar[]): void;
299
339
  endBar(bar: StrategyBar): void;
340
+ private foldPending;
341
+ private keepInCurve;
300
342
  finish(lastBar: StrategyBar | undefined): void;
301
343
  get equity(): number;
302
344
  get result(): StrategyResult;
@@ -318,8 +360,6 @@ export declare class StrategyEngine {
318
360
  private runYears;
319
361
  private cagr;
320
362
  private sortino;
321
- private barReturns;
322
- private peakAt;
323
363
  private barsInPositionTotal;
324
364
  private sharpe;
325
365
  }
@@ -0,0 +1,40 @@
1
+ export type ChunkPlan = {
2
+ fromNs: bigint;
3
+ toNs: bigint;
4
+ };
5
+ /** `coveredTo`: how far the source actually got - adapters cap responses and report the cap here, not as an error. */
6
+ export type RangeFetchResult<B> = {
7
+ bars: B[];
8
+ coveredTo: bigint | null;
9
+ };
10
+ export type RangeFetch<B> = (opts: {
11
+ fromNs: bigint;
12
+ toNs: bigint;
13
+ barNs: bigint;
14
+ }) => Promise<RangeFetchResult<B>>;
15
+ export declare const DEFAULT_STREAM_DEPTH = 4;
16
+ export declare const DEFAULT_MAX_CONTINUATIONS = 16;
17
+ /**
18
+ * Chunk boundaries decided up front, not lazily - a request has to fire before
19
+ * its predecessor answers. `chunkBars` is bars in a chunk, not the width: a
20
+ * hundred-bar chunk spans ninety-nine periods, ends inclusive.
21
+ */
22
+ export declare function planChunks(fromNs: bigint, toNs: bigint, barNs: bigint, chunkBars: bigint, maxChunks: number): ChunkPlan[];
23
+ export type StreamOptions<B> = {
24
+ plan: readonly ChunkPlan[];
25
+ barNs: bigint;
26
+ fetch: RangeFetch<B>;
27
+ /** Pieces, not whole chunks - a two-request chunk arrives as two calls rather than paying to join them. */
28
+ deliver: (bars: B[], chunkIndex: number) => void;
29
+ /** False once superseded - checked after every await so a stale walk's late chunks don't reach the new run. */
30
+ isAlive?: () => boolean;
31
+ onProgress?: (delivered: number, total: number) => void;
32
+ depth?: number;
33
+ maxContinuations?: number;
34
+ };
35
+ /**
36
+ * Rejects with the first failure *in plan order*, not time order - otherwise a
37
+ * later failure could skip the bars an earlier, still-outstanding chunk owns,
38
+ * and a run that quietly drops a year still produces a plausible equity curve.
39
+ */
40
+ export declare function streamRangeChunks<B>(opts: StreamOptions<B>): Promise<number>;
@@ -163,6 +163,18 @@ export interface PluginContext {
163
163
  dataLevel: DataLevel;
164
164
  horizon: () => number;
165
165
  getData(): Readonly<PluginDataSnapshot>;
166
+ fetchRange(opts: {
167
+ /** Defaults to the focused symbol. */
168
+ symbol?: string;
169
+ fromNs: bigint;
170
+ toNs: bigint;
171
+ /** Bar period to fetch at. Defaults to the chart's current timeframe. */
172
+ barNs?: bigint;
173
+ }): Promise<{
174
+ bars: OhlcvBar[];
175
+ hasMore: boolean;
176
+ coveredTo: bigint | null;
177
+ }>;
166
178
  /**
167
179
  * The bar still being built at the playback horizon, or `null` before any
168
180
  * data has arrived.