@reefclaw/openclaw-plugin 0.1.4 → 0.1.6
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/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/config/plugin-config-io.d.ts +6 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +567 -57
- package/lifecycle/trading-operation-lock.d.ts +17 -0
- package/lifecycle/trading-operation-lock.js +14 -0
- package/live/bracket-id.d.ts +2 -3
- package/live/bracket-id.js +22 -9
- package/live/live-adapter.d.ts +24 -1
- package/live/live-adapter.js +114 -2
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/proposal-decision-listener.d.ts +6 -0
- package/live/proposal-decision-listener.js +4 -0
- package/live/stop-watcher.d.ts +27 -1
- package/live/stop-watcher.js +59 -2
- package/onboarding/runtime.d.ts +13 -0
- package/onboarding/runtime.js +22 -2
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/persistence/state-manager.d.ts +24 -0
- package/persistence/state-manager.js +62 -4
- package/portfolio/wave9-admission.d.ts +67 -0
- package/portfolio/wave9-admission.js +262 -0
- package/portfolio/wave9-policy.d.ts +36 -0
- package/portfolio/wave9-policy.js +183 -0
- package/signals/conditions/registry.js +50 -0
- package/simulator/exchange-simulator.js +5 -0
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +6 -1
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +2 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +36 -0
- package/tools/cancel-all-orders.d.ts +2 -0
- package/tools/cancel-all-orders.js +4 -1
- package/tools/cancel-order.d.ts +4 -0
- package/tools/cancel-order.js +49 -3
- package/tools/close-position.d.ts +23 -0
- package/tools/close-position.js +286 -13
- package/tools/create-order.d.ts +28 -0
- package/tools/create-order.js +1364 -192
- package/tools/get-analytics.js +2 -2
- package/tools/get-basis.js +2 -2
- package/tools/get-cascade-risk.js +2 -2
- package/tools/get-crypto-metrics.js +20 -3
- package/tools/get-cvd.js +2 -2
- package/tools/get-divergences.js +2 -2
- package/tools/get-funding-context.js +2 -2
- package/tools/get-liquidation-levels.js +2 -2
- package/tools/get-liquidation-pulse.js +2 -2
- package/tools/get-pattern-scan.js +2 -2
- package/tools/get-regime.js +2 -2
- package/tools/get-resting-liquidity.js +2 -2
- package/tools/get-risk-scenario.js +2 -2
- package/tools/get-session-review.js +2 -2
- package/tools/get-setup-detail.js +9 -1
- package/tools/get-signals.js +2 -2
- package/tools/get-sizing.js +2 -2
- package/tools/get-trade-feedback.js +2 -2
- package/tools/get-trade-flow.js +2 -2
- package/tools/get-volume-profile.js +2 -2
- package/tools/get-wave9-status.d.ts +127 -0
- package/tools/get-wave9-status.js +796 -0
- package/tools/intel-api.d.ts +20 -0
- package/tools/intel-api.js +67 -0
- package/tools/intel-cache.d.ts +1 -1
- package/tools/intel-cache.js +20 -5
- package/tools/list-strategies.d.ts +11 -1
- package/tools/list-strategies.js +17 -0
- package/tools/modify-stop.d.ts +4 -0
- package/tools/modify-stop.js +63 -24
- package/tools/modify-target.d.ts +4 -0
- package/tools/modify-target.js +62 -23
- package/tools/scan-pairs.js +19 -8
- package/tools/toggle-strategy.js +7 -0
- package/types.d.ts +5 -0
- package/venues/hyperliquid/hl-balance.d.ts +116 -0
- package/venues/hyperliquid/hl-balance.js +145 -0
- package/venues/hyperliquid/hl-brackets.d.ts +102 -0
- package/venues/hyperliquid/hl-brackets.js +172 -0
- package/venues/hyperliquid/hl-cloid.d.ts +22 -0
- package/venues/hyperliquid/hl-cloid.js +82 -0
- package/venues/hyperliquid/hl-info-cache.d.ts +46 -0
- package/venues/hyperliquid/hl-info-cache.js +125 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +88 -0
- package/venues/hyperliquid/hl-live-adapter.js +353 -0
- package/venues/hyperliquid/hl-precision.d.ts +61 -0
- package/venues/hyperliquid/hl-precision.js +176 -0
- package/venues/hyperliquid/hl-private.d.ts +88 -0
- package/venues/hyperliquid/hl-private.js +357 -0
- package/venues/hyperliquid/hl-public.d.ts +31 -4
- package/venues/hyperliquid/hl-public.js +155 -11
- package/venues/hyperliquid/hl-rate-gate.d.ts +57 -0
- package/venues/hyperliquid/hl-rate-gate.js +220 -0
- package/venues/hyperliquid/hl-user-stream.d.ts +90 -0
- package/venues/hyperliquid/hl-user-stream.js +220 -0
- package/venues/registry.d.ts +28 -9
- package/venues/registry.js +19 -13
- package/venues/symbols.d.ts +43 -0
- package/venues/symbols.js +107 -0
- package/wave9/live-account-capture.d.ts +67 -0
- package/wave9/live-account-capture.js +435 -0
- package/wave9/live-autonomous-protection.d.ts +39 -0
- package/wave9/live-autonomous-protection.js +112 -0
- package/wave9/live-durable-reconciliation-scheduler.d.ts +33 -0
- package/wave9/live-durable-reconciliation-scheduler.js +115 -0
- package/wave9/live-execution-ledger.d.ts +107 -0
- package/wave9/live-execution-ledger.js +498 -0
- package/wave9/live-position-confirmation.d.ts +18 -0
- package/wave9/live-position-confirmation.js +111 -0
- package/wave9/live-residual-protection.d.ts +18 -0
- package/wave9/live-residual-protection.js +250 -0
- package/wave9/live-startup-reconciliation.d.ts +38 -0
- package/wave9/live-startup-reconciliation.js +454 -0
- package/wave9/live-symbol-ownership.d.ts +20 -0
- package/wave9/live-symbol-ownership.js +132 -0
- package/wave9/paper-admission-guard.d.ts +199 -0
- package/wave9/paper-admission-guard.js +650 -0
- package/wave9/usdm-evidence-provider.d.ts +42 -0
- package/wave9/usdm-evidence-provider.js +133 -0
package/onboarding/runtime.js
CHANGED
|
@@ -55,6 +55,8 @@ export class PluginRuntime {
|
|
|
55
55
|
* stops fire and NAV/uPnL stay live for non-dashboard symbols). Lives for
|
|
56
56
|
* the runtime's lifetime; started in paper, stopped in live. */
|
|
57
57
|
_marketFeed;
|
|
58
|
+
operationLock;
|
|
59
|
+
wave9LiveLifecycleHooks;
|
|
58
60
|
/** Reconnect is serialized — a second caller waits for the first to finish
|
|
59
61
|
* so we never tear down an adapter that's mid-rebuild. */
|
|
60
62
|
reconnectInFlight = null;
|
|
@@ -64,11 +66,21 @@ export class PluginRuntime {
|
|
|
64
66
|
this.simulator = initial.simulator;
|
|
65
67
|
this._stopWatcher = initial.stopWatcher ?? null;
|
|
66
68
|
this._marketFeed = initial.marketFeed ?? null;
|
|
69
|
+
this.operationLock = initial.operationLock;
|
|
67
70
|
}
|
|
68
71
|
get adapter() { return this._adapter; }
|
|
69
72
|
get mode() { return this._mode; }
|
|
70
73
|
get stopWatcher() { return this._stopWatcher; }
|
|
71
74
|
get marketFeed() { return this._marketFeed; }
|
|
75
|
+
setWave9LiveLifecycleHooks(hooks) {
|
|
76
|
+
this.wave9LiveLifecycleHooks = hooks;
|
|
77
|
+
if (this._adapter instanceof LiveAdapter) {
|
|
78
|
+
hooks?.configureLiveAdapter?.(this._adapter);
|
|
79
|
+
}
|
|
80
|
+
if (this._stopWatcher) {
|
|
81
|
+
hooks?.configurePositionWatcher?.(this._stopWatcher, this._adapter);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
72
84
|
/**
|
|
73
85
|
* Swap the current adapter for a new one built from `next`. The old
|
|
74
86
|
* adapter's background loops are stopped before the new one is wired in.
|
|
@@ -82,7 +94,9 @@ export class PluginRuntime {
|
|
|
82
94
|
if (this.reconnectInFlight) {
|
|
83
95
|
await this.reconnectInFlight;
|
|
84
96
|
}
|
|
85
|
-
const run = this.
|
|
97
|
+
const run = this.operationLock
|
|
98
|
+
? this.operationLock.withAccountLock('runtime-reconnect', () => this.doReconnect(next, deps))
|
|
99
|
+
: this.doReconnect(next, deps);
|
|
86
100
|
this.reconnectInFlight = run.finally(() => {
|
|
87
101
|
this.reconnectInFlight = null;
|
|
88
102
|
});
|
|
@@ -117,6 +131,11 @@ export class PluginRuntime {
|
|
|
117
131
|
microLive: next.microLive,
|
|
118
132
|
simulator: this.simulator,
|
|
119
133
|
});
|
|
134
|
+
// Install autonomous protection callbacks before initialization can emit
|
|
135
|
+
// user-data or bracket-reconciler events.
|
|
136
|
+
if (fresh instanceof LiveAdapter) {
|
|
137
|
+
this.wave9LiveLifecycleHooks?.configureLiveAdapter?.(fresh);
|
|
138
|
+
}
|
|
120
139
|
// 4. Fire async init for live adapters (non-blocking — readiness flips
|
|
121
140
|
// INIT_PENDING → READY/DEGRADED/BLOCKED on its own).
|
|
122
141
|
if (fresh instanceof LiveAdapter) {
|
|
@@ -134,7 +153,8 @@ export class PluginRuntime {
|
|
|
134
153
|
this._mode = next.mode;
|
|
135
154
|
deps.adapterDeps.adapter = fresh;
|
|
136
155
|
// 6. Start a new stop-watcher bound to the new adapter.
|
|
137
|
-
const watcher = new PositionWatcher(fresh);
|
|
156
|
+
const watcher = new PositionWatcher(fresh, undefined, this.operationLock);
|
|
157
|
+
this.wave9LiveLifecycleHooks?.configurePositionWatcher?.(watcher, fresh);
|
|
138
158
|
watcher.start();
|
|
139
159
|
this._stopWatcher = watcher;
|
|
140
160
|
// 7. Paper market feed follows the mode: run it when the new adapter is
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "ReefClaw trading plugin for OpenClaw \u2014 paper trading with real Binance market data, plus the ReefClaw dashboard connector (supervised by OpenClaw, no service manager needed). Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -39,4 +39,28 @@ export declare class StateManager {
|
|
|
39
39
|
loadSync(): SimulatorState | null;
|
|
40
40
|
/** Synchronous load-or-create-default — for use in synchronous plugin register(). */
|
|
41
41
|
loadOrDefaultSync(startingBalance: number, quoteCurrency: string): SimulatorState;
|
|
42
|
+
private saveSyncSafe;
|
|
42
43
|
}
|
|
44
|
+
export interface PaperQuoteMigrationResult {
|
|
45
|
+
state: SimulatorState;
|
|
46
|
+
migrated: boolean;
|
|
47
|
+
fromQuote: string;
|
|
48
|
+
releasedPositions: number;
|
|
49
|
+
droppedOrders: number;
|
|
50
|
+
}
|
|
51
|
+
/** Migrate a persisted paper state whose quote currency no longer matches the
|
|
52
|
+
* configured venue's (issue #174 — e.g. a USDT wallet after switching to the
|
|
53
|
+
* USDC-quoted hyperliquid venue). Pure; returns the input untouched when the
|
|
54
|
+
* quotes already match.
|
|
55
|
+
*
|
|
56
|
+
* Semantics (equity-preserving):
|
|
57
|
+
* - Open positions are released at their entry price: opening deducted the
|
|
58
|
+
* full entry notional from the wallet (spot-collateral model), so that
|
|
59
|
+
* notional is credited back before the position rows — which reference the
|
|
60
|
+
* OLD venue's symbols and are unpriceable on the new one — are dropped.
|
|
61
|
+
* - Unfilled open orders never debited the wallet (fills do) — just dropped.
|
|
62
|
+
* - Balances carry 1:1 (USDT↔USDC are both dollar stables; paper precision),
|
|
63
|
+
* merging into any existing balance under the new quote.
|
|
64
|
+
* - Trade history is kept (display-only). savedAt is bumped so the other
|
|
65
|
+
* process's replaceState() staleness guard accepts the migrated snapshot. */
|
|
66
|
+
export declare function migratePaperQuoteCurrency(state: SimulatorState, expectedQuote: string): PaperQuoteMigrationResult;
|
|
@@ -144,11 +144,26 @@ export class StateManager {
|
|
|
144
144
|
/** Synchronous load-or-create-default — for use in synchronous plugin register(). */
|
|
145
145
|
loadOrDefaultSync(startingBalance, quoteCurrency) {
|
|
146
146
|
const loaded = this.loadSync();
|
|
147
|
-
if (loaded)
|
|
148
|
-
|
|
147
|
+
if (loaded) {
|
|
148
|
+
// Venue switch heal (issue #174): a persisted wallet quoted in the OLD
|
|
149
|
+
// venue's currency reads as $0 equity on the new venue. Migrate 1:1
|
|
150
|
+
// (USDT↔USDC), preserving total equity exactly.
|
|
151
|
+
const mig = migratePaperQuoteCurrency(loaded, quoteCurrency);
|
|
152
|
+
if (mig.migrated) {
|
|
153
|
+
logger.info(TAG, `Paper wallet quote migrated ${mig.fromQuote} → ${quoteCurrency} (venue switch): ` +
|
|
154
|
+
`released ${mig.releasedPositions} open position(s) at entry price, ` +
|
|
155
|
+
`dropped ${mig.droppedOrders} unfilled order(s), balances carried 1:1`);
|
|
156
|
+
this.saveSyncSafe(mig.state, 'migrated');
|
|
157
|
+
}
|
|
158
|
+
return mig.state;
|
|
159
|
+
}
|
|
149
160
|
const state = createDefaultState(startingBalance, quoteCurrency);
|
|
150
161
|
logger.info(TAG, `Initialized default state: ${startingBalance} ${quoteCurrency}`);
|
|
151
162
|
// Save synchronously so state persists immediately
|
|
163
|
+
this.saveSyncSafe(state, 'initial');
|
|
164
|
+
return state;
|
|
165
|
+
}
|
|
166
|
+
saveSyncSafe(state, label) {
|
|
152
167
|
try {
|
|
153
168
|
const dir = dirname(this.statePath);
|
|
154
169
|
if (!existsSync(dir)) {
|
|
@@ -157,8 +172,51 @@ export class StateManager {
|
|
|
157
172
|
writeFileSync(this.statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
158
173
|
}
|
|
159
174
|
catch (err) {
|
|
160
|
-
logger.error(TAG, `Failed to save
|
|
175
|
+
logger.error(TAG, `Failed to save ${label} state: ${err instanceof Error ? err.message : String(err)}`);
|
|
161
176
|
}
|
|
162
|
-
return state;
|
|
163
177
|
}
|
|
164
178
|
}
|
|
179
|
+
/** Migrate a persisted paper state whose quote currency no longer matches the
|
|
180
|
+
* configured venue's (issue #174 — e.g. a USDT wallet after switching to the
|
|
181
|
+
* USDC-quoted hyperliquid venue). Pure; returns the input untouched when the
|
|
182
|
+
* quotes already match.
|
|
183
|
+
*
|
|
184
|
+
* Semantics (equity-preserving):
|
|
185
|
+
* - Open positions are released at their entry price: opening deducted the
|
|
186
|
+
* full entry notional from the wallet (spot-collateral model), so that
|
|
187
|
+
* notional is credited back before the position rows — which reference the
|
|
188
|
+
* OLD venue's symbols and are unpriceable on the new one — are dropped.
|
|
189
|
+
* - Unfilled open orders never debited the wallet (fills do) — just dropped.
|
|
190
|
+
* - Balances carry 1:1 (USDT↔USDC are both dollar stables; paper precision),
|
|
191
|
+
* merging into any existing balance under the new quote.
|
|
192
|
+
* - Trade history is kept (display-only). savedAt is bumped so the other
|
|
193
|
+
* process's replaceState() staleness guard accepts the migrated snapshot. */
|
|
194
|
+
export function migratePaperQuoteCurrency(state, expectedQuote) {
|
|
195
|
+
const fromQuote = state.config?.quoteCurrency ?? 'USDT';
|
|
196
|
+
if (fromQuote === expectedQuote) {
|
|
197
|
+
return { state, migrated: false, fromQuote, releasedPositions: 0, droppedOrders: 0 };
|
|
198
|
+
}
|
|
199
|
+
const next = JSON.parse(JSON.stringify(state));
|
|
200
|
+
next.config = { ...next.config, quoteCurrency: expectedQuote };
|
|
201
|
+
const old = next.wallet[fromQuote] ?? { total: 0, available: 0, locked: 0 };
|
|
202
|
+
let releasedPositions = 0;
|
|
203
|
+
for (const pos of next.positions ?? []) {
|
|
204
|
+
const notional = pos.entryPrice * pos.quantity;
|
|
205
|
+
if (Number.isFinite(notional) && notional > 0) {
|
|
206
|
+
old.total += notional;
|
|
207
|
+
old.available += notional;
|
|
208
|
+
}
|
|
209
|
+
releasedPositions++;
|
|
210
|
+
}
|
|
211
|
+
const droppedOrders = (next.openOrders ?? []).length;
|
|
212
|
+
next.positions = [];
|
|
213
|
+
next.openOrders = [];
|
|
214
|
+
const target = next.wallet[expectedQuote] ?? { total: 0, available: 0, locked: 0 };
|
|
215
|
+
target.total += old.total;
|
|
216
|
+
target.available += old.available;
|
|
217
|
+
target.locked += old.locked;
|
|
218
|
+
next.wallet[expectedQuote] = target;
|
|
219
|
+
delete next.wallet[fromQuote];
|
|
220
|
+
next.savedAt = Date.now();
|
|
221
|
+
return { state: next, migrated: true, fromQuote, releasedPositions, droppedOrders };
|
|
222
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Direction } from '../signals/types.js';
|
|
2
|
+
import { type Wave9StrategyName } from './wave9-policy.js';
|
|
3
|
+
export interface Wave9AdmissionPolicy {
|
|
4
|
+
riskPct: number;
|
|
5
|
+
maxPositions: number;
|
|
6
|
+
maxOpenRiskPct: number;
|
|
7
|
+
maxGrossExposureMultiple: number;
|
|
8
|
+
maxPositionUsd: number;
|
|
9
|
+
feeBps: number;
|
|
10
|
+
slippageBps: number;
|
|
11
|
+
capEnforcement: 'admission_only_no_forced_deleveraging';
|
|
12
|
+
riskMeasure: 'fixed_initial_stop_risk_usd';
|
|
13
|
+
}
|
|
14
|
+
export declare const WAVE9_ADMISSION_POLICY: Readonly<Wave9AdmissionPolicy>;
|
|
15
|
+
export interface Wave9AdmissionPosition {
|
|
16
|
+
candidateId: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
strategy: Wave9StrategyName;
|
|
19
|
+
direction: Direction;
|
|
20
|
+
entryPrice: number;
|
|
21
|
+
quantity: number;
|
|
22
|
+
markPrice: number;
|
|
23
|
+
initialStop: number;
|
|
24
|
+
dollarRiskUsd: number;
|
|
25
|
+
accruedFundingUsd: number;
|
|
26
|
+
}
|
|
27
|
+
export interface Wave9AdmissionState {
|
|
28
|
+
cashEquityUsd: number;
|
|
29
|
+
positions: Wave9AdmissionPosition[];
|
|
30
|
+
}
|
|
31
|
+
export interface Wave9AdmissionCandidate {
|
|
32
|
+
id: string;
|
|
33
|
+
eventTime: string;
|
|
34
|
+
symbol: string;
|
|
35
|
+
strategy: Wave9StrategyName;
|
|
36
|
+
direction: Direction;
|
|
37
|
+
referencePrice: number;
|
|
38
|
+
stopPrice: number;
|
|
39
|
+
}
|
|
40
|
+
export type Wave9AdmissionReason = 'accepted' | 'invalidGeometry' | 'symbolAlreadyOpen' | 'maxPositions' | 'maxGrossExposure' | 'maxOpenRisk' | 'invalidSizing';
|
|
41
|
+
export interface Wave9AdmissionLedgerRow extends Wave9AdmissionPosition {
|
|
42
|
+
estimatedExitCostUsd: number;
|
|
43
|
+
}
|
|
44
|
+
export interface Wave9AdmissionDecision extends Wave9AdmissionCandidate {
|
|
45
|
+
accepted: boolean;
|
|
46
|
+
reason: Wave9AdmissionReason;
|
|
47
|
+
entryPrice?: number;
|
|
48
|
+
quantity?: number;
|
|
49
|
+
notionalUsd?: number;
|
|
50
|
+
riskUsd?: number;
|
|
51
|
+
projectedCashEquityUsd?: number;
|
|
52
|
+
projectedLiquidationEquityUsd?: number;
|
|
53
|
+
projectedGrossExposureUsd?: number;
|
|
54
|
+
projectedOpenRiskUsd?: number;
|
|
55
|
+
projectedOpenPositions?: number;
|
|
56
|
+
projectedPositionLedger?: Wave9AdmissionLedgerRow[];
|
|
57
|
+
}
|
|
58
|
+
export interface Wave9AdmissionBatchResult {
|
|
59
|
+
decisions: Wave9AdmissionDecision[];
|
|
60
|
+
state: Wave9AdmissionState;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Apply a same-event candidate batch in the exact frozen priority. The caller
|
|
64
|
+
* must release stops and completed-daily reversals before invoking this
|
|
65
|
+
* function, just as the chronological research engine does.
|
|
66
|
+
*/
|
|
67
|
+
export declare function applyWave9AdmissionBatch(inputState: Wave9AdmissionState, rawCandidates: readonly Wave9AdmissionCandidate[], policy?: Readonly<Wave9AdmissionPolicy>): Wave9AdmissionBatchResult;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
// Canonical source of truth: shared/src/portfolio/wave9-admission.ts
|
|
3
|
+
// Regenerate: node scripts/sync-shared-code.mjs (enforced by shared-code-sync.test.ts)
|
|
4
|
+
//
|
|
5
|
+
// This copy exists because this package builds with tsc and deploys as a
|
|
6
|
+
// self-contained tree that strips workspace deps, so it cannot import
|
|
7
|
+
// @reefclaw/shared runtime code across the deploy boundary.
|
|
8
|
+
import { WAVE9_LONG_STRATEGY, WAVE9_STRATEGY_PRIORITY, WAVE9_SYMBOL_PRIORITY, } from './wave9-policy.js';
|
|
9
|
+
export const WAVE9_ADMISSION_POLICY = Object.freeze({
|
|
10
|
+
riskPct: 0.5,
|
|
11
|
+
maxPositions: 4,
|
|
12
|
+
maxOpenRiskPct: 2,
|
|
13
|
+
maxGrossExposureMultiple: 1.5,
|
|
14
|
+
maxPositionUsd: 10_000,
|
|
15
|
+
feeBps: 5,
|
|
16
|
+
slippageBps: 10,
|
|
17
|
+
capEnforcement: 'admission_only_no_forced_deleveraging',
|
|
18
|
+
riskMeasure: 'fixed_initial_stop_risk_usd',
|
|
19
|
+
});
|
|
20
|
+
function finitePositive(value) {
|
|
21
|
+
return Number.isFinite(value) && value > 0;
|
|
22
|
+
}
|
|
23
|
+
function nearlyEqual(left, right) {
|
|
24
|
+
return Math.abs(left - right) <= 1e-9 * Math.max(1, Math.abs(left), Math.abs(right));
|
|
25
|
+
}
|
|
26
|
+
function validatePolicy(policy) {
|
|
27
|
+
for (const [label, value] of Object.entries({
|
|
28
|
+
riskPct: policy.riskPct,
|
|
29
|
+
maxOpenRiskPct: policy.maxOpenRiskPct,
|
|
30
|
+
maxGrossExposureMultiple: policy.maxGrossExposureMultiple,
|
|
31
|
+
maxPositionUsd: policy.maxPositionUsd,
|
|
32
|
+
})) {
|
|
33
|
+
if (!finitePositive(value))
|
|
34
|
+
throw new Error(`${label} must be positive and finite`);
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isInteger(policy.maxPositions) || policy.maxPositions <= 0) {
|
|
37
|
+
throw new Error('maxPositions must be a positive integer');
|
|
38
|
+
}
|
|
39
|
+
for (const [label, value] of Object.entries({
|
|
40
|
+
feeBps: policy.feeBps,
|
|
41
|
+
slippageBps: policy.slippageBps,
|
|
42
|
+
})) {
|
|
43
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
44
|
+
throw new Error(`${label} must be finite and non-negative`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (policy.capEnforcement !== 'admission_only_no_forced_deleveraging'
|
|
48
|
+
|| policy.riskMeasure !== 'fixed_initial_stop_risk_usd') {
|
|
49
|
+
throw new Error('unsupported Wave 9 admission policy semantics');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function rank(value, values, label) {
|
|
53
|
+
const result = values.indexOf(value);
|
|
54
|
+
if (result < 0)
|
|
55
|
+
throw new Error(`${label} is not in the frozen Wave 9 priority`);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function validateCandidateIdentity(id, eventTime, symbol, strategy, direction) {
|
|
59
|
+
const eventMs = Date.parse(eventTime);
|
|
60
|
+
if (!Number.isFinite(eventMs) || new Date(eventMs).toISOString() !== eventTime) {
|
|
61
|
+
throw new Error('candidate eventTime must be a canonical ISO timestamp');
|
|
62
|
+
}
|
|
63
|
+
const expectedId = `${eventTime}|${symbol}|${strategy}|${direction}`;
|
|
64
|
+
if (id !== expectedId) {
|
|
65
|
+
throw new Error(`candidate ID does not match its frozen identity: ${id}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function copyPosition(position) {
|
|
69
|
+
return { ...position };
|
|
70
|
+
}
|
|
71
|
+
function ledgerFor(positions, policy) {
|
|
72
|
+
const exitCostRate = (policy.feeBps + policy.slippageBps) / 10_000;
|
|
73
|
+
return positions.map((position) => ({
|
|
74
|
+
...copyPosition(position),
|
|
75
|
+
estimatedExitCostUsd: position.markPrice * position.quantity * exitCostRate,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
function summarize(cashEquityUsd, ledger) {
|
|
79
|
+
let unrealized = 0;
|
|
80
|
+
let funding = 0;
|
|
81
|
+
let exitCosts = 0;
|
|
82
|
+
let gross = 0;
|
|
83
|
+
let risk = 0;
|
|
84
|
+
for (const row of ledger) {
|
|
85
|
+
unrealized += row.direction === 'LONG'
|
|
86
|
+
? (row.markPrice - row.entryPrice) * row.quantity
|
|
87
|
+
: (row.entryPrice - row.markPrice) * row.quantity;
|
|
88
|
+
funding += row.accruedFundingUsd;
|
|
89
|
+
exitCosts += row.estimatedExitCostUsd;
|
|
90
|
+
gross += Math.abs(row.markPrice * row.quantity);
|
|
91
|
+
risk += row.dollarRiskUsd;
|
|
92
|
+
}
|
|
93
|
+
return { equity: cashEquityUsd + unrealized - funding - exitCosts, gross, risk };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply a same-event candidate batch in the exact frozen priority. The caller
|
|
97
|
+
* must release stops and completed-daily reversals before invoking this
|
|
98
|
+
* function, just as the chronological research engine does.
|
|
99
|
+
*/
|
|
100
|
+
export function applyWave9AdmissionBatch(inputState, rawCandidates, policy = WAVE9_ADMISSION_POLICY) {
|
|
101
|
+
validatePolicy(policy);
|
|
102
|
+
if (!Number.isFinite(inputState.cashEquityUsd)) {
|
|
103
|
+
throw new Error('cashEquityUsd must be finite');
|
|
104
|
+
}
|
|
105
|
+
const positions = inputState.positions.map(copyPosition);
|
|
106
|
+
const ids = new Set();
|
|
107
|
+
for (const position of positions) {
|
|
108
|
+
if (ids.has(position.candidateId))
|
|
109
|
+
throw new Error('open candidate IDs must be unique');
|
|
110
|
+
ids.add(position.candidateId);
|
|
111
|
+
rank(position.symbol, WAVE9_SYMBOL_PRIORITY, 'open-position symbol');
|
|
112
|
+
rank(position.strategy, WAVE9_STRATEGY_PRIORITY, 'open-position strategy');
|
|
113
|
+
const identity = position.candidateId.split('|');
|
|
114
|
+
if (identity.length !== 4) {
|
|
115
|
+
throw new Error(`open-position candidate ID is malformed: ${position.candidateId}`);
|
|
116
|
+
}
|
|
117
|
+
validateCandidateIdentity(position.candidateId, identity[0], position.symbol, position.strategy, position.direction);
|
|
118
|
+
if (!finitePositive(position.entryPrice)
|
|
119
|
+
|| !finitePositive(position.quantity)
|
|
120
|
+
|| !finitePositive(position.markPrice)
|
|
121
|
+
|| !finitePositive(position.initialStop)
|
|
122
|
+
|| !Number.isFinite(position.dollarRiskUsd)
|
|
123
|
+
|| position.dollarRiskUsd < 0
|
|
124
|
+
|| !Number.isFinite(position.accruedFundingUsd)) {
|
|
125
|
+
throw new Error(`invalid open position ${position.candidateId}`);
|
|
126
|
+
}
|
|
127
|
+
const expectedDirection = position.strategy === WAVE9_LONG_STRATEGY ? 'LONG' : 'SHORT';
|
|
128
|
+
const geometryValid = position.direction === expectedDirection
|
|
129
|
+
&& (position.direction === 'LONG'
|
|
130
|
+
? position.initialStop < position.entryPrice
|
|
131
|
+
: position.initialStop > position.entryPrice);
|
|
132
|
+
if (!geometryValid) {
|
|
133
|
+
throw new Error(`invalid open-position geometry ${position.candidateId}`);
|
|
134
|
+
}
|
|
135
|
+
const expectedRisk = Math.abs(position.entryPrice - position.initialStop)
|
|
136
|
+
* position.quantity;
|
|
137
|
+
if (!nearlyEqual(position.dollarRiskUsd, expectedRisk)) {
|
|
138
|
+
throw new Error(`open-position risk mismatch ${position.candidateId}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (new Set(positions.map((position) => position.symbol)).size !== positions.length) {
|
|
142
|
+
throw new Error('Wave 9 allows at most one open position per symbol');
|
|
143
|
+
}
|
|
144
|
+
const candidates = rawCandidates.map((candidate) => ({ ...candidate }));
|
|
145
|
+
const candidateIds = new Set();
|
|
146
|
+
let batchEventTime;
|
|
147
|
+
for (const candidate of candidates) {
|
|
148
|
+
if (candidateIds.has(candidate.id))
|
|
149
|
+
throw new Error('candidate IDs must be unique');
|
|
150
|
+
candidateIds.add(candidate.id);
|
|
151
|
+
validateCandidateIdentity(candidate.id, candidate.eventTime, candidate.symbol, candidate.strategy, candidate.direction);
|
|
152
|
+
if (batchEventTime === undefined)
|
|
153
|
+
batchEventTime = candidate.eventTime;
|
|
154
|
+
if (candidate.eventTime !== batchEventTime) {
|
|
155
|
+
throw new Error('a Wave 9 admission batch must contain exactly one eventTime');
|
|
156
|
+
}
|
|
157
|
+
rank(candidate.symbol, WAVE9_SYMBOL_PRIORITY, 'candidate symbol');
|
|
158
|
+
rank(candidate.strategy, WAVE9_STRATEGY_PRIORITY, 'candidate strategy');
|
|
159
|
+
}
|
|
160
|
+
candidates.sort((left, right) => rank(left.symbol, WAVE9_SYMBOL_PRIORITY, 'candidate symbol')
|
|
161
|
+
- rank(right.symbol, WAVE9_SYMBOL_PRIORITY, 'candidate symbol')
|
|
162
|
+
|| rank(left.strategy, WAVE9_STRATEGY_PRIORITY, 'candidate strategy')
|
|
163
|
+
- rank(right.strategy, WAVE9_STRATEGY_PRIORITY, 'candidate strategy')
|
|
164
|
+
|| left.id.localeCompare(right.id));
|
|
165
|
+
let cashEquityUsd = inputState.cashEquityUsd;
|
|
166
|
+
const decisions = [];
|
|
167
|
+
const feeRate = policy.feeBps / 10_000;
|
|
168
|
+
const slippageRate = policy.slippageBps / 10_000;
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
const decision = {
|
|
171
|
+
...candidate,
|
|
172
|
+
accepted: false,
|
|
173
|
+
reason: 'invalidSizing',
|
|
174
|
+
};
|
|
175
|
+
const expectedDirection = candidate.strategy === WAVE9_LONG_STRATEGY ? 'LONG' : 'SHORT';
|
|
176
|
+
if (candidate.direction !== expectedDirection
|
|
177
|
+
|| !finitePositive(candidate.referencePrice)
|
|
178
|
+
|| !finitePositive(candidate.stopPrice)) {
|
|
179
|
+
decision.reason = 'invalidGeometry';
|
|
180
|
+
decisions.push(decision);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const entryPrice = candidate.direction === 'LONG'
|
|
184
|
+
? candidate.referencePrice * (1 + slippageRate)
|
|
185
|
+
: candidate.referencePrice * (1 - slippageRate);
|
|
186
|
+
decision.entryPrice = entryPrice;
|
|
187
|
+
if ((candidate.direction === 'LONG' && candidate.stopPrice >= entryPrice)
|
|
188
|
+
|| (candidate.direction === 'SHORT' && candidate.stopPrice <= entryPrice)) {
|
|
189
|
+
decision.reason = 'invalidGeometry';
|
|
190
|
+
decisions.push(decision);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (positions.some((position) => position.symbol === candidate.symbol)) {
|
|
194
|
+
decision.reason = 'symbolAlreadyOpen';
|
|
195
|
+
decisions.push(decision);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (positions.length >= policy.maxPositions) {
|
|
199
|
+
decision.reason = 'maxPositions';
|
|
200
|
+
decisions.push(decision);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const current = summarize(cashEquityUsd, ledgerFor(positions, policy));
|
|
204
|
+
const riskPerUnit = Math.abs(entryPrice - candidate.stopPrice);
|
|
205
|
+
const riskBudget = current.equity * policy.riskPct / 100;
|
|
206
|
+
const quantity = Math.min(riskBudget / riskPerUnit, policy.maxPositionUsd / entryPrice);
|
|
207
|
+
const notionalUsd = quantity * entryPrice;
|
|
208
|
+
const riskUsd = quantity * riskPerUnit;
|
|
209
|
+
Object.assign(decision, { quantity, notionalUsd, riskUsd });
|
|
210
|
+
if (!finitePositive(quantity) || current.equity <= 0) {
|
|
211
|
+
decision.reason = 'invalidSizing';
|
|
212
|
+
decisions.push(decision);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const position = {
|
|
216
|
+
candidateId: candidate.id,
|
|
217
|
+
symbol: candidate.symbol,
|
|
218
|
+
strategy: candidate.strategy,
|
|
219
|
+
direction: candidate.direction,
|
|
220
|
+
entryPrice,
|
|
221
|
+
quantity,
|
|
222
|
+
markPrice: candidate.referencePrice,
|
|
223
|
+
initialStop: candidate.stopPrice,
|
|
224
|
+
dollarRiskUsd: riskUsd,
|
|
225
|
+
accruedFundingUsd: 0,
|
|
226
|
+
};
|
|
227
|
+
const projectedPositions = [...positions, position];
|
|
228
|
+
const projectedCash = cashEquityUsd - notionalUsd * feeRate;
|
|
229
|
+
const projectedLedger = ledgerFor(projectedPositions, policy);
|
|
230
|
+
const projected = summarize(projectedCash, projectedLedger);
|
|
231
|
+
Object.assign(decision, {
|
|
232
|
+
projectedCashEquityUsd: projectedCash,
|
|
233
|
+
projectedLiquidationEquityUsd: projected.equity,
|
|
234
|
+
projectedGrossExposureUsd: projected.gross,
|
|
235
|
+
projectedOpenRiskUsd: projected.risk,
|
|
236
|
+
projectedOpenPositions: projectedPositions.length,
|
|
237
|
+
projectedPositionLedger: projectedLedger,
|
|
238
|
+
});
|
|
239
|
+
if (projected.equity <= 0) {
|
|
240
|
+
decision.reason = 'invalidSizing';
|
|
241
|
+
}
|
|
242
|
+
else if (projected.gross
|
|
243
|
+
> projected.equity * policy.maxGrossExposureMultiple + 1e-9) {
|
|
244
|
+
decision.reason = 'maxGrossExposure';
|
|
245
|
+
}
|
|
246
|
+
else if (projected.risk
|
|
247
|
+
> projected.equity * policy.maxOpenRiskPct / 100 + 1e-9) {
|
|
248
|
+
decision.reason = 'maxOpenRisk';
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
decision.accepted = true;
|
|
252
|
+
decision.reason = 'accepted';
|
|
253
|
+
cashEquityUsd = projectedCash;
|
|
254
|
+
positions.push(position);
|
|
255
|
+
}
|
|
256
|
+
decisions.push(decision);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
decisions,
|
|
260
|
+
state: { cashEquityUsd, positions: positions.map(copyPosition) },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Direction, OhlcvBar } from '../signals/types.js';
|
|
2
|
+
export declare const DAY_MS: number;
|
|
3
|
+
export declare const WAVE9_LOOKBACK_DAYS = 28;
|
|
4
|
+
export declare const WAVE9_ATR_PERIOD = 14;
|
|
5
|
+
export declare const WAVE9_ATR_MULTIPLE = 3;
|
|
6
|
+
export declare const WAVE9_CONTEXT_BARS = 250;
|
|
7
|
+
export declare const WAVE9_LONG_STRATEGY = "tsmom_28d_long_flat_reversal_1d";
|
|
8
|
+
export declare const WAVE9_SHORT_STRATEGY = "tsmom_28d_short_flat_reversal_1d";
|
|
9
|
+
export type Wave9StrategyName = typeof WAVE9_LONG_STRATEGY | typeof WAVE9_SHORT_STRATEGY;
|
|
10
|
+
export declare const WAVE9_SYMBOL_PRIORITY: readonly ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
11
|
+
export declare const WAVE9_STRATEGY_PRIORITY: readonly ["tsmom_28d_long_flat_reversal_1d", "tsmom_28d_short_flat_reversal_1d"];
|
|
12
|
+
export interface Wave9EntrySignal {
|
|
13
|
+
strategy: Wave9StrategyName;
|
|
14
|
+
direction: Direction;
|
|
15
|
+
stopPrice: number;
|
|
16
|
+
}
|
|
17
|
+
export interface Wave9DailyDecision {
|
|
18
|
+
eventTime: string;
|
|
19
|
+
completedDailyOpen: string;
|
|
20
|
+
currentReturn: number;
|
|
21
|
+
previousReturn: number;
|
|
22
|
+
atr14: number;
|
|
23
|
+
referencePrice: number;
|
|
24
|
+
entries: Wave9EntrySignal[];
|
|
25
|
+
exitLong: boolean;
|
|
26
|
+
exitShort: boolean;
|
|
27
|
+
sourceBarCount: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Return the exact completed-daily context visible at a UTC daily boundary.
|
|
31
|
+
* The function is intentionally fail-closed: unordered, duplicated, gapped,
|
|
32
|
+
* non-UTC, or stale candles are rejected rather than silently repaired.
|
|
33
|
+
*/
|
|
34
|
+
export declare function completedDailyContext(rawBars: readonly OhlcvBar[], eventTime: Date): OhlcvBar[];
|
|
35
|
+
/** Evaluate the exact Wave 9 entry-transition and signal-reversal semantics. */
|
|
36
|
+
export declare function evaluateWave9Daily(rawBars: readonly OhlcvBar[], eventTime: Date): Wave9DailyDecision;
|