@reefclaw/connect 0.1.16 → 0.1.17
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/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/venues/hyperliquid/hl-day-anchor.d.ts +36 -0
- package/assets/plugin/venues/hyperliquid/hl-day-anchor.js +114 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +5 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +29 -1
- package/assets/plugin/venues/hyperliquid/hl-private.d.ts +16 -0
- package/assets/plugin/venues/hyperliquid/hl-private.js +47 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "reefclaw-paper-trading",
|
|
3
3
|
"name": "ReefClaw Trading",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.10",
|
|
5
5
|
"description": "Supervised trading plugin for the ReefClaw dashboard: paper trading with real market data (no API keys required), and optional live trading on Binance or Hyperliquid behind explicit operator opt-in, exchange API credentials, and always-on protective stop brackets. Includes the dashboard connector bridge, heartbeat automation, and remote SKILL.md instruction updates from the ReefClaw webapp.",
|
|
6
6
|
"author": "ReefClaw",
|
|
7
7
|
"activation": {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type HlFillForAnchor, type HlFundingForAnchor } from './hl-balance.js';
|
|
2
|
+
/** The two anchor-input reads the private API supplies. `HyperliquidPrivateApi`
|
|
3
|
+
* satisfies this structurally, so production passes `this.api`; tests pass a fake. */
|
|
4
|
+
export interface HlAnchorFetchers {
|
|
5
|
+
fetchFillsSince(sinceMs: number): Promise<HlFillForAnchor[] | null>;
|
|
6
|
+
fetchFundingSince(sinceMs: number): Promise<HlFundingForAnchor[] | null>;
|
|
7
|
+
}
|
|
8
|
+
export declare class HlDayPnlAnchor {
|
|
9
|
+
private readonly refreshIntervalMs;
|
|
10
|
+
private sessionStartNav;
|
|
11
|
+
private realizedPnlToday;
|
|
12
|
+
private anchorUtcDay;
|
|
13
|
+
private lastRefreshMs;
|
|
14
|
+
constructor(refreshIntervalMs?: number);
|
|
15
|
+
/** `wallet_at_midnight` (+ any capital flows since). `null` until the first
|
|
16
|
+
* successful refresh — the caller then emits nothing rather than a fabricated
|
|
17
|
+
* anchor, and the skill's safe fallback (self-computed) applies. */
|
|
18
|
+
getSessionStartNav(): number | null;
|
|
19
|
+
/** `netNonTransfer` since UTC midnight = Σ closedPnl − fees + funding. Matches
|
|
20
|
+
* the HL app's "today's realized" decomposition (fees + funding included). */
|
|
21
|
+
getRealizedPnlToday(): number;
|
|
22
|
+
/** True when a recompute is due: bootstrap (no anchor yet), UTC-day rollover, or
|
|
23
|
+
* the throttle has elapsed. */
|
|
24
|
+
shouldRefresh(now: number): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Recompute the anchor from this UTC day's fills + funding. `walletNow` is
|
|
27
|
+
* `nav.wallet` (= accountValue − ΣuPnl). Best-effort — NEVER throws for the
|
|
28
|
+
* caller (a failed fetch keeps the last good anchor). Returns whether the anchor
|
|
29
|
+
* was (re)computed this call.
|
|
30
|
+
*/
|
|
31
|
+
refresh(args: {
|
|
32
|
+
walletNow: number;
|
|
33
|
+
fetchers: HlAnchorFetchers;
|
|
34
|
+
now?: number;
|
|
35
|
+
}): Promise<boolean>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Hyperliquid Day-P&L anchor — the stateful orchestration around the pure
|
|
2
|
+
// reconstruction in `hl-balance.ts`. This is the HL analog of the income-anchor
|
|
3
|
+
// half of Binance's `LiveBalanceEnricher`: HL has no `/fapi/v1/income` endpoint,
|
|
4
|
+
// so `sessionStartNav` (the UTC-midnight NAV baseline) is REBUILT each refresh
|
|
5
|
+
// from `userFillsByTime` + `userFunding` since midnight.
|
|
6
|
+
//
|
|
7
|
+
// ★ WHY THIS EXISTS — the incident it closes (2026-07-24). Before it was wired,
|
|
8
|
+
// the HL adapter's `getBalance()` returned a BARE balance with no
|
|
9
|
+
// `sessionStartNav`/`realizedPnlToday`/`equity`. The skill therefore fell back
|
|
10
|
+
// to self-computing the anchor ONCE per day from `computeEquity()` — a snapshot
|
|
11
|
+
// of equity at whatever instant it first polled, that neither tracked capital
|
|
12
|
+
// flows nor re-anchored to true UTC midnight. On a real micro-live account that
|
|
13
|
+
// printed a phantom **−$91.85 / −29.91% Day P&L** while the account was actually
|
|
14
|
+
// flat (real equity $215, unrealized −$0.19). This is the "T-4 KPI parity vs the
|
|
15
|
+
// HL app" gate (docs/CLAUDE/hyperliquid.md, plan §5.8).
|
|
16
|
+
//
|
|
17
|
+
// ★ SELF-CORRECTING BY CONSTRUCTION. `sessionStartNav = wallet_now − netNonTransfer`
|
|
18
|
+
// is recomputed from scratch every refresh. `netNonTransfer` (Σ closedPnl − fees
|
|
19
|
+
// + funding) EXCLUDES capital flows — a deposit/withdrawal is neither a fill nor
|
|
20
|
+
// funding — so a flow raises `wallet_now` and the anchor by the SAME signed
|
|
21
|
+
// amount and cancels out of Day-P&L automatically, with no incremental ledger
|
|
22
|
+
// bookkeeping that could drift. (`applyLedgerDelta` in hl-balance.ts remains for
|
|
23
|
+
// a possible future incremental path; the recompute makes it unnecessary here.)
|
|
24
|
+
//
|
|
25
|
+
// ★ null ≠ empty. A FAILED fills/funding fetch must NOT recompute the anchor from
|
|
26
|
+
// partial data — that would understate `netNonTransfer` and print exactly the
|
|
27
|
+
// phantom Day-P&L this fixes. On a failed fetch the refresh KEEPS the last good
|
|
28
|
+
// anchor and retries next cycle; the anchor is invariant within a UTC day except
|
|
29
|
+
// for capital flows, so a held value is correct, never fabricated.
|
|
30
|
+
import { logger } from '../../logger.js';
|
|
31
|
+
import { computeNetNonTransfer, deriveSessionStartNav, utcMidnightMs, } from './hl-balance.js';
|
|
32
|
+
const TAG = 'hl-day-anchor';
|
|
33
|
+
/** Recompute cadence. The anchor is invariant within a UTC day except for capital
|
|
34
|
+
* flows, so a modest throttle is plenty; forced on bootstrap + UTC-day rollover.
|
|
35
|
+
* Each refresh costs ~40 IP weight (userFills 20 + userFunding 20) against the
|
|
36
|
+
* 1200/min budget — trivial at heartbeat cadence. */
|
|
37
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 60_000;
|
|
38
|
+
/** UTC day (YYYY-MM-DD) — the Day-P&L rollover key. */
|
|
39
|
+
function utcDayString(ms) {
|
|
40
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
export class HlDayPnlAnchor {
|
|
43
|
+
refreshIntervalMs;
|
|
44
|
+
sessionStartNav = null;
|
|
45
|
+
realizedPnlToday = 0;
|
|
46
|
+
anchorUtcDay = null;
|
|
47
|
+
lastRefreshMs = 0;
|
|
48
|
+
constructor(refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS) {
|
|
49
|
+
this.refreshIntervalMs = refreshIntervalMs;
|
|
50
|
+
}
|
|
51
|
+
/** `wallet_at_midnight` (+ any capital flows since). `null` until the first
|
|
52
|
+
* successful refresh — the caller then emits nothing rather than a fabricated
|
|
53
|
+
* anchor, and the skill's safe fallback (self-computed) applies. */
|
|
54
|
+
getSessionStartNav() {
|
|
55
|
+
return this.sessionStartNav;
|
|
56
|
+
}
|
|
57
|
+
/** `netNonTransfer` since UTC midnight = Σ closedPnl − fees + funding. Matches
|
|
58
|
+
* the HL app's "today's realized" decomposition (fees + funding included). */
|
|
59
|
+
getRealizedPnlToday() {
|
|
60
|
+
return this.realizedPnlToday;
|
|
61
|
+
}
|
|
62
|
+
/** True when a recompute is due: bootstrap (no anchor yet), UTC-day rollover, or
|
|
63
|
+
* the throttle has elapsed. */
|
|
64
|
+
shouldRefresh(now) {
|
|
65
|
+
if (this.sessionStartNav === null)
|
|
66
|
+
return true;
|
|
67
|
+
if (utcDayString(now) !== this.anchorUtcDay)
|
|
68
|
+
return true;
|
|
69
|
+
return now - this.lastRefreshMs >= this.refreshIntervalMs;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Recompute the anchor from this UTC day's fills + funding. `walletNow` is
|
|
73
|
+
* `nav.wallet` (= accountValue − ΣuPnl). Best-effort — NEVER throws for the
|
|
74
|
+
* caller (a failed fetch keeps the last good anchor). Returns whether the anchor
|
|
75
|
+
* was (re)computed this call.
|
|
76
|
+
*/
|
|
77
|
+
async refresh(args) {
|
|
78
|
+
const now = args.now ?? Date.now();
|
|
79
|
+
if (!this.shouldRefresh(now))
|
|
80
|
+
return false;
|
|
81
|
+
const sinceMs = utcMidnightMs(now);
|
|
82
|
+
const [fills, fundings] = await Promise.all([
|
|
83
|
+
args.fetchers.fetchFillsSince(sinceMs),
|
|
84
|
+
args.fetchers.fetchFundingSince(sinceMs),
|
|
85
|
+
]);
|
|
86
|
+
// null ≠ empty: a failed read must not recompute from partial data. Keep the
|
|
87
|
+
// last good anchor (invariant within the day bar capital flows) and retry.
|
|
88
|
+
if (fills === null || fundings === null) {
|
|
89
|
+
logger.warn(TAG, `anchor refresh skipped — fetch failed (fills=${fills === null ? 'FAIL' : 'ok'}, ` +
|
|
90
|
+
`funding=${fundings === null ? 'FAIL' : 'ok'}); keeping last anchor ` +
|
|
91
|
+
`(sessionStartNav=${this.sessionStartNav ?? 'unset'})`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const { netNonTransfer, realizedPnlGross, fees, funding } = computeNetNonTransfer({
|
|
95
|
+
fills,
|
|
96
|
+
fundings,
|
|
97
|
+
sinceMs,
|
|
98
|
+
});
|
|
99
|
+
const prev = this.sessionStartNav;
|
|
100
|
+
const today = utcDayString(now);
|
|
101
|
+
const dayRolled = prev !== null && today !== this.anchorUtcDay;
|
|
102
|
+
this.sessionStartNav = deriveSessionStartNav({ walletNow: args.walletNow, netNonTransfer });
|
|
103
|
+
this.realizedPnlToday = netNonTransfer;
|
|
104
|
+
this.anchorUtcDay = today;
|
|
105
|
+
this.lastRefreshMs = now;
|
|
106
|
+
if (prev === null || dayRolled) {
|
|
107
|
+
logger.info(TAG, `Day-P&L anchor ${prev === null ? 'established' : `rolled to ${today}`}: ` +
|
|
108
|
+
`sessionStartNav=${this.sessionStartNav.toFixed(4)} ` +
|
|
109
|
+
`(wallet ${args.walletNow.toFixed(4)} − netNonTransfer ${netNonTransfer.toFixed(4)}; ` +
|
|
110
|
+
`realizedGross ${realizedPnlGross.toFixed(4)}, fees ${fees.toFixed(4)}, funding ${funding.toFixed(4)})`);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -30,6 +30,11 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
30
30
|
private truthCheckRunning;
|
|
31
31
|
private _readiness;
|
|
32
32
|
private openOrdersUnavailableUntil;
|
|
33
|
+
/** UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app, §5.8). HL has no
|
|
34
|
+
* income endpoint, so the anchor is rebuilt from userFillsByTime + userFunding
|
|
35
|
+
* each balance fetch. Without this the skill self-computes a bogus anchor and
|
|
36
|
+
* prints a phantom Day P&L (the 2026-07-24 −$91.85 incident). */
|
|
37
|
+
private readonly dayAnchor;
|
|
33
38
|
constructor(opts: HlLiveAdapterOptions);
|
|
34
39
|
/** The HL bracket orchestrator — the venue-aware tools (attach_brackets /
|
|
35
40
|
* modify_stop / modify_target / audit) drive brackets through this. */
|
|
@@ -31,6 +31,7 @@ import { HyperliquidInfoCache } from './hl-info-cache.js';
|
|
|
31
31
|
import { HyperliquidPublicApi } from './hl-public.js';
|
|
32
32
|
import { planBracket, planResize, buildBracketOrders, bracketCoversPosition, } from './hl-brackets.js';
|
|
33
33
|
import { deriveHlNav, toCcxtBalance } from './hl-balance.js';
|
|
34
|
+
import { HlDayPnlAnchor } from './hl-day-anchor.js';
|
|
34
35
|
import { buildHlOrderCloid, parseHlBracketCloid } from './hl-cloid.js';
|
|
35
36
|
import { BracketLedger } from '../../live/bracket-ledger.js';
|
|
36
37
|
import { generateBracketId } from '../../live/bracket-id.js';
|
|
@@ -69,6 +70,11 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
69
70
|
truthCheckRunning = false;
|
|
70
71
|
_readiness = 'INIT_PENDING';
|
|
71
72
|
openOrdersUnavailableUntil = 0;
|
|
73
|
+
/** UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app, §5.8). HL has no
|
|
74
|
+
* income endpoint, so the anchor is rebuilt from userFillsByTime + userFunding
|
|
75
|
+
* each balance fetch. Without this the skill self-computes a bogus anchor and
|
|
76
|
+
* prints a phantom Day P&L (the 2026-07-24 −$91.85 incident). */
|
|
77
|
+
dayAnchor = new HlDayPnlAnchor();
|
|
72
78
|
constructor(opts) {
|
|
73
79
|
super();
|
|
74
80
|
this.opts = opts;
|
|
@@ -467,7 +473,29 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
467
473
|
if (!nav) {
|
|
468
474
|
throw new Error('getBalance: clearinghouseState unreadable — balance UNKNOWN (never reported as $0)');
|
|
469
475
|
}
|
|
470
|
-
|
|
476
|
+
// Recompute the UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app §5.8).
|
|
477
|
+
// Best-effort + self-correcting: never throws, keeps the last good anchor on a
|
|
478
|
+
// failed fills/funding fetch (null ≠ empty). Emitting sessionStartNav here is
|
|
479
|
+
// what lets the skill's non-PAPER sync (gateway.ts) show a correct Day P&L
|
|
480
|
+
// instead of self-computing a bogus one (the 2026-07-24 −$91.85 incident).
|
|
481
|
+
try {
|
|
482
|
+
await this.dayAnchor.refresh({ walletNow: nav.wallet, fetchers: this.api });
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
logger.warn(TAG, `day-anchor refresh error (non-fatal): ${msg(err)}`);
|
|
486
|
+
}
|
|
487
|
+
const round4 = (n) => +n.toFixed(4);
|
|
488
|
+
const enriched = { ...toCcxtBalance(nav) };
|
|
489
|
+
const sessionStartNav = this.dayAnchor.getSessionStartNav();
|
|
490
|
+
// Only emit a POSITIVE anchor: the skill's non-PAPER sync gate is
|
|
491
|
+
// `pluginNav > 0`, and a null/0 anchor must fall through to the safe fallback
|
|
492
|
+
// rather than pin Day P&L. `equity` = accountValue (HL app "Account Equity").
|
|
493
|
+
if (sessionStartNav !== null && sessionStartNav > 0) {
|
|
494
|
+
enriched.sessionStartNav = round4(sessionStartNav);
|
|
495
|
+
enriched.realizedPnlToday = round4(this.dayAnchor.getRealizedPnlToday());
|
|
496
|
+
}
|
|
497
|
+
enriched.equity = round4(nav.equity);
|
|
498
|
+
return enriched;
|
|
471
499
|
}
|
|
472
500
|
/** Display contract (`?? []`) — the 20+ KPI/display callers. */
|
|
473
501
|
async getPositions(symbol) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CcxtOrder, CcxtPosition, CcxtBalance } from '../../types.js';
|
|
2
|
+
import type { HlFillForAnchor, HlFundingForAnchor } from './hl-balance.js';
|
|
2
3
|
export interface HlCredentials {
|
|
3
4
|
/** MASTER account address (0x…). Queries always use this — an agent wallet
|
|
4
5
|
* holds no balance and no positions (learned the hard way 2026-07-12). */
|
|
@@ -64,6 +65,21 @@ export declare class HyperliquidPrivateApi {
|
|
|
64
65
|
* fills exist server-side — deep history is NOT queryable on HL, which is why
|
|
65
66
|
* the `trades` table must be WS-first. */
|
|
66
67
|
fetchMyTrades(symbol?: string, since?: number, limit?: number): Promise<unknown[] | null>;
|
|
68
|
+
/** Fills at/after `sinceMs` — the Day-P&L anchor's realized+fee input
|
|
69
|
+
* (`userFillsByTime`, plan §3.6 / §5.8; weight 20). The raw rows structurally
|
|
70
|
+
* satisfy `HlFillForAnchor` ({time, closedPnl, fee, builderFee?}) so they feed
|
|
71
|
+
* `computeNetNonTransfer` directly. `null` = fetch FAILED (state unknown) — the
|
|
72
|
+
* anchor MUST keep its last good value, NEVER recompute from partial data
|
|
73
|
+
* (`null ≠ empty`; a fabricated anchor prints a phantom Day-P&L). NOTE: HL's
|
|
74
|
+
* `fee` is inclusive of `builderFee`, but we hard-disable the builder fee
|
|
75
|
+
* (`options.builderFee:false`, pinned by hl-private.test.ts) so `builderFee` is
|
|
76
|
+
* absent/0 on our fills and the sum is exact either way. */
|
|
77
|
+
fetchFillsSince(sinceMs: number): Promise<HlFillForAnchor[] | null>;
|
|
78
|
+
/** Funding deltas at/after `sinceMs` — the Day-P&L anchor's funding input
|
|
79
|
+
* (`userFunding`, plan §3.6 / §5.8; weight 20, shed-able). Raw rows satisfy
|
|
80
|
+
* `HlFundingForAnchor` via `delta.usdc` (signed USDC; negative = paid).
|
|
81
|
+
* `null` = fetch FAILED / paced-shed — anchor keeps its last good value. */
|
|
82
|
+
fetchFundingSince(sinceMs: number): Promise<HlFundingForAnchor[] | null>;
|
|
67
83
|
/** Refresh the ADDRESS action budget (the starvation guard). Weight 20 — call
|
|
68
84
|
* every ~5 min, never per-heartbeat. */
|
|
69
85
|
refreshAddressBudget(): Promise<void>;
|
|
@@ -206,6 +206,53 @@ export class HyperliquidPrivateApi {
|
|
|
206
206
|
return null;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
+
/** Fills at/after `sinceMs` — the Day-P&L anchor's realized+fee input
|
|
210
|
+
* (`userFillsByTime`, plan §3.6 / §5.8; weight 20). The raw rows structurally
|
|
211
|
+
* satisfy `HlFillForAnchor` ({time, closedPnl, fee, builderFee?}) so they feed
|
|
212
|
+
* `computeNetNonTransfer` directly. `null` = fetch FAILED (state unknown) — the
|
|
213
|
+
* anchor MUST keep its last good value, NEVER recompute from partial data
|
|
214
|
+
* (`null ≠ empty`; a fabricated anchor prints a phantom Day-P&L). NOTE: HL's
|
|
215
|
+
* `fee` is inclusive of `builderFee`, but we hard-disable the builder fee
|
|
216
|
+
* (`options.builderFee:false`, pinned by hl-private.test.ts) so `builderFee` is
|
|
217
|
+
* absent/0 on our fills and the sum is exact either way. */
|
|
218
|
+
async fetchFillsSince(sinceMs) {
|
|
219
|
+
try {
|
|
220
|
+
assertNotLimited('userFills');
|
|
221
|
+
const raw = await this.rawInfo({
|
|
222
|
+
type: 'userFillsByTime',
|
|
223
|
+
user: this.creds.walletAddress,
|
|
224
|
+
startTime: Math.floor(sinceMs),
|
|
225
|
+
});
|
|
226
|
+
noteSuccess('userFills');
|
|
227
|
+
return Array.isArray(raw) ? raw : null;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
noteError(err, 'fetchFillsSince');
|
|
231
|
+
logger.warn(TAG, `fetchFillsSince failed: ${msg(err)}`);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Funding deltas at/after `sinceMs` — the Day-P&L anchor's funding input
|
|
236
|
+
* (`userFunding`, plan §3.6 / §5.8; weight 20, shed-able). Raw rows satisfy
|
|
237
|
+
* `HlFundingForAnchor` via `delta.usdc` (signed USDC; negative = paid).
|
|
238
|
+
* `null` = fetch FAILED / paced-shed — anchor keeps its last good value. */
|
|
239
|
+
async fetchFundingSince(sinceMs) {
|
|
240
|
+
try {
|
|
241
|
+
assertNotLimited('userFunding');
|
|
242
|
+
const raw = await this.rawInfo({
|
|
243
|
+
type: 'userFunding',
|
|
244
|
+
user: this.creds.walletAddress,
|
|
245
|
+
startTime: Math.floor(sinceMs),
|
|
246
|
+
});
|
|
247
|
+
noteSuccess('userFunding');
|
|
248
|
+
return Array.isArray(raw) ? raw : null;
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
noteError(err, 'fetchFundingSince');
|
|
252
|
+
logger.warn(TAG, `fetchFundingSince failed: ${msg(err)}`);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
209
256
|
/** Refresh the ADDRESS action budget (the starvation guard). Weight 20 — call
|
|
210
257
|
* every ~5 min, never per-heartbeat. */
|
|
211
258
|
async refreshAddressBudget() {
|