@reefclaw/connect 0.1.19 → 0.1.21
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 +19 -0
- package/assets/bridge/gateway/event-parser.js +52 -0
- package/assets/bridge/gateway/gateway-config.d.ts +16 -5
- package/assets/bridge/gateway/gateway-config.js +68 -12
- package/assets/bridge/gateway/heartbeat-cron.d.ts +1 -0
- package/assets/bridge/gateway/heartbeat-cron.js +22 -1
- package/assets/bridge/gateway/poller.js +18 -8
- package/assets/bridge/providers/emergency-commands.d.ts +9 -1
- package/assets/bridge/providers/emergency-commands.js +38 -1
- package/assets/bridge/providers/gateway.d.ts +72 -1
- package/assets/bridge/providers/gateway.js +241 -29
- package/assets/bridge/providers/onboarding-commands.d.ts +8 -0
- package/assets/bridge/providers/onboarding-commands.js +4 -4
- package/assets/bridge/types.d.ts +30 -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 +21 -7
- package/assets/plugin/ccxt/binance-public.js +70 -6
- package/assets/plugin/config/operator-provenance.d.ts +6 -0
- package/assets/plugin/config/operator-provenance.js +50 -0
- package/assets/plugin/config/plugin-config-io.d.ts +15 -1
- package/assets/plugin/config/plugin-config-io.js +24 -0
- package/assets/plugin/index.js +216 -173
- package/assets/plugin/ingest/event-loop-monitor.d.ts +11 -0
- package/assets/plugin/ingest/event-loop-monitor.js +113 -0
- package/assets/plugin/ingest/position-auto-capture.d.ts +5 -0
- package/assets/plugin/ingest/position-auto-capture.js +14 -5
- package/assets/plugin/ingest/readiness-reporter.d.ts +17 -6
- package/assets/plugin/ingest/readiness-reporter.js +88 -9
- package/assets/plugin/ingest/skill-version-reader.d.ts +16 -0
- package/assets/plugin/ingest/skill-version-reader.js +64 -0
- package/assets/plugin/live/approval-lifecycle.d.ts +30 -0
- package/assets/plugin/live/approval-lifecycle.js +80 -0
- package/assets/plugin/live/bracket-types.d.ts +9 -0
- package/assets/plugin/live/live-adapter.d.ts +0 -1
- package/assets/plugin/live/proposal-decision-listener.d.ts +20 -0
- package/assets/plugin/live/proposal-decision-listener.js +211 -48
- package/assets/plugin/onboarding/runtime.d.ts +34 -1
- package/assets/plugin/onboarding/runtime.js +56 -5
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/simulator/exchange-simulator.d.ts +45 -2
- package/assets/plugin/simulator/exchange-simulator.js +96 -4
- package/assets/plugin/simulator/types.d.ts +17 -0
- package/assets/plugin/tools/attach-brackets.js +50 -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 +29 -1
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +59 -2
- package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.js +45 -13
- package/assets/plugin/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/assets/plugin/venues/hyperliquid/hl-fill-ingest.js +69 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +155 -12
- 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/assets/plugin/venues/hyperliquid/hl-public.d.ts +12 -5
- package/assets/plugin/venues/hyperliquid/hl-public.js +24 -3
- package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/assets/plugin/venues/hyperliquid/hl-user-stream.js +4 -1
- package/assets/shared/index.d.ts +3 -3
- package/assets/shared/index.js +2 -2
- package/assets/shared/readiness.d.ts +38 -2
- package/assets/shared/readiness.js +40 -8
- 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
|
@@ -5,22 +5,30 @@
|
|
|
5
5
|
// renders generically without duplicating the copy. Advisory only: readiness
|
|
6
6
|
// explains failures, it never blocks trading or the safety floor.
|
|
7
7
|
// See docs/AGENT_READINESS_GATE_PLAN.md.
|
|
8
|
+
/** A failed probe is read as a venue failure only while it finished within
|
|
9
|
+
* this multiple of its own timeout budget; past it, the clock says the
|
|
10
|
+
* process — not the network — is what broke. 2× leaves generous room for
|
|
11
|
+
* ordinary scheduling jitter and TLS/DNS slack while sitting far below the
|
|
12
|
+
* 8×-and-worse overshoots a real freeze produces. */
|
|
13
|
+
export const REACHABILITY_STALL_FACTOR = 2;
|
|
8
14
|
/** The canonical connect-phase checks + their plain-English fixes. Single source
|
|
9
15
|
* of copy; the plugin inlines these into each report so the webapp stays dumb. */
|
|
10
16
|
export const READINESS_CHECK_COPY = {
|
|
17
|
+
// ★ The DEFAULT reachability copy must describe possible causes, never assert
|
|
18
|
+
// one: this hint is attached to the debounced 'unreachable' warn, where all we
|
|
19
|
+
// know is that two probes did not complete. Asserting DNS/firewall/region sent
|
|
20
|
+
// an operator hunting a firewall that did not exist (issue #265) — the actual
|
|
21
|
+
// cause was the agent's own host being starved. A CONFIRMED geo-block (the
|
|
22
|
+
// server answered 451/403) gets the assertive copy below, passed explicitly.
|
|
11
23
|
binance_reachable: {
|
|
12
24
|
label: 'Binance reachable',
|
|
13
25
|
phase: 'connect',
|
|
14
|
-
fixHint: 'Your agent host
|
|
26
|
+
fixHint: 'Your agent host did not reach the Binance API on two consecutive checks. Possible causes: outbound HTTPS/DNS blocked, a transient Binance outage, or the host itself being starved of CPU/memory (check "Agent host responsive"). If it persists, verify connectivity and that the host region is Binance-permitted.',
|
|
15
27
|
},
|
|
16
|
-
// Dormant until the Hyperliquid venue ships (docs/HYPERLIQUID_INTEGRATION_PLAN.md
|
|
17
|
-
// Phase 1): the reporter probes only the CONFIGURED venue, so this id is never
|
|
18
|
-
// emitted while venue='binance'. Landed early so older webapps render it via the
|
|
19
|
-
// string-widened ReadinessCheck.id the moment a newer plugin starts sending it.
|
|
20
28
|
hyperliquid_reachable: {
|
|
21
29
|
label: 'Hyperliquid reachable',
|
|
22
30
|
phase: 'connect',
|
|
23
|
-
fixHint: 'Your agent host
|
|
31
|
+
fixHint: 'Your agent host did not reach the Hyperliquid API (api.hyperliquid.xyz) on two consecutive checks. Possible causes: outbound HTTPS/DNS blocked, a transient Hyperliquid outage, or the host itself being starved of CPU/memory (check "Agent host responsive"). If it persists, verify connectivity and that the host region is permitted.',
|
|
24
32
|
},
|
|
25
33
|
clock_in_sync: {
|
|
26
34
|
label: 'Host clock in sync',
|
|
@@ -37,9 +45,33 @@ export const READINESS_CHECK_COPY = {
|
|
|
37
45
|
phase: 'connect',
|
|
38
46
|
fixHint: 'No trading tools are registered. Ensure tools.alsoAllow includes "group:plugins", then restart the gateway.',
|
|
39
47
|
},
|
|
48
|
+
// The signal that actually matters on a trading rig, and the one that was
|
|
49
|
+
// invisible while a starved host masqueraded as an unreachable venue
|
|
50
|
+
// (issue #265): the agent process was runnable but not getting scheduled for
|
|
51
|
+
// minutes at a time. Fed by the event-loop delay the plugin samples locally.
|
|
52
|
+
host_responsive: {
|
|
53
|
+
label: 'Agent host responsive',
|
|
54
|
+
phase: 'connect',
|
|
55
|
+
fixHint: 'Your agent host is starved of CPU/memory — the agent process froze for seconds at a time, so order handling, stop attachment and bracket resync can be delayed by minutes. Give the host more CPU/RAM, or reduce what else runs on it (other containers, agents or a leaking process competing for the same box).',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
/** Assertive copy for a CONFIRMED geo-block, passed explicitly as the `fixHint`
|
|
59
|
+
* override. This is the one case where naming the cause is honest: the server
|
|
60
|
+
* answered 451/403, so we are reporting what it told us — not an inference from
|
|
61
|
+
* a request that never completed. */
|
|
62
|
+
export const GEO_BLOCK_FIX_HINT = {
|
|
63
|
+
binance: 'Your agent host is geo-blocked by Binance (HTTP 451) — Binance itself refused the request. Run OpenClaw from a Binance-permitted region; most EU / several Asia VPS regions work.',
|
|
64
|
+
hyperliquid: 'Your agent host is blocked by Hyperliquid (api.hyperliquid.xyz answered HTTP 451/403) — the API itself refused the request, so this is a region/policy block rather than a network fault. Run OpenClaw from a permitted region.',
|
|
40
65
|
};
|
|
41
66
|
/** Build a self-describing check, pulling label/phase/fixHint from the copy map.
|
|
42
|
-
* fixHint is attached only when there is something to fix (warn/fail).
|
|
67
|
+
* fixHint is attached only when there is something to fix (warn/fail).
|
|
68
|
+
*
|
|
69
|
+
* `fixHint` in opts OVERRIDES the copy-map default. The static copy asserts one
|
|
70
|
+
* cause per check id, which is right for a definitive signal (a 451 IS a geo
|
|
71
|
+
* block) but wrong when the same id fires on an inference: a reachability warn
|
|
72
|
+
* sent the operator hunting a firewall that was never the problem (issue #265).
|
|
73
|
+
* Callers that know the status was inferred pass a hint that describes the
|
|
74
|
+
* possibilities instead of asserting one. */
|
|
43
75
|
export function makeReadinessCheck(id, status, opts = {}) {
|
|
44
76
|
const copy = READINESS_CHECK_COPY[id];
|
|
45
77
|
const needsFix = status === 'warn' || status === 'fail';
|
|
@@ -49,7 +81,7 @@ export function makeReadinessCheck(id, status, opts = {}) {
|
|
|
49
81
|
phase: copy.phase,
|
|
50
82
|
status,
|
|
51
83
|
...(opts.detail !== undefined ? { detail: opts.detail } : {}),
|
|
52
|
-
...(needsFix ? { fixHint: copy.fixHint } : {}),
|
|
84
|
+
...(needsFix ? { fixHint: opts.fixHint ?? copy.fixHint } : {}),
|
|
53
85
|
checkedAt: opts.checkedAt ?? Date.now(),
|
|
54
86
|
};
|
|
55
87
|
}
|
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
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare function computeEMA(data: number[], period: number): number;
|
|
2
|
-
export declare function computeATR(highs: number[], lows: number[], closes: number[], period: number): number;
|
|
3
|
-
export declare function computeATRSeries(highs: number[], lows: number[], closes: number[], period: number): number[];
|
|
4
|
-
export declare function computeRSI(closes: number[], period: number): number;
|
|
5
|
-
export declare function linearSlope(y: number[], x?: number[]): number;
|
|
6
|
-
export declare function computeStd(data: number[]): number;
|
|
7
|
-
export declare function mean(data: number[]): number;
|
|
8
|
-
/** Find swing highs and lows from OHLCV bars (simple pivot-point method). */
|
|
9
|
-
export declare function findSwingPoints(bars: {
|
|
10
|
-
high: number;
|
|
11
|
-
low: number;
|
|
12
|
-
}[], lookback?: number): {
|
|
13
|
-
highs: number[];
|
|
14
|
-
lows: number[];
|
|
15
|
-
};
|