desic-okx-agent 0.2.0 → 0.3.0
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.
- package/README.en.md +353 -0
- package/README.md +194 -190
- package/dist/account/private-websocket.js +4 -4
- package/dist/account/private-websocket.js.map +1 -1
- package/dist/account/service.d.ts +12 -1
- package/dist/account/service.js +18 -0
- package/dist/account/service.js.map +1 -1
- package/dist/bars/rate-limiter.d.ts +18 -0
- package/dist/bars/rate-limiter.js +84 -0
- package/dist/bars/rate-limiter.js.map +1 -0
- package/dist/bars/schema.d.ts +36 -0
- package/dist/bars/schema.js +134 -0
- package/dist/bars/schema.js.map +1 -0
- package/dist/bars/service.d.ts +60 -0
- package/dist/bars/service.js +120 -0
- package/dist/bars/service.js.map +1 -0
- package/dist/bars/store.d.ts +105 -0
- package/dist/bars/store.js +415 -0
- package/dist/bars/store.js.map +1 -0
- package/dist/bars/timeframe.d.ts +40 -0
- package/dist/bars/timeframe.js +146 -0
- package/dist/bars/timeframe.js.map +1 -0
- package/dist/bars/types.d.ts +68 -0
- package/dist/bars/types.js +13 -0
- package/dist/bars/types.js.map +1 -0
- package/dist/cli/data-render.d.ts +37 -0
- package/dist/cli/data-render.js +143 -0
- package/dist/cli/data-render.js.map +1 -0
- package/dist/cli/doctor.js +7 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +758 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/live-render.d.ts +24 -0
- package/dist/cli/live-render.js +85 -0
- package/dist/cli/live-render.js.map +1 -0
- package/dist/cli/range.d.ts +28 -0
- package/dist/cli/range.js +63 -0
- package/dist/cli/range.js.map +1 -0
- package/dist/cli/render.js +3 -0
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/strategy-render.d.ts +36 -0
- package/dist/cli/strategy-render.js +391 -0
- package/dist/cli/strategy-render.js.map +1 -0
- package/dist/cli/width.d.ts +18 -0
- package/dist/cli/width.js +71 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/config/loader.js +1 -1
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/core/okx-client.d.ts +9 -1
- package/dist/core/okx-client.js +14 -5
- package/dist/core/okx-client.js.map +1 -1
- package/dist/i18n/locale.d.ts +24 -0
- package/dist/i18n/locale.js +65 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/i18n/messages.d.ts +333 -0
- package/dist/i18n/messages.js +660 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/live/account-snapshot.d.ts +30 -0
- package/dist/live/account-snapshot.js +130 -0
- package/dist/live/account-snapshot.js.map +1 -0
- package/dist/live/cutoff-queue.d.ts +42 -0
- package/dist/live/cutoff-queue.js +69 -0
- package/dist/live/cutoff-queue.js.map +1 -0
- package/dist/live/execution-key.d.ts +23 -0
- package/dist/live/execution-key.js +31 -0
- package/dist/live/execution-key.js.map +1 -0
- package/dist/live/failures.d.ts +37 -0
- package/dist/live/failures.js +57 -0
- package/dist/live/failures.js.map +1 -0
- package/dist/live/gates.d.ts +65 -0
- package/dist/live/gates.js +136 -0
- package/dist/live/gates.js.map +1 -0
- package/dist/live/loop.d.ts +56 -0
- package/dist/live/loop.js +197 -0
- package/dist/live/loop.js.map +1 -0
- package/dist/live/preconditions.d.ts +48 -0
- package/dist/live/preconditions.js +69 -0
- package/dist/live/preconditions.js.map +1 -0
- package/dist/live/reconcile.d.ts +46 -0
- package/dist/live/reconcile.js +104 -0
- package/dist/live/reconcile.js.map +1 -0
- package/dist/live/runner.d.ts +57 -0
- package/dist/live/runner.js +160 -0
- package/dist/live/runner.js.map +1 -0
- package/dist/live/schema.d.ts +18 -0
- package/dist/live/schema.js +91 -0
- package/dist/live/schema.js.map +1 -0
- package/dist/live/service.d.ts +144 -0
- package/dist/live/service.js +303 -0
- package/dist/live/service.js.map +1 -0
- package/dist/live/session.d.ts +85 -0
- package/dist/live/session.js +234 -0
- package/dist/live/session.js.map +1 -0
- package/dist/live/sizing.d.ts +62 -0
- package/dist/live/sizing.js +79 -0
- package/dist/live/sizing.js.map +1 -0
- package/dist/live/store.d.ts +123 -0
- package/dist/live/store.js +350 -0
- package/dist/live/store.js.map +1 -0
- package/dist/live/types.d.ts +82 -0
- package/dist/live/types.js +2 -0
- package/dist/live/types.js.map +1 -0
- package/dist/market/websocket.d.ts +16 -1
- package/dist/market/websocket.js +60 -5
- package/dist/market/websocket.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +15 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/network/connectivity.d.ts +9 -1
- package/dist/network/connectivity.js +28 -1
- package/dist/network/connectivity.js.map +1 -1
- package/dist/report/chart-script.d.ts +12 -0
- package/dist/report/chart-script.js +146 -0
- package/dist/report/chart-script.js.map +1 -0
- package/dist/report/compare-html.d.ts +8 -0
- package/dist/report/compare-html.js +254 -0
- package/dist/report/compare-html.js.map +1 -0
- package/dist/report/compare-script.d.ts +12 -0
- package/dist/report/compare-script.js +109 -0
- package/dist/report/compare-script.js.map +1 -0
- package/dist/report/compare.d.ts +61 -0
- package/dist/report/compare.js +205 -0
- package/dist/report/compare.js.map +1 -0
- package/dist/report/fetch.d.ts +20 -0
- package/dist/report/fetch.js +56 -0
- package/dist/report/fetch.js.map +1 -0
- package/dist/report/html.d.ts +54 -0
- package/dist/report/html.js +641 -0
- package/dist/report/html.js.map +1 -0
- package/dist/report/open.d.ts +42 -0
- package/dist/report/open.js +114 -0
- package/dist/report/open.js.map +1 -0
- package/dist/runtime/server.d.ts +16 -1
- package/dist/runtime/server.js +112 -9
- package/dist/runtime/server.js.map +1 -1
- package/dist/setup/installer.d.ts +1 -0
- package/dist/setup/installer.js +8 -0
- package/dist/setup/installer.js.map +1 -1
- package/dist/setup/wizard.d.ts +3 -0
- package/dist/setup/wizard.js +131 -6
- package/dist/setup/wizard.js.map +1 -1
- package/dist/strategy/constants.d.ts +23 -0
- package/dist/strategy/constants.js +24 -0
- package/dist/strategy/constants.js.map +1 -0
- package/dist/strategy/environment.d.ts +52 -0
- package/dist/strategy/environment.js +187 -0
- package/dist/strategy/environment.js.map +1 -0
- package/dist/strategy/instrument.d.ts +29 -0
- package/dist/strategy/instrument.js +39 -0
- package/dist/strategy/instrument.js.map +1 -0
- package/dist/strategy/optimize.d.ts +73 -0
- package/dist/strategy/optimize.js +113 -0
- package/dist/strategy/optimize.js.map +1 -0
- package/dist/strategy/parameter-space.d.ts +59 -0
- package/dist/strategy/parameter-space.js +221 -0
- package/dist/strategy/parameter-space.js.map +1 -0
- package/dist/strategy/python-bridge.d.ts +24 -0
- package/dist/strategy/python-bridge.js +114 -0
- package/dist/strategy/python-bridge.js.map +1 -0
- package/dist/strategy/schema.d.ts +9 -0
- package/dist/strategy/schema.js +91 -0
- package/dist/strategy/schema.js.map +1 -0
- package/dist/strategy/service.d.ts +138 -0
- package/dist/strategy/service.js +745 -0
- package/dist/strategy/service.js.map +1 -0
- package/dist/strategy/settings.d.ts +162 -0
- package/dist/strategy/settings.js +243 -0
- package/dist/strategy/settings.js.map +1 -0
- package/dist/strategy/store.d.ts +96 -0
- package/dist/strategy/store.js +367 -0
- package/dist/strategy/store.js.map +1 -0
- package/dist/strategy/templates.d.ts +11 -0
- package/dist/strategy/templates.js +134 -0
- package/dist/strategy/templates.js.map +1 -0
- package/dist/strategy/types.d.ts +111 -0
- package/dist/strategy/types.js +2 -0
- package/dist/strategy/types.js.map +1 -0
- package/dist/tools/catalog.d.ts +16 -0
- package/dist/tools/catalog.js +118 -17
- package/dist/tools/catalog.js.map +1 -1
- package/dist/trade/service.d.ts +12 -0
- package/dist/trade/service.js +24 -7
- package/dist/trade/service.js.map +1 -1
- package/dist/tui/app.d.ts +23 -0
- package/dist/tui/app.js +322 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/commands.d.ts +70 -0
- package/dist/tui/commands.js +313 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/entries.d.ts +17 -0
- package/dist/tui/entries.js +24 -0
- package/dist/tui/entries.js.map +1 -0
- package/dist/tui/execute.d.ts +26 -0
- package/dist/tui/execute.js +664 -0
- package/dist/tui/execute.js.map +1 -0
- package/dist/tui/history.d.ts +17 -0
- package/dist/tui/history.js +48 -0
- package/dist/tui/history.js.map +1 -0
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.js +48 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/line-editor.d.ts +44 -0
- package/dist/tui/line-editor.js +98 -0
- package/dist/tui/line-editor.js.map +1 -0
- package/dist/tui/progress.d.ts +23 -0
- package/dist/tui/progress.js +46 -0
- package/dist/tui/progress.js.map +1 -0
- package/dist/tui/settings-editor.d.ts +18 -0
- package/dist/tui/settings-editor.js +115 -0
- package/dist/tui/settings-editor.js.map +1 -0
- package/docs/live-trading.md +455 -0
- package/docs/strategy-research.md +597 -0
- package/package.json +11 -1
- package/python/desic_strategy/__init__.py +34 -0
- package/python/desic_strategy/actions.py +158 -0
- package/python/desic_strategy/context.py +164 -0
- package/python/desic_strategy/engine.py +614 -0
- package/python/desic_strategy/indicators.py +159 -0
- package/python/desic_strategy/live.py +253 -0
- package/python/desic_strategy/policy.py +193 -0
- package/python/desic_strategy/portfolio.py +152 -0
- package/python/desic_strategy/report.py +319 -0
- package/python/desic_strategy/runner.py +574 -0
- package/python/desic_strategy/timeframe.py +150 -0
- package/python/main.py +18 -0
- package/skills/okx-live-trading/SKILL.md +117 -0
- package/skills/okx-live-trading/agents/openai.yaml +9 -0
- package/skills/okx-live-trading/references/lifecycle.md +128 -0
- package/skills/okx-strategy-research/SKILL.md +113 -0
- package/skills/okx-strategy-research/agents/openai.yaml +9 -0
- package/skills/okx-strategy-research/references/execution-semantics.md +107 -0
- package/skills/okx-strategy-research/references/field-traps.md +142 -0
- package/skills/okx-strategy-research/references/python-api.md +121 -0
- package/skills/okx-strategy-research/references/tools-and-data.md +192 -0
- package/skills/okx-trading/SKILL.md +11 -10
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { RuntimeError } from "../core/errors.js";
|
|
2
|
+
import { PublicRestLimiter } from "./rate-limiter.js";
|
|
3
|
+
import { BAR_INDEX_SQL, BAR_MIGRATION_STEPS, BAR_SCHEMA_SQL } from "./schema.js";
|
|
4
|
+
import { environmentHeaders } from "../network/connectivity.js";
|
|
5
|
+
import { ONE_MINUTE_MS, alignMinuteOpen, expectedBarCount } from "./types.js";
|
|
6
|
+
/** OKX returns at most 300 rows per candle page. */
|
|
7
|
+
const PAGE_LIMIT = 300;
|
|
8
|
+
/**
|
|
9
|
+
* Extra pages allowed beyond the number a gap actually needs.
|
|
10
|
+
*
|
|
11
|
+
* The ceiling is derived from the gap's own width rather than fixed: a flat 400
|
|
12
|
+
* was fewer than the 480 pages a 100-day range requires, so a long download
|
|
13
|
+
* stopped short and reported the rest as unavailable. The budget still bounds
|
|
14
|
+
* the loop if OKX keeps returning rows that never reach the lower bound.
|
|
15
|
+
*/
|
|
16
|
+
const GAP_PAGE_SLACK = 32;
|
|
17
|
+
/** Hard stop, so a pathological response cannot page forever. */
|
|
18
|
+
const MAX_PAGES_PER_GAP = 8_000;
|
|
19
|
+
/**
|
|
20
|
+
* Persists confirmed one-minute bars and repairs an exact closed window.
|
|
21
|
+
*
|
|
22
|
+
* `syncWindow` deliberately targets an exact inclusive range instead of a
|
|
23
|
+
* rolling retention period: a reproducible backtest needs the bars belonging
|
|
24
|
+
* to its own snapshot repaired, and nothing else.
|
|
25
|
+
*/
|
|
26
|
+
export class BarStore {
|
|
27
|
+
db;
|
|
28
|
+
client;
|
|
29
|
+
limiter;
|
|
30
|
+
now;
|
|
31
|
+
confirmedListeners = new Set();
|
|
32
|
+
environment;
|
|
33
|
+
constructor(db, options) {
|
|
34
|
+
this.db = db;
|
|
35
|
+
this.client = options.client;
|
|
36
|
+
this.limiter = options.limiter ?? new PublicRestLimiter();
|
|
37
|
+
this.now = options.now ?? Date.now;
|
|
38
|
+
this.environment = options.environment ?? "live";
|
|
39
|
+
this.db.exec(BAR_SCHEMA_SQL);
|
|
40
|
+
this.migrate();
|
|
41
|
+
// Indexes naming the environment column go last: on a pre-environment database the
|
|
42
|
+
// column only exists once the migration above has run.
|
|
43
|
+
this.db.exec(BAR_INDEX_SQL);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Brings a pre-environment database up to the current schema.
|
|
47
|
+
*
|
|
48
|
+
* `bars_1m` and `bar_coverage` are keyed on the environment and SQLite cannot add a
|
|
49
|
+
* column to an existing primary key, so both are rebuilt. Existing rows become
|
|
50
|
+
* `live`, which is where they came from — demo is opt-in and could not have been
|
|
51
|
+
* written before this column existed.
|
|
52
|
+
*/
|
|
53
|
+
migrate() {
|
|
54
|
+
for (const step of BAR_MIGRATION_STEPS) {
|
|
55
|
+
const columns = new Set(this.db.prepare(`PRAGMA table_info(${step.table})`).all().map((row) => row.name));
|
|
56
|
+
if (columns.has("environment"))
|
|
57
|
+
continue;
|
|
58
|
+
// One transaction per table: a failure rolls back rather than leaving a
|
|
59
|
+
// half-migrated store that later reads would misinterpret.
|
|
60
|
+
this.db.transaction(() => this.db.exec(step.sql))();
|
|
61
|
+
}
|
|
62
|
+
const syncColumns = new Set(this.db.prepare("PRAGMA table_info(bar_sync_runs)").all().map((row) => row.name));
|
|
63
|
+
// An audit table with an autoincrement key takes a plain column addition.
|
|
64
|
+
if (!syncColumns.has("environment")) {
|
|
65
|
+
this.db.exec("ALTER TABLE bar_sync_runs ADD COLUMN environment TEXT NOT NULL DEFAULT 'live'");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Subscribes to bars arriving from the live feed.
|
|
70
|
+
*
|
|
71
|
+
* Only `appendConfirmed` notifies, which is the websocket's path. Backfill uses
|
|
72
|
+
* a bulk insert and stays silent on purpose: repairing history must not look
|
|
73
|
+
* like a minute that just closed, or downloading three months of data would
|
|
74
|
+
* emit three months of trading decisions.
|
|
75
|
+
*
|
|
76
|
+
* Returns a function that removes the listener.
|
|
77
|
+
*/
|
|
78
|
+
onConfirmed(listener) {
|
|
79
|
+
this.confirmedListeners.add(listener);
|
|
80
|
+
return () => this.confirmedListeners.delete(listener);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Records one confirmed bar arriving from the live websocket feed.
|
|
84
|
+
*
|
|
85
|
+
* This writes synchronously. A deferred write would let a live evaluation
|
|
86
|
+
* read a window that is missing the minute that just triggered it.
|
|
87
|
+
*/
|
|
88
|
+
appendConfirmed(instId, bar, source = "ws") {
|
|
89
|
+
this.db
|
|
90
|
+
.prepare(`INSERT INTO bars_1m(environment,inst_id,open_time,close_time,open,high,low,close,volume,source,updated_at)
|
|
91
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
92
|
+
ON CONFLICT(environment,inst_id,open_time) DO UPDATE SET
|
|
93
|
+
close_time=excluded.close_time, open=excluded.open, high=excluded.high,
|
|
94
|
+
low=excluded.low, close=excluded.close, volume=excluded.volume,
|
|
95
|
+
source=excluded.source, updated_at=excluded.updated_at`)
|
|
96
|
+
.run(this.environment, instId, bar.openTimeMs, bar.closeTimeMs, bar.open, bar.high, bar.low, bar.close, bar.volume, source, this.now());
|
|
97
|
+
this.refreshCoverage(instId);
|
|
98
|
+
// Notified only after the row and its coverage are committed, so a listener
|
|
99
|
+
// that immediately reads the window sees the minute that triggered it.
|
|
100
|
+
this.notifyConfirmed(instId, bar);
|
|
101
|
+
}
|
|
102
|
+
notifyConfirmed(instId, bar) {
|
|
103
|
+
for (const listener of this.confirmedListeners) {
|
|
104
|
+
try {
|
|
105
|
+
listener(instId, bar);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// A throwing listener must not abort the write path: the bar is already
|
|
109
|
+
// stored, and losing the websocket feed over one subscriber's failure
|
|
110
|
+
// would stop every other instrument too.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Every instrument with locally stored history, newest coverage first. */
|
|
115
|
+
coverageAll() {
|
|
116
|
+
const rows = this.db
|
|
117
|
+
.prepare("SELECT inst_id FROM bar_coverage WHERE environment=? ORDER BY inst_id ASC")
|
|
118
|
+
.all(this.environment);
|
|
119
|
+
return rows
|
|
120
|
+
.map((row) => this.coverage(row.inst_id))
|
|
121
|
+
.filter((row) => row !== undefined);
|
|
122
|
+
}
|
|
123
|
+
coverage(instId) {
|
|
124
|
+
const row = this.db
|
|
125
|
+
.prepare("SELECT inst_id,oldest_open,newest_open,bar_count,exhausted_before,updated_at FROM bar_coverage WHERE environment=? AND inst_id=?")
|
|
126
|
+
.get(this.environment, instId);
|
|
127
|
+
if (!row)
|
|
128
|
+
return undefined;
|
|
129
|
+
const expected = expectedBarCount(row.oldest_open, row.newest_open);
|
|
130
|
+
return {
|
|
131
|
+
instId: row.inst_id,
|
|
132
|
+
oldestOpen: row.oldest_open,
|
|
133
|
+
newestOpen: row.newest_open,
|
|
134
|
+
barCount: row.bar_count,
|
|
135
|
+
exhaustedBefore: row.exhausted_before,
|
|
136
|
+
updatedAt: row.updated_at,
|
|
137
|
+
missingCount: Math.max(0, expected - row.bar_count)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Contiguous ranges of missing open times inside an inclusive window, each
|
|
142
|
+
* returned as `[firstMissingOpen, lastMissingOpen]`.
|
|
143
|
+
*/
|
|
144
|
+
gaps(instId, startOpen, endOpen) {
|
|
145
|
+
const present = this.presentOpenTimes(instId, startOpen, endOpen);
|
|
146
|
+
const gaps = [];
|
|
147
|
+
let cursor = startOpen;
|
|
148
|
+
for (const open of present) {
|
|
149
|
+
if (open > cursor)
|
|
150
|
+
gaps.push([cursor, open - ONE_MINUTE_MS]);
|
|
151
|
+
cursor = open + ONE_MINUTE_MS;
|
|
152
|
+
}
|
|
153
|
+
if (cursor <= endOpen)
|
|
154
|
+
gaps.push([cursor, endOpen]);
|
|
155
|
+
return gaps;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Loads an exact inclusive window and verifies it is contiguous.
|
|
159
|
+
*
|
|
160
|
+
* This fails closed. A backtest that silently skipped an absent minute would
|
|
161
|
+
* report performance for a timeline that never happened.
|
|
162
|
+
*/
|
|
163
|
+
loadWindow(instId, startOpen, endOpen) {
|
|
164
|
+
if (endOpen < startOpen) {
|
|
165
|
+
throw new RuntimeError("VALIDATION", "Bar window must end at or after it starts");
|
|
166
|
+
}
|
|
167
|
+
const rows = this.db
|
|
168
|
+
.prepare(`SELECT open_time,close_time,open,high,low,close,volume FROM bars_1m
|
|
169
|
+
WHERE environment=? AND inst_id=? AND open_time>=? AND open_time<=? ORDER BY open_time ASC`)
|
|
170
|
+
.all(this.environment, instId, startOpen, endOpen);
|
|
171
|
+
const expected = expectedBarCount(startOpen, endOpen);
|
|
172
|
+
if (rows.length !== expected) {
|
|
173
|
+
const missing = this.gaps(instId, startOpen, endOpen);
|
|
174
|
+
throw new RuntimeError("STALE_DATA", `Local one-minute history does not fully cover ${instId}: ${expected - rows.length} of ${expected} bars are missing`, true, { instId, startOpen, endOpen, expected, present: rows.length, gaps: missing.slice(0, 20) });
|
|
175
|
+
}
|
|
176
|
+
return rows.map((row, index) => {
|
|
177
|
+
const openTimeMs = startOpen + index * ONE_MINUTE_MS;
|
|
178
|
+
if (row.open_time !== openTimeMs) {
|
|
179
|
+
throw new RuntimeError("STALE_DATA", `Local one-minute history for ${instId} is not contiguous`, true, {
|
|
180
|
+
expectedOpenTimeMs: openTimeMs,
|
|
181
|
+
storedOpenTimeMs: row.open_time
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
openTimeMs: row.open_time,
|
|
186
|
+
closeTimeMs: row.close_time,
|
|
187
|
+
open: row.open,
|
|
188
|
+
high: row.high,
|
|
189
|
+
low: row.low,
|
|
190
|
+
close: row.close,
|
|
191
|
+
volume: row.volume
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Repairs an exact inclusive one-minute window, fetching only the bars that
|
|
197
|
+
* are actually absent.
|
|
198
|
+
*/
|
|
199
|
+
async syncWindow(instId, startOpenInput, endOpenInput, onProgress) {
|
|
200
|
+
const startOpen = alignMinuteOpen(startOpenInput);
|
|
201
|
+
const endOpen = alignMinuteOpen(endOpenInput);
|
|
202
|
+
const startedAt = this.now();
|
|
203
|
+
if (endOpen < startOpen) {
|
|
204
|
+
throw new RuntimeError("VALIDATION", "Synchronization window must end at or after it starts");
|
|
205
|
+
}
|
|
206
|
+
const expected = expectedBarCount(startOpen, endOpen);
|
|
207
|
+
const existing = this.countInRange(instId, startOpen, endOpen);
|
|
208
|
+
const runId = this.beginSyncRun(instId, startOpen, endOpen, expected, existing, startedAt);
|
|
209
|
+
let fetched = 0;
|
|
210
|
+
let inserted = 0;
|
|
211
|
+
let exhaustedBefore = this.coverage(instId)?.exhaustedBefore ?? null;
|
|
212
|
+
const report = () => onProgress?.({ instId, expected, present: existing + inserted, fetched, inserted });
|
|
213
|
+
report();
|
|
214
|
+
try {
|
|
215
|
+
// Repair newest-first: `history-candles` pages backwards from `after`, so
|
|
216
|
+
// walking gaps in reverse keeps every page useful for the gap it targets.
|
|
217
|
+
for (const [gapStart, gapEnd] of this.gaps(instId, startOpen, endOpen).reverse()) {
|
|
218
|
+
if (exhaustedBefore !== null && gapEnd < exhaustedBefore)
|
|
219
|
+
continue;
|
|
220
|
+
const outcome = await this.fillGap(instId, gapStart, gapEnd, (pageFetched, pageInserted) => {
|
|
221
|
+
fetched += pageFetched;
|
|
222
|
+
inserted += pageInserted;
|
|
223
|
+
report();
|
|
224
|
+
});
|
|
225
|
+
if (outcome.exhaustedBefore !== null) {
|
|
226
|
+
exhaustedBefore =
|
|
227
|
+
exhaustedBefore === null ? outcome.exhaustedBefore : Math.min(exhaustedBefore, outcome.exhaustedBefore);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const message = error instanceof Error ? error.message : "Bar synchronization failed";
|
|
233
|
+
this.finishSyncRun(runId, "failed", fetched, inserted, message, this.now());
|
|
234
|
+
this.refreshCoverage(instId, exhaustedBefore);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
this.refreshCoverage(instId, exhaustedBefore);
|
|
238
|
+
const finalPresent = this.countInRange(instId, startOpen, endOpen);
|
|
239
|
+
const missing = Math.max(0, expected - finalPresent);
|
|
240
|
+
const status = missing === 0 ? "completed" : "incomplete";
|
|
241
|
+
const message = missing === 0
|
|
242
|
+
? `Synchronized ${expected} one-minute bars`
|
|
243
|
+
: `${missing} of ${expected} one-minute bars remain unavailable`;
|
|
244
|
+
const finishedAt = this.now();
|
|
245
|
+
this.finishSyncRun(runId, status, fetched, inserted, message, finishedAt);
|
|
246
|
+
return {
|
|
247
|
+
instId,
|
|
248
|
+
status,
|
|
249
|
+
startOpen,
|
|
250
|
+
endOpen,
|
|
251
|
+
expected,
|
|
252
|
+
existing,
|
|
253
|
+
missing,
|
|
254
|
+
fetched,
|
|
255
|
+
inserted,
|
|
256
|
+
exhaustedBefore,
|
|
257
|
+
startedAt,
|
|
258
|
+
finishedAt,
|
|
259
|
+
message
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Backfills one contiguous gap by paging `history-candles` backwards from
|
|
264
|
+
* just past `gapEnd` until the requested lower bound is reached.
|
|
265
|
+
*/
|
|
266
|
+
async fillGap(instId, gapStart, gapEnd, onPage) {
|
|
267
|
+
let after = gapEnd + ONE_MINUTE_MS;
|
|
268
|
+
// Size the budget to the gap: a fixed ceiling silently truncated any range
|
|
269
|
+
// wider than it, which is how a 100-day download lost its oldest month.
|
|
270
|
+
const needed = Math.ceil(expectedBarCount(gapStart, gapEnd) / PAGE_LIMIT);
|
|
271
|
+
const budget = Math.min(MAX_PAGES_PER_GAP, needed + GAP_PAGE_SLACK);
|
|
272
|
+
for (let page = 0; page < budget; page += 1) {
|
|
273
|
+
const rows = await this.fetchHistoryPage(instId, after);
|
|
274
|
+
if (rows.length === 0) {
|
|
275
|
+
// OKX has no data before `after`; record it so later runs skip this
|
|
276
|
+
// range instead of re-requesting it every time.
|
|
277
|
+
return { exhaustedBefore: after };
|
|
278
|
+
}
|
|
279
|
+
const bars = rows
|
|
280
|
+
.map((row) => normalizeCandle(row))
|
|
281
|
+
.filter((bar) => bar !== undefined);
|
|
282
|
+
if (bars.length === 0)
|
|
283
|
+
return { exhaustedBefore: after };
|
|
284
|
+
let oldestOpen = after;
|
|
285
|
+
const inRange = [];
|
|
286
|
+
for (const bar of bars) {
|
|
287
|
+
oldestOpen = Math.min(oldestOpen, bar.openTimeMs);
|
|
288
|
+
if (bar.openTimeMs >= gapStart && bar.openTimeMs <= gapEnd)
|
|
289
|
+
inRange.push(bar);
|
|
290
|
+
}
|
|
291
|
+
const insertedCount = this.insertBars(instId, inRange, "history");
|
|
292
|
+
onPage(bars.length, insertedCount);
|
|
293
|
+
if (oldestOpen <= gapStart)
|
|
294
|
+
return { exhaustedBefore: null };
|
|
295
|
+
if (oldestOpen >= after) {
|
|
296
|
+
// No backward progress: stop rather than request the same page forever.
|
|
297
|
+
return { exhaustedBefore: null };
|
|
298
|
+
}
|
|
299
|
+
after = oldestOpen;
|
|
300
|
+
}
|
|
301
|
+
return { exhaustedBefore: null };
|
|
302
|
+
}
|
|
303
|
+
async fetchHistoryPage(instId, after) {
|
|
304
|
+
const path = "/api/v5/market/history-candles";
|
|
305
|
+
return this.limiter.run(path, () =>
|
|
306
|
+
// Public market data differs per environment, so the header matters here as much
|
|
307
|
+
// as on a private call: without it a demo store would backfill live bars and
|
|
308
|
+
// hold two incompatible sources under one snapshot id.
|
|
309
|
+
this.client.publicGet(path, { instId, bar: "1m", after, limit: PAGE_LIMIT }, environmentHeaders(this.environment)));
|
|
310
|
+
}
|
|
311
|
+
/** Inserts bars in one transaction and returns how many rows were new. */
|
|
312
|
+
insertBars(instId, bars, source) {
|
|
313
|
+
if (bars.length === 0)
|
|
314
|
+
return 0;
|
|
315
|
+
const statement = this.db.prepare(`INSERT INTO bars_1m(environment,inst_id,open_time,close_time,open,high,low,close,volume,source,updated_at)
|
|
316
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
317
|
+
ON CONFLICT(environment,inst_id,open_time) DO NOTHING`);
|
|
318
|
+
const updatedAt = this.now();
|
|
319
|
+
const transaction = this.db.transaction((items) => {
|
|
320
|
+
let count = 0;
|
|
321
|
+
for (const bar of items) {
|
|
322
|
+
const result = statement.run(this.environment, instId, bar.openTimeMs, bar.closeTimeMs, bar.open, bar.high, bar.low, bar.close, bar.volume, source, updatedAt);
|
|
323
|
+
count += result.changes;
|
|
324
|
+
}
|
|
325
|
+
return count;
|
|
326
|
+
});
|
|
327
|
+
return transaction(bars);
|
|
328
|
+
}
|
|
329
|
+
presentOpenTimes(instId, startOpen, endOpen) {
|
|
330
|
+
const rows = this.db
|
|
331
|
+
.prepare("SELECT open_time FROM bars_1m WHERE environment=? AND inst_id=? AND open_time>=? AND open_time<=? ORDER BY open_time ASC")
|
|
332
|
+
.all(this.environment, instId, startOpen, endOpen);
|
|
333
|
+
return rows.map((row) => row.open_time);
|
|
334
|
+
}
|
|
335
|
+
countInRange(instId, startOpen, endOpen) {
|
|
336
|
+
const row = this.db
|
|
337
|
+
.prepare("SELECT COUNT(*) AS count FROM bars_1m WHERE environment=? AND inst_id=? AND open_time>=? AND open_time<=?")
|
|
338
|
+
.get(this.environment, instId, startOpen, endOpen);
|
|
339
|
+
return Number(row.count);
|
|
340
|
+
}
|
|
341
|
+
/** Recomputes the derived coverage row from the stored bars. */
|
|
342
|
+
refreshCoverage(instId, exhaustedBefore) {
|
|
343
|
+
const row = this.db
|
|
344
|
+
.prepare("SELECT MIN(open_time) AS oldest, MAX(open_time) AS newest, COUNT(*) AS count FROM bars_1m WHERE environment=? AND inst_id=?")
|
|
345
|
+
.get(this.environment, instId);
|
|
346
|
+
if (row.oldest === null || row.newest === null || row.count === 0) {
|
|
347
|
+
this.db.prepare("DELETE FROM bar_coverage WHERE environment=? AND inst_id=?").run(this.environment, instId);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const previous = exhaustedBefore === undefined ? this.coverage(instId)?.exhaustedBefore ?? null : exhaustedBefore;
|
|
351
|
+
this.db
|
|
352
|
+
.prepare(`INSERT INTO bar_coverage(environment,inst_id,oldest_open,newest_open,bar_count,exhausted_before,updated_at)
|
|
353
|
+
VALUES(?,?,?,?,?,?,?)
|
|
354
|
+
ON CONFLICT(environment,inst_id) DO UPDATE SET
|
|
355
|
+
oldest_open=excluded.oldest_open, newest_open=excluded.newest_open,
|
|
356
|
+
bar_count=excluded.bar_count, exhausted_before=excluded.exhausted_before,
|
|
357
|
+
updated_at=excluded.updated_at`)
|
|
358
|
+
.run(this.environment, instId, row.oldest, row.newest, Number(row.count), previous, this.now());
|
|
359
|
+
}
|
|
360
|
+
beginSyncRun(instId, startOpen, endOpen, expected, existing, startedAt) {
|
|
361
|
+
const result = this.db
|
|
362
|
+
.prepare(`INSERT INTO bar_sync_runs(environment,inst_id,status,start_open,end_open,expected,existing,missing,fetched,inserted,started_at,message)
|
|
363
|
+
VALUES(?,?,'running',?,?,?,?,?,0,0,?,'')`)
|
|
364
|
+
.run(this.environment, instId, startOpen, endOpen, expected, existing, Math.max(0, expected - existing), startedAt);
|
|
365
|
+
return Number(result.lastInsertRowid);
|
|
366
|
+
}
|
|
367
|
+
finishSyncRun(id, status, fetched, inserted, message, finishedAt) {
|
|
368
|
+
this.db
|
|
369
|
+
.prepare("UPDATE bar_sync_runs SET status=?,fetched=?,inserted=?,message=?,finished_at=? WHERE id=?")
|
|
370
|
+
.run(status, fetched, inserted, message, finishedAt, id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* OKX candle row: `[ts, o, h, l, c, vol, volCcy, volCcyQuote, confirm]`.
|
|
375
|
+
* Unconfirmed rows are rejected — only closed bars belong in storage.
|
|
376
|
+
*/
|
|
377
|
+
export function normalizeCandle(row) {
|
|
378
|
+
if (row.length < 6)
|
|
379
|
+
return undefined;
|
|
380
|
+
const openTimeMs = Number(row[0]);
|
|
381
|
+
if (!Number.isFinite(openTimeMs) || openTimeMs < 0)
|
|
382
|
+
return undefined;
|
|
383
|
+
if (row.length >= 9 && row[8] !== "1")
|
|
384
|
+
return undefined;
|
|
385
|
+
const [, open, high, low, close, volume] = row;
|
|
386
|
+
if (open === undefined ||
|
|
387
|
+
high === undefined ||
|
|
388
|
+
low === undefined ||
|
|
389
|
+
close === undefined ||
|
|
390
|
+
volume === undefined) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
for (const value of [open, high, low, close]) {
|
|
394
|
+
const parsed = Number(value);
|
|
395
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const parsedVolume = Number(volume);
|
|
399
|
+
if (!Number.isFinite(parsedVolume) || parsedVolume < 0)
|
|
400
|
+
return undefined;
|
|
401
|
+
if (Number(high) < Math.max(Number(open), Number(close)))
|
|
402
|
+
return undefined;
|
|
403
|
+
if (Number(low) > Math.min(Number(open), Number(close)))
|
|
404
|
+
return undefined;
|
|
405
|
+
return {
|
|
406
|
+
openTimeMs: alignMinuteOpen(openTimeMs),
|
|
407
|
+
closeTimeMs: alignMinuteOpen(openTimeMs) + ONE_MINUTE_MS,
|
|
408
|
+
open,
|
|
409
|
+
high,
|
|
410
|
+
low,
|
|
411
|
+
close,
|
|
412
|
+
volume
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/bars/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type AggregateBar, type Bar } from "./types.js";
|
|
2
|
+
/** Timeframes derived from the confirmed one-minute source. */
|
|
3
|
+
export declare const SUPPORTED_INTERVALS: readonly ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D"];
|
|
4
|
+
export type SupportedInterval = (typeof SUPPORTED_INTERVALS)[number];
|
|
5
|
+
export declare function isSupportedInterval(value: string): value is SupportedInterval;
|
|
6
|
+
export declare function intervalMs(interval: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* Incrementally aggregates a contiguous confirmed one-minute feed into one
|
|
9
|
+
* higher timeframe.
|
|
10
|
+
*
|
|
11
|
+
* Two rules make the output safe to hand to a strategy:
|
|
12
|
+
*
|
|
13
|
+
* 1. When the supplied history starts mid-bucket, that leading partial bucket
|
|
14
|
+
* is discarded rather than presented as a completed candle — publishing it
|
|
15
|
+
* would manufacture a bar that never existed in the local snapshot.
|
|
16
|
+
* 2. Only the final item may be unconfirmed. Its OHLCV contains solely the
|
|
17
|
+
* minutes already known, never a future minute, so a developing-value rule
|
|
18
|
+
* is expressible without leaking look-ahead data.
|
|
19
|
+
*/
|
|
20
|
+
export declare class TimeframeAggregator {
|
|
21
|
+
readonly interval: SupportedInterval;
|
|
22
|
+
private readonly maximumBars;
|
|
23
|
+
private readonly completed;
|
|
24
|
+
private current?;
|
|
25
|
+
private started;
|
|
26
|
+
private lastInputOpen?;
|
|
27
|
+
constructor(interval: SupportedInterval, maximumBars?: number);
|
|
28
|
+
get intervalMs(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Incorporates one confirmed one-minute bar. Duplicate, out-of-order, or
|
|
31
|
+
* gapped input is rejected: silently accepting it would double-count volume
|
|
32
|
+
* or fabricate a bucket from discontiguous minutes.
|
|
33
|
+
*/
|
|
34
|
+
push(bar: Bar): void;
|
|
35
|
+
/** Time-ascending view; the last item may be an active, unconfirmed bucket. */
|
|
36
|
+
snapshot(): AggregateBar[];
|
|
37
|
+
latest(): AggregateBar | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** Convenience: aggregate a complete one-minute window in one pass. */
|
|
40
|
+
export declare function aggregate(bars: Bar[], interval: SupportedInterval, maximumBars?: number): AggregateBar[];
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { RuntimeError } from "../core/errors.js";
|
|
2
|
+
import { ONE_MINUTE_MS } from "./types.js";
|
|
3
|
+
/** Timeframes derived from the confirmed one-minute source. */
|
|
4
|
+
export const SUPPORTED_INTERVALS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D"];
|
|
5
|
+
const INTERVAL_MS = {
|
|
6
|
+
"1m": ONE_MINUTE_MS,
|
|
7
|
+
"3m": 3 * ONE_MINUTE_MS,
|
|
8
|
+
"5m": 5 * ONE_MINUTE_MS,
|
|
9
|
+
"15m": 15 * ONE_MINUTE_MS,
|
|
10
|
+
"30m": 30 * ONE_MINUTE_MS,
|
|
11
|
+
"1H": 60 * ONE_MINUTE_MS,
|
|
12
|
+
"2H": 120 * ONE_MINUTE_MS,
|
|
13
|
+
"4H": 240 * ONE_MINUTE_MS,
|
|
14
|
+
"6H": 360 * ONE_MINUTE_MS,
|
|
15
|
+
"12H": 720 * ONE_MINUTE_MS,
|
|
16
|
+
"1D": 1_440 * ONE_MINUTE_MS
|
|
17
|
+
};
|
|
18
|
+
export function isSupportedInterval(value) {
|
|
19
|
+
return Object.hasOwn(INTERVAL_MS, value);
|
|
20
|
+
}
|
|
21
|
+
export function intervalMs(interval) {
|
|
22
|
+
if (!isSupportedInterval(interval)) {
|
|
23
|
+
throw new RuntimeError("VALIDATION", `Unsupported interval '${interval}'`, false, {
|
|
24
|
+
supported: [...SUPPORTED_INTERVALS]
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return INTERVAL_MS[interval];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Incrementally aggregates a contiguous confirmed one-minute feed into one
|
|
31
|
+
* higher timeframe.
|
|
32
|
+
*
|
|
33
|
+
* Two rules make the output safe to hand to a strategy:
|
|
34
|
+
*
|
|
35
|
+
* 1. When the supplied history starts mid-bucket, that leading partial bucket
|
|
36
|
+
* is discarded rather than presented as a completed candle — publishing it
|
|
37
|
+
* would manufacture a bar that never existed in the local snapshot.
|
|
38
|
+
* 2. Only the final item may be unconfirmed. Its OHLCV contains solely the
|
|
39
|
+
* minutes already known, never a future minute, so a developing-value rule
|
|
40
|
+
* is expressible without leaking look-ahead data.
|
|
41
|
+
*/
|
|
42
|
+
export class TimeframeAggregator {
|
|
43
|
+
interval;
|
|
44
|
+
maximumBars;
|
|
45
|
+
completed = [];
|
|
46
|
+
current;
|
|
47
|
+
started = false;
|
|
48
|
+
lastInputOpen;
|
|
49
|
+
constructor(interval, maximumBars = 20_000) {
|
|
50
|
+
this.interval = interval;
|
|
51
|
+
this.maximumBars = maximumBars;
|
|
52
|
+
if (maximumBars <= 0)
|
|
53
|
+
throw new RuntimeError("VALIDATION", "maximumBars must be greater than zero");
|
|
54
|
+
}
|
|
55
|
+
get intervalMs() {
|
|
56
|
+
return INTERVAL_MS[this.interval];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Incorporates one confirmed one-minute bar. Duplicate, out-of-order, or
|
|
60
|
+
* gapped input is rejected: silently accepting it would double-count volume
|
|
61
|
+
* or fabricate a bucket from discontiguous minutes.
|
|
62
|
+
*/
|
|
63
|
+
push(bar) {
|
|
64
|
+
if (bar.closeTimeMs - bar.openTimeMs !== ONE_MINUTE_MS) {
|
|
65
|
+
throw new RuntimeError("VALIDATION", "Timeframe aggregation requires closed one-minute bars", false, {
|
|
66
|
+
openTimeMs: bar.openTimeMs,
|
|
67
|
+
closeTimeMs: bar.closeTimeMs
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (this.lastInputOpen !== undefined) {
|
|
71
|
+
if (bar.openTimeMs === this.lastInputOpen) {
|
|
72
|
+
throw new RuntimeError("STALE_DATA", "Timeframe aggregation received a duplicate one-minute bar", false, {
|
|
73
|
+
openTimeMs: bar.openTimeMs
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (bar.openTimeMs < this.lastInputOpen) {
|
|
77
|
+
throw new RuntimeError("STALE_DATA", "Timeframe aggregation received an out-of-order one-minute bar", false, {
|
|
78
|
+
openTimeMs: bar.openTimeMs,
|
|
79
|
+
previousOpenTimeMs: this.lastInputOpen
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (bar.openTimeMs !== this.lastInputOpen + ONE_MINUTE_MS) {
|
|
83
|
+
throw new RuntimeError("STALE_DATA", "Timeframe aggregation detected a missing one-minute bar", false, {
|
|
84
|
+
expectedOpenTimeMs: this.lastInputOpen + ONE_MINUTE_MS,
|
|
85
|
+
receivedOpenTimeMs: bar.openTimeMs
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.lastInputOpen = bar.openTimeMs;
|
|
90
|
+
const step = this.intervalMs;
|
|
91
|
+
const bucketOpen = Math.floor(bar.openTimeMs / step) * step;
|
|
92
|
+
const bucketClose = bucketOpen + step;
|
|
93
|
+
if (!this.started) {
|
|
94
|
+
// Wait for the first natural UTC boundary so no partial bucket is ever
|
|
95
|
+
// published as history.
|
|
96
|
+
if (bar.openTimeMs !== bucketOpen)
|
|
97
|
+
return;
|
|
98
|
+
this.started = true;
|
|
99
|
+
}
|
|
100
|
+
const open = Number(bar.open);
|
|
101
|
+
const high = Number(bar.high);
|
|
102
|
+
const low = Number(bar.low);
|
|
103
|
+
const close = Number(bar.close);
|
|
104
|
+
const volume = Number(bar.volume);
|
|
105
|
+
if (this.current && this.current.openTimeMs === bucketOpen) {
|
|
106
|
+
this.current.high = Math.max(this.current.high, high);
|
|
107
|
+
this.current.low = Math.min(this.current.low, low);
|
|
108
|
+
this.current.close = close;
|
|
109
|
+
this.current.volume += volume;
|
|
110
|
+
this.current.confirmed = bar.closeTimeMs === this.current.closeTimeMs;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (this.current) {
|
|
114
|
+
this.completed.push(this.current);
|
|
115
|
+
while (this.completed.length > this.maximumBars)
|
|
116
|
+
this.completed.shift();
|
|
117
|
+
}
|
|
118
|
+
this.current = {
|
|
119
|
+
openTimeMs: bucketOpen,
|
|
120
|
+
closeTimeMs: bucketClose,
|
|
121
|
+
open,
|
|
122
|
+
high,
|
|
123
|
+
low,
|
|
124
|
+
close,
|
|
125
|
+
volume,
|
|
126
|
+
confirmed: bar.closeTimeMs === bucketClose
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Time-ascending view; the last item may be an active, unconfirmed bucket. */
|
|
130
|
+
snapshot() {
|
|
131
|
+
return this.current ? [...this.completed, { ...this.current }] : [...this.completed];
|
|
132
|
+
}
|
|
133
|
+
latest() {
|
|
134
|
+
return this.current ? { ...this.current } : undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Convenience: aggregate a complete one-minute window in one pass. */
|
|
138
|
+
export function aggregate(bars, interval, maximumBars = 20_000) {
|
|
139
|
+
if (!isSupportedInterval(interval))
|
|
140
|
+
throw new RuntimeError("VALIDATION", `Unsupported interval '${interval}'`);
|
|
141
|
+
const aggregator = new TimeframeAggregator(interval, maximumBars);
|
|
142
|
+
for (const bar of bars)
|
|
143
|
+
aggregator.push(bar);
|
|
144
|
+
return aggregator.snapshot();
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=timeframe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeframe.js","sourceRoot":"","sources":["../../src/bars/timeframe.ts"],"names":[],"mappings":"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"}
|