@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,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;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — DO NOT EDIT.
|
|
2
|
+
// Canonical source of truth: shared/src/portfolio/wave9-policy.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
|
+
// Canonical frozen Wave 9 daily signal policy used by the default-off runtime parity
|
|
9
|
+
// observer. This deliberately calls the same generated condition registry and
|
|
10
|
+
// ATR implementation as the intelligence backtest instead of reimplementing
|
|
11
|
+
// momentum semantics in the plugin.
|
|
12
|
+
import { computeATR } from '../shared/indicators.js';
|
|
13
|
+
import { evaluateConditions } from '../signals/conditions/registry.js';
|
|
14
|
+
export const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
15
|
+
export const WAVE9_LOOKBACK_DAYS = 28;
|
|
16
|
+
export const WAVE9_ATR_PERIOD = 14;
|
|
17
|
+
export const WAVE9_ATR_MULTIPLE = 3;
|
|
18
|
+
export const WAVE9_CONTEXT_BARS = 250;
|
|
19
|
+
export const WAVE9_LONG_STRATEGY = 'tsmom_28d_long_flat_reversal_1d';
|
|
20
|
+
export const WAVE9_SHORT_STRATEGY = 'tsmom_28d_short_flat_reversal_1d';
|
|
21
|
+
export const WAVE9_SYMBOL_PRIORITY = [
|
|
22
|
+
'BTCUSDT',
|
|
23
|
+
'ETHUSDT',
|
|
24
|
+
'SOLUSDT',
|
|
25
|
+
'BNBUSDT',
|
|
26
|
+
'XRPUSDT',
|
|
27
|
+
'DOGEUSDT',
|
|
28
|
+
'ADAUSDT',
|
|
29
|
+
'LINKUSDT',
|
|
30
|
+
];
|
|
31
|
+
export const WAVE9_STRATEGY_PRIORITY = [
|
|
32
|
+
WAVE9_LONG_STRATEGY,
|
|
33
|
+
WAVE9_SHORT_STRATEGY,
|
|
34
|
+
];
|
|
35
|
+
function requireFinitePositive(value, label) {
|
|
36
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
37
|
+
throw new Error(`${label} must be a positive finite number`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Return the exact completed-daily context visible at a UTC daily boundary.
|
|
42
|
+
* The function is intentionally fail-closed: unordered, duplicated, gapped,
|
|
43
|
+
* non-UTC, or stale candles are rejected rather than silently repaired.
|
|
44
|
+
*/
|
|
45
|
+
export function completedDailyContext(rawBars, eventTime) {
|
|
46
|
+
const eventMs = eventTime.getTime();
|
|
47
|
+
if (!Number.isFinite(eventMs) || eventMs % DAY_MS !== 0) {
|
|
48
|
+
throw new Error('Wave 9 evaluation time must be a finite UTC daily boundary');
|
|
49
|
+
}
|
|
50
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
51
|
+
for (const [index, bar] of rawBars.entries()) {
|
|
52
|
+
const time = bar.time.getTime();
|
|
53
|
+
if (!Number.isFinite(time) || time % DAY_MS !== 0) {
|
|
54
|
+
throw new Error(`daily bar ${index} is not aligned to UTC midnight`);
|
|
55
|
+
}
|
|
56
|
+
if (time <= previous) {
|
|
57
|
+
throw new Error('daily bars must be strictly chronological and unique');
|
|
58
|
+
}
|
|
59
|
+
previous = time;
|
|
60
|
+
requireFinitePositive(bar.open, `daily bar ${index} open`);
|
|
61
|
+
requireFinitePositive(bar.high, `daily bar ${index} high`);
|
|
62
|
+
requireFinitePositive(bar.low, `daily bar ${index} low`);
|
|
63
|
+
requireFinitePositive(bar.close, `daily bar ${index} close`);
|
|
64
|
+
if (!Number.isFinite(bar.volume) || bar.volume < 0) {
|
|
65
|
+
throw new Error(`daily bar ${index} volume must be finite and non-negative`);
|
|
66
|
+
}
|
|
67
|
+
if (bar.high < Math.max(bar.open, bar.close) || bar.low > Math.min(bar.open, bar.close)) {
|
|
68
|
+
throw new Error(`daily bar ${index} has invalid OHLC geometry`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const completed = rawBars
|
|
72
|
+
.filter((bar) => bar.time.getTime() + DAY_MS <= eventMs)
|
|
73
|
+
.slice(-WAVE9_CONTEXT_BARS);
|
|
74
|
+
if (completed.length <= WAVE9_LOOKBACK_DAYS + 1) {
|
|
75
|
+
throw new Error(`Wave 9 requires at least ${WAVE9_LOOKBACK_DAYS + 2} completed daily bars`);
|
|
76
|
+
}
|
|
77
|
+
const expectedLastOpen = eventMs - DAY_MS;
|
|
78
|
+
const lastOpen = completed[completed.length - 1].time.getTime();
|
|
79
|
+
if (lastOpen !== expectedLastOpen) {
|
|
80
|
+
throw new Error(`latest completed daily bar must open at ${new Date(expectedLastOpen).toISOString()}`);
|
|
81
|
+
}
|
|
82
|
+
for (let index = 1; index < completed.length; index++) {
|
|
83
|
+
const prior = completed[index - 1].time.getTime();
|
|
84
|
+
const current = completed[index].time.getTime();
|
|
85
|
+
if (current - prior !== DAY_MS) {
|
|
86
|
+
throw new Error(`daily history has a gap between ${completed[index - 1].time.toISOString()} and ${completed[index].time.toISOString()}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return completed.map((bar) => ({ ...bar, time: new Date(bar.time) }));
|
|
90
|
+
}
|
|
91
|
+
function makeContext(bars, atr14) {
|
|
92
|
+
const latest = bars[bars.length - 1];
|
|
93
|
+
return {
|
|
94
|
+
symbol: 'WAVE9',
|
|
95
|
+
timestamp: new Date(latest.time.getTime() + DAY_MS),
|
|
96
|
+
ohlcv1h: bars,
|
|
97
|
+
ohlcv5m: [],
|
|
98
|
+
ohlcv4h: [],
|
|
99
|
+
ohlcv1d: bars,
|
|
100
|
+
currentPrice: latest.close,
|
|
101
|
+
atr14,
|
|
102
|
+
obImbalance: 0.5,
|
|
103
|
+
fundingRates: [],
|
|
104
|
+
fundingMean: 0,
|
|
105
|
+
fundingStd: 0.0001,
|
|
106
|
+
oiValues: [],
|
|
107
|
+
liqNearEntry: 0,
|
|
108
|
+
tradeFlow: [],
|
|
109
|
+
regime: 'UNKNOWN',
|
|
110
|
+
regimeConfidence: 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function conditionMet(context, type, params, direction) {
|
|
114
|
+
const result = evaluateConditions([{ type, params }], context, direction);
|
|
115
|
+
if (result.conditions.length !== 1 || result.conditions[0].name !== type) {
|
|
116
|
+
throw new Error(`canonical condition registry did not evaluate ${type}`);
|
|
117
|
+
}
|
|
118
|
+
return result.conditions[0].met;
|
|
119
|
+
}
|
|
120
|
+
/** Evaluate the exact Wave 9 entry-transition and signal-reversal semantics. */
|
|
121
|
+
export function evaluateWave9Daily(rawBars, eventTime) {
|
|
122
|
+
const bars = completedDailyContext(rawBars, eventTime);
|
|
123
|
+
const highs = bars.map((bar) => bar.high);
|
|
124
|
+
const lows = bars.map((bar) => bar.low);
|
|
125
|
+
const closes = bars.map((bar) => bar.close);
|
|
126
|
+
const atr14 = computeATR(highs, lows, closes, WAVE9_ATR_PERIOD);
|
|
127
|
+
requireFinitePositive(atr14, 'Wave 9 daily ATR(14)');
|
|
128
|
+
const context = makeContext(bars, atr14);
|
|
129
|
+
const currentClose = closes[closes.length - 1];
|
|
130
|
+
const currentPrior = closes[closes.length - 1 - WAVE9_LOOKBACK_DAYS];
|
|
131
|
+
const previousClose = closes[closes.length - 2];
|
|
132
|
+
const previousPrior = closes[closes.length - 2 - WAVE9_LOOKBACK_DAYS];
|
|
133
|
+
const currentReturn = (currentClose - currentPrior) / currentPrior;
|
|
134
|
+
const previousReturn = (previousClose - previousPrior) / previousPrior;
|
|
135
|
+
if (!Number.isFinite(currentReturn) || !Number.isFinite(previousReturn)) {
|
|
136
|
+
throw new Error('Wave 9 momentum inputs are not finite');
|
|
137
|
+
}
|
|
138
|
+
const longEntry = conditionMet(context, 'return_momentum_zero_cross', { tfHours: 24, lookback: WAVE9_LOOKBACK_DAYS, dirSign: 1 }, 'LONG');
|
|
139
|
+
const shortEntry = conditionMet(context, 'return_momentum_zero_cross', { tfHours: 24, lookback: WAVE9_LOOKBACK_DAYS, dirSign: -1 }, 'SHORT');
|
|
140
|
+
const exitLong = conditionMet(context, 'return_momentum', {
|
|
141
|
+
tfHours: 24,
|
|
142
|
+
lookback: WAVE9_LOOKBACK_DAYS,
|
|
143
|
+
minReturnPct: 0,
|
|
144
|
+
maxAbsReturnPct: 0,
|
|
145
|
+
dirSign: -1,
|
|
146
|
+
mode: 1,
|
|
147
|
+
}, 'SHORT');
|
|
148
|
+
const exitShort = conditionMet(context, 'return_momentum', {
|
|
149
|
+
tfHours: 24,
|
|
150
|
+
lookback: WAVE9_LOOKBACK_DAYS,
|
|
151
|
+
minReturnPct: 0,
|
|
152
|
+
maxAbsReturnPct: 0,
|
|
153
|
+
dirSign: 1,
|
|
154
|
+
mode: 1,
|
|
155
|
+
}, 'LONG');
|
|
156
|
+
const entries = [];
|
|
157
|
+
if (longEntry) {
|
|
158
|
+
entries.push({
|
|
159
|
+
strategy: WAVE9_LONG_STRATEGY,
|
|
160
|
+
direction: 'LONG',
|
|
161
|
+
stopPrice: currentClose - atr14 * WAVE9_ATR_MULTIPLE,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (shortEntry) {
|
|
165
|
+
entries.push({
|
|
166
|
+
strategy: WAVE9_SHORT_STRATEGY,
|
|
167
|
+
direction: 'SHORT',
|
|
168
|
+
stopPrice: currentClose + atr14 * WAVE9_ATR_MULTIPLE,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
eventTime: eventTime.toISOString(),
|
|
173
|
+
completedDailyOpen: bars[bars.length - 1].time.toISOString(),
|
|
174
|
+
currentReturn,
|
|
175
|
+
previousReturn,
|
|
176
|
+
atr14,
|
|
177
|
+
referencePrice: currentClose,
|
|
178
|
+
entries,
|
|
179
|
+
exitLong,
|
|
180
|
+
exitShort,
|
|
181
|
+
sourceBarCount: bars.length,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -160,11 +160,20 @@ register('price_sweep', (ctx, params, _dir, condCtx) => {
|
|
|
160
160
|
sweepLevel = nearestHigh;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
// Store in shared context for entry/stop rules and direction
|
|
163
|
+
// Store in shared context for entry/stop rules and direction.
|
|
164
|
+
// sweptDirection is written non-destructively, like the funding_extreme /
|
|
165
|
+
// cvd_divergence setters: only when a sweep actually resolved a side. An
|
|
166
|
+
// unconditional write (including null on a no-sweep bar) clobbered a
|
|
167
|
+
// direction set by an earlier condition in the same evaluation, so a
|
|
168
|
+
// from_sweep/from_funding strategy listing price_sweep AFTER another
|
|
169
|
+
// setter could never fire.
|
|
164
170
|
condCtx.sweepLevel = sweepLevel;
|
|
165
171
|
condCtx.recentLow = recentLow;
|
|
166
172
|
condCtx.recentHigh = recentHigh;
|
|
167
|
-
|
|
173
|
+
if (sweptLow)
|
|
174
|
+
condCtx.sweptDirection = 'LONG';
|
|
175
|
+
else if (sweptHigh)
|
|
176
|
+
condCtx.sweptDirection = 'SHORT';
|
|
168
177
|
const met = sweptLow || sweptHigh;
|
|
169
178
|
return {
|
|
170
179
|
met,
|
|
@@ -777,6 +786,56 @@ register('return_momentum', (ctx, params, direction) => {
|
|
|
777
786
|
: `${lookback}-bar ${tfHours}h return ${(ret * 100).toFixed(2)}% fails ${label} threshold ${(minReturnPct * 100).toFixed(1)}%${capText}`,
|
|
778
787
|
};
|
|
779
788
|
});
|
|
789
|
+
// Strict return-momentum episode transition. Unlike return_momentum, this is
|
|
790
|
+
// an event rather than a level: it fires once when the lookback return crosses
|
|
791
|
+
// zero (or an explicit threshold), then remains false until the sign resets.
|
|
792
|
+
// That makes stop-outs and capacity-rejected entries naturally lock out for
|
|
793
|
+
// the rest of the same momentum episode.
|
|
794
|
+
register('return_momentum_zero_cross', (ctx, params, direction) => {
|
|
795
|
+
const lookback = Math.max(1, Math.floor(params.lookback ?? 28));
|
|
796
|
+
const thresholdPct = Math.max(0, params.thresholdPct ?? 0);
|
|
797
|
+
const dirSign = params.dirSign ?? 0;
|
|
798
|
+
const tfHours = params.tfHours ?? 24;
|
|
799
|
+
const bars = pickBars(ctx, tfHours);
|
|
800
|
+
if (bars.length <= lookback + 1) {
|
|
801
|
+
return {
|
|
802
|
+
met: false,
|
|
803
|
+
value: 0,
|
|
804
|
+
threshold: thresholdPct * 100,
|
|
805
|
+
description: `Insufficient ${tfHours}h bars for return_momentum_zero_cross`,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const currentClose = bars[bars.length - 1].close;
|
|
809
|
+
const currentPrior = bars[bars.length - 1 - lookback].close;
|
|
810
|
+
const previousClose = bars[bars.length - 2].close;
|
|
811
|
+
const previousPrior = bars[bars.length - 2 - lookback].close;
|
|
812
|
+
const currentReturn = currentPrior > 0
|
|
813
|
+
? (currentClose - currentPrior) / currentPrior
|
|
814
|
+
: Number.NaN;
|
|
815
|
+
const previousReturn = previousPrior > 0
|
|
816
|
+
? (previousClose - previousPrior) / previousPrior
|
|
817
|
+
: Number.NaN;
|
|
818
|
+
const sign = dirSign !== 0
|
|
819
|
+
? Math.sign(dirSign)
|
|
820
|
+
: direction === 'LONG' ? 1 : direction === 'SHORT' ? -1 : 0;
|
|
821
|
+
const met = Number.isFinite(currentReturn)
|
|
822
|
+
&& Number.isFinite(previousReturn)
|
|
823
|
+
&& (sign > 0
|
|
824
|
+
? previousReturn <= thresholdPct && currentReturn > thresholdPct
|
|
825
|
+
: sign < 0
|
|
826
|
+
? previousReturn >= -thresholdPct && currentReturn < -thresholdPct
|
|
827
|
+
: false);
|
|
828
|
+
return {
|
|
829
|
+
met,
|
|
830
|
+
value: Number.isFinite(currentReturn)
|
|
831
|
+
? Math.round(currentReturn * 10000) / 100
|
|
832
|
+
: 0,
|
|
833
|
+
threshold: thresholdPct * 100,
|
|
834
|
+
description: Number.isFinite(currentReturn) && Number.isFinite(previousReturn)
|
|
835
|
+
? `${lookback}-bar ${tfHours}h return moved from ${(previousReturn * 100).toFixed(2)}% to ${(currentReturn * 100).toFixed(2)}%`
|
|
836
|
+
: `Invalid ${tfHours}h prices for return_momentum_zero_cross`,
|
|
837
|
+
};
|
|
838
|
+
});
|
|
780
839
|
// ─── 32. rsi_multi_pivot_divergence ──────────────────────────────────
|
|
781
840
|
// Strict N-pivot RSI divergence: price makes successively lower lows (or
|
|
782
841
|
// higher highs) while RSI makes the opposite, with optional volume dry-up
|
|
@@ -170,15 +170,25 @@ export function adaptStrategy(config, gateNamespace) {
|
|
|
170
170
|
const { conditions: pass1, condCtx } = evaluateConditions(config.conditions, ectx, null);
|
|
171
171
|
// Determine direction
|
|
172
172
|
const direction = resolveDirection(config.directionRule, ectx, condCtx);
|
|
173
|
-
// Pass 2:
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
// Pass 2: once the direction is known, re-evaluate the FULL condition
|
|
174
|
+
// array with it. Many registry conditions branch on `direction`
|
|
175
|
+
// (orderbook_imbalance, funding_contrarian, macd_crossover,
|
|
176
|
+
// bollinger_breakout, vwap_position, stoch_rsi_extreme, ichimoku_cloud,
|
|
177
|
+
// supertrend_direction, …) and their pass-1 result was computed against
|
|
178
|
+
// direction=null — typically the permissive either-side branch. A
|
|
179
|
+
// hand-curated allowlist here (the pre-fix 5-type set) silently drifted
|
|
180
|
+
// out of sync with the registry, so non-listed direction-dependent
|
|
181
|
+
// conditions kept their permissive pass-1 result forever (e.g. a
|
|
182
|
+
// fixed_long strategy's macd_crossover accepted a BEARISH crossover).
|
|
183
|
+
// Pass 2 always evaluated the whole array anyway — use it wholesale.
|
|
184
|
+
// Direction-insensitive conditions are pure functions of (ctx, params)
|
|
185
|
+
// and return identical results in both passes; entry/stop computation
|
|
186
|
+
// below deliberately keeps pass 1's condCtx (setter values don't depend
|
|
187
|
+
// on direction).
|
|
177
188
|
let finalConditions = pass1;
|
|
178
|
-
if (direction
|
|
189
|
+
if (direction) {
|
|
179
190
|
const { conditions: pass2 } = evaluateConditions(config.conditions, ectx, direction);
|
|
180
|
-
|
|
181
|
-
finalConditions = pass1.map((c, i) => directionSensitive.has(config.conditions[i].type) ? pass2[i] : c);
|
|
191
|
+
finalConditions = pass2;
|
|
182
192
|
}
|
|
183
193
|
const allMet = finalConditions.every(c => c.met);
|
|
184
194
|
let trade;
|
|
@@ -79,6 +79,18 @@ export declare class ExchangeSimulator extends EventEmitter {
|
|
|
79
79
|
* reload never regresses the agent's give-back signal. */
|
|
80
80
|
replaceState(newState: SimulatorState): void;
|
|
81
81
|
getState(): SimulatorState;
|
|
82
|
+
/** Default max quote age a NEW-exposure fill may price against (issue #202).
|
|
83
|
+
* Generous vs the 5s paper feed cadence; env RC_PAPER_MAX_QUOTE_AGE_MS
|
|
84
|
+
* overrides. */
|
|
85
|
+
static readonly DEFAULT_MAX_QUOTE_AGE_MS = 45000;
|
|
86
|
+
private maxQuoteAgeMs;
|
|
87
|
+
/** Quote age from the ticker's own timestamp. A missing/invalid timestamp
|
|
88
|
+
* reads as age 0 (fail-open — the guard cannot fire on it). */
|
|
89
|
+
private quoteAgeMs;
|
|
90
|
+
/** Reject fills that would OPEN or GROW exposure on a stale quote. Risk-
|
|
91
|
+
* reducing fills (closes/partials against an existing position) are always
|
|
92
|
+
* allowed — blocking a close on a broken feed compounds the risk. */
|
|
93
|
+
private assertQuoteFresh;
|
|
82
94
|
private shouldFillLimit;
|
|
83
95
|
private executeMarketFill;
|
|
84
96
|
private executeLimitFill;
|