@reefclaw/openclaw-plugin 0.1.6 → 0.1.8
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/public-market-data-api.d.ts +14 -0
- package/ccxt/public-market-data-api.js +15 -1
- package/config/plugin-config-io.d.ts +7 -0
- package/config/plugin-config-io.js +15 -0
- package/index.js +116 -31
- 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/live/fill-price.d.ts +13 -0
- package/live/fill-price.js +37 -0
- package/live/live-adapter.d.ts +33 -1
- package/live/live-adapter.js +176 -47
- package/live/position-state-store.d.ts +4 -0
- package/live/stop-watcher.d.ts +8 -1
- package/live/stop-watcher.js +5 -2
- package/onboarding/runtime.d.ts +18 -0
- package/onboarding/runtime.js +45 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/portfolio/reentry-tracker.d.ts +36 -0
- package/portfolio/reentry-tracker.js +127 -0
- package/scripts/assemble.mjs +18 -2
- package/signals/conditions/registry.js +11 -2
- package/signals/strategy-adapter.js +17 -7
- package/simulator/exchange-simulator.d.ts +12 -0
- package/simulator/exchange-simulator.js +73 -3
- package/simulator/types.d.ts +4 -0
- package/skills/reefclaw/SKILL.md +2 -0
- package/tools/assessment-validation.d.ts +21 -0
- package/tools/assessment-validation.js +58 -0
- package/tools/attach-brackets.js +165 -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/create-order.d.ts +7 -0
- package/tools/create-order.js +42 -3
- package/tools/get-setup-detail.js +12 -1
- package/tools/modify-stop.js +5 -5
- package/tools/modify-target.js +5 -5
- package/tools/scan-pairs.d.ts +4 -0
- package/tools/scan-pairs.js +4 -1
- package/tools/set-trading-mode.js +23 -6
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +123 -0
- package/venues/hyperliquid/hl-bracket-coordinator.js +533 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +61 -3
- package/venues/hyperliquid/hl-live-adapter.js +380 -5
- package/venues/hyperliquid/hl-public.js +8 -1
|
@@ -43,17 +43,10 @@ const TAG = 'reconcile-db-vs-exchange';
|
|
|
43
43
|
function canonical(symbol) {
|
|
44
44
|
return symbol.split(':')[0];
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
* Close webapp `positions` rows that are status='open' but absent from the
|
|
48
|
-
* (trusted) exchange snapshot. Returns the number of synthetic closes posted.
|
|
49
|
-
*
|
|
50
|
-
* @param exchangeSymbols symbols from a TRUSTED snapshot (getPositionsOrNull()
|
|
51
|
-
* !== null). Pass only when the fetch genuinely succeeded.
|
|
52
|
-
*/
|
|
53
|
-
export async function reconcileDbOpenVsExchange(ctx, exchangeSymbols, nowMs = Date.now()) {
|
|
46
|
+
export async function reconcileDbOpenVsExchange(ctx, exchangeSymbols, nowMs = Date.now(), opts = {}) {
|
|
54
47
|
if (!ctx.decisionsClient || !ctx.userId)
|
|
55
48
|
return 0;
|
|
56
|
-
const resp = await ctx.decisionsClient.getOpenPositions(ctx.userId);
|
|
49
|
+
const resp = await ctx.decisionsClient.getOpenPositions(ctx.userId, ctx.resolveMode?.(), ctx.resolveExchange?.());
|
|
57
50
|
if (!resp) {
|
|
58
51
|
// null = fetch failed (network / terminal). NEVER treat as "no open rows"
|
|
59
52
|
// (null≠empty) — skip; the next boot / periodic sweep retries.
|
|
@@ -77,12 +70,17 @@ export async function reconcileDbOpenVsExchange(ctx, exchangeSymbols, nowMs = Da
|
|
|
77
70
|
logger.warn(TAG, `${o.symbol}: bad size/price from DB (size=${o.remainingSize}, entry=${o.entryPrice}) — skipping close`);
|
|
78
71
|
continue;
|
|
79
72
|
}
|
|
73
|
+
const lastExitInfo = opts.describeLastExit?.(o.symbol);
|
|
74
|
+
if (lastExitInfo) {
|
|
75
|
+
logger.warn(TAG, `${o.symbol}: capture-miss attribution — ${lastExitInfo}`);
|
|
76
|
+
}
|
|
80
77
|
const close = {
|
|
81
78
|
positionId: o.id,
|
|
82
79
|
closeAt: nowMs,
|
|
83
80
|
closeReason: 'reconciler_observed_flat',
|
|
84
81
|
closeAssessment: {
|
|
85
|
-
source: 'db_exchange_sweep_boot',
|
|
82
|
+
source: opts.source ?? 'db_exchange_sweep_boot',
|
|
83
|
+
...(lastExitInfo ? { last_engine_trade: lastExitInfo } : {}),
|
|
86
84
|
synthetic: true,
|
|
87
85
|
note: 'Position open in DB but absent from the exchange snapshot. It was ' +
|
|
88
86
|
'closed on-exchange via a path that bypassed close_position (bracket ' +
|
|
@@ -112,3 +110,61 @@ export async function reconcileDbOpenVsExchange(ctx, exchangeSymbols, nowMs = Da
|
|
|
112
110
|
}
|
|
113
111
|
return posted;
|
|
114
112
|
}
|
|
113
|
+
// ---- Periodic sweep (issues #199/#203) ----
|
|
114
|
+
//
|
|
115
|
+
// The boot-only sweep left phantoms alive for DAYS between restarts (paper:
|
|
116
|
+
// the LTC phantom of issue #199 lived 26.5h; live: 38 of 85 closes in 14 days
|
|
117
|
+
// were harvested only at the daily restart, issue #203). This interval runs
|
|
118
|
+
// the same DB-authoritative pass continuously with the same trusted-snapshot
|
|
119
|
+
// discipline: getPositionsOrNull() null → skip, never treat as flat.
|
|
120
|
+
export const DEFAULT_DB_RECONCILE_INTERVAL_MS = 300_000;
|
|
121
|
+
const MIN_DB_RECONCILE_INTERVAL_MS = 60_000;
|
|
122
|
+
/** Resolve the sweep interval from RC_DB_RECONCILE_INTERVAL_MS.
|
|
123
|
+
* 'off' or '0' disables (returns null); values are clamped to ≥60s. */
|
|
124
|
+
export function resolveDbReconcileIntervalMs(raw = process.env.RC_DB_RECONCILE_INTERVAL_MS) {
|
|
125
|
+
if (raw === 'off' || raw === '0')
|
|
126
|
+
return null;
|
|
127
|
+
const n = Number(raw);
|
|
128
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
129
|
+
return DEFAULT_DB_RECONCILE_INTERVAL_MS;
|
|
130
|
+
return Math.max(MIN_DB_RECONCILE_INTERVAL_MS, n);
|
|
131
|
+
}
|
|
132
|
+
export function startPeriodicDbReconcile(deps, intervalMs = resolveDbReconcileIntervalMs()) {
|
|
133
|
+
if (intervalMs === null) {
|
|
134
|
+
logger.info(TAG, 'periodic sweep disabled (RC_DB_RECONCILE_INTERVAL_MS=off)');
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
let running = false;
|
|
138
|
+
const runOnce = async () => {
|
|
139
|
+
if (running)
|
|
140
|
+
return 0; // concurrency guard — a slow sweep never overlaps
|
|
141
|
+
running = true;
|
|
142
|
+
try {
|
|
143
|
+
const adapter = deps.resolveAdapter();
|
|
144
|
+
if (!adapter)
|
|
145
|
+
return 0;
|
|
146
|
+
const positions = await adapter.getPositionsOrNull();
|
|
147
|
+
if (positions === null) {
|
|
148
|
+
// Untrusted snapshot — NEVER read as flat (null≠empty).
|
|
149
|
+
logger.warn(TAG, 'periodic sweep skipped — positions fetch untrusted (null)');
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
return await reconcileDbOpenVsExchange(deps, positions.map((p) => p.symbol), Date.now(), { source: 'db_exchange_sweep_periodic', describeLastExit: deps.describeLastExit });
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
logger.warn(TAG, `periodic sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
running = false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const timer = setInterval(() => { void runOnce(); }, intervalMs);
|
|
163
|
+
// Never keep the process alive just for the sweep.
|
|
164
|
+
timer.unref?.();
|
|
165
|
+
logger.info(TAG, `periodic DB-vs-exchange sweep started (interval ${intervalMs}ms)`);
|
|
166
|
+
return {
|
|
167
|
+
stop: () => clearInterval(timer),
|
|
168
|
+
runOnce,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Minimal order shape needed to resolve a fill price. */
|
|
2
|
+
export interface FillPriceOrder {
|
|
3
|
+
average: number | null;
|
|
4
|
+
filled: number;
|
|
5
|
+
cost: number;
|
|
6
|
+
/** Present only to make explicit that we deliberately DO NOT read it. */
|
|
7
|
+
price?: number | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The average price a filled order actually executed at, or null if it can't
|
|
11
|
+
* be determined from the response. Never returns the limit price.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fillPriceFromOrder(order: FillPriceOrder): number | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Resolve the ACTUAL average fill price of a (partially) filled order.
|
|
2
|
+
//
|
|
3
|
+
// Issue #196 (prod LTC/USDT 2026-07-14, TAO/USDT 2026-07-15): for marketable
|
|
4
|
+
// LIMIT orders, Binance's synchronous USD-M order response
|
|
5
|
+
// (newOrderRespType=RESULT) can carry avgPrice=0 even though executedQty>0 —
|
|
6
|
+
// per the Binance doc, RESULT only guarantees the final fill for MARKET orders
|
|
7
|
+
// and LIMIT orders with a special timeInForce, NOT a plain GTC LIMIT. Code that
|
|
8
|
+
// then used `order.price` (the LIMIT price) as the entry proxy validated bracket
|
|
9
|
+
// geometry / journaled the entry against a price the order never filled at —
|
|
10
|
+
// several percent off for a marketable limit that crossed. That produced:
|
|
11
|
+
// - false "bracket direction invalid" auto-flattens (LTC: SELL limit 43 filled
|
|
12
|
+
// 44.49, target 43.21 compared vs 43 → "must be BELOW 43" → needless scratch), and
|
|
13
|
+
// - wrong journal entry prices (TAO: BUY limit 202 filled ~196.23 → journaled 202).
|
|
14
|
+
//
|
|
15
|
+
// The LIMIT price is NEVER a valid substitute for the fill. This resolver
|
|
16
|
+
// returns the true average fill from the response alone (no network), else null;
|
|
17
|
+
// callers must re-query or fall back to a mark price, never to `order.price`.
|
|
18
|
+
/**
|
|
19
|
+
* The average price a filled order actually executed at, or null if it can't
|
|
20
|
+
* be determined from the response. Never returns the limit price.
|
|
21
|
+
*/
|
|
22
|
+
export function fillPriceFromOrder(order) {
|
|
23
|
+
// 1. The exchange-reported average fill price, when present and positive.
|
|
24
|
+
if (typeof order.average === 'number' && Number.isFinite(order.average) && order.average > 0) {
|
|
25
|
+
return order.average;
|
|
26
|
+
}
|
|
27
|
+
// 2. VWAP from cost/filled. For USD-M futures ccxt maps cost = cumQuote =
|
|
28
|
+
// Σ(price×qty), so cost/filled = Σ(price×qty)/Σqty = the exact avgPrice.
|
|
29
|
+
// Covers the case where avgPrice=0 in the response but cumQuote settled.
|
|
30
|
+
if (typeof order.cost === 'number' && Number.isFinite(order.cost) && order.cost > 0 &&
|
|
31
|
+
typeof order.filled === 'number' && Number.isFinite(order.filled) && order.filled > 0) {
|
|
32
|
+
return order.cost / order.filled;
|
|
33
|
+
}
|
|
34
|
+
// 3. Unknown. The caller MUST NOT fall back to order.price (the limit) — that
|
|
35
|
+
// is exactly the marketable-limit bug this helper exists to prevent.
|
|
36
|
+
return null;
|
|
37
|
+
}
|
package/live/live-adapter.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { BracketManager } from './bracket-manager.js';
|
|
|
17
17
|
import { type BracketMode } from '../config/brackets-config.js';
|
|
18
18
|
import { type UserDataStreamMode, type UserDataStreamTunables } from '../config/user-data-stream-config.js';
|
|
19
19
|
import { UserDataStreamController } from './user-data-stream-controller.js';
|
|
20
|
-
import type
|
|
20
|
+
import { type AutoCaptureContext } from '../ingest/position-auto-capture.js';
|
|
21
21
|
import type { TradeStoreClient } from '../ingest/trade-store-client.js';
|
|
22
22
|
/** Optional audit-trail wiring (TRADE_AUDIT_TRAIL_PLAN Phase 1). Caller
|
|
23
23
|
* passes this only when userDataStream.dbWrite='on' AND the WEBAPP_INGEST_TOKEN
|
|
@@ -79,6 +79,7 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
|
|
|
79
79
|
private lastIncomeRefreshMs;
|
|
80
80
|
private incomeAnchorUtcDay;
|
|
81
81
|
private liveMetadata;
|
|
82
|
+
private autoCapture?;
|
|
82
83
|
private _readiness;
|
|
83
84
|
get readiness(): AdapterReadiness;
|
|
84
85
|
/** Session-start NAV, captured once at initialization. Used for drawdown calculation. */
|
|
@@ -143,6 +144,27 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
|
|
|
143
144
|
* the subsequent flatten.
|
|
144
145
|
*/
|
|
145
146
|
private attachBracketsAsync;
|
|
147
|
+
/**
|
|
148
|
+
* Patch `order.average` with the authoritative average fill price when a
|
|
149
|
+
* filled order came back without one (marketable-limit RESULT quirk, #196).
|
|
150
|
+
* Re-queries by clientOrderId (GET /fapi/v1/order, weight 1). Best-effort:
|
|
151
|
+
* on any failure or an unresolvable re-query the order is left as-is —
|
|
152
|
+
* downstream then falls back to a mark price / skips capture, NEVER to the
|
|
153
|
+
* limit price. Mutates `order` in place.
|
|
154
|
+
*/
|
|
155
|
+
private enrichFilledAvgPrice;
|
|
156
|
+
/**
|
|
157
|
+
* Journal the close produced by an internal auto-flatten
|
|
158
|
+
* (`bracket_attach_failed`), so the position row can't orphan as
|
|
159
|
+
* status='open' (issue #196). These flattens go straight to
|
|
160
|
+
* `this.closePosition`, bypassing the close_position tool, so
|
|
161
|
+
* `onClosePositionFilled` never runs for them. Fire-and-forget + fail-open —
|
|
162
|
+
* never blocks or throws into the flatten path. The entry journal may not be
|
|
163
|
+
* ready at flatten time (the entry's WS fill can land ~1-2s later), so
|
|
164
|
+
* `onAutoFlattenClose` polls briefly for the webappPositionId; the boot DB
|
|
165
|
+
* reconcile sweep is the backstop if the entry never journals.
|
|
166
|
+
*/
|
|
167
|
+
private captureAutoFlattenClose;
|
|
146
168
|
/** Install the Wave 9-only autonomous recovery bridge. With no handler,
|
|
147
169
|
* every generic bracket lifecycle remains byte-for-byte behaviorally
|
|
148
170
|
* unchanged. Runtime reapplies this setter after adapter reconnects. */
|
|
@@ -259,6 +281,16 @@ export declare class LiveAdapter extends EventEmitter implements IExchangeAdapte
|
|
|
259
281
|
* drift shape (re-emitted upstream as `bracket_drift`) plus a loud log so
|
|
260
282
|
* the operator knows re-protection is required. */
|
|
261
283
|
private reattachBracketsAfterFailedClose;
|
|
284
|
+
/** Null-honest balance read: null = the fetch FAILED (429 / weight-paced /
|
|
285
|
+
* banned) — the caller MUST treat it as UNKNOWN, never as a zero balance.
|
|
286
|
+
* Same null≠empty contract as getPositionsOrNull. Decision paths (sizing,
|
|
287
|
+
* pre-trade risk) must use this: the legacy getBalance() collapse below
|
|
288
|
+
* reads as walletTotal=0 → ~−100% drawdown → RED zone. */
|
|
289
|
+
getBalanceOrNull(): Promise<CcxtBalance | null>;
|
|
290
|
+
/** Legacy display-path read. Collapses a FAILED fetch to an empty balance
|
|
291
|
+
* object — acceptable for read-only surfaces (fetch_balance tool,
|
|
292
|
+
* risk-summary display), a phantom-zero hazard for anything that decides.
|
|
293
|
+
* Decision paths use getBalanceOrNull(). */
|
|
262
294
|
getBalance(): Promise<CcxtBalance>;
|
|
263
295
|
/** Re-anchor the realized-today seed + sessionStartNav from /fapi/v1/income.
|
|
264
296
|
* Throttled to {@link INCOME_REFRESH_MIN_INTERVAL_MS}; UTC date rollover
|
package/live/live-adapter.js
CHANGED
|
@@ -22,9 +22,11 @@ import { BracketReconciler } from './bracket-reconciler.js';
|
|
|
22
22
|
import { LiveBracketApi } from './live-bracket-api.js';
|
|
23
23
|
import { generateBracketId, buildBracketCid, parseBracketCid } from './bracket-id.js';
|
|
24
24
|
import { validateStopDirection, validateTargetDirection } from './bracket-params.js';
|
|
25
|
+
import { fillPriceFromOrder } from './fill-price.js';
|
|
25
26
|
import { bracketsEnabled } from '../config/brackets-config.js';
|
|
26
27
|
import { userDataStreamEnabled, userDataStreamAuthoritative, DEFAULT_TUNABLES as USER_DATA_STREAM_DEFAULT_TUNABLES, } from '../config/user-data-stream-config.js';
|
|
27
28
|
import { UserDataStreamController } from './user-data-stream-controller.js';
|
|
29
|
+
import { onAutoFlattenClose } from '../ingest/position-auto-capture.js';
|
|
28
30
|
import { assertNotShuttingDown, registerOp } from '../lifecycle/shutdown-coordinator.js';
|
|
29
31
|
import { updateMfe } from '../mfe.js';
|
|
30
32
|
import { computeInvalidationHit } from '../pinned-plan.js';
|
|
@@ -171,6 +173,11 @@ export class LiveAdapter extends EventEmitter {
|
|
|
171
173
|
// getPositions tick, dropped on closePosition or when a symbol vanishes from
|
|
172
174
|
// the open-positions snapshot.
|
|
173
175
|
liveMetadata = new Map();
|
|
176
|
+
// Position-decision auto-capture wiring (optional). Retained so the internal
|
|
177
|
+
// bracket_attach_failed auto-flatten can journal its close (issue #196): that
|
|
178
|
+
// flatten bypasses the close_position tool, so onClosePositionFilled never
|
|
179
|
+
// runs and the row would orphan as status='open'.
|
|
180
|
+
autoCapture;
|
|
174
181
|
// Readiness state — mutable, starts INIT_PENDING
|
|
175
182
|
_readiness = 'INIT_PENDING';
|
|
176
183
|
get readiness() {
|
|
@@ -194,6 +201,11 @@ export class LiveAdapter extends EventEmitter {
|
|
|
194
201
|
this.mode = mode;
|
|
195
202
|
this.bracketMode = bracketMode;
|
|
196
203
|
this.userDataStreamMode = userDataStreamMode;
|
|
204
|
+
// Retained for the auto-flatten journal close capture (issue #196): the
|
|
205
|
+
// adapter-internal `closePosition(…, 'bracket_attach_failed')` calls bypass
|
|
206
|
+
// the close_position tool, so onClosePositionFilled never runs for them —
|
|
207
|
+
// the row orphaned as status='open' until this capture existed.
|
|
208
|
+
this.autoCapture = autoCapture;
|
|
197
209
|
this.api = new BinancePrivateApi(config);
|
|
198
210
|
if (mode === 'MICRO_LIVE') {
|
|
199
211
|
// sizeCapMultiplier is 1.0 — the agent already sizes for the real wallet.
|
|
@@ -742,6 +754,19 @@ export class LiveAdapter extends EventEmitter {
|
|
|
742
754
|
}
|
|
743
755
|
// ---- Post-submission bookkeeping ----
|
|
744
756
|
this.intentJournal.updateIntent(clientOrderId, { status: 'confirmed', exchangeOrderId: order.id });
|
|
757
|
+
// ---- Fill-price enrichment (issue #196) ----
|
|
758
|
+
// For a marketable LIMIT order Binance's synchronous RESULT response can
|
|
759
|
+
// come back avgPrice=0 even though it filled (RESULT only guarantees the
|
|
760
|
+
// fill for MARKET / special-timeInForce LIMIT orders). Every downstream
|
|
761
|
+
// consumer — bracket direction validation, slippage, journal entry capture
|
|
762
|
+
// — MUST key off the real fill, never the limit price. Re-query by
|
|
763
|
+
// clientOrderId (weight 1) to patch order.average BEFORE any of them read
|
|
764
|
+
// it. Market orders report avgPrice reliably, so this fires only on the
|
|
765
|
+
// filled-limit edge; best-effort — a failed re-query leaves order.average
|
|
766
|
+
// as-is (consumers then use a mark price / skip, never the limit).
|
|
767
|
+
if (order.status === 'closed' && order.filled > 0 && fillPriceFromOrder(order) === null) {
|
|
768
|
+
await this.enrichFilledAvgPrice(order, clientOrderId, symbol);
|
|
769
|
+
}
|
|
745
770
|
// Track slippage for market orders that filled immediately
|
|
746
771
|
if (order.status === 'closed' && order.average != null && expectedPrice > 0) {
|
|
747
772
|
this.slippageTracker.recordFill({
|
|
@@ -781,60 +806,81 @@ export class LiveAdapter extends EventEmitter {
|
|
|
781
806
|
// leg, register the bracket and (for market orders) attach immediately.
|
|
782
807
|
// Limit orders defer attach to the poller's 'filled' event handler.
|
|
783
808
|
if (this.bracketManager && this.bracketLedger && metadata && (metadata.stopPrice !== undefined || metadata.targetPrice !== undefined)) {
|
|
784
|
-
// Defense-in-depth
|
|
785
|
-
// errors against the ref price
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
809
|
+
// Defense-in-depth direction re-check. The pre-submission check above
|
|
810
|
+
// already rejected sign errors against the ref price; this re-checks once
|
|
811
|
+
// the order's outcome is known. Reference price by order state:
|
|
812
|
+
// - FILLED (status=closed, filled>0): the ACTUAL average fill — NEVER
|
|
813
|
+
// the limit price (issue #196). A marketable limit can fill percent-
|
|
814
|
+
// scale away from its limit, so comparing the bracket to the limit
|
|
815
|
+
// gives a FALSE invalid (needless auto-flatten — LTC 2026-07-14) or
|
|
816
|
+
// lets a genuinely wrong stop through. If the fill price is
|
|
817
|
+
// unresolvable, SKIP the check — the bracket attach (Binance -2021 on
|
|
818
|
+
// a wrong-side stop) is the arbiter; validating against the limit
|
|
819
|
+
// would resurrect the bug.
|
|
820
|
+
// - still-OPEN resting limit: no fill yet, so the limit price IS the
|
|
821
|
+
// correct reference (the order fills at/through its limit).
|
|
822
|
+
// A failure HERE is post-submission — clean up (flatten the filled
|
|
823
|
+
// position / cancel the resting order) BEFORE throwing.
|
|
824
|
+
let dirRef = null;
|
|
825
|
+
if (order.status === 'closed' && order.filled > 0) {
|
|
826
|
+
dirRef = fillPriceFromOrder(order);
|
|
827
|
+
if (dirRef === null) {
|
|
828
|
+
logger.warn(TAG, `Post-fill bracket direction check skipped for ${symbol}: actual fill price unresolvable (avg=${order.average ?? 'null'} cost=${order.cost} filled=${order.filled}); NOT validating against the limit price — the bracket attach (Binance -2021) is the arbiter`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else if (order.status === 'open') {
|
|
832
|
+
const lim = order.price ?? price ?? 0;
|
|
833
|
+
dirRef = lim > 0 ? lim : null;
|
|
834
|
+
}
|
|
835
|
+
let dirMsg = null;
|
|
836
|
+
if (dirRef !== null && dirRef > 0) {
|
|
793
837
|
if (metadata.stopPrice !== undefined) {
|
|
794
|
-
dirMsg = validateStopDirection(side,
|
|
838
|
+
dirMsg = validateStopDirection(side, dirRef, metadata.stopPrice);
|
|
795
839
|
}
|
|
796
840
|
if (!dirMsg && metadata.targetPrice !== undefined) {
|
|
797
|
-
dirMsg = validateTargetDirection(side,
|
|
841
|
+
dirMsg = validateTargetDirection(side, dirRef, metadata.targetPrice);
|
|
798
842
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
catch (err) {
|
|
817
|
-
logger.warn(TAG, `Post-fill flatten attempt ${a}/4 for ${symbol} failed: ${formatError(err)}`);
|
|
818
|
-
if (a < 4)
|
|
819
|
-
await sleep(1500);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
if (!flattened) {
|
|
823
|
-
logger.error(TAG, `CRITICAL: post-fill flatten of ${symbol} FAILED after 4 attempts — position is NAKED, immediate operator intervention required`);
|
|
824
|
-
this.emit('emergency_progress', { action: 'flatten', status: 'failed', symbol, message: `Naked ${symbol} after invalid bracket — manual close required` });
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
else if (order.status === 'open') {
|
|
828
|
-
logger.error(TAG, `Post-submit bracket direction invalid for ${symbol} (${dirMsg}) — cancelling the resting entry order so it can't fill unprotected`);
|
|
843
|
+
}
|
|
844
|
+
if (dirMsg) {
|
|
845
|
+
if (order.status === 'closed' && order.filled > 0) {
|
|
846
|
+
logger.error(TAG, `Post-fill bracket direction invalid for ${symbol} (${dirMsg}, fill=${dirRef}) — flattening the just-filled position to avoid a naked entry`);
|
|
847
|
+
// We have HARD evidence the entry filled (status=closed, filled>0).
|
|
848
|
+
// closePosition throws "No open position" when its attempt-1
|
|
849
|
+
// fetchPositions returns a *confirmed-empty* snapshot — which, in
|
|
850
|
+
// the moments right after a fill, is almost always Binance
|
|
851
|
+
// position-state lagging the order response, NOT a real flat.
|
|
852
|
+
// Accepting that single failure would fail open and leave the
|
|
853
|
+
// position naked (the exact high-risk branch this cleanup exists
|
|
854
|
+
// for). Retry across the lag window instead of trusting one snapshot.
|
|
855
|
+
// closePosition journals the auto-flatten close itself (keyed on the
|
|
856
|
+
// 'bracket_attach_failed' reason) so the row can't orphan (#196).
|
|
857
|
+
let flattened = false;
|
|
858
|
+
for (let a = 1; a <= 4 && !flattened; a++) {
|
|
829
859
|
try {
|
|
830
|
-
await this.
|
|
860
|
+
await this.closePosition(symbol, 'bracket_attach_failed');
|
|
861
|
+
flattened = true;
|
|
831
862
|
}
|
|
832
863
|
catch (err) {
|
|
833
|
-
logger.
|
|
864
|
+
logger.warn(TAG, `Post-fill flatten attempt ${a}/4 for ${symbol} failed: ${formatError(err)}`);
|
|
865
|
+
if (a < 4)
|
|
866
|
+
await sleep(1500);
|
|
834
867
|
}
|
|
835
868
|
}
|
|
836
|
-
|
|
869
|
+
if (!flattened) {
|
|
870
|
+
logger.error(TAG, `CRITICAL: post-fill flatten of ${symbol} FAILED after 4 attempts — position is NAKED, immediate operator intervention required`);
|
|
871
|
+
this.emit('emergency_progress', { action: 'flatten', status: 'failed', symbol, message: `Naked ${symbol} after invalid bracket — manual close required` });
|
|
872
|
+
}
|
|
837
873
|
}
|
|
874
|
+
else if (order.status === 'open') {
|
|
875
|
+
logger.error(TAG, `Post-submit bracket direction invalid for ${symbol} (${dirMsg}) — cancelling the resting entry order so it can't fill unprotected`);
|
|
876
|
+
try {
|
|
877
|
+
await this.api.cancelOrderByClientId(clientOrderId, symbol);
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
logger.error(TAG, `Cancel of resting entry after invalid bracket failed for ${symbol}: ${formatError(err)}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
throw new Error(`Bracket rejected (post-submit): ${dirMsg}`);
|
|
838
884
|
}
|
|
839
885
|
const bracketId = generateBracketId();
|
|
840
886
|
this.bracketManager.registerEntry({ symbol, side, stopPrice: metadata.stopPrice, targetPrice: metadata.targetPrice }, bracketId, clientOrderId);
|
|
@@ -866,7 +912,8 @@ export class LiveAdapter extends EventEmitter {
|
|
|
866
912
|
const result = await this.bracketManager.attachBrackets(symbol, entryAmount);
|
|
867
913
|
if (!result.ok) {
|
|
868
914
|
logger.error(TAG, `Bracket attach FAILED for ${symbol} after ${result.attempts} attempts: ${result.error}. Auto-flattening position.`);
|
|
869
|
-
// Best-effort emergency close. closePosition has its own retry logic
|
|
915
|
+
// Best-effort emergency close. closePosition has its own retry logic +
|
|
916
|
+
// journals the auto-flatten close itself (keyed on the reason, #196).
|
|
870
917
|
try {
|
|
871
918
|
await this.closePosition(symbol, 'bracket_attach_failed');
|
|
872
919
|
}
|
|
@@ -880,6 +927,64 @@ export class LiveAdapter extends EventEmitter {
|
|
|
880
927
|
logger.error(TAG, `attachBracketsAsync unexpected error for ${symbol}: ${formatError(err)}`);
|
|
881
928
|
}
|
|
882
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Patch `order.average` with the authoritative average fill price when a
|
|
932
|
+
* filled order came back without one (marketable-limit RESULT quirk, #196).
|
|
933
|
+
* Re-queries by clientOrderId (GET /fapi/v1/order, weight 1). Best-effort:
|
|
934
|
+
* on any failure or an unresolvable re-query the order is left as-is —
|
|
935
|
+
* downstream then falls back to a mark price / skips capture, NEVER to the
|
|
936
|
+
* limit price. Mutates `order` in place.
|
|
937
|
+
*/
|
|
938
|
+
async enrichFilledAvgPrice(order, clientOrderId, symbol) {
|
|
939
|
+
const rawAvg = order.average;
|
|
940
|
+
try {
|
|
941
|
+
const requeried = await this.api.fetchOrderByClientId(clientOrderId, symbol);
|
|
942
|
+
const resolved = requeried ? fillPriceFromOrder(requeried) : null;
|
|
943
|
+
if (resolved !== null && resolved > 0) {
|
|
944
|
+
order.average = resolved;
|
|
945
|
+
// Carry cumQuote across too, so cost-based consumers (VWAP, slippage)
|
|
946
|
+
// see the settled notional rather than 0.
|
|
947
|
+
if ((order.cost === 0 || order.cost == null) && requeried && requeried.cost > 0) {
|
|
948
|
+
order.cost = requeried.cost;
|
|
949
|
+
}
|
|
950
|
+
logger.info(TAG, `Fill-price enriched for ${symbol}: avg ${rawAvg ?? 'null'} → ${resolved} (re-query by cid; limit price ${order.price ?? 'n/a'})`);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
logger.warn(TAG, `Fill-price enrichment for ${symbol} could not resolve an average (re-query avg=${requeried?.average ?? 'null'} cost=${requeried?.cost ?? 'null'} filled=${requeried?.filled ?? 'null'}); downstream uses mark price / skips capture, NOT the limit price ${order.price ?? 'n/a'}`);
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
logger.warn(TAG, `Fill-price enrichment re-query for ${symbol} threw: ${formatError(err)}; leaving average as-is`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Journal the close produced by an internal auto-flatten
|
|
961
|
+
* (`bracket_attach_failed`), so the position row can't orphan as
|
|
962
|
+
* status='open' (issue #196). These flattens go straight to
|
|
963
|
+
* `this.closePosition`, bypassing the close_position tool, so
|
|
964
|
+
* `onClosePositionFilled` never runs for them. Fire-and-forget + fail-open —
|
|
965
|
+
* never blocks or throws into the flatten path. The entry journal may not be
|
|
966
|
+
* ready at flatten time (the entry's WS fill can land ~1-2s later), so
|
|
967
|
+
* `onAutoFlattenClose` polls briefly for the webappPositionId; the boot DB
|
|
968
|
+
* reconcile sweep is the backstop if the entry never journals.
|
|
969
|
+
*/
|
|
970
|
+
captureAutoFlattenClose(symbol, closeOrder) {
|
|
971
|
+
if (!this.autoCapture)
|
|
972
|
+
return;
|
|
973
|
+
const fillPrice = fillPriceFromOrder(closeOrder);
|
|
974
|
+
// A synthetic "already flat" return (filled=0, avg=null) means another path
|
|
975
|
+
// (a bracket fill / external close) owns the real close — don't mislabel it
|
|
976
|
+
// as bracket_attach_failed and don't invent a price.
|
|
977
|
+
if (fillPrice === null || !(closeOrder.filled > 0))
|
|
978
|
+
return;
|
|
979
|
+
void onAutoFlattenClose(this.autoCapture, {
|
|
980
|
+
symbol,
|
|
981
|
+
fillPrice,
|
|
982
|
+
fillSize: Math.abs(closeOrder.filled),
|
|
983
|
+
...(typeof closeOrder.id === 'string' && closeOrder.id.length > 0
|
|
984
|
+
? { exchangeTradeId: closeOrder.id }
|
|
985
|
+
: {}),
|
|
986
|
+
}).catch((err) => logger.warn(TAG, `auto-flatten close capture for ${symbol} threw: ${formatError(err)}`));
|
|
987
|
+
}
|
|
883
988
|
/** Install the Wave 9-only autonomous recovery bridge. With no handler,
|
|
884
989
|
* every generic bracket lifecycle remains byte-for-byte behaviorally
|
|
885
990
|
* unchanged. Runtime reapplies this setter after adapter reconnects. */
|
|
@@ -1379,6 +1484,14 @@ export class LiveAdapter extends EventEmitter {
|
|
|
1379
1484
|
const order = await this.api.createOrder(symbol, closeSide, 'market', position.contracts, undefined, ccxtParams);
|
|
1380
1485
|
logger.info(TAG, `FLATTEN: ${symbol} close order ${order.id} status=${order.status}`);
|
|
1381
1486
|
this.emit('emergency_progress', { action: 'flatten', status: 'completed', symbol });
|
|
1487
|
+
// Auto-flatten (bracket_attach_failed) bypasses the close_position tool,
|
|
1488
|
+
// so onClosePositionFilled never journals its close and the position row
|
|
1489
|
+
// orphans as status='open' (issue #196). Journal it here from the real
|
|
1490
|
+
// close fill. Other reasons (operator_command, agent close_position) are
|
|
1491
|
+
// journaled by their own paths — don't double-capture.
|
|
1492
|
+
if (closeReason === 'bracket_attach_failed') {
|
|
1493
|
+
this.captureAutoFlattenClose(symbol, order);
|
|
1494
|
+
}
|
|
1382
1495
|
return order;
|
|
1383
1496
|
}
|
|
1384
1497
|
catch (err) {
|
|
@@ -1456,12 +1569,17 @@ export class LiveAdapter extends EventEmitter {
|
|
|
1456
1569
|
emitUnprotected(`threw: ${formatError(err)}`);
|
|
1457
1570
|
}
|
|
1458
1571
|
}
|
|
1459
|
-
|
|
1572
|
+
/** Null-honest balance read: null = the fetch FAILED (429 / weight-paced /
|
|
1573
|
+
* banned) — the caller MUST treat it as UNKNOWN, never as a zero balance.
|
|
1574
|
+
* Same null≠empty contract as getPositionsOrNull. Decision paths (sizing,
|
|
1575
|
+
* pre-trade risk) must use this: the legacy getBalance() collapse below
|
|
1576
|
+
* reads as walletTotal=0 → ~−100% drawdown → RED zone. */
|
|
1577
|
+
async getBalanceOrNull() {
|
|
1460
1578
|
const balance = await this.api.fetchBalance();
|
|
1461
1579
|
this.syncRateLimits();
|
|
1462
1580
|
this.rateLimiter.recordQuery(ENDPOINT_WEIGHTS.fetchBalance ?? 5);
|
|
1463
1581
|
if (!balance) {
|
|
1464
|
-
return
|
|
1582
|
+
return null;
|
|
1465
1583
|
}
|
|
1466
1584
|
// Binance USDⓈ-M quirk: `availableBalance` is only populated for the primary
|
|
1467
1585
|
// margin asset (USDT). Secondary collaterals like USDC come back with
|
|
@@ -1499,6 +1617,17 @@ export class LiveAdapter extends EventEmitter {
|
|
|
1499
1617
|
const cachedPositions = this.reconciler.getLastExchangePositions();
|
|
1500
1618
|
return this.enricher.enrich(balance, cachedPositions);
|
|
1501
1619
|
}
|
|
1620
|
+
/** Legacy display-path read. Collapses a FAILED fetch to an empty balance
|
|
1621
|
+
* object — acceptable for read-only surfaces (fetch_balance tool,
|
|
1622
|
+
* risk-summary display), a phantom-zero hazard for anything that decides.
|
|
1623
|
+
* Decision paths use getBalanceOrNull(). */
|
|
1624
|
+
async getBalance() {
|
|
1625
|
+
const balance = await this.getBalanceOrNull();
|
|
1626
|
+
if (!balance) {
|
|
1627
|
+
return { free: {}, used: {}, total: {} };
|
|
1628
|
+
}
|
|
1629
|
+
return balance;
|
|
1630
|
+
}
|
|
1502
1631
|
/** Re-anchor the realized-today seed + sessionStartNav from /fapi/v1/income.
|
|
1503
1632
|
* Throttled to {@link INCOME_REFRESH_MIN_INTERVAL_MS}; UTC date rollover
|
|
1504
1633
|
* forces an immediate refresh. Best-effort — failures are logged and
|
|
@@ -10,6 +10,10 @@ export interface PositionStateEntry {
|
|
|
10
10
|
webappPositionId?: string;
|
|
11
11
|
/** exchangeTradeId of the opening fill (audit trail). */
|
|
12
12
|
openedFromExchangeTradeId?: string;
|
|
13
|
+
/** setup_type from the entry metadata (e.g. 'pullback_trend_short_4h').
|
|
14
|
+
* Carried so close paths can record the exit into the re-entry tracker
|
|
15
|
+
* keyed by (symbol, setup) — issue #204. Optional; older entries lack it. */
|
|
16
|
+
setupType?: string;
|
|
13
17
|
/** Last record_position_reviews call timestamp (epoch ms). Drives stale-gate. */
|
|
14
18
|
lastReviewAt?: number;
|
|
15
19
|
lastVerdict?: 'hold' | 'add_on' | 'close_recommended';
|
package/live/stop-watcher.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
|
-
import type { CcxtPosition } from '../types.js';
|
|
3
|
+
import type { CcxtOrder, CcxtPosition } from '../types.js';
|
|
4
4
|
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
5
5
|
export declare const DEFAULT_INTERVAL_MS = 10000;
|
|
6
6
|
export interface StopTriggeredEvent {
|
|
@@ -10,6 +10,13 @@ export interface StopTriggeredEvent {
|
|
|
10
10
|
markPrice: number;
|
|
11
11
|
quantity: number;
|
|
12
12
|
}
|
|
13
|
+
/** Emitted with 'stop_closed' — carries the executed close order so listeners
|
|
14
|
+
* (the journal auto-capture wiring in index.ts) can record the close with the
|
|
15
|
+
* real fill. Without this, watcher closes bypassed the Position Decision
|
|
16
|
+
* Journal entirely and left phantom-open journal positions (issue #199). */
|
|
17
|
+
export interface StopClosedEvent extends StopTriggeredEvent {
|
|
18
|
+
order?: CcxtOrder;
|
|
19
|
+
}
|
|
13
20
|
export type Wave9StopCloseOutcome = {
|
|
14
21
|
status: 'flat';
|
|
15
22
|
detail: string;
|
package/live/stop-watcher.js
CHANGED
|
@@ -138,6 +138,9 @@ export class PositionWatcher extends EventEmitter {
|
|
|
138
138
|
quantity: position.contracts,
|
|
139
139
|
};
|
|
140
140
|
this.emit('stop_triggered', event);
|
|
141
|
+
// Captured close order for the 'stop_closed' event — set inside close()
|
|
142
|
+
// (which may run under the symbol lock) and read after it resolves.
|
|
143
|
+
let closeOrder;
|
|
141
144
|
const close = async () => {
|
|
142
145
|
let trustedPosition = position;
|
|
143
146
|
// Re-read after acquiring the shared trading boundary. A Wave 9
|
|
@@ -169,7 +172,7 @@ export class PositionWatcher extends EventEmitter {
|
|
|
169
172
|
throw new Error(`Wave 9 ownership resolution failed before stop close submission: ${ownershipError}; ` +
|
|
170
173
|
'IMMEDIATE MANUAL INTERVENTION REQUIRED');
|
|
171
174
|
}
|
|
172
|
-
await this.adapter.closePosition(trustedPosition.symbol, 'stop_watcher');
|
|
175
|
+
closeOrder = await this.adapter.closePosition(trustedPosition.symbol, 'stop_watcher');
|
|
173
176
|
if (!candidateId || !this.wave9CloseLifecycle)
|
|
174
177
|
return 'closed';
|
|
175
178
|
const outcome = await this.wave9CloseLifecycle.settleAfterClose(candidateId, trustedPosition.symbol);
|
|
@@ -190,7 +193,7 @@ export class PositionWatcher extends EventEmitter {
|
|
|
190
193
|
if (result !== 'closed')
|
|
191
194
|
return;
|
|
192
195
|
logger.info(TAG, `Auto-closed ${position.symbol} (reason=stop_watcher)`);
|
|
193
|
-
this.emit('stop_closed', event);
|
|
196
|
+
this.emit('stop_closed', { ...event, order: closeOrder });
|
|
194
197
|
}
|
|
195
198
|
catch (err) {
|
|
196
199
|
const msg = formatError(err);
|
package/onboarding/runtime.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { ExchangeConfig, TradingMode } from '../types.js';
|
|
|
3
3
|
import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
|
|
4
4
|
import type { PaperMarketFeed } from '../simulator/paper-market-feed.js';
|
|
5
5
|
import { LiveAdapter } from '../live/live-adapter.js';
|
|
6
|
+
import type { HlCredentials } from '../venues/hyperliquid/hl-private.js';
|
|
7
|
+
import { type VenueId } from '../venues/registry.js';
|
|
6
8
|
import { PositionWatcher } from '../live/stop-watcher.js';
|
|
7
9
|
import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
|
|
8
10
|
export interface MicroLiveConfig {
|
|
@@ -14,6 +16,14 @@ export interface BuildAdapterInput {
|
|
|
14
16
|
exchange: ExchangeConfig | null;
|
|
15
17
|
microLive?: MicroLiveConfig;
|
|
16
18
|
simulator: ExchangeSimulator;
|
|
19
|
+
/** Venue seam for the RUNTIME transition path (issue #217). Absent →
|
|
20
|
+
* 'binance' (every pre-venue caller byte-identical). The BOOT path got
|
|
21
|
+
* this seam in Phase 3 (createLiveAdapter in index.ts); the runtime flip
|
|
22
|
+
* — set_trading_mode, the product's go-live moment — missed it and
|
|
23
|
+
* constructed a Binance LiveAdapter unconditionally. */
|
|
24
|
+
venue?: VenueId;
|
|
25
|
+
/** Required for a live-mode build on the hyperliquid venue. */
|
|
26
|
+
hlCredentials?: HlCredentials | null;
|
|
17
27
|
}
|
|
18
28
|
/** Wave 9 safety wiring is created only after its durable ledger is loaded.
|
|
19
29
|
* Runtime reapplies these hooks to both the bootstrap objects and every
|
|
@@ -47,6 +57,11 @@ export declare class PluginRuntime {
|
|
|
47
57
|
private readonly _marketFeed;
|
|
48
58
|
private readonly operationLock?;
|
|
49
59
|
private wave9LiveLifecycleHooks?;
|
|
60
|
+
/** Observer applied to EVERY stop-watcher this runtime creates (reconnects
|
|
61
|
+
* included). index.ts uses it to attach the journal auto-capture listener
|
|
62
|
+
* for watcher closes (issue #199) — without it, a live<->paper reconnect
|
|
63
|
+
* would silently shed the capture wiring. */
|
|
64
|
+
private readonly onWatcherCreated?;
|
|
50
65
|
/** Reconnect is serialized — a second caller waits for the first to finish
|
|
51
66
|
* so we never tear down an adapter that's mid-rebuild. */
|
|
52
67
|
private reconnectInFlight;
|
|
@@ -57,6 +72,7 @@ export declare class PluginRuntime {
|
|
|
57
72
|
stopWatcher?: PositionWatcher | null;
|
|
58
73
|
marketFeed?: PaperMarketFeed | null;
|
|
59
74
|
operationLock?: TradingOperationLock;
|
|
75
|
+
onWatcherCreated?: (watcher: PositionWatcher) => void;
|
|
60
76
|
});
|
|
61
77
|
get adapter(): IExchangeAdapter;
|
|
62
78
|
get mode(): TradingMode;
|
|
@@ -75,6 +91,8 @@ export declare class PluginRuntime {
|
|
|
75
91
|
mode: TradingMode;
|
|
76
92
|
exchange: ExchangeConfig | null;
|
|
77
93
|
microLive?: MicroLiveConfig;
|
|
94
|
+
venue?: VenueId;
|
|
95
|
+
hlCredentials?: HlCredentials | null;
|
|
78
96
|
}, deps: {
|
|
79
97
|
adapterDeps: {
|
|
80
98
|
adapter: IExchangeAdapter;
|