@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.
Files changed (77) hide show
  1. package/assets/bridge/gateway/event-parser.d.ts +19 -0
  2. package/assets/bridge/gateway/event-parser.js +52 -0
  3. package/assets/bridge/gateway/gateway-config.d.ts +16 -5
  4. package/assets/bridge/gateway/gateway-config.js +68 -12
  5. package/assets/bridge/gateway/heartbeat-cron.d.ts +1 -0
  6. package/assets/bridge/gateway/heartbeat-cron.js +22 -1
  7. package/assets/bridge/gateway/poller.js +18 -8
  8. package/assets/bridge/providers/emergency-commands.d.ts +9 -1
  9. package/assets/bridge/providers/emergency-commands.js +38 -1
  10. package/assets/bridge/providers/gateway.d.ts +72 -1
  11. package/assets/bridge/providers/gateway.js +241 -29
  12. package/assets/bridge/providers/onboarding-commands.d.ts +8 -0
  13. package/assets/bridge/providers/onboarding-commands.js +4 -4
  14. package/assets/bridge/types.d.ts +30 -0
  15. package/assets/bridge/utils/identity-name.d.ts +24 -0
  16. package/assets/bridge/utils/identity-name.js +54 -0
  17. package/assets/plugin/ccxt/binance-public.d.ts +21 -7
  18. package/assets/plugin/ccxt/binance-public.js +70 -6
  19. package/assets/plugin/config/operator-provenance.d.ts +6 -0
  20. package/assets/plugin/config/operator-provenance.js +50 -0
  21. package/assets/plugin/config/plugin-config-io.d.ts +15 -1
  22. package/assets/plugin/config/plugin-config-io.js +24 -0
  23. package/assets/plugin/index.js +216 -173
  24. package/assets/plugin/ingest/event-loop-monitor.d.ts +11 -0
  25. package/assets/plugin/ingest/event-loop-monitor.js +113 -0
  26. package/assets/plugin/ingest/position-auto-capture.d.ts +5 -0
  27. package/assets/plugin/ingest/position-auto-capture.js +14 -5
  28. package/assets/plugin/ingest/readiness-reporter.d.ts +17 -6
  29. package/assets/plugin/ingest/readiness-reporter.js +88 -9
  30. package/assets/plugin/ingest/skill-version-reader.d.ts +16 -0
  31. package/assets/plugin/ingest/skill-version-reader.js +64 -0
  32. package/assets/plugin/live/approval-lifecycle.d.ts +30 -0
  33. package/assets/plugin/live/approval-lifecycle.js +80 -0
  34. package/assets/plugin/live/bracket-types.d.ts +9 -0
  35. package/assets/plugin/live/live-adapter.d.ts +0 -1
  36. package/assets/plugin/live/proposal-decision-listener.d.ts +20 -0
  37. package/assets/plugin/live/proposal-decision-listener.js +211 -48
  38. package/assets/plugin/onboarding/runtime.d.ts +34 -1
  39. package/assets/plugin/onboarding/runtime.js +56 -5
  40. package/assets/plugin/openclaw.plugin.json +1 -1
  41. package/assets/plugin/simulator/exchange-simulator.d.ts +45 -2
  42. package/assets/plugin/simulator/exchange-simulator.js +96 -4
  43. package/assets/plugin/simulator/types.d.ts +17 -0
  44. package/assets/plugin/tools/attach-brackets.js +50 -1
  45. package/assets/plugin/tools/create-order.d.ts +11 -0
  46. package/assets/plugin/tools/create-order.js +23 -2
  47. package/assets/plugin/tools/get-risk-summary.d.ts +4 -0
  48. package/assets/plugin/tools/get-risk-summary.js +62 -23
  49. package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.d.ts +29 -1
  50. package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +59 -2
  51. package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +10 -0
  52. package/assets/plugin/venues/hyperliquid/hl-brackets.js +45 -13
  53. package/assets/plugin/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
  54. package/assets/plugin/venues/hyperliquid/hl-fill-ingest.js +69 -0
  55. package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
  56. package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +155 -12
  57. package/assets/plugin/venues/hyperliquid/hl-order.d.ts +35 -0
  58. package/assets/plugin/venues/hyperliquid/hl-order.js +123 -0
  59. package/assets/plugin/venues/hyperliquid/hl-position.d.ts +36 -0
  60. package/assets/plugin/venues/hyperliquid/hl-position.js +127 -0
  61. package/assets/plugin/venues/hyperliquid/hl-private.d.ts +20 -3
  62. package/assets/plugin/venues/hyperliquid/hl-private.js +37 -6
  63. package/assets/plugin/venues/hyperliquid/hl-public.d.ts +12 -5
  64. package/assets/plugin/venues/hyperliquid/hl-public.js +24 -3
  65. package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +13 -1
  66. package/assets/plugin/venues/hyperliquid/hl-user-stream.js +4 -1
  67. package/assets/shared/index.d.ts +3 -3
  68. package/assets/shared/index.js +2 -2
  69. package/assets/shared/readiness.d.ts +38 -2
  70. package/assets/shared/readiness.js +40 -8
  71. package/dist/cli.js +31 -10
  72. package/dist/gateway-tuning.js +112 -0
  73. package/package.json +1 -1
  74. package/assets/shared/signals/indicators-extended.d.ts +0 -52
  75. package/assets/shared/signals/indicators-extended.js +0 -284
  76. package/assets/shared/signals/indicators.d.ts +0 -15
  77. 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 is geo-blocked by Binance (HTTP 451). Run OpenClaw from a Binance-permitted region most EU / several Asia VPS regions work.',
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 cannot reach the Hyperliquid API (api.hyperliquid.xyz). Check outbound HTTPS/DNS/firewall; if the host region is restricted, run OpenClaw from a permitted region.',
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 (!which('openclaw')) {
99
- info('openclaw CLI not on PATH — restart your gateway manually to load the trading tools.');
100
- return;
101
- }
102
- const r = run('openclaw', ['gateway', 'restart'], { timeoutMs: 120_000 });
103
- if (r.ok) {
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
- warn('could not restart the gateway automatically.');
108
- info('Restart it yourself (e.g. `openclaw gateway restart`) so the trading tools load.');
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- };