@reefclaw/openclaw-plugin 0.1.5 → 0.1.7
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/bridge/gateway/event-parser.d.ts +6 -1
- package/bridge/gateway/event-parser.js +19 -2
- package/bridge/gateway/poller.d.ts +1 -0
- package/bridge/gateway/poller.js +14 -2
- package/bridge/providers/gateway.d.ts +22 -2
- package/bridge/providers/gateway.js +67 -9
- package/ccxt/binance-private.d.ts +21 -0
- package/ccxt/binance-private.js +132 -22
- package/ccxt/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +13 -0
- package/config/plugin-config-io.js +15 -0
- package/config/tool-gate.js +3 -0
- package/exchange-adapter.d.ts +16 -0
- package/index.js +658 -83
- package/ingest/position-auto-capture.d.ts +68 -0
- package/ingest/position-auto-capture.js +321 -23
- package/ingest/position-decisions-client.d.ts +7 -2
- package/ingest/position-decisions-client.js +13 -3
- package/ingest/reconcile-db-vs-exchange.d.ts +39 -1
- package/ingest/reconcile-db-vs-exchange.js +66 -10
- 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/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +57 -2
- package/live/live-adapter.js +290 -49
- package/live/local-signal-service.js +11 -6
- package/live/local-strategy-evaluator.js +4 -0
- package/live/position-state-store.d.ts +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 +35 -2
- package/live/stop-watcher.js +63 -3
- package/onboarding/runtime.d.ts +19 -0
- package/onboarding/runtime.js +34 -3
- package/openclaw.plugin.json +1 -0
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- 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 +61 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +78 -3
- package/simulator/fill-engine.js +5 -1
- package/simulator/types.d.ts +10 -1
- package/skills/reefclaw/SKILL.md +2 -0
- package/strategy/evaluator.d.ts +3 -0
- package/strategy/evaluator.js +5 -0
- package/tools/assessment-validation.d.ts +23 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.d.ts +7 -2
- package/tools/attach-brackets.js +201 -0
- package/tools/audit-bracket-protection.js +157 -1
- package/tools/bracket-control.d.ts +12 -0
- package/tools/bracket-control.js +35 -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 +1390 -190
- 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 +14 -4
- 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 +21 -2
- 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.d.ts +4 -0
- package/tools/scan-pairs.js +22 -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-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -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 +146 -0
- package/venues/hyperliquid/hl-live-adapter.js +728 -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 +163 -12
- 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 +23 -9
- package/venues/registry.js +12 -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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { Wave9LiveExecutionLedger } from './live-execution-ledger.js';
|
|
3
|
+
import { type Wave9LiveFlatConfirmation, type Wave9LiveFlatConfirmationOptions } from './live-position-confirmation.js';
|
|
4
|
+
import { reprotectWave9LiveResidual, type Wave9LiveResidualProtectionResult } from './live-residual-protection.js';
|
|
5
|
+
export interface Wave9LiveFlatRecoveryResult {
|
|
6
|
+
status: 'flat';
|
|
7
|
+
detail: string;
|
|
8
|
+
candidateId: string;
|
|
9
|
+
symbol: string;
|
|
10
|
+
}
|
|
11
|
+
export type Wave9LiveAutonomousProtectionResult = Wave9LiveFlatRecoveryResult | Wave9LiveResidualProtectionResult;
|
|
12
|
+
/** Test seams are optional; production callers always use the exact defaults. */
|
|
13
|
+
export interface Wave9LiveAutonomousProtectionOptions {
|
|
14
|
+
flatConfirmation?: Wave9LiveFlatConfirmationOptions;
|
|
15
|
+
confirmFlat?: (adapter: IExchangeAdapter, symbol: string, options?: Wave9LiveFlatConfirmationOptions) => Promise<Wave9LiveFlatConfirmation>;
|
|
16
|
+
reprotect?: typeof reprotectWave9LiveResidual;
|
|
17
|
+
/**
|
|
18
|
+
* Optional crash-recovery identity fence. Startup reconciliation supplies
|
|
19
|
+
* this after binding the durable candidate to its exact exchange entry CID.
|
|
20
|
+
* It is re-evaluated immediately before every operation that may cancel,
|
|
21
|
+
* replace, or close same-symbol exposure. A failed/unknown fence leaves the
|
|
22
|
+
* durable row unresolved and performs no exchange mutation.
|
|
23
|
+
*/
|
|
24
|
+
authorizeExposureMutation?: () => Promise<{
|
|
25
|
+
authorized: boolean;
|
|
26
|
+
detail: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Wave 9 LIVE exposure after a close acknowledgement or a native
|
|
31
|
+
* stop-loss event. The only successful terminal state is stable exchange
|
|
32
|
+
* flatness persisted to the durable execution ledger. Otherwise the exact
|
|
33
|
+
* frozen candidate stop is proven/re-armed. If that cannot be done, issue one
|
|
34
|
+
* emergency reduce-only close, prove flatness again, and make one final
|
|
35
|
+
* re-protection attempt for any residual. Every unresolved state stays OPEN.
|
|
36
|
+
*
|
|
37
|
+
* The caller must hold the shared trading-operation lock for the whole call.
|
|
38
|
+
*/
|
|
39
|
+
export declare function recoverWave9LiveExposure(adapter: IExchangeAdapter, executionLedger: Wave9LiveExecutionLedger, candidateId: string, symbol: string, options?: Wave9LiveAutonomousProtectionOptions): Promise<Wave9LiveAutonomousProtectionResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { confirmWave9LivePositionFlat, } from './live-position-confirmation.js';
|
|
2
|
+
import { reprotectWave9LiveResidual, } from './live-residual-protection.js';
|
|
3
|
+
function manualResult(ledger, candidateId, symbol, detail) {
|
|
4
|
+
return {
|
|
5
|
+
status: 'manual_intervention',
|
|
6
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: ${detail}`,
|
|
7
|
+
candidateId,
|
|
8
|
+
symbol,
|
|
9
|
+
stopPrice: ledger.getByCandidateId(candidateId)?.initialStop ?? Number.NaN,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function combineManualDetail(result, prefix) {
|
|
13
|
+
return {
|
|
14
|
+
...result,
|
|
15
|
+
detail: `IMMEDIATE MANUAL INTERVENTION REQUIRED: ${prefix}; ${result.detail}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function authorizeMutation(ledger, candidateId, symbol, action, authorize) {
|
|
19
|
+
if (!authorize)
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const result = await authorize();
|
|
23
|
+
if (result.authorized)
|
|
24
|
+
return undefined;
|
|
25
|
+
return manualResult(ledger, candidateId, symbol, `${action} was refused by the exact entry/position identity fence: ${result.detail}`);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return manualResult(ledger, candidateId, symbol, `${action} identity verification threw: ${String(error)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function markConfirmedFlat(ledger, candidateId, symbol, confirmation) {
|
|
32
|
+
try {
|
|
33
|
+
ledger.markClosed(candidateId);
|
|
34
|
+
return {
|
|
35
|
+
status: 'flat',
|
|
36
|
+
detail: confirmation.detail,
|
|
37
|
+
candidateId,
|
|
38
|
+
symbol,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
return manualResult(ledger, candidateId, symbol, `exchange flatness was proven but durable closure could not be persisted: ${String(error)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a Wave 9 LIVE exposure after a close acknowledgement or a native
|
|
47
|
+
* stop-loss event. The only successful terminal state is stable exchange
|
|
48
|
+
* flatness persisted to the durable execution ledger. Otherwise the exact
|
|
49
|
+
* frozen candidate stop is proven/re-armed. If that cannot be done, issue one
|
|
50
|
+
* emergency reduce-only close, prove flatness again, and make one final
|
|
51
|
+
* re-protection attempt for any residual. Every unresolved state stays OPEN.
|
|
52
|
+
*
|
|
53
|
+
* The caller must hold the shared trading-operation lock for the whole call.
|
|
54
|
+
*/
|
|
55
|
+
export async function recoverWave9LiveExposure(adapter, executionLedger, candidateId, symbol, options = {}) {
|
|
56
|
+
const confirmFlat = options.confirmFlat ?? confirmWave9LivePositionFlat;
|
|
57
|
+
const reprotect = options.reprotect ?? reprotectWave9LiveResidual;
|
|
58
|
+
const firstFlat = await confirmFlat(adapter, symbol, options.flatConfirmation);
|
|
59
|
+
if (firstFlat.confirmed) {
|
|
60
|
+
return markConfirmedFlat(executionLedger, candidateId, symbol, firstFlat);
|
|
61
|
+
}
|
|
62
|
+
const initialMutationRefusal = await authorizeMutation(executionLedger, candidateId, symbol, 'initial residual re-protection', options.authorizeExposureMutation);
|
|
63
|
+
if (initialMutationRefusal)
|
|
64
|
+
return initialMutationRefusal;
|
|
65
|
+
let firstProtection;
|
|
66
|
+
try {
|
|
67
|
+
firstProtection = await reprotect(adapter, executionLedger, candidateId, symbol);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
firstProtection = manualResult(executionLedger, candidateId, symbol, `initial residual re-protection threw: ${String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
if (firstProtection.status === 'protected')
|
|
73
|
+
return firstProtection;
|
|
74
|
+
const closeRefusal = await authorizeMutation(executionLedger, candidateId, symbol, 'emergency close', options.authorizeExposureMutation);
|
|
75
|
+
if (closeRefusal) {
|
|
76
|
+
return combineManualDetail(closeRefusal, firstProtection.detail);
|
|
77
|
+
}
|
|
78
|
+
let emergencyCloseError;
|
|
79
|
+
try {
|
|
80
|
+
await adapter.closePosition(symbol, 'emergency');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// A post-submit exception is not proof the exchange rejected the close.
|
|
84
|
+
// Always inspect stable exchange position state before deciding what is
|
|
85
|
+
// left to protect.
|
|
86
|
+
emergencyCloseError = String(error);
|
|
87
|
+
}
|
|
88
|
+
const afterEmergencyFlat = await confirmFlat(adapter, symbol, options.flatConfirmation);
|
|
89
|
+
if (afterEmergencyFlat.confirmed) {
|
|
90
|
+
return markConfirmedFlat(executionLedger, candidateId, symbol, afterEmergencyFlat);
|
|
91
|
+
}
|
|
92
|
+
const finalMutationRefusal = await authorizeMutation(executionLedger, candidateId, symbol, 'final residual re-protection', options.authorizeExposureMutation);
|
|
93
|
+
if (finalMutationRefusal) {
|
|
94
|
+
const closeDetail = emergencyCloseError
|
|
95
|
+
? `emergency close threw (${emergencyCloseError}) and stable flatness was not proven`
|
|
96
|
+
: 'emergency close was acknowledged but stable flatness was not proven';
|
|
97
|
+
return combineManualDetail(finalMutationRefusal, `${firstProtection.detail}; ${closeDetail}`);
|
|
98
|
+
}
|
|
99
|
+
let finalProtection;
|
|
100
|
+
try {
|
|
101
|
+
finalProtection = await reprotect(adapter, executionLedger, candidateId, symbol);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
finalProtection = manualResult(executionLedger, candidateId, symbol, `final residual re-protection threw: ${String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
if (finalProtection.status === 'protected')
|
|
107
|
+
return finalProtection;
|
|
108
|
+
const closeDetail = emergencyCloseError
|
|
109
|
+
? `emergency close threw (${emergencyCloseError}) and stable flatness was not proven`
|
|
110
|
+
: 'emergency close was acknowledged but stable flatness was not proven';
|
|
111
|
+
return combineManualDetail(finalProtection, `${firstProtection.detail}; ${closeDetail}; final residual remains unverified`);
|
|
112
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AdapterReadiness, IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
|
+
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
3
|
+
import type { Wave9LiveStartupReconciliationResult } from './live-startup-reconciliation.js';
|
|
4
|
+
export interface Wave9LiveReadySource extends IExchangeAdapter {
|
|
5
|
+
on(event: 'readiness_changed', listener: (readiness: AdapterReadiness, detail?: string) => void): unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface Wave9LiveDurableReconciliationSchedulerOptions {
|
|
8
|
+
operationLock: TradingOperationLock;
|
|
9
|
+
isCurrentSource(source: Wave9LiveReadySource): boolean;
|
|
10
|
+
reconcile(source: Wave9LiveReadySource): Promise<Wave9LiveStartupReconciliationResult>;
|
|
11
|
+
onResult?(trigger: string, result: Wave9LiveStartupReconciliationResult): void;
|
|
12
|
+
onError?(trigger: string, error: unknown): void;
|
|
13
|
+
retryDelaysMs?: readonly number[];
|
|
14
|
+
periodicMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Source-bound READY/periodic driver for durable Wave 9 recovery. At most one
|
|
18
|
+
* timer and one lock-serialized pass exist for the active adapter. A replaced
|
|
19
|
+
* adapter is stopped synchronously and an in-flight old pass cannot reschedule
|
|
20
|
+
* itself. Timers are unref'ed so this safety loop never keeps Node alive.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Wave9LiveDurableReconciliationScheduler {
|
|
23
|
+
private readonly options;
|
|
24
|
+
private readonly retryDelaysMs;
|
|
25
|
+
private readonly periodicMs;
|
|
26
|
+
private active?;
|
|
27
|
+
constructor(options: Wave9LiveDurableReconciliationSchedulerOptions);
|
|
28
|
+
attach(source: Wave9LiveReadySource): void;
|
|
29
|
+
stopIfSourceChanged(current: IExchangeAdapter): void;
|
|
30
|
+
stop(): void;
|
|
31
|
+
private isCurrentReady;
|
|
32
|
+
private schedule;
|
|
33
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const DEFAULT_RETRY_DELAYS_MS = [1_000, 3_000, 10_000];
|
|
2
|
+
const DEFAULT_PERIODIC_MS = 60_000;
|
|
3
|
+
/**
|
|
4
|
+
* Source-bound READY/periodic driver for durable Wave 9 recovery. At most one
|
|
5
|
+
* timer and one lock-serialized pass exist for the active adapter. A replaced
|
|
6
|
+
* adapter is stopped synchronously and an in-flight old pass cannot reschedule
|
|
7
|
+
* itself. Timers are unref'ed so this safety loop never keeps Node alive.
|
|
8
|
+
*/
|
|
9
|
+
export class Wave9LiveDurableReconciliationScheduler {
|
|
10
|
+
options;
|
|
11
|
+
retryDelaysMs;
|
|
12
|
+
periodicMs;
|
|
13
|
+
active;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
this.retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
17
|
+
this.periodicMs = options.periodicMs ?? DEFAULT_PERIODIC_MS;
|
|
18
|
+
if (this.retryDelaysMs.some((delay) => !Number.isFinite(delay) || delay < 0)) {
|
|
19
|
+
throw new Error('Wave 9 reconciliation retry delays must be finite and non-negative');
|
|
20
|
+
}
|
|
21
|
+
if (!Number.isFinite(this.periodicMs) || this.periodicMs <= 0) {
|
|
22
|
+
throw new Error('Wave 9 reconciliation periodic interval must be positive and finite');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
attach(source) {
|
|
26
|
+
if (this.active?.source === source && !this.active.stopped)
|
|
27
|
+
return;
|
|
28
|
+
this.stop();
|
|
29
|
+
const state = { source, stopped: false };
|
|
30
|
+
this.active = state;
|
|
31
|
+
source.on('readiness_changed', (readiness) => {
|
|
32
|
+
// Runtime installs this listener immediately before it publishes a
|
|
33
|
+
// reconnect adapter. Queue first, then let the timer callback verify
|
|
34
|
+
// current source identity, so a synchronously-fast READY cannot be lost.
|
|
35
|
+
if (readiness === 'READY' && !state.stopped) {
|
|
36
|
+
this.schedule(state, 0, 0, 'ready');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (source.readiness === 'READY') {
|
|
40
|
+
this.schedule(state, 0, 0, 'ready_replay');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
stopIfSourceChanged(current) {
|
|
44
|
+
if (this.active && this.active.source !== current)
|
|
45
|
+
this.stop();
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
const state = this.active;
|
|
49
|
+
if (!state)
|
|
50
|
+
return;
|
|
51
|
+
state.stopped = true;
|
|
52
|
+
if (state.timer !== undefined)
|
|
53
|
+
clearTimeout(state.timer);
|
|
54
|
+
state.timer = undefined;
|
|
55
|
+
this.active = undefined;
|
|
56
|
+
}
|
|
57
|
+
isCurrentReady(state) {
|
|
58
|
+
const source = state.source;
|
|
59
|
+
return !state.stopped
|
|
60
|
+
&& this.active === state
|
|
61
|
+
&& this.options.isCurrentSource(source)
|
|
62
|
+
&& source.isLive
|
|
63
|
+
&& source.mode === 'LIVE'
|
|
64
|
+
&& source.readiness === 'READY';
|
|
65
|
+
}
|
|
66
|
+
schedule(state, delayMs, retryIndex, trigger) {
|
|
67
|
+
if (state.stopped || this.active !== state || state.timer !== undefined || state.inFlight) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
state.timer = undefined;
|
|
72
|
+
if (!this.isCurrentReady(state)) {
|
|
73
|
+
if (!this.options.isCurrentSource(state.source)) {
|
|
74
|
+
state.stopped = true;
|
|
75
|
+
if (this.active === state)
|
|
76
|
+
this.active = undefined;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let passResult;
|
|
81
|
+
let passFailed = false;
|
|
82
|
+
const pass = this.options.operationLock.withAccountLock(`wave9_live_durable_reconciliation:${trigger}`, async () => {
|
|
83
|
+
if (!this.isCurrentReady(state))
|
|
84
|
+
return;
|
|
85
|
+
passResult = await this.options.reconcile(state.source);
|
|
86
|
+
this.options.onResult?.(trigger, passResult);
|
|
87
|
+
}).catch((error) => {
|
|
88
|
+
passFailed = true;
|
|
89
|
+
this.options.onError?.(trigger, error);
|
|
90
|
+
}).finally(() => {
|
|
91
|
+
state.inFlight = undefined;
|
|
92
|
+
if (!this.isCurrentReady(state)) {
|
|
93
|
+
if (!this.options.isCurrentSource(state.source)) {
|
|
94
|
+
state.stopped = true;
|
|
95
|
+
if (this.active === state)
|
|
96
|
+
this.active = undefined;
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const retryable = passFailed || passResult?.retryable === true;
|
|
101
|
+
if (retryable && retryIndex < this.retryDelaysMs.length) {
|
|
102
|
+
this.schedule(state, this.retryDelaysMs[retryIndex], retryIndex + 1, `ready_retry_${retryIndex + 1}`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Always retain a periodic pass. It catches ATTEMPTED/OPEN rows born
|
|
106
|
+
// after READY when a live submit's exact CID read was transiently
|
|
107
|
+
// unknown, while healthy rows are a cheap exact-stop no-op.
|
|
108
|
+
this.schedule(state, this.periodicMs, 0, 'periodic');
|
|
109
|
+
});
|
|
110
|
+
state.inFlight = pass;
|
|
111
|
+
}, delayMs);
|
|
112
|
+
timer.unref();
|
|
113
|
+
state.timer = timer;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type Wave9StrategyName } from '../portfolio/wave9-policy.js';
|
|
2
|
+
export type Wave9LiveExecutionState = 'attempted' | 'open' | 'closed';
|
|
3
|
+
export type Wave9LiveDirection = 'LONG' | 'SHORT';
|
|
4
|
+
export type Wave9LiveOrderSide = 'buy' | 'sell';
|
|
5
|
+
export type Wave9LivePositionSide = 'long' | 'short';
|
|
6
|
+
export interface Wave9LiveExecutionRecord {
|
|
7
|
+
candidateId: string;
|
|
8
|
+
missionId: string;
|
|
9
|
+
eventTime: string;
|
|
10
|
+
/** Canonical frozen-universe form, e.g. BTCUSDT. */
|
|
11
|
+
symbol: string;
|
|
12
|
+
strategy: Wave9StrategyName;
|
|
13
|
+
direction: Wave9LiveDirection;
|
|
14
|
+
side: Wave9LiveOrderSide;
|
|
15
|
+
/** Exact quantity authorized for the market-order attempt. */
|
|
16
|
+
quantity: number;
|
|
17
|
+
initialStop: number;
|
|
18
|
+
/** Deterministic Binance-safe idempotency key. */
|
|
19
|
+
clientOrderId: string;
|
|
20
|
+
state: Wave9LiveExecutionState;
|
|
21
|
+
attemptedAt: number;
|
|
22
|
+
updatedAt: number;
|
|
23
|
+
openedAt?: number;
|
|
24
|
+
closedAt?: number;
|
|
25
|
+
filledQuantity?: number;
|
|
26
|
+
entryPrice?: number;
|
|
27
|
+
exchangeOrderId?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Wave9LiveAttemptInput {
|
|
30
|
+
candidateId: string;
|
|
31
|
+
missionId: string;
|
|
32
|
+
eventTime: string;
|
|
33
|
+
symbol: string;
|
|
34
|
+
strategy: Wave9StrategyName;
|
|
35
|
+
direction: Wave9LiveDirection;
|
|
36
|
+
side: Wave9LiveOrderSide;
|
|
37
|
+
quantity: number;
|
|
38
|
+
initialStop: number;
|
|
39
|
+
clientOrderId: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Wave9LiveOpenInput {
|
|
42
|
+
filledQuantity?: number;
|
|
43
|
+
entryPrice?: number;
|
|
44
|
+
exchangeOrderId?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Final fill learned by resolving the exact deterministic entry CID. */
|
|
47
|
+
export interface Wave9LiveExactOrderFillInput {
|
|
48
|
+
filledQuantity: number;
|
|
49
|
+
exchangeOrderId: string;
|
|
50
|
+
entryPrice?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface Wave9LiveExecutionLedgerOptions {
|
|
53
|
+
pluginId?: string;
|
|
54
|
+
basePath?: string;
|
|
55
|
+
now?: () => number;
|
|
56
|
+
}
|
|
57
|
+
/** Stable Binance-compatible client order id (35 chars; Binance max is 36). */
|
|
58
|
+
export declare function wave9ClientOrderId(candidateId: string): string;
|
|
59
|
+
/** Backwards-compatible descriptive alias. */
|
|
60
|
+
export declare const wave9LiveClientOrderId: typeof wave9ClientOrderId;
|
|
61
|
+
export declare class Wave9LiveExecutionLedger {
|
|
62
|
+
private readonly filePath;
|
|
63
|
+
private readonly dir;
|
|
64
|
+
private readonly now;
|
|
65
|
+
private readonly entries;
|
|
66
|
+
constructor(options?: Wave9LiveExecutionLedgerOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Persist an execution attempt before exchange submission. Repeating the
|
|
69
|
+
* exact immutable authorization is idempotent; any mismatch fails closed.
|
|
70
|
+
*/
|
|
71
|
+
recordAttempt(input: Wave9LiveAttemptInput): Wave9LiveExecutionRecord;
|
|
72
|
+
/** Mark a confirmed fill. Repeating the exact confirmation is idempotent. */
|
|
73
|
+
markOpen(candidateId: string, input?: Wave9LiveOpenInput): Wave9LiveExecutionRecord;
|
|
74
|
+
/**
|
|
75
|
+
* Enrich an already-OPEN row after an exact deterministic entry lookup has
|
|
76
|
+
* proven the order terminal. This is deliberately narrower than markOpen:
|
|
77
|
+
* the final fill may populate a previously missing value or increase a
|
|
78
|
+
* crash-time partial fill, but it can never decrease, exceed authorization,
|
|
79
|
+
* or replace an already-bound exchange order identity.
|
|
80
|
+
*/
|
|
81
|
+
reconcileOpenFillFromExactOrder(candidateId: string, input: Wave9LiveExactOrderFillInput): Wave9LiveExecutionRecord;
|
|
82
|
+
/** Mark an attempted/open record terminal. Terminal rows are never deleted. */
|
|
83
|
+
markClosed(candidateId: string): Wave9LiveExecutionRecord;
|
|
84
|
+
getByCandidateId(candidateId: string): Wave9LiveExecutionRecord | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Latest confirmed-open row matching an exchange position.
|
|
87
|
+
*
|
|
88
|
+
* An `attempted` row is a durable replay tombstone: the exchange outcome
|
|
89
|
+
* may be ambiguous, so the candidate must never execute again, but the row
|
|
90
|
+
* is not proof that a later same-symbol/same-side position belongs to Wave 9.
|
|
91
|
+
* Only `markOpen()` establishes durable position ownership.
|
|
92
|
+
*/
|
|
93
|
+
findLatestForPosition(symbol: string, side: Wave9LivePositionSide | Wave9LiveOrderSide): Wave9LiveExecutionRecord | undefined;
|
|
94
|
+
/** Attempts count as executed because exchange outcome can be crash-ambiguous. */
|
|
95
|
+
getExecutedCandidateIds(): string[];
|
|
96
|
+
getAll(): Wave9LiveExecutionRecord[];
|
|
97
|
+
getFilePath(): string;
|
|
98
|
+
private currentTimestamp;
|
|
99
|
+
private requireCandidate;
|
|
100
|
+
private validateAttemptInput;
|
|
101
|
+
private validateOpenInput;
|
|
102
|
+
private sameAttempt;
|
|
103
|
+
private loadOrInit;
|
|
104
|
+
private persist;
|
|
105
|
+
private validateFile;
|
|
106
|
+
private validateStoredRecord;
|
|
107
|
+
}
|