@reefclaw/connect 0.1.18 → 0.1.20
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/gateway/event-parser.d.ts +42 -0
- package/assets/bridge/gateway/event-parser.js +88 -0
- package/assets/bridge/gateway/heartbeat-cron.d.ts +1 -0
- package/assets/bridge/gateway/heartbeat-cron.js +22 -1
- package/assets/bridge/providers/gateway.d.ts +49 -0
- package/assets/bridge/providers/gateway.js +167 -13
- package/assets/bridge/types.d.ts +34 -0
- package/assets/bridge/utils/identity-name.d.ts +24 -0
- package/assets/bridge/utils/identity-name.js +54 -0
- package/assets/plugin/ccxt/binance-public.d.ts +4 -2
- package/assets/plugin/ccxt/binance-public.js +39 -3
- package/assets/plugin/live/proposal-decision-listener.d.ts +20 -0
- package/assets/plugin/live/proposal-decision-listener.js +211 -48
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/tools/create-order.d.ts +11 -0
- package/assets/plugin/tools/create-order.js +23 -2
- package/assets/plugin/tools/get-risk-summary.d.ts +4 -0
- package/assets/plugin/tools/get-risk-summary.js +62 -23
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.d.ts +4 -0
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +2 -2
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +12 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +69 -6
- package/assets/plugin/venues/hyperliquid/hl-order.d.ts +35 -0
- package/assets/plugin/venues/hyperliquid/hl-order.js +123 -0
- package/assets/plugin/venues/hyperliquid/hl-position.d.ts +36 -0
- package/assets/plugin/venues/hyperliquid/hl-position.js +127 -0
- package/assets/plugin/venues/hyperliquid/hl-private.d.ts +20 -3
- package/assets/plugin/venues/hyperliquid/hl-private.js +37 -6
- package/dist/cli.js +31 -10
- package/dist/gateway-tuning.js +112 -0
- package/package.json +1 -1
- package/assets/shared/signals/indicators-extended.d.ts +0 -52
- package/assets/shared/signals/indicators-extended.js +0 -284
- package/assets/shared/signals/indicators.d.ts +0 -15
- package/assets/shared/signals/indicators.js +0 -107
|
@@ -49,16 +49,33 @@ export declare class HyperliquidPrivateApi {
|
|
|
49
49
|
getExchange(): any;
|
|
50
50
|
loadMarkets(): Promise<boolean>;
|
|
51
51
|
/** Positions for the MASTER account. `null` = fetch failed (state unknown);
|
|
52
|
-
* `[]` = the exchange confirmed flat.
|
|
52
|
+
* `[]` = the exchange confirmed flat.
|
|
53
|
+
*
|
|
54
|
+
* ★ Rows are normalized through `normalizeHlPosition` — ccxt leaves
|
|
55
|
+
* `markPrice`/`contractSize`/`timestamp`/`datetime` UNDEFINED on this venue
|
|
56
|
+
* (clearinghouseState carries no markPx) while `CcxtPosition` declares them
|
|
57
|
+
* required, which crashed `get_risk_summary` and degraded every other
|
|
58
|
+
* mark-reading consumer. See hl-position.ts for the doc-exact derivation.
|
|
59
|
+
* A row that cannot be priced makes the WHOLE snapshot null (unknown) — never
|
|
60
|
+
* a partial list, never a fabricated zero. */
|
|
53
61
|
fetchPositions(symbol?: string): Promise<CcxtPosition[] | null>;
|
|
54
62
|
/** Open orders INCLUDING trigger/TPSL legs. CCXT's HL `fetchOpenOrders`
|
|
55
63
|
* defaults to `frontendOpenOrders`, which is the only endpoint that returns
|
|
56
|
-
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
64
|
+
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
65
|
+
*
|
|
66
|
+
* ★ Rows are normalized through `normalizeHlOrder` (ccxt leaves cost/fee/
|
|
67
|
+
* average/timeInForce undefined and leaks 'take profit market' past the
|
|
68
|
+
* type union — see hl-order.ts); `info` + `clientOrderId` survive verbatim
|
|
69
|
+
* (protective classification + bracket reconciliation read them). */
|
|
57
70
|
fetchOpenOrders(symbol?: string): Promise<CcxtOrder[] | null>;
|
|
58
71
|
fetchBalance(): Promise<CcxtBalance | null>;
|
|
59
72
|
/** Per-order status — the liveness resolver's REST tier (Tier 2 of the
|
|
60
73
|
* 3-tier rule). Weight 2. `null` = lookup FAILED (unknown), which callers
|
|
61
|
-
* must treat as "do not act", NOT as "gone".
|
|
74
|
+
* must treat as "do not act", NOT as "gone".
|
|
75
|
+
*
|
|
76
|
+
* Normalized like fetchOpenOrders. Unknown statuses collapse to 'open'
|
|
77
|
+
* (= not confirmed terminal — the conservative direction for both the
|
|
78
|
+
* liveness resolver and the entry poller); raw status stays in `info`. */
|
|
62
79
|
fetchOrder(orderId: string, symbol?: string): Promise<CcxtOrder | null>;
|
|
63
80
|
/** Own fills. WS is the authoritative ingress (plan + the audit-trail rule);
|
|
64
81
|
* this is the gap-fill/truth-check path. NOTE: only the 10,000 most recent
|
|
@@ -29,6 +29,8 @@ import { logger } from '../../logger.js';
|
|
|
29
29
|
import { toCcxtSymbol } from '../symbols.js';
|
|
30
30
|
import { assertNotLimited, noteError, noteSuccess, exchangeIpWeight, updateAddressBudget, } from './hl-rate-gate.js';
|
|
31
31
|
import { isValidHlCloid } from './hl-cloid.js';
|
|
32
|
+
import { normalizeHlPosition } from './hl-position.js';
|
|
33
|
+
import { normalizeHlOrder } from './hl-order.js';
|
|
32
34
|
// ccxt via CJS require — OpenClaw's ESM loader yields the wrong module shape
|
|
33
35
|
// (same rationale as binance-private.ts / hl-public.ts).
|
|
34
36
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -124,7 +126,15 @@ export class HyperliquidPrivateApi {
|
|
|
124
126
|
}
|
|
125
127
|
// ---- Reads (null on failure — NEVER []) ----
|
|
126
128
|
/** Positions for the MASTER account. `null` = fetch failed (state unknown);
|
|
127
|
-
* `[]` = the exchange confirmed flat.
|
|
129
|
+
* `[]` = the exchange confirmed flat.
|
|
130
|
+
*
|
|
131
|
+
* ★ Rows are normalized through `normalizeHlPosition` — ccxt leaves
|
|
132
|
+
* `markPrice`/`contractSize`/`timestamp`/`datetime` UNDEFINED on this venue
|
|
133
|
+
* (clearinghouseState carries no markPx) while `CcxtPosition` declares them
|
|
134
|
+
* required, which crashed `get_risk_summary` and degraded every other
|
|
135
|
+
* mark-reading consumer. See hl-position.ts for the doc-exact derivation.
|
|
136
|
+
* A row that cannot be priced makes the WHOLE snapshot null (unknown) — never
|
|
137
|
+
* a partial list, never a fabricated zero. */
|
|
128
138
|
async fetchPositions(symbol) {
|
|
129
139
|
try {
|
|
130
140
|
assertNotLimited('clearinghouseState');
|
|
@@ -133,7 +143,19 @@ export class HyperliquidPrivateApi {
|
|
|
133
143
|
noteSuccess('clearinghouseState');
|
|
134
144
|
if (!Array.isArray(raw))
|
|
135
145
|
return null;
|
|
136
|
-
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const p of raw) {
|
|
148
|
+
if (Math.abs(Number(p?.contracts ?? 0)) <= 0)
|
|
149
|
+
continue; // flat row — information, not failure
|
|
150
|
+
const normalized = normalizeHlPosition(p);
|
|
151
|
+
if (!normalized) {
|
|
152
|
+
logger.error(TAG, `fetchPositions: unparseable position row for ${String(p?.symbol ?? 'unknown symbol')} ` +
|
|
153
|
+
'— returning null (snapshot UNTRUSTED; a dropped row would read as "flat")');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
out.push(normalized);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
137
159
|
}
|
|
138
160
|
catch (err) {
|
|
139
161
|
noteError(err, 'fetchPositions');
|
|
@@ -143,14 +165,19 @@ export class HyperliquidPrivateApi {
|
|
|
143
165
|
}
|
|
144
166
|
/** Open orders INCLUDING trigger/TPSL legs. CCXT's HL `fetchOpenOrders`
|
|
145
167
|
* defaults to `frontendOpenOrders`, which is the only endpoint that returns
|
|
146
|
-
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
168
|
+
* trigger orders (plan §3.6) — the analog of Binance's merged algo endpoints.
|
|
169
|
+
*
|
|
170
|
+
* ★ Rows are normalized through `normalizeHlOrder` (ccxt leaves cost/fee/
|
|
171
|
+
* average/timeInForce undefined and leaks 'take profit market' past the
|
|
172
|
+
* type union — see hl-order.ts); `info` + `clientOrderId` survive verbatim
|
|
173
|
+
* (protective classification + bracket reconciliation read them). */
|
|
147
174
|
async fetchOpenOrders(symbol) {
|
|
148
175
|
try {
|
|
149
176
|
assertNotLimited('frontendOpenOrders');
|
|
150
177
|
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
151
178
|
const raw = await this.exchange.fetchOpenOrders(s);
|
|
152
179
|
noteSuccess('frontendOpenOrders');
|
|
153
|
-
return Array.isArray(raw) ? raw : null;
|
|
180
|
+
return Array.isArray(raw) ? raw.map((o) => normalizeHlOrder(o)) : null;
|
|
154
181
|
}
|
|
155
182
|
catch (err) {
|
|
156
183
|
noteError(err, 'fetchOpenOrders');
|
|
@@ -173,14 +200,18 @@ export class HyperliquidPrivateApi {
|
|
|
173
200
|
}
|
|
174
201
|
/** Per-order status — the liveness resolver's REST tier (Tier 2 of the
|
|
175
202
|
* 3-tier rule). Weight 2. `null` = lookup FAILED (unknown), which callers
|
|
176
|
-
* must treat as "do not act", NOT as "gone".
|
|
203
|
+
* must treat as "do not act", NOT as "gone".
|
|
204
|
+
*
|
|
205
|
+
* Normalized like fetchOpenOrders. Unknown statuses collapse to 'open'
|
|
206
|
+
* (= not confirmed terminal — the conservative direction for both the
|
|
207
|
+
* liveness resolver and the entry poller); raw status stays in `info`. */
|
|
177
208
|
async fetchOrder(orderId, symbol) {
|
|
178
209
|
try {
|
|
179
210
|
assertNotLimited('orderStatus');
|
|
180
211
|
const s = symbol ? toCcxtSymbol('hyperliquid', symbol) : undefined;
|
|
181
212
|
const raw = await this.exchange.fetchOrder(orderId, s);
|
|
182
213
|
noteSuccess('orderStatus');
|
|
183
|
-
return raw
|
|
214
|
+
return raw ? normalizeHlOrder(raw) : null;
|
|
184
215
|
}
|
|
185
216
|
catch (err) {
|
|
186
217
|
noteError(err, 'fetchOrder');
|
package/dist/cli.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
// message from reefclaw.com/onboarding to their agent (paste-to-agent stays the
|
|
8
8
|
// primary connect path). The bridge reads that config from openclaw.json and
|
|
9
9
|
// connects within seconds.
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
10
13
|
import { installPlugin } from './plugin.js';
|
|
11
14
|
import { installBridge } from './bridge.js';
|
|
12
15
|
import { installSkill } from './skill.js';
|
|
13
16
|
import { enableConnectorSupervisor } from './supervisor-config.js';
|
|
17
|
+
import { tuneGatewayMemory } from './gateway-tuning.js';
|
|
14
18
|
import { checkGateway, checkBinanceRegion } from './validate.js';
|
|
15
19
|
import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthDowngradeNeeded, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
|
|
16
20
|
import { run, which } from './exec.js';
|
|
@@ -92,21 +96,35 @@ function wireConfig(pre) {
|
|
|
92
96
|
/** The gateway only loads plugins at startup — a plugin registered while it
|
|
93
97
|
* runs stays invisible until restart (the bridge would log "No trading tools
|
|
94
98
|
* found" forever). Best-effort: a user without a managed gateway just gets
|
|
95
|
-
* the manual hint.
|
|
99
|
+
* the manual hint.
|
|
100
|
+
*
|
|
101
|
+
* Fallback: on systemd-user installs `openclaw gateway restart` can fail
|
|
102
|
+
* while `systemctl --user restart` works (observed live 2026-07-24 — the
|
|
103
|
+
* installer printed "could not restart automatically" and the new plugin
|
|
104
|
+
* never loaded until a manual systemctl restart). Try the unit directly
|
|
105
|
+
* before giving up. */
|
|
96
106
|
function restartGateway() {
|
|
97
107
|
step('Restarting the OpenClaw gateway to load the plugin');
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
ok('gateway restarted');
|
|
108
|
+
if (which('openclaw')) {
|
|
109
|
+
const r = run('openclaw', ['gateway', 'restart'], { timeoutMs: 120_000 });
|
|
110
|
+
if (r.ok) {
|
|
111
|
+
ok('gateway restarted');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
105
114
|
}
|
|
106
115
|
else {
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
info('openclaw CLI not on PATH — trying the systemd user service directly.');
|
|
117
|
+
}
|
|
118
|
+
if (process.platform === 'linux' &&
|
|
119
|
+
existsSync(join(homedir(), '.config', 'systemd', 'user', 'openclaw-gateway.service'))) {
|
|
120
|
+
const s = run('systemctl', ['--user', 'restart', 'openclaw-gateway'], { timeoutMs: 120_000 });
|
|
121
|
+
if (s.ok) {
|
|
122
|
+
ok('gateway restarted (systemd user service)');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
109
125
|
}
|
|
126
|
+
warn('could not restart the gateway automatically.');
|
|
127
|
+
info('Restart it yourself (e.g. `openclaw gateway restart`) so the trading tools load.');
|
|
110
128
|
}
|
|
111
129
|
function nextSteps() {
|
|
112
130
|
banner(green('✓ ReefClaw is installed.'));
|
|
@@ -135,6 +153,9 @@ async function main() {
|
|
|
135
153
|
// launchd/Task-Scheduler, works in containers. The gateway restart below is
|
|
136
154
|
// therefore also what STARTS the connector.
|
|
137
155
|
const supervised = enableConnectorSupervisor();
|
|
156
|
+
// Size the gateway heap cap to this box BEFORE the restart below picks it up.
|
|
157
|
+
// Re-runs after a VPS resize; leaves operator-authored overrides untouched.
|
|
158
|
+
tuneGatewayMemory();
|
|
138
159
|
if (plugin.registered)
|
|
139
160
|
restartGateway();
|
|
140
161
|
await checkGateway(readGatewayPort(merged));
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Box-size-aware gateway memory tuning (systemd-user installs only).
|
|
2
|
+
//
|
|
3
|
+
// Writes a NODE_OPTIONS heap cap + cgroup MemoryHigh override for the
|
|
4
|
+
// openclaw-gateway user service, sized to the box's total RAM. Two field-proven
|
|
5
|
+
// operating points (2026-07-22 / 2026-07-24 incidents):
|
|
6
|
+
// - ~2 GB box: uncapped node children balloon into the kernel OOM killer
|
|
7
|
+
// (heartbeats die at ~10 min) → cap 320 MB, MemoryHigh 1500M.
|
|
8
|
+
// - ~4 GB box: a STALE 320 MB cap left over from before a resize makes the
|
|
9
|
+
// gateway V8-OOM mid-heartbeat every ~15 min ("FATAL ERROR: Reached heap
|
|
10
|
+
// limit") with 2.5 GB free → cap 1536 MB, MemoryHigh 3000M.
|
|
11
|
+
// The second incident is why this module re-runs on every install/update and
|
|
12
|
+
// OVERWRITES a stale ReefClaw-authored override: resizing the droplet does not
|
|
13
|
+
// raise the cap by itself. Files we did not author are never touched.
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir, totalmem } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { run } from './exec.js';
|
|
18
|
+
import { step, ok, info, warn } from './ui.js';
|
|
19
|
+
const GiB = 1024 * 1024 * 1024;
|
|
20
|
+
/** First line of every override we write — the ownership marker that makes the
|
|
21
|
+
* file safe to overwrite on the next run. Operator-authored files without it
|
|
22
|
+
* are left alone. */
|
|
23
|
+
export const REEFCLAW_TUNING_MARKER = '# ReefClaw gateway memory tuning';
|
|
24
|
+
/** Pick the tuning for a box, or null when the box is large enough that node's
|
|
25
|
+
* defaults are fine and no override should be written. Pure — unit-testable. */
|
|
26
|
+
export function computeHeapTuning(totalBytes) {
|
|
27
|
+
if (totalBytes <= 2.75 * GiB) {
|
|
28
|
+
// 1-2 GB class: survival mode. Node children must GC hard instead of
|
|
29
|
+
// ballooning into the kernel OOM killer.
|
|
30
|
+
return { maxOldSpaceMb: 320, memoryHigh: '1500M' };
|
|
31
|
+
}
|
|
32
|
+
if (totalBytes <= 6 * GiB) {
|
|
33
|
+
// 4 GB class (the recommended floor): room to breathe, still backstopped
|
|
34
|
+
// below the kernel OOM threshold.
|
|
35
|
+
return { maxOldSpaceMb: 1536, memoryHigh: '3000M' };
|
|
36
|
+
}
|
|
37
|
+
// 8 GB+: node's own defaults are appropriate; do not constrain.
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Render the override file content for a tuning. */
|
|
41
|
+
export function renderTuningConf(tuning, totalBytes) {
|
|
42
|
+
const totalGb = (totalBytes / GiB).toFixed(1);
|
|
43
|
+
return (`${REEFCLAW_TUNING_MARKER} — written by @reefclaw/connect for a ${totalGb} GB box.\n` +
|
|
44
|
+
`# Re-running the installer after a VPS resize regenerates this file with the\n` +
|
|
45
|
+
`# right cap (a stale small-box cap V8-OOMs the gateway mid-heartbeat).\n` +
|
|
46
|
+
`# Delete this file and its marker line to manage these values yourself.\n` +
|
|
47
|
+
`[Service]\n` +
|
|
48
|
+
`Environment=NODE_OPTIONS=--max-old-space-size=${tuning.maxOldSpaceMb}\n` +
|
|
49
|
+
`MemoryHigh=${tuning.memoryHigh}\n`);
|
|
50
|
+
}
|
|
51
|
+
/** True when the existing override file is ours to overwrite (absent, or
|
|
52
|
+
* carries a ReefClaw marker — including the pre-installer hand-written
|
|
53
|
+
* variants whose first line starts with "# ReefClaw"). Pure. */
|
|
54
|
+
export function canReplaceOverride(existingContent) {
|
|
55
|
+
if (existingContent === null)
|
|
56
|
+
return true;
|
|
57
|
+
return existingContent.trimStart().startsWith('# ReefClaw');
|
|
58
|
+
}
|
|
59
|
+
/** Best-effort: returns true when the override is in place (written now or
|
|
60
|
+
* already correct), false when skipped. Never throws — resource tuning must
|
|
61
|
+
* not fail an install. */
|
|
62
|
+
export function tuneGatewayMemory() {
|
|
63
|
+
if (process.platform !== 'linux')
|
|
64
|
+
return false;
|
|
65
|
+
const unitPath = join(homedir(), '.config', 'systemd', 'user', 'openclaw-gateway.service');
|
|
66
|
+
if (!existsSync(unitPath)) {
|
|
67
|
+
// Container / non-systemd install (e.g. the connector-supervisor path) —
|
|
68
|
+
// there is no unit to override.
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
step('Tuning gateway memory for this box');
|
|
72
|
+
try {
|
|
73
|
+
const tuning = computeHeapTuning(totalmem());
|
|
74
|
+
const dropInDir = `${unitPath}.d`;
|
|
75
|
+
// Historical name kept for continuity with the hand-written mitigation
|
|
76
|
+
// files already in the field (same file = systemd merge rules stay simple).
|
|
77
|
+
const confPath = join(dropInDir, 'small-box.conf');
|
|
78
|
+
const existing = existsSync(confPath) ? readFileSync(confPath, 'utf-8') : null;
|
|
79
|
+
if (!canReplaceOverride(existing)) {
|
|
80
|
+
info(`found an operator-managed override at ${confPath} — leaving it untouched.`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (tuning === null) {
|
|
84
|
+
if (existing !== null) {
|
|
85
|
+
info('box has ample RAM — existing ReefClaw override left as-is (delete it to use node defaults).');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
ok('box has ample RAM — no memory override needed');
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const content = renderTuningConf(tuning, totalmem());
|
|
93
|
+
if (existing === content) {
|
|
94
|
+
ok(`gateway memory override already current (heap ${tuning.maxOldSpaceMb} MB, MemoryHigh ${tuning.memoryHigh})`);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
mkdirSync(dropInDir, { recursive: true });
|
|
98
|
+
writeFileSync(confPath, content, 'utf-8');
|
|
99
|
+
// Reload so the values apply on the next service (re)start — the gateway
|
|
100
|
+
// restart later in the install flow picks them up.
|
|
101
|
+
const reload = run('systemctl', ['--user', 'daemon-reload'], { timeoutMs: 30_000 });
|
|
102
|
+
if (!reload.ok) {
|
|
103
|
+
warn('wrote the memory override but `systemctl --user daemon-reload` failed — it applies after the next reload/restart.');
|
|
104
|
+
}
|
|
105
|
+
ok(`gateway heap cap set to ${tuning.maxOldSpaceMb} MB (MemoryHigh ${tuning.memoryHigh}) for this box size`);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
warn(`could not tune gateway memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export interface OhlcvInput {
|
|
2
|
-
open: number;
|
|
3
|
-
high: number;
|
|
4
|
-
low: number;
|
|
5
|
-
close: number;
|
|
6
|
-
volume: number;
|
|
7
|
-
}
|
|
8
|
-
export interface MACDResult {
|
|
9
|
-
line: number;
|
|
10
|
-
signal: number;
|
|
11
|
-
histogram: number;
|
|
12
|
-
crossover: 'bullish' | 'bearish' | 'none';
|
|
13
|
-
}
|
|
14
|
-
export interface BollingerResult {
|
|
15
|
-
upper: number;
|
|
16
|
-
middle: number;
|
|
17
|
-
lower: number;
|
|
18
|
-
bandwidth: number;
|
|
19
|
-
percentB: number;
|
|
20
|
-
}
|
|
21
|
-
export interface StochRSIResult {
|
|
22
|
-
k: number;
|
|
23
|
-
d: number;
|
|
24
|
-
}
|
|
25
|
-
export interface IchimokuResult {
|
|
26
|
-
tenkan: number;
|
|
27
|
-
kijun: number;
|
|
28
|
-
senkouA: number;
|
|
29
|
-
senkouB: number;
|
|
30
|
-
chikou: number;
|
|
31
|
-
cloudPosition: 'above' | 'below' | 'inside';
|
|
32
|
-
}
|
|
33
|
-
export interface SupertrendResult {
|
|
34
|
-
value: number;
|
|
35
|
-
direction: 'bullish' | 'bearish';
|
|
36
|
-
}
|
|
37
|
-
export declare function computeMACD(closes: number[], fast?: number, slow?: number, signal?: number): MACDResult;
|
|
38
|
-
export declare function computeBollingerBands(closes: number[], period?: number, stdDev?: number): BollingerResult;
|
|
39
|
-
export declare function computeVWAP(bars: OhlcvInput[]): number;
|
|
40
|
-
export declare function computeStochRSI(closes: number[], rsiPeriod?: number, stochPeriod?: number, kSmooth?: number, dSmooth?: number): StochRSIResult;
|
|
41
|
-
export declare function computeADX(highs: number[], lows: number[], closes: number[], period?: number): {
|
|
42
|
-
adx: number;
|
|
43
|
-
plusDI: number;
|
|
44
|
-
minusDI: number;
|
|
45
|
-
};
|
|
46
|
-
export declare function computeIchimoku(highs: number[], lows: number[], closes: number[], tenkanPeriod?: number, kijunPeriod?: number, senkouBPeriod?: number): IchimokuResult;
|
|
47
|
-
export declare function computeOBV(closes: number[], volumes: number[]): {
|
|
48
|
-
obv: number;
|
|
49
|
-
slope: 'rising' | 'falling' | 'flat';
|
|
50
|
-
};
|
|
51
|
-
export declare function computeSupertrend(highs: number[], lows: number[], closes: number[], period?: number, multiplier?: number): SupertrendResult;
|
|
52
|
-
export declare function computeWilliamsR(highs: number[], lows: number[], closes: number[], period?: number): number;
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
// Extended indicator computations for Phase 13 — Expanded Indicators.
|
|
2
|
-
// Wraps technicalindicators library + custom implementations.
|
|
3
|
-
// All functions take OHLCV arrays (oldest first) and return latest values.
|
|
4
|
-
import { computeATRSeries, computeRSI, mean, computeStd } from './indicators.js';
|
|
5
|
-
// ─── MACD (12, 26, 9) ──────────────────────────────────────────────────
|
|
6
|
-
export function computeMACD(closes, fast = 12, slow = 26, signal = 9) {
|
|
7
|
-
if (closes.length < slow + signal) {
|
|
8
|
-
return { line: 0, signal: 0, histogram: 0, crossover: 'none' };
|
|
9
|
-
}
|
|
10
|
-
// Compute full EMA series (both aligned to start at index `slow - 1`)
|
|
11
|
-
const emaFastSeries = emaSeries(closes, fast);
|
|
12
|
-
const emaSlowSeries = emaSeries(closes, slow);
|
|
13
|
-
// Align: fast series starts earlier, so take the tail matching slow series length
|
|
14
|
-
const offset = emaFastSeries.length - emaSlowSeries.length;
|
|
15
|
-
const macdLine = [];
|
|
16
|
-
for (let i = 0; i < emaSlowSeries.length; i++) {
|
|
17
|
-
macdLine.push(emaFastSeries[i + offset] - emaSlowSeries[i]);
|
|
18
|
-
}
|
|
19
|
-
// Signal line = EMA of MACD line
|
|
20
|
-
const signalSeries = emaSeries(macdLine, signal);
|
|
21
|
-
const sigOffset = macdLine.length - signalSeries.length;
|
|
22
|
-
const currentLine = macdLine[macdLine.length - 1];
|
|
23
|
-
const currentSignal = signalSeries[signalSeries.length - 1];
|
|
24
|
-
const prevLine = macdLine.length >= 2 ? macdLine[macdLine.length - 2] : currentLine;
|
|
25
|
-
const prevSignalIdx = signalSeries.length >= 2 ? signalSeries.length - 2 : signalSeries.length - 1;
|
|
26
|
-
const prevSignal = signalSeries[prevSignalIdx];
|
|
27
|
-
let crossover = 'none';
|
|
28
|
-
if (prevLine <= prevSignal && currentLine > currentSignal)
|
|
29
|
-
crossover = 'bullish';
|
|
30
|
-
else if (prevLine >= prevSignal && currentLine < currentSignal)
|
|
31
|
-
crossover = 'bearish';
|
|
32
|
-
return {
|
|
33
|
-
line: currentLine,
|
|
34
|
-
signal: currentSignal,
|
|
35
|
-
histogram: currentLine - currentSignal,
|
|
36
|
-
crossover,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
// ─── Bollinger Bands (20, 2σ) ───────────────────────────────────────────
|
|
40
|
-
export function computeBollingerBands(closes, period = 20, stdDev = 2) {
|
|
41
|
-
if (closes.length < period) {
|
|
42
|
-
const p = closes[closes.length - 1] ?? 0;
|
|
43
|
-
return { upper: p, middle: p, lower: p, bandwidth: 0, percentB: 0.5 };
|
|
44
|
-
}
|
|
45
|
-
const slice = closes.slice(-period);
|
|
46
|
-
const middle = mean(slice);
|
|
47
|
-
const std = computeStd(slice);
|
|
48
|
-
const upper = middle + stdDev * std;
|
|
49
|
-
const lower = middle - stdDev * std;
|
|
50
|
-
const bandwidth = middle > 0 ? ((upper - lower) / middle) * 100 : 0;
|
|
51
|
-
const price = closes[closes.length - 1];
|
|
52
|
-
const percentB = upper !== lower ? (price - lower) / (upper - lower) : 0.5;
|
|
53
|
-
return { upper, middle, lower, bandwidth, percentB };
|
|
54
|
-
}
|
|
55
|
-
// ─── VWAP ───────────────────────────────────────────────────────────────
|
|
56
|
-
export function computeVWAP(bars) {
|
|
57
|
-
if (bars.length === 0)
|
|
58
|
-
return 0;
|
|
59
|
-
let cumVolume = 0;
|
|
60
|
-
let cumTPxVol = 0;
|
|
61
|
-
for (const bar of bars) {
|
|
62
|
-
const tp = (bar.high + bar.low + bar.close) / 3;
|
|
63
|
-
cumVolume += bar.volume;
|
|
64
|
-
cumTPxVol += tp * bar.volume;
|
|
65
|
-
}
|
|
66
|
-
return cumVolume > 0 ? cumTPxVol / cumVolume : bars[bars.length - 1].close;
|
|
67
|
-
}
|
|
68
|
-
// ─── Stochastic RSI (14, 14, 3, 3) ─────────────────────────────────────
|
|
69
|
-
export function computeStochRSI(closes, rsiPeriod = 14, stochPeriod = 14, kSmooth = 3, dSmooth = 3) {
|
|
70
|
-
if (closes.length < rsiPeriod + stochPeriod + dSmooth) {
|
|
71
|
-
return { k: 50, d: 50 };
|
|
72
|
-
}
|
|
73
|
-
// Compute RSI series
|
|
74
|
-
const rsiValues = [];
|
|
75
|
-
for (let i = rsiPeriod + 1; i <= closes.length; i++) {
|
|
76
|
-
rsiValues.push(computeRSI(closes.slice(0, i), rsiPeriod));
|
|
77
|
-
}
|
|
78
|
-
if (rsiValues.length < stochPeriod)
|
|
79
|
-
return { k: 50, d: 50 };
|
|
80
|
-
// Stochastic of RSI
|
|
81
|
-
const rawK = [];
|
|
82
|
-
for (let i = stochPeriod - 1; i < rsiValues.length; i++) {
|
|
83
|
-
const window = rsiValues.slice(i - stochPeriod + 1, i + 1);
|
|
84
|
-
const min = Math.min(...window);
|
|
85
|
-
const max = Math.max(...window);
|
|
86
|
-
rawK.push(max !== min ? ((rsiValues[i] - min) / (max - min)) * 100 : 50);
|
|
87
|
-
}
|
|
88
|
-
// %K = SMA of raw stochastic
|
|
89
|
-
const kValues = sma(rawK, kSmooth);
|
|
90
|
-
// %D = SMA of %K
|
|
91
|
-
const dValues = sma(kValues, dSmooth);
|
|
92
|
-
return {
|
|
93
|
-
k: Math.round(kValues[kValues.length - 1] ?? 50),
|
|
94
|
-
d: Math.round(dValues[dValues.length - 1] ?? 50),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
// ─── ADX (14) ───────────────────────────────────────────────────────────
|
|
98
|
-
export function computeADX(highs, lows, closes, period = 14) {
|
|
99
|
-
if (highs.length < period * 2 + 1) {
|
|
100
|
-
return { adx: 0, plusDI: 0, minusDI: 0 };
|
|
101
|
-
}
|
|
102
|
-
const plusDM = [];
|
|
103
|
-
const minusDM = [];
|
|
104
|
-
const tr = [];
|
|
105
|
-
for (let i = 1; i < highs.length; i++) {
|
|
106
|
-
const upMove = highs[i] - highs[i - 1];
|
|
107
|
-
const downMove = lows[i - 1] - lows[i];
|
|
108
|
-
plusDM.push(upMove > downMove && upMove > 0 ? upMove : 0);
|
|
109
|
-
minusDM.push(downMove > upMove && downMove > 0 ? downMove : 0);
|
|
110
|
-
tr.push(Math.max(highs[i] - lows[i], Math.abs(highs[i] - closes[i - 1]), Math.abs(lows[i] - closes[i - 1])));
|
|
111
|
-
}
|
|
112
|
-
// Smooth with Wilder's smoothing (equivalent to EMA with alpha=1/period)
|
|
113
|
-
const smoothPlusDM = wilderSmooth(plusDM, period);
|
|
114
|
-
const smoothMinusDM = wilderSmooth(minusDM, period);
|
|
115
|
-
const smoothTR = wilderSmooth(tr, period);
|
|
116
|
-
// +DI and -DI series
|
|
117
|
-
const plusDISeries = [];
|
|
118
|
-
const minusDISeries = [];
|
|
119
|
-
for (let i = 0; i < smoothTR.length; i++) {
|
|
120
|
-
plusDISeries.push(smoothTR[i] > 0 ? (smoothPlusDM[i] / smoothTR[i]) * 100 : 0);
|
|
121
|
-
minusDISeries.push(smoothTR[i] > 0 ? (smoothMinusDM[i] / smoothTR[i]) * 100 : 0);
|
|
122
|
-
}
|
|
123
|
-
// DX series
|
|
124
|
-
const dxSeries = [];
|
|
125
|
-
for (let i = 0; i < plusDISeries.length; i++) {
|
|
126
|
-
const sum = plusDISeries[i] + minusDISeries[i];
|
|
127
|
-
dxSeries.push(sum > 0 ? (Math.abs(plusDISeries[i] - minusDISeries[i]) / sum) * 100 : 0);
|
|
128
|
-
}
|
|
129
|
-
// ADX = Wilder smooth of DX
|
|
130
|
-
const adxSeries = wilderSmooth(dxSeries, period);
|
|
131
|
-
return {
|
|
132
|
-
adx: Math.round(adxSeries[adxSeries.length - 1] ?? 0),
|
|
133
|
-
plusDI: Math.round(plusDISeries[plusDISeries.length - 1] ?? 0),
|
|
134
|
-
minusDI: Math.round(minusDISeries[minusDISeries.length - 1] ?? 0),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
// ─── Ichimoku Cloud ─────────────────────────────────────────────────────
|
|
138
|
-
export function computeIchimoku(highs, lows, closes, tenkanPeriod = 9, kijunPeriod = 26, senkouBPeriod = 52) {
|
|
139
|
-
const n = highs.length;
|
|
140
|
-
if (n < senkouBPeriod) {
|
|
141
|
-
const p = closes[n - 1] ?? 0;
|
|
142
|
-
return { tenkan: p, kijun: p, senkouA: p, senkouB: p, chikou: p, cloudPosition: 'inside' };
|
|
143
|
-
}
|
|
144
|
-
const midpoint = (arr, period, end) => {
|
|
145
|
-
const slice = arr.slice(Math.max(0, end - period + 1), end + 1);
|
|
146
|
-
return (Math.max(...slice) + Math.min(...slice)) / 2;
|
|
147
|
-
};
|
|
148
|
-
const tenkan = midpoint(highs.concat().map((h, i) => Math.max(h, lows[i])), tenkanPeriod, n - 1);
|
|
149
|
-
const kijun = midpoint(highs.concat().map((h, i) => Math.max(h, lows[i])), kijunPeriod, n - 1);
|
|
150
|
-
// Recalculate properly using highs and lows separately
|
|
151
|
-
const tenkanH = Math.max(...highs.slice(-tenkanPeriod));
|
|
152
|
-
const tenkanL = Math.min(...lows.slice(-tenkanPeriod));
|
|
153
|
-
const tenkanVal = (tenkanH + tenkanL) / 2;
|
|
154
|
-
const kijunH = Math.max(...highs.slice(-kijunPeriod));
|
|
155
|
-
const kijunL = Math.min(...lows.slice(-kijunPeriod));
|
|
156
|
-
const kijunVal = (kijunH + kijunL) / 2;
|
|
157
|
-
const senkouA = (tenkanVal + kijunVal) / 2;
|
|
158
|
-
const senkouBH = Math.max(...highs.slice(-senkouBPeriod));
|
|
159
|
-
const senkouBL = Math.min(...lows.slice(-senkouBPeriod));
|
|
160
|
-
const senkouB = (senkouBH + senkouBL) / 2;
|
|
161
|
-
const chikou = closes[n - 1]; // Current close (projected back 26 periods)
|
|
162
|
-
const price = closes[n - 1];
|
|
163
|
-
const cloudTop = Math.max(senkouA, senkouB);
|
|
164
|
-
const cloudBottom = Math.min(senkouA, senkouB);
|
|
165
|
-
const cloudPosition = price > cloudTop ? 'above' : price < cloudBottom ? 'below' : 'inside';
|
|
166
|
-
return { tenkan: tenkanVal, kijun: kijunVal, senkouA, senkouB, chikou, cloudPosition };
|
|
167
|
-
}
|
|
168
|
-
// ─── OBV (On-Balance Volume) ────────────────────────────────────────────
|
|
169
|
-
export function computeOBV(closes, volumes) {
|
|
170
|
-
if (closes.length < 2)
|
|
171
|
-
return { obv: 0, slope: 'flat' };
|
|
172
|
-
let obv = 0;
|
|
173
|
-
const obvSeries = [0];
|
|
174
|
-
for (let i = 1; i < closes.length; i++) {
|
|
175
|
-
if (closes[i] > closes[i - 1])
|
|
176
|
-
obv += volumes[i];
|
|
177
|
-
else if (closes[i] < closes[i - 1])
|
|
178
|
-
obv -= volumes[i];
|
|
179
|
-
obvSeries.push(obv);
|
|
180
|
-
}
|
|
181
|
-
// Slope over last 10 bars
|
|
182
|
-
const lookback = Math.min(10, obvSeries.length);
|
|
183
|
-
const recent = obvSeries.slice(-lookback);
|
|
184
|
-
const first = recent[0];
|
|
185
|
-
const last = recent[recent.length - 1];
|
|
186
|
-
const threshold = Math.abs(first) * 0.01; // 1% threshold
|
|
187
|
-
const slope = last - first > threshold ? 'rising' : last - first < -threshold ? 'falling' : 'flat';
|
|
188
|
-
return { obv, slope };
|
|
189
|
-
}
|
|
190
|
-
// ─── Supertrend (10, 3) ─────────────────────────────────────────────────
|
|
191
|
-
export function computeSupertrend(highs, lows, closes, period = 10, multiplier = 3) {
|
|
192
|
-
const atrSeries = computeATRSeries(highs, lows, closes, period);
|
|
193
|
-
if (atrSeries.length === 0) {
|
|
194
|
-
return { value: closes[closes.length - 1] ?? 0, direction: 'bullish' };
|
|
195
|
-
}
|
|
196
|
-
// ATR series starts at index 1 (needs previous close for TR)
|
|
197
|
-
// Align: atrSeries[i] corresponds to bar index i+1
|
|
198
|
-
let upperBand = 0;
|
|
199
|
-
let lowerBand = 0;
|
|
200
|
-
let supertrend = 0;
|
|
201
|
-
let direction = 'bullish';
|
|
202
|
-
for (let i = 0; i < atrSeries.length; i++) {
|
|
203
|
-
const barIdx = i + 1; // offset for TR calculation
|
|
204
|
-
const hl2 = (highs[barIdx] + lows[barIdx]) / 2;
|
|
205
|
-
const atr = atrSeries[i];
|
|
206
|
-
const basicUpper = hl2 + multiplier * atr;
|
|
207
|
-
const basicLower = hl2 - multiplier * atr;
|
|
208
|
-
upperBand = i > 0 && basicUpper < upperBand && closes[barIdx - 1] > upperBand ? upperBand : basicUpper;
|
|
209
|
-
lowerBand = i > 0 && basicLower > lowerBand && closes[barIdx - 1] < lowerBand ? lowerBand : basicLower;
|
|
210
|
-
if (i === 0) {
|
|
211
|
-
supertrend = closes[barIdx] > upperBand ? lowerBand : upperBand;
|
|
212
|
-
direction = closes[barIdx] > upperBand ? 'bullish' : 'bearish';
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
if (direction === 'bullish') {
|
|
216
|
-
if (closes[barIdx] < lowerBand) {
|
|
217
|
-
direction = 'bearish';
|
|
218
|
-
supertrend = upperBand;
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
supertrend = lowerBand;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
if (closes[barIdx] > upperBand) {
|
|
226
|
-
direction = 'bullish';
|
|
227
|
-
supertrend = lowerBand;
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
supertrend = upperBand;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return { value: supertrend, direction };
|
|
236
|
-
}
|
|
237
|
-
// ─── Williams %R (14) ───────────────────────────────────────────────────
|
|
238
|
-
export function computeWilliamsR(highs, lows, closes, period = 14) {
|
|
239
|
-
if (highs.length < period)
|
|
240
|
-
return -50;
|
|
241
|
-
const recentHighs = highs.slice(-period);
|
|
242
|
-
const recentLows = lows.slice(-period);
|
|
243
|
-
const hh = Math.max(...recentHighs);
|
|
244
|
-
const ll = Math.min(...recentLows);
|
|
245
|
-
const close = closes[closes.length - 1];
|
|
246
|
-
return hh !== ll ? ((hh - close) / (hh - ll)) * -100 : -50;
|
|
247
|
-
}
|
|
248
|
-
// ─── Helper: EMA series ─────────────────────────────────────────────────
|
|
249
|
-
function emaSeries(data, period) {
|
|
250
|
-
if (data.length === 0)
|
|
251
|
-
return [];
|
|
252
|
-
if (data.length < period)
|
|
253
|
-
return [data[data.length - 1]];
|
|
254
|
-
const k = 2 / (period + 1);
|
|
255
|
-
const result = [];
|
|
256
|
-
let ema = mean(data.slice(0, period));
|
|
257
|
-
result.push(ema);
|
|
258
|
-
for (let i = period; i < data.length; i++) {
|
|
259
|
-
ema = data[i] * k + ema * (1 - k);
|
|
260
|
-
result.push(ema);
|
|
261
|
-
}
|
|
262
|
-
return result;
|
|
263
|
-
}
|
|
264
|
-
// ─── Helper: SMA series ─────────────────────────────────────────────────
|
|
265
|
-
function sma(data, period) {
|
|
266
|
-
if (data.length < period)
|
|
267
|
-
return data.length > 0 ? [mean(data)] : [];
|
|
268
|
-
const result = [];
|
|
269
|
-
for (let i = period - 1; i < data.length; i++) {
|
|
270
|
-
result.push(mean(data.slice(i - period + 1, i + 1)));
|
|
271
|
-
}
|
|
272
|
-
return result;
|
|
273
|
-
}
|
|
274
|
-
// ─── Helper: Wilder's smoothing ─────────────────────────────────────────
|
|
275
|
-
function wilderSmooth(data, period) {
|
|
276
|
-
if (data.length < period)
|
|
277
|
-
return [];
|
|
278
|
-
const result = [];
|
|
279
|
-
result.push(mean(data.slice(0, period)));
|
|
280
|
-
for (let i = period; i < data.length; i++) {
|
|
281
|
-
result.push((result[result.length - 1] * (period - 1) + data[i]) / period);
|
|
282
|
-
}
|
|
283
|
-
return result;
|
|
284
|
-
}
|