@reefclaw/connect 0.1.30 → 0.1.32
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/assets/bridge/bridge.js +72 -5
- package/assets/bridge/connector.d.ts +3 -1
- package/assets/bridge/connector.js +51 -4
- package/assets/bridge/gateway/heartbeat-cron.js +31 -7
- package/assets/bridge/gateway/poller.d.ts +5 -0
- package/assets/bridge/gateway/poller.js +9 -0
- package/assets/bridge/index.js +4 -0
- package/assets/bridge/provider.d.ts +15 -0
- package/assets/bridge/providers/connector-update.d.ts +89 -0
- package/assets/bridge/providers/connector-update.js +212 -0
- package/assets/bridge/providers/emergency-commands.d.ts +36 -0
- package/assets/bridge/providers/emergency-commands.js +91 -0
- package/assets/bridge/providers/gateway.d.ts +26 -1
- package/assets/bridge/providers/gateway.js +159 -8
- package/assets/bridge/providers/mock.js +1 -0
- package/assets/bridge/types.d.ts +5 -1
- package/assets/bridge/types.js +5 -0
- package/assets/bridge/utils/instance-id.d.ts +3 -0
- package/assets/bridge/utils/instance-id.js +48 -0
- package/assets/plugin/ccxt/binance-private.js +2 -1
- package/assets/plugin/ccxt/binance-public.js +6 -1
- package/assets/plugin/config/agent-config-client.d.ts +5 -2
- package/assets/plugin/config/agent-config-client.js +13 -0
- package/assets/plugin/config/agent-config-poller.js +5 -1
- package/assets/plugin/config/gate-store.d.ts +9 -0
- package/assets/plugin/config/gate-store.js +17 -2
- package/assets/plugin/config/plugin-config-io.js +24 -2
- package/assets/plugin/http/keepalive-fetch.d.ts +5 -0
- package/assets/plugin/http/keepalive-fetch.js +50 -0
- package/assets/plugin/index.js +53 -6
- package/assets/plugin/ingest/position-auto-capture.js +14 -2
- package/assets/plugin/ingest/position-decisions-client.d.ts +6 -0
- package/assets/plugin/ingest/position-decisions-client.js +27 -9
- package/assets/plugin/live/approval-lifecycle.d.ts +10 -0
- package/assets/plugin/live/approval-lifecycle.js +16 -2
- package/assets/plugin/live/microstructure-assembler.js +11 -2
- package/assets/plugin/live/proposal-decision-listener.d.ts +21 -0
- package/assets/plugin/live/proposal-decision-listener.js +39 -0
- package/assets/plugin/live/proposal-manager.d.ts +12 -0
- package/assets/plugin/live/proposal-manager.js +47 -0
- package/assets/plugin/live/stop-watcher.d.ts +16 -1
- package/assets/plugin/live/stop-watcher.js +48 -8
- package/assets/plugin/onboarding/runtime.js +4 -0
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/persistence/state-manager.d.ts +7 -0
- package/assets/plugin/persistence/state-manager.js +28 -1
- package/assets/plugin/simulator/exchange-simulator.d.ts +27 -1
- package/assets/plugin/simulator/exchange-simulator.js +98 -38
- package/assets/plugin/tools/audit-bracket-protection.js +11 -7
- package/assets/plugin/tools/close-position.js +10 -1
- package/assets/plugin/tools/create-order.js +49 -7
- package/assets/plugin/tools/get-funding-context.js +6 -1
- package/assets/plugin/tools/get-liquidation-levels.js +5 -1
- package/assets/plugin/tools/get-liquidation-pulse.js +7 -1
- package/assets/plugin/tools/get-market-intel.js +2 -1
- package/assets/plugin/tools/get-relevant-learnings.js +20 -1
- package/assets/plugin/tools/get-resting-liquidity.js +6 -1
- package/assets/plugin/tools/get-wave9-status.js +17 -0
- package/assets/plugin/tools/intel-api.d.ts +9 -0
- package/assets/plugin/tools/intel-api.js +32 -1
- package/assets/plugin/tools/record-position-reviews.js +2 -2
- package/assets/plugin/tools/scan-pairs.js +20 -11
- package/assets/plugin/types.d.ts +7 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +27 -2
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +101 -13
- package/assets/shared/readiness.js +5 -1
- package/dist/cli.js +27 -6
- package/dist/openclaw.js +1 -1
- package/dist/validate.js +46 -9
- package/package.json +33 -32
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Shared helper for Intelligence API calls.
|
|
2
2
|
// All intelligence tools use Bearer token auth against intel.reefclaw.com.
|
|
3
|
+
import { keepAliveFetch } from '../http/keepalive-fetch.js';
|
|
3
4
|
import { HL_INTEL_PREFIX, fromIntelSymbol, toIntelSymbol } from '../venues/symbols.js';
|
|
4
5
|
// ─── Venue-aware intel-symbol mapping (plan §5.3: the agent keeps using
|
|
5
6
|
// canonical symbols like BTC/USDC; these helpers translate at the intel
|
|
@@ -67,6 +68,13 @@ export function intelSymbolOnVenue(deps, intelSymbol) {
|
|
|
67
68
|
const isHl = intelSymbol.startsWith(HL_INTEL_PREFIX);
|
|
68
69
|
return (deps.venue ?? 'binance') === 'hyperliquid' ? isHl : !isHl;
|
|
69
70
|
}
|
|
71
|
+
const getCache = new Map();
|
|
72
|
+
const GET_CACHE_MAX_ENTRIES = 256;
|
|
73
|
+
/** Test support: drop every cached GET response (the cache is module-level,
|
|
74
|
+
* so suites that stub fetch with per-test responses must clear it). */
|
|
75
|
+
export function clearIntelGetCache() {
|
|
76
|
+
getCache.clear();
|
|
77
|
+
}
|
|
70
78
|
/** Encode a value for safe use in a URL path segment */
|
|
71
79
|
export const enc = encodeURIComponent;
|
|
72
80
|
export async function fetchIntelApi(path, deps, options) {
|
|
@@ -78,6 +86,29 @@ export async function fetchIntelApi(path, deps, options) {
|
|
|
78
86
|
return { error: 'No intelligence URL configured. Set intelligenceUrl in openclaw.json plugin config.' };
|
|
79
87
|
}
|
|
80
88
|
const url = `${intelligenceUrl}${path}`;
|
|
89
|
+
const ttl = options?.cacheTtlMs ?? 0;
|
|
90
|
+
if (ttl > 0 && (options?.method ?? 'GET') === 'GET') {
|
|
91
|
+
const key = `${connectionToken}:${url}`;
|
|
92
|
+
const hit = getCache.get(key);
|
|
93
|
+
if (hit && Date.now() - hit.at < ttl) {
|
|
94
|
+
// structuredClone: callers must never alias each other's response object.
|
|
95
|
+
return hit.value.then((v) => structuredClone(v));
|
|
96
|
+
}
|
|
97
|
+
const value = fetchIntelApiUncached(url, connectionToken, options);
|
|
98
|
+
getCache.set(key, { at: Date.now(), value });
|
|
99
|
+
// Failures don't stick for the TTL — next caller retries fresh.
|
|
100
|
+
void value.then((v) => { if (v && typeof v === 'object' && 'error' in v)
|
|
101
|
+
getCache.delete(key); }, () => getCache.delete(key));
|
|
102
|
+
if (getCache.size > GET_CACHE_MAX_ENTRIES) {
|
|
103
|
+
const oldest = getCache.keys().next().value;
|
|
104
|
+
if (oldest !== undefined)
|
|
105
|
+
getCache.delete(oldest);
|
|
106
|
+
}
|
|
107
|
+
return value.then((v) => structuredClone(v));
|
|
108
|
+
}
|
|
109
|
+
return fetchIntelApiUncached(url, connectionToken, options);
|
|
110
|
+
}
|
|
111
|
+
async function fetchIntelApiUncached(url, connectionToken, options) {
|
|
81
112
|
const headers = { Authorization: `Bearer ${connectionToken}` };
|
|
82
113
|
const fetchInit = {
|
|
83
114
|
method: options?.method ?? 'GET',
|
|
@@ -89,7 +120,7 @@ export async function fetchIntelApi(path, deps, options) {
|
|
|
89
120
|
fetchInit.body = JSON.stringify(options.body);
|
|
90
121
|
}
|
|
91
122
|
try {
|
|
92
|
-
const res = await
|
|
123
|
+
const res = await keepAliveFetch(url, fetchInit);
|
|
93
124
|
if (res.status === 401) {
|
|
94
125
|
return { error: 'Invalid or expired ReefClaw connection token.' };
|
|
95
126
|
}
|
|
@@ -94,7 +94,6 @@ export async function recordPositionReviewsTool(args, deps) {
|
|
|
94
94
|
// not load-bearing (each row carries its own reviewAt).
|
|
95
95
|
let filed = 0;
|
|
96
96
|
if (deps.decisionsClient && deps.userId) {
|
|
97
|
-
const promises = [];
|
|
98
97
|
for (const r of reviews) {
|
|
99
98
|
const stateEntry = deps.stateStore?.get(r.symbol);
|
|
100
99
|
const positionId = stateEntry?.webappPositionId;
|
|
@@ -137,7 +136,8 @@ export async function recordPositionReviewsTool(args, deps) {
|
|
|
137
136
|
deps.stateStore.recordReview(r.symbol, r.verdict, undefined, r.thesis_status);
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
|
-
|
|
139
|
+
// POSTs above are fire-and-forget by design — durability is the client's
|
|
140
|
+
// in-flight set + drain(), not an await here.
|
|
141
141
|
}
|
|
142
142
|
else {
|
|
143
143
|
logger.info(TAG, `record_position_reviews validated ${reviews.length} reviews; no decisionsClient configured (off-mode).`);
|
|
@@ -129,8 +129,20 @@ function learningMatches(learning, setupType, regime) {
|
|
|
129
129
|
}
|
|
130
130
|
export async function scanPairsTool(args, deps, decisionsDeps) {
|
|
131
131
|
const minScore = args.min_score ?? 4;
|
|
132
|
-
//
|
|
133
|
-
|
|
132
|
+
// Facts, strategies, and entry learnings are three independent reads (two
|
|
133
|
+
// intel, one webapp) — start them all now and await in the original order,
|
|
134
|
+
// so error precedence (facts → strategies → learnings-fail-quiet) and every
|
|
135
|
+
// early-return body stay byte-identical while the round-trips overlap.
|
|
136
|
+
const factsPromise = getAllFactsCached(deps);
|
|
137
|
+
const stratPromise = getStrategiesCached(deps);
|
|
138
|
+
const learningsPromise = decisionsDeps?.decisionsClient && decisionsDeps.userId
|
|
139
|
+
? getEntryLearningsCached(decisionsDeps.decisionsClient, decisionsDeps.userId)
|
|
140
|
+
: Promise.resolve([]);
|
|
141
|
+
// An early return below must not leave a floating rejection behind.
|
|
142
|
+
stratPromise.catch(() => { });
|
|
143
|
+
learningsPromise.catch(() => { });
|
|
144
|
+
// 1. All symbol facts (cached, shared with get_setup_detail)
|
|
145
|
+
const factsRes = await factsPromise;
|
|
134
146
|
if ('error' in factsRes)
|
|
135
147
|
return factsRes;
|
|
136
148
|
// Venue scope: only rank symbols this box can actually trade. Without this
|
|
@@ -154,8 +166,8 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
|
|
|
154
166
|
: 'No symbol facts available. Intelligence service may still be computing initial data.',
|
|
155
167
|
};
|
|
156
168
|
}
|
|
157
|
-
// 2.
|
|
158
|
-
const stratRes = await
|
|
169
|
+
// 2. User's active strategies (cached, shared with get_setup_detail)
|
|
170
|
+
const stratRes = await stratPromise;
|
|
159
171
|
if ('error' in stratRes)
|
|
160
172
|
return stratRes;
|
|
161
173
|
const strategies = stratRes;
|
|
@@ -171,13 +183,10 @@ export async function scanPairsTool(args, deps, decisionsDeps) {
|
|
|
171
183
|
}
|
|
172
184
|
// 3. Evaluate all strategies against all facts
|
|
173
185
|
const results = scanAllPairs(strategies, facts, minScore);
|
|
174
|
-
// 4.
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
if (decisionsDeps?.decisionsClient && decisionsDeps.userId) {
|
|
179
|
-
entryLearnings = await getEntryLearningsCached(decisionsDeps.decisionsClient, decisionsDeps.userId);
|
|
180
|
-
}
|
|
186
|
+
// 4. User's confirmed entry-time learnings (optional path — resolves []
|
|
187
|
+
// when the decisions client isn't wired, e.g. in dev or in tests that
|
|
188
|
+
// exercise the pre-learning behaviour).
|
|
189
|
+
const entryLearnings = await learningsPromise;
|
|
181
190
|
const rankings = [];
|
|
182
191
|
const vetoed = [];
|
|
183
192
|
for (const r of results) {
|
package/assets/plugin/types.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface CcxtPosition {
|
|
|
63
63
|
contractSize: number;
|
|
64
64
|
entryPrice: number;
|
|
65
65
|
markPrice: number;
|
|
66
|
+
/** Paper only: true when NO quote was available for this symbol, so
|
|
67
|
+
* `markPrice` is a fabricated fallback (entryPrice) rather than a real mark.
|
|
68
|
+
* Consumers that make protective decisions MUST NOT read `markPrice` as
|
|
69
|
+
* truth when this is set — a fabricated "flat since entry" mark reads as a
|
|
70
|
+
* healthy position and is how a breached stop stayed invisible for 35h.
|
|
71
|
+
* Undefined on live (real marks come from the exchange). */
|
|
72
|
+
markPriceStale?: boolean;
|
|
66
73
|
notional: number;
|
|
67
74
|
unrealizedPnl: number;
|
|
68
75
|
percentage: number;
|
|
@@ -6,6 +6,7 @@ import { type HlCredentials } from './hl-private.js';
|
|
|
6
6
|
import type { BracketId } from '../../live/bracket-types.js';
|
|
7
7
|
import { BracketLedger } from '../../live/bracket-ledger.js';
|
|
8
8
|
import { HlBracketCoordinator } from './hl-bracket-coordinator.js';
|
|
9
|
+
import { type AutoCaptureContext } from '../../ingest/position-auto-capture.js';
|
|
9
10
|
import type { TradeIngestWiring } from '../../live/live-adapter.js';
|
|
10
11
|
export interface HlLiveAdapterOptions {
|
|
11
12
|
credentials: HlCredentials;
|
|
@@ -23,6 +24,16 @@ export interface HlLiveAdapterOptions {
|
|
|
23
24
|
* via userFillsByTime. Same object boot passes the Binance adapter —
|
|
24
25
|
* `exchange` MUST be fillExchangeId('hyperliquid'). */
|
|
25
26
|
tradeIngest?: TradeIngestWiring;
|
|
27
|
+
/** Position-decision journal wiring. When present, a close-direction user-
|
|
28
|
+
* stream fill (dir "Close …" or a liquidation fill) is routed through
|
|
29
|
+
* `onWsFillObserved` so an exchange-native bracket SL/TP fill journals an
|
|
30
|
+
* EXACT close (reason `bracket_fill`) instead of leaking as status='open'
|
|
31
|
+
* until the reconciler heals it as `reconciler_observed_flat` — the HL
|
|
32
|
+
* analog of the wiring Binance's ws-ingest has carried since PR #205.
|
|
33
|
+
* Entry/scale-in fills are deliberately NOT routed (the sync create_order
|
|
34
|
+
* path captures them; the WS dedup key is unverified on HL — see
|
|
35
|
+
* onUserFill). */
|
|
36
|
+
autoCapture?: AutoCaptureContext;
|
|
26
37
|
/** Test seams. Production omits both. */
|
|
27
38
|
bracketLedger?: BracketLedger;
|
|
28
39
|
disableUserStream?: boolean;
|
|
@@ -54,6 +65,10 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
54
65
|
/** exchangeTime of the newest fill ingested (WS or backfill) — the overlap
|
|
55
66
|
* low-water mark the reconnect gap backfill widens from. */
|
|
56
67
|
private lastFillIngestMs;
|
|
68
|
+
/** oid → cloid backfill for fills that omit `cloid` (the journal close path
|
|
69
|
+
* recognizes bracket legs by client id). Populated from `orderUpdates`,
|
|
70
|
+
* which always carries both. Bounded, insertion-order eviction. */
|
|
71
|
+
private readonly oidToCloid;
|
|
57
72
|
/** UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app, §5.8). HL has no
|
|
58
73
|
* income endpoint, so the anchor is rebuilt from userFillsByTime + userFunding
|
|
59
74
|
* each balance fetch. Without this the skill self-computes a bogus anchor and
|
|
@@ -187,10 +202,20 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
187
202
|
* `startPosition` is the position BEFORE this fill — the WS-authoritative
|
|
188
203
|
* way to know the after-fill total without an extra REST read. */
|
|
189
204
|
private onUserFill;
|
|
205
|
+
/** Close-direction fill → position-decision journal (close-bypass fix, HL
|
|
206
|
+
* arm). Fire-and-forget: a journal POST blip must never touch the WS hot
|
|
207
|
+
* path. Reduce-only is derived from `dir` (HL fills carry no reduceOnly
|
|
208
|
+
* flag): "Close Long"/"Close Short", plus the liquidation marker. Flip
|
|
209
|
+
* dirs ("Long > Short") are NOT closes of a tracked side we understand —
|
|
210
|
+
* they stay with the reconciler backstop. */
|
|
211
|
+
private captureCloseFill;
|
|
190
212
|
/** `orderUpdates` is authoritative for leg lifecycle (the ALGO_UPDATE
|
|
191
|
-
* analog). A trigger = the exchange closed the position — surface the
|
|
213
|
+
* analog). A trigger = the exchange closed the position — surface the
|
|
192
214
|
* `drift_detected` close shape the Binance reconciler emits so the journal
|
|
193
|
-
* close-bypass cleanup fires
|
|
215
|
+
* close-bypass cleanup fires fast — but AFTER a short grace, so the close
|
|
216
|
+
* FILL (the exact-close journal path, `captureCloseFill`) wins the
|
|
217
|
+
* undocumented orderUpdates/userFills frame ordering. The cleanup is
|
|
218
|
+
* idempotent: state already dropped by the fill path ⇒ no-op. */
|
|
194
219
|
private onUserOrderUpdate;
|
|
195
220
|
/** T-5 REST truth-check — serialized so a slow pass can't stack. */
|
|
196
221
|
private runTruthCheck;
|
|
@@ -38,7 +38,8 @@ import { generateBracketId } from '../../live/bracket-id.js';
|
|
|
38
38
|
import { validateStopDirection, validateTargetDirection } from '../../live/bracket-params.js';
|
|
39
39
|
import { HlBracketCoordinator, isRejectedOrder, isTerminalBracketState, } from './hl-bracket-coordinator.js';
|
|
40
40
|
import { HyperliquidUserStream } from './hl-user-stream.js';
|
|
41
|
-
import { hlFillToFillEvent } from './hl-fill-ingest.js';
|
|
41
|
+
import { hlFillToFillEvent, hlCoinToCanonical } from './hl-fill-ingest.js';
|
|
42
|
+
import { onWsFillObserved } from '../../ingest/position-auto-capture.js';
|
|
42
43
|
import { formatError } from '../../logger.js';
|
|
43
44
|
const TAG = 'hl-live-adapter';
|
|
44
45
|
/** Venue-distinct ledger storage — a venue switch on the same box must never
|
|
@@ -55,6 +56,21 @@ const DEFAULT_MARKET_SLIPPAGE = 0.005;
|
|
|
55
56
|
/** Sticky cooldown after a failed open-orders fetch (the Binance lesson: the
|
|
56
57
|
* SKILL.md audit→attach loop re-calls every ~3s and would pin the budget). */
|
|
57
58
|
const OPEN_ORDERS_COOLDOWN_MS = 45_000;
|
|
59
|
+
/** Grace before a bracket-trigger `drift_detected` emit. A trigger's close FILL
|
|
60
|
+
* arrives on `userFills` and journals the exact close (real price, real PnL,
|
|
61
|
+
* reason `bracket_fill`); the drift path's cleanup can only post a generic
|
|
62
|
+
* `reconciler_observed_flat`. HL's WS frame ordering between `orderUpdates`
|
|
63
|
+
* and `userFills` is undocumented, so without this grace the generic close
|
|
64
|
+
* routinely won the race and 50% of HL live closes carried no close reason
|
|
65
|
+
* (wisekid, 30d to 2026-08-17: 150/299). The cleanup is idempotent — when the
|
|
66
|
+
* fill already journaled + dropped state, the delayed drift is a no-op; when
|
|
67
|
+
* the fill never arrives (T-5 gap), the drift still heals, 10s late instead
|
|
68
|
+
* of instant (previously ≤5 min via the periodic sweep). */
|
|
69
|
+
const TRIGGER_DRIFT_GRACE_MS = 10_000;
|
|
70
|
+
/** Bound on the oid→cloid map (fills MAY omit `cloid` — facts ledger §3.4 —
|
|
71
|
+
* while `orderUpdates` always carries both, so the map backfills the fill's
|
|
72
|
+
* client id for bracket recognition). Insertion-ordered eviction. */
|
|
73
|
+
const OID_CLOID_MAP_MAX = 512;
|
|
58
74
|
export class HyperliquidLiveAdapter extends EventEmitter {
|
|
59
75
|
opts;
|
|
60
76
|
api;
|
|
@@ -82,6 +98,10 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
82
98
|
/** exchangeTime of the newest fill ingested (WS or backfill) — the overlap
|
|
83
99
|
* low-water mark the reconnect gap backfill widens from. */
|
|
84
100
|
lastFillIngestMs = 0;
|
|
101
|
+
/** oid → cloid backfill for fills that omit `cloid` (the journal close path
|
|
102
|
+
* recognizes bracket legs by client id). Populated from `orderUpdates`,
|
|
103
|
+
* which always carries both. Bounded, insertion-order eviction. */
|
|
104
|
+
oidToCloid = new Map();
|
|
85
105
|
/** UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app, §5.8). HL has no
|
|
86
106
|
* income endpoint, so the anchor is rebuilt from userFillsByTime + userFunding
|
|
87
107
|
* each balance fetch. Without this the skill self-computes a bogus anchor and
|
|
@@ -821,6 +841,19 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
821
841
|
// healed it, in a repeating flap. Exchange truth is the resync's job.
|
|
822
842
|
if (meta?.isSnapshot)
|
|
823
843
|
return;
|
|
844
|
+
// Journal close capture: a close-direction fill (bracket SL/TP trigger,
|
|
845
|
+
// liquidation, external reduce) bypasses close_position, and before this
|
|
846
|
+
// wiring the journal only learned about it from the reconciler — 50% of
|
|
847
|
+
// wisekid's 30d closes were `reconciler_observed_flat` heals with the
|
|
848
|
+
// close reason lost. Route it through the SAME generic path Binance's
|
|
849
|
+
// ws-ingest uses; `handleReduceOnlyExit` posts the exact close when the
|
|
850
|
+
// position goes flat and defers to close_position/backstop otherwise.
|
|
851
|
+
// ONLY close-direction fills are routed: entry/scale-in fills stay with
|
|
852
|
+
// the synchronous create_order capture, because the WS dedup key
|
|
853
|
+
// (`openedFromExchangeTradeId === exchangeOrderId`) is unverified against
|
|
854
|
+
// ccxt's HL order.id shape and a dedup miss would re-mint the 38-duplicate-
|
|
855
|
+
// pairs class (issue #199 twin bug).
|
|
856
|
+
this.captureCloseFill(fill);
|
|
824
857
|
try {
|
|
825
858
|
// Matches the primary entry cid OR any additional cid recorded for a
|
|
826
859
|
// scale-in / second resting entry (F5) — matching on `entryCid` alone
|
|
@@ -848,27 +881,82 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
848
881
|
logger.error(TAG, `onUserFill handler error: ${formatError(err)}`);
|
|
849
882
|
}
|
|
850
883
|
}
|
|
884
|
+
/** Close-direction fill → position-decision journal (close-bypass fix, HL
|
|
885
|
+
* arm). Fire-and-forget: a journal POST blip must never touch the WS hot
|
|
886
|
+
* path. Reduce-only is derived from `dir` (HL fills carry no reduceOnly
|
|
887
|
+
* flag): "Close Long"/"Close Short", plus the liquidation marker. Flip
|
|
888
|
+
* dirs ("Long > Short") are NOT closes of a tracked side we understand —
|
|
889
|
+
* they stay with the reconciler backstop. */
|
|
890
|
+
captureCloseFill(fill) {
|
|
891
|
+
const capture = this.opts.autoCapture;
|
|
892
|
+
if (!capture)
|
|
893
|
+
return;
|
|
894
|
+
const dir = (fill.dir ?? '').toLowerCase();
|
|
895
|
+
const isClose = dir.startsWith('close') || fill.liquidation !== undefined;
|
|
896
|
+
if (!isClose)
|
|
897
|
+
return;
|
|
898
|
+
const price = Number(fill.px);
|
|
899
|
+
const size = Number(fill.sz);
|
|
900
|
+
if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(size) || size <= 0)
|
|
901
|
+
return;
|
|
902
|
+
const realizedPnl = Number(fill.closedPnl);
|
|
903
|
+
const cloid = typeof fill.cloid === 'string' && fill.cloid.length > 0
|
|
904
|
+
? fill.cloid
|
|
905
|
+
: this.oidToCloid.get(fill.oid);
|
|
906
|
+
onWsFillObserved(capture, {
|
|
907
|
+
symbol: hlCoinToCanonical(fill.coin),
|
|
908
|
+
side: fill.side === 'B' ? 'buy' : 'sell',
|
|
909
|
+
exchangeOrderId: String(fill.oid),
|
|
910
|
+
exchangeTradeId: String(fill.tid),
|
|
911
|
+
fillPrice: price,
|
|
912
|
+
fillSize: size,
|
|
913
|
+
reduceOnly: true,
|
|
914
|
+
realizedPnl: Number.isFinite(realizedPnl) ? realizedPnl : undefined,
|
|
915
|
+
clientOrderId: cloid,
|
|
916
|
+
exchangeTimeMs: Number.isFinite(fill.time) ? fill.time : undefined,
|
|
917
|
+
}).catch((err) => {
|
|
918
|
+
logger.warn(TAG, `journal close capture failed for ${fill.coin} oid=${fill.oid}: ${msg(err)}`);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
851
921
|
/** `orderUpdates` is authoritative for leg lifecycle (the ALGO_UPDATE
|
|
852
|
-
* analog). A trigger = the exchange closed the position — surface the
|
|
922
|
+
* analog). A trigger = the exchange closed the position — surface the
|
|
853
923
|
* `drift_detected` close shape the Binance reconciler emits so the journal
|
|
854
|
-
* close-bypass cleanup fires
|
|
924
|
+
* close-bypass cleanup fires fast — but AFTER a short grace, so the close
|
|
925
|
+
* FILL (the exact-close journal path, `captureCloseFill`) wins the
|
|
926
|
+
* undocumented orderUpdates/userFills frame ordering. The cleanup is
|
|
927
|
+
* idempotent: state already dropped by the fill path ⇒ no-op. */
|
|
855
928
|
onUserOrderUpdate(update) {
|
|
856
929
|
try {
|
|
930
|
+
// oid→cloid backfill for fills that omit their client id (see
|
|
931
|
+
// captureCloseFill). orderUpdates always carries both.
|
|
932
|
+
const { oid, cloid } = update.order;
|
|
933
|
+
if (typeof oid === 'number' && typeof cloid === 'string' && cloid.length > 0) {
|
|
934
|
+
this.oidToCloid.set(oid, cloid);
|
|
935
|
+
if (this.oidToCloid.size > OID_CLOID_MAP_MAX) {
|
|
936
|
+
const oldest = this.oidToCloid.keys().next().value;
|
|
937
|
+
if (oldest !== undefined)
|
|
938
|
+
this.oidToCloid.delete(oldest);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
857
941
|
const transition = this.getHlBracketCoordinator().handleOrderUpdate(update);
|
|
858
942
|
if (transition === 'triggered_sl' || transition === 'triggered_tp' || transition === 'forced_close') {
|
|
859
943
|
const row = this.getHlBracketCoordinator().getLedger().getAll().find((r) => update.order.cloid && (r.slCid === update.order.cloid || r.tpCid === update.order.cloid));
|
|
860
944
|
const symbol = row?.symbol;
|
|
861
945
|
if (symbol) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
946
|
+
const qty = row?.qty ?? 0;
|
|
947
|
+
const timer = setTimeout(() => {
|
|
948
|
+
this.emit('drift_detected', {
|
|
949
|
+
timestamp: new Date().toISOString(),
|
|
950
|
+
drifts: [
|
|
951
|
+
{
|
|
952
|
+
type: 'closed',
|
|
953
|
+
symbol,
|
|
954
|
+
localContracts: qty,
|
|
955
|
+
},
|
|
956
|
+
],
|
|
957
|
+
});
|
|
958
|
+
}, TRIGGER_DRIFT_GRACE_MS);
|
|
959
|
+
timer.unref?.();
|
|
872
960
|
}
|
|
873
961
|
}
|
|
874
962
|
}
|
|
@@ -33,7 +33,11 @@ export const READINESS_CHECK_COPY = {
|
|
|
33
33
|
clock_in_sync: {
|
|
34
34
|
label: 'Host clock in sync',
|
|
35
35
|
phase: 'connect',
|
|
36
|
-
|
|
36
|
+
// Venue-neutral on purpose: the same check runs on a Hyperliquid box,
|
|
37
|
+
// where naming Binance is a false alarm for an exchange the user does not
|
|
38
|
+
// use. Both venues reject/miscompute signed requests on a skewed clock
|
|
39
|
+
// (Binance -1021 recvWindow; Hyperliquid signs an epoch-ms nonce).
|
|
40
|
+
fixHint: 'The host clock drifts from your exchange’s server time. Enable NTP/chrony so signed orders are not rejected (Binance rejects with -1021; Hyperliquid nonces are epoch-ms and must not skew).',
|
|
37
41
|
},
|
|
38
42
|
plugin_loaded: {
|
|
39
43
|
label: 'ReefClaw plugin loaded',
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { installBridge } from './bridge.js';
|
|
|
15
15
|
import { installSkill } from './skill.js';
|
|
16
16
|
import { enableConnectorSupervisor } from './supervisor-config.js';
|
|
17
17
|
import { tuneGatewayMemory } from './gateway-tuning.js';
|
|
18
|
-
import { checkGateway, checkBinanceRegion } from './validate.js';
|
|
18
|
+
import { checkGateway, checkBinanceRegion, checkHyperliquidRegion } from './validate.js';
|
|
19
19
|
import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthAlreadyRelaxed, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
|
|
20
20
|
import { run, which } from './exec.js';
|
|
21
21
|
import { step, ok, info, warn, fail, banner, bold, green, cyan, dim } from './ui.js';
|
|
@@ -136,13 +136,34 @@ function nextSteps() {
|
|
|
136
136
|
` 3. The dashboard lights up ${green('green / Connected')} on its own within a few seconds.\n\n` +
|
|
137
137
|
dim(`Dashboard: ${DASHBOARD}\n`));
|
|
138
138
|
}
|
|
139
|
+
/** `--venue hyperliquid` / `--venue=binance`. Absent = unknown: the user has
|
|
140
|
+
* not chosen yet (venue is picked on the dashboard AFTER connecting), so we
|
|
141
|
+
* run the Binance probe but phrase its failure as "Binance is blocked here",
|
|
142
|
+
* never as "trading cannot work here". */
|
|
143
|
+
function parseVenueArg(argv) {
|
|
144
|
+
for (let i = 0; i < argv.length; i++) {
|
|
145
|
+
const a = argv[i];
|
|
146
|
+
const v = a.startsWith('--venue=') ? a.slice('--venue='.length) : a === '--venue' ? argv[i + 1] : undefined;
|
|
147
|
+
if (v === 'binance' || v === 'hyperliquid')
|
|
148
|
+
return v;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
139
152
|
async function main() {
|
|
140
153
|
banner(bold(cyan('ReefClaw connect')) + dim(' — link your OpenClaw agent to ReefClaw (paper trading)'));
|
|
141
154
|
preflight();
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
155
|
+
// Venue-aware reachability. ReefClaw trades Binance AND Hyperliquid; probing
|
|
156
|
+
// Binance for a Hyperliquid user produced a red "trading cannot work from
|
|
157
|
+
// here" on hosts where Hyperliquid works perfectly — the first thing such a
|
|
158
|
+
// user saw. `--venue hyperliquid` probes the venue they actually want.
|
|
159
|
+
const venue = parseVenueArg(process.argv.slice(2));
|
|
160
|
+
let binanceReachable = true;
|
|
161
|
+
if (venue === 'hyperliquid') {
|
|
162
|
+
await checkHyperliquidRegion();
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
binanceReachable = await checkBinanceRegion();
|
|
166
|
+
}
|
|
146
167
|
const pre = readConfig();
|
|
147
168
|
const plugin = installPlugin();
|
|
148
169
|
const merged = wireConfig(pre);
|
|
@@ -171,7 +192,7 @@ async function main() {
|
|
|
171
192
|
warn('Connector supervision was not enabled — see the message above to finish it.');
|
|
172
193
|
}
|
|
173
194
|
if (!binanceReachable) {
|
|
174
|
-
warn('This host looks geo-blocked by
|
|
195
|
+
warn('This host looks geo-blocked by BINANCE (HTTP 451). Binance trading will not work here — run the agent from a Binance-permitted region (most EU / several Asia VPS regions), or choose HYPERLIQUID on the dashboard, which is reachable from most regions including this one.');
|
|
175
196
|
}
|
|
176
197
|
nextSteps();
|
|
177
198
|
}
|
package/dist/openclaw.js
CHANGED
|
@@ -74,7 +74,7 @@ export function mergeReefClawConfig(input, connect = {}) {
|
|
|
74
74
|
// --- tool profile widening ---
|
|
75
75
|
// `openclaw setup` defaults tools.profile to "coding" (2026.6+), whose fixed
|
|
76
76
|
// core allowlist filters out ALL plugin tools — the agent would see none of
|
|
77
|
-
// the
|
|
77
|
+
// the 66 trading tools. tools.alsoAllow explicitly widens the profile;
|
|
78
78
|
// group:plugins covers every plugin-provided tool. Union, never replace.
|
|
79
79
|
const existingAlsoAllow = Array.isArray(tools.alsoAllow) ? tools.alsoAllow.map(String) : [];
|
|
80
80
|
if (!existingAlsoAllow.includes('group:plugins')) {
|
package/dist/validate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Light post-install checks. We can only verify the LOCAL gateway here — the
|
|
2
2
|
// relay handshake only succeeds once the user pastes their connect message
|
|
3
3
|
// (that's the onboarding "auto-detect" step on the dashboard).
|
|
4
|
-
import { step, ok, warn, info
|
|
4
|
+
import { step, ok, warn, info } from './ui.js';
|
|
5
5
|
/** Any HTTP response from the local gateway means OpenClaw is up and reachable.
|
|
6
6
|
* A connection refused means OpenClaw isn't running. */
|
|
7
7
|
export async function checkGateway(port) {
|
|
@@ -27,11 +27,17 @@ export async function checkGateway(port) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
/** Probe whether Binance USD-M Futures is reachable from THIS host. A `451` is
|
|
30
|
-
* Binance's geo-restriction signal — the host's region is blocked,
|
|
31
|
-
* (
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* readiness surface confirm a real, persistent problem later. Never fatal.
|
|
30
|
+
* Binance's geo-restriction signal — the host's region is blocked, so Binance
|
|
31
|
+
* trading (and Binance-priced paper mode) cannot work from here. The probe is
|
|
32
|
+
* advisory: only a definitive `451` returns false; a network hiccup / timeout
|
|
33
|
+
* is NOT a geo-block, so we stay out of the way and let the dashboard
|
|
34
|
+
* readiness surface confirm a real, persistent problem later. Never fatal.
|
|
35
|
+
*
|
|
36
|
+
* ★ The copy deliberately names BINANCE, not "trading" — ReefClaw also trades
|
|
37
|
+
* Hyperliquid, which is reachable from most regions INCLUDING Binance-blocked
|
|
38
|
+
* ones (verified live). Telling a Hyperliquid user to relocate their server
|
|
39
|
+
* because of Binance is a false alarm, and it used to be the first thing a
|
|
40
|
+
* Hyperliquid-only user saw. Pass `--venue hyperliquid` to skip this entirely. */
|
|
35
41
|
export async function checkBinanceRegion() {
|
|
36
42
|
step('Checking Binance reachability from this host');
|
|
37
43
|
const url = 'https://fapi.binance.com/fapi/v1/ping';
|
|
@@ -40,9 +46,11 @@ export async function checkBinanceRegion() {
|
|
|
40
46
|
try {
|
|
41
47
|
const res = await fetch(url, { signal: ctrl.signal });
|
|
42
48
|
if (res.status === 451) {
|
|
43
|
-
|
|
44
|
-
info('
|
|
45
|
-
info('
|
|
49
|
+
warn('Binance returned HTTP 451 — this host is in a Binance-restricted region.');
|
|
50
|
+
info('BINANCE trading (and paper mode, which uses live Binance prices) cannot work from here.');
|
|
51
|
+
info('Either run the agent from a Binance-permitted region (most EU / several Asia VPS');
|
|
52
|
+
info('regions work), or trade HYPERLIQUID instead — it is reachable from most regions,');
|
|
53
|
+
info('including this one. You pick the exchange on the dashboard after connecting.');
|
|
46
54
|
return false;
|
|
47
55
|
}
|
|
48
56
|
ok('Binance is reachable from this host');
|
|
@@ -58,3 +66,32 @@ export async function checkBinanceRegion() {
|
|
|
58
66
|
clearTimeout(t);
|
|
59
67
|
}
|
|
60
68
|
}
|
|
69
|
+
/** Probe Hyperliquid reachability — the venue-correct check for a user who
|
|
70
|
+
* told us (`--venue hyperliquid`) that is what they intend to trade. */
|
|
71
|
+
export async function checkHyperliquidRegion() {
|
|
72
|
+
step('Checking Hyperliquid reachability from this host');
|
|
73
|
+
const ctrl = new AbortController();
|
|
74
|
+
const t = setTimeout(() => ctrl.abort(), 5000);
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch('https://api.hyperliquid.xyz/info', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ type: 'meta' }),
|
|
80
|
+
signal: ctrl.signal,
|
|
81
|
+
});
|
|
82
|
+
if (res.ok) {
|
|
83
|
+
ok('Hyperliquid is reachable from this host');
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
warn(`Hyperliquid responded HTTP ${res.status} from this host.`);
|
|
87
|
+
info('The dashboard re-checks this after you connect.');
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
info('Could not probe Hyperliquid reachability (network hiccup) — skipping; the dashboard verifies it after connect.');
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(t);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@reefclaw/connect",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"reefclaw-connect": "dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"assets"
|
|
12
|
-
],
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=20"
|
|
15
|
-
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"bundle-assets": "node scripts/bundle-assets.mjs",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"test
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@reefclaw/connect",
|
|
3
|
+
"version": "0.1.32",
|
|
4
|
+
"description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"reefclaw-connect": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"assets"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"bundle-assets": "node scripts/bundle-assets.mjs",
|
|
18
|
+
"prepack": "npm run build",
|
|
19
|
+
"build": "tsc && node scripts/bundle-assets.mjs",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"test:run": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"json5": "2.2.3"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20",
|
|
28
|
+
"typescript": "^5",
|
|
29
|
+
"vitest": "^4.0.18"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"homepage": "https://reefclaw.com"
|
|
33
|
+
}
|