@reefclaw/connect 0.1.5 → 0.1.7

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 (38) hide show
  1. package/assets/bridge/bridge.d.ts +28 -1
  2. package/assets/bridge/bridge.js +145 -7
  3. package/assets/bridge/utils/skill-version.d.ts +9 -0
  4. package/assets/bridge/utils/skill-version.js +45 -1
  5. package/assets/plugin/ccxt/binance-ban-gate.js +12 -1
  6. package/assets/plugin/ccxt/binance-public.d.ts +15 -0
  7. package/assets/plugin/ccxt/binance-public.js +34 -1
  8. package/assets/plugin/config/agent-config-client.d.ts +22 -0
  9. package/assets/plugin/config/agent-config-client.js +44 -1
  10. package/assets/plugin/config/agent-config-poller.d.ts +8 -1
  11. package/assets/plugin/config/agent-config-poller.js +1 -0
  12. package/assets/plugin/config/entitlement-gate.d.ts +51 -0
  13. package/assets/plugin/config/entitlement-gate.js +137 -0
  14. package/assets/plugin/index.js +54 -23
  15. package/assets/plugin/ingest/pending-entry-metadata.d.ts +31 -9
  16. package/assets/plugin/ingest/pending-entry-metadata.js +70 -16
  17. package/assets/plugin/ingest/position-auto-capture.js +14 -3
  18. package/assets/plugin/ingest/readiness-reporter.d.ts +19 -0
  19. package/assets/plugin/ingest/readiness-reporter.js +142 -0
  20. package/assets/plugin/live/exchange-info-cache.d.ts +3 -1
  21. package/assets/plugin/live/exchange-info-cache.js +17 -2
  22. package/assets/plugin/signals/strategy-adapter.d.ts +35 -2
  23. package/assets/plugin/signals/strategy-adapter.js +87 -10
  24. package/assets/plugin/tools/create-order.js +26 -20
  25. package/assets/shared/index.d.ts +2 -0
  26. package/assets/shared/index.js +1 -0
  27. package/assets/shared/readiness.d.ts +50 -0
  28. package/assets/shared/readiness.js +58 -0
  29. package/assets/shared/signals/strategy-adapter.d.ts +35 -2
  30. package/assets/shared/signals/strategy-adapter.js +87 -10
  31. package/assets/skill/SKILL.md +6 -0
  32. package/dist/cli.js +36 -1
  33. package/dist/validate.js +39 -7
  34. package/package.json +1 -1
  35. package/assets/shared/signals/indicators-extended.d.ts +0 -52
  36. package/assets/shared/signals/indicators-extended.js +0 -284
  37. package/assets/shared/signals/indicators.d.ts +0 -15
  38. package/assets/shared/signals/indicators.js +0 -107
@@ -4,6 +4,7 @@ import { evaluateConditions } from './conditions/registry.js';
4
4
  import { resolveDirection } from './direction-rules.js';
5
5
  import { computeEntry } from './entry-rules.js';
6
6
  import { computeStop } from './stop-rules.js';
7
+ import { computeATR } from '../shared/indicators.js';
7
8
  /**
8
9
  * Higher-timeframe tick gating state, keyed by
9
10
  * `${gateNamespace}\x1f${strategyName}:${symbol}`.
@@ -36,6 +37,73 @@ function pickTimeframeBars(ctx, tf) {
36
37
  return ctx.ohlcv4h ?? [];
37
38
  return ctx.ohlcv1h;
38
39
  }
40
+ const TF_MS = {
41
+ '1h': 3_600_000,
42
+ '4h': 4 * 3_600_000,
43
+ '1d': 24 * 3_600_000,
44
+ };
45
+ /** Median spacing of the last few bars — robust bar-cadence probe. */
46
+ function barSpacingMs(bars) {
47
+ const n = bars.length;
48
+ if (n < 2)
49
+ return 0;
50
+ const deltas = [];
51
+ for (let i = Math.max(1, n - 4); i < n; i++) {
52
+ deltas.push(bars[i].time.getTime() - bars[i - 1].time.getTime());
53
+ }
54
+ deltas.sort((a, b) => a - b);
55
+ return deltas[Math.floor(deltas.length / 2)];
56
+ }
57
+ /**
58
+ * Live parity for higher-timeframe strategies (the tfHours-aware-stops fix,
59
+ * 2026-07 — see docs/STRATEGY_RESEARCH_2026-07.md §6.4 / CLAUDE.md ★).
60
+ *
61
+ * Every implicit bar read in this engine — stop rules
62
+ * (`findSwingPoints(ctx.ohlcv1h.slice(-48))`), entry rules
63
+ * (`computeEMA(ctx.ohlcv1h…)`), conditions without a `tfHours` param
64
+ * (ema_proximity, stoch_rsi_extreme, adx_*, …) and `ctx.atr14` — targets the
65
+ * `ohlcv1h` slot. The backtest engine feeds MAIN-timeframe bars into that
66
+ * slot (and computes atr14 from them), so a 4h/1d strategy backtests against
67
+ * primary-timeframe geometry. LIVE contexts put real 1h bars there, so the
68
+ * same strategy would compute stops/EMAs/ATR from 1h data — a 1d ATR is ~8×
69
+ * the 1h ATR, so live stops came out ~8× too tight. This helper gives the
70
+ * evaluation the exact context shape the backtest validated: primary bars in
71
+ * the `ohlcv1h` slot, atr14 recomputed from them (same computeATR the
72
+ * backtest and live context builders use).
73
+ *
74
+ * Detection, not configuration: when the `ohlcv1h` slot already carries
75
+ * primary-cadence bars (median spacing ≥ 90% of the primary bar duration —
76
+ * i.e. a backtest context), the context is returned UNTOUCHED, so backtest
77
+ * behaviour is byte-identical by construction (including warm-up: the
78
+ * backtest engine already refuses to build a context below 50 main bars).
79
+ * A live 1h series can only look primary-spaced through a data gap, in
80
+ * which case we fall back to the untouched context (pre-fix behaviour)
81
+ * rather than guessing.
82
+ *
83
+ * Returns null for a LIVE context whose primary-timeframe history is below
84
+ * the backtest's 50-bar warm-up — the caller skips evaluation, mirroring
85
+ * the backtest's null-context warm-up window.
86
+ *
87
+ * Exported for tests.
88
+ */
89
+ export function resolvePrimaryContext(ctx, tf, tfBars) {
90
+ if (tf === '1h')
91
+ return ctx;
92
+ const spacing = barSpacingMs(ctx.ohlcv1h);
93
+ if (spacing === 0 || spacing >= TF_MS[tf] * 0.9)
94
+ return ctx; // already primary (backtest) or undecidable
95
+ if (tfBars.length < 50)
96
+ return null; // live warm-up parity with the backtest engine
97
+ const highs = tfBars.map(b => b.high);
98
+ const lows = tfBars.map(b => b.low);
99
+ const closes = tfBars.map(b => b.close);
100
+ const atr14 = computeATR(highs, lows, closes, 14);
101
+ return {
102
+ ...ctx,
103
+ ohlcv1h: tfBars,
104
+ atr14: Number.isFinite(atr14) && atr14 > 0 ? atr14 : ctx.atr14,
105
+ };
106
+ }
39
107
  /** Empty no-signal evaluation — used when gating skips a strategy. */
40
108
  const SKIPPED = { direction: null, conditions: [], trade: undefined };
41
109
  /**
@@ -75,40 +143,49 @@ export function adaptStrategy(config, gateNamespace) {
75
143
  return SKIPPED;
76
144
  }
77
145
  lastEvaluatedBarTime.set(gateKey, latestBarTime);
146
+ // ─── Higher-timeframe live parity ──────────────────────────────
147
+ // Evaluate against a context whose implicit-1h slot carries
148
+ // primary-timeframe bars — see resolvePrimaryContext. Backtest
149
+ // contexts pass through untouched; only live contexts for 4h/1d
150
+ // strategies are adapted, and a live context below the backtest's
151
+ // 50-bar warm-up resolves to null → skip.
152
+ const ectx = resolvePrimaryContext(ctx, tf, tfBars);
153
+ if (ectx === null)
154
+ return SKIPPED;
78
155
  // ─── SkipIf gates ──────────────────────────────────────────────
79
156
  // Portfolio-wide / cross-symbol filters. If any are met the
80
157
  // strategy is skipped this tick. Evaluated before main conditions
81
158
  // so the bulk of the work is short-circuited.
82
159
  if (config.skipIf && config.skipIf.length > 0) {
83
- const { conditions: skipResults } = evaluateConditions(config.skipIf, ctx, null);
160
+ const { conditions: skipResults } = evaluateConditions(config.skipIf, ectx, null);
84
161
  if (skipResults.some(c => c.met))
85
162
  return SKIPPED;
86
163
  }
87
164
  // Pass 1: evaluate conditions with direction = null
88
- const { conditions: pass1, condCtx } = evaluateConditions(config.conditions, ctx, null);
165
+ const { conditions: pass1, condCtx } = evaluateConditions(config.conditions, ectx, null);
89
166
  // Determine direction
90
- const direction = resolveDirection(config.directionRule, ctx, condCtx);
167
+ const direction = resolveDirection(config.directionRule, ectx, condCtx);
91
168
  // Pass 2: re-evaluate direction-sensitive conditions now that we know direction
92
169
  // (orderbook_imbalance and funding_contrarian behave differently per direction)
93
170
  const directionSensitive = new Set(['orderbook_imbalance', 'funding_contrarian', 'funding_extreme_skip', 'funding_position_ok', 'return_momentum']);
94
171
  const hasDirSensitive = config.conditions.some(c => directionSensitive.has(c.type));
95
172
  let finalConditions = pass1;
96
173
  if (direction && hasDirSensitive) {
97
- const { conditions: pass2 } = evaluateConditions(config.conditions, ctx, direction);
174
+ const { conditions: pass2 } = evaluateConditions(config.conditions, ectx, direction);
98
175
  // Merge: use pass2 results for direction-sensitive, pass1 for others
99
176
  finalConditions = pass1.map((c, i) => directionSensitive.has(config.conditions[i].type) ? pass2[i] : c);
100
177
  }
101
178
  const allMet = finalConditions.every(c => c.met);
102
179
  let trade;
103
180
  if (allMet && direction) {
104
- const entryZone = computeEntry(config.entryRule, ctx, direction, condCtx);
105
- const stopLevel = computeStop(config.stopRule, ctx, direction, condCtx);
181
+ const entryZone = computeEntry(config.entryRule, ectx, direction, condCtx);
182
+ const stopLevel = computeStop(config.stopRule, ectx, direction, condCtx);
106
183
  const risk = direction === 'LONG'
107
- ? ctx.currentPrice - stopLevel
108
- : stopLevel - ctx.currentPrice;
184
+ ? ectx.currentPrice - stopLevel
185
+ : stopLevel - ectx.currentPrice;
109
186
  const targets = config.targetRMultiples.map(rm => direction === 'LONG'
110
- ? ctx.currentPrice + risk * rm
111
- : ctx.currentPrice - risk * rm);
187
+ ? ectx.currentPrice + risk * rm
188
+ : ectx.currentPrice - risk * rm);
112
189
  trade = { entryZone, stopLevel, targets };
113
190
  }
114
191
  return { direction, conditions: finalConditions, trade };
@@ -39,6 +39,12 @@ You are being connected to **ReefClaw**, a supervised trading control room. Your
39
39
 
40
40
  **Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still a bootstrap version (0.0.x), the connection has not completed — finish connecting first.
41
41
 
42
+ ## Host requirements (check before connecting)
43
+
44
+ ReefClaw trades on Binance USD-M Futures, so **the machine running this OpenClaw must be able to reach Binance** (`fapi.binance.com`). Binance geo-restricts many locations: cheap US cloud regions — and some other jurisdictions — return **HTTP 451** ("restricted location"), and the agent then cannot fetch market data or trade. This applies even in **paper mode**, which uses live Binance prices.
45
+
46
+ If your market-data calls fail with `451`, this host is geo-blocked — move the agent to a Binance-permitted region (most EU and several Asia VPS regions work). This is a hosting requirement; ReefClaw cannot configure around it.
47
+
42
48
  ## Connecting (first run) — saving the connect message
43
49
 
44
50
  The onboarding page at [reefclaw.com/onboarding](https://reefclaw.com/onboarding) gives the user a single **connect message** that carries THREE fields:
package/dist/cli.js CHANGED
@@ -11,12 +11,28 @@ import { installPlugin } from './plugin.js';
11
11
  import { installBridge } from './bridge.js';
12
12
  import { installSkill } from './skill.js';
13
13
  import { enableConnectorSupervisor } from './supervisor-config.js';
14
- import { checkGateway } from './validate.js';
14
+ import { checkGateway, checkBinanceRegion } from './validate.js';
15
15
  import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthDowngradeNeeded, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
16
16
  import { run, which } from './exec.js';
17
17
  import { step, ok, info, warn, fail, banner, bold, green, cyan, dim } from './ui.js';
18
18
  const DASHBOARD = 'https://www.reefclaw.com/dashboard';
19
19
  const ONBOARDING = 'https://www.reefclaw.com/onboarding';
20
+ const OPENCLAW_FLOOR = [2026, 6, 0];
21
+ /** Parse a `2026.6.11`-style version out of `openclaw --version` output. */
22
+ function parseOpenClawVersion(out) {
23
+ const m = out.match(/(\d{4})\.(\d+)\.(\d+)/);
24
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
25
+ }
26
+ /** True when `v` >= `floor` (calendar-semver tuple compare). */
27
+ function meetsFloor(v, floor) {
28
+ for (let i = 0; i < 3; i += 1) {
29
+ if (v[i] > floor[i])
30
+ return true;
31
+ if (v[i] < floor[i])
32
+ return false;
33
+ }
34
+ return true;
35
+ }
20
36
  function preflight() {
21
37
  const major = Number(process.versions.node.split('.')[0]);
22
38
  if (Number.isFinite(major) && major < 20) {
@@ -29,6 +45,18 @@ function preflight() {
29
45
  info(`(If your config lives elsewhere, set OPENCLAW_CONFIG_PATH. Looked at: ${openClawConfigPath()})`);
30
46
  process.exit(1);
31
47
  }
48
+ // OpenClaw must be new enough to load the plugin. 2026.4's install scanner
49
+ // hard-blocks the plugin and its plugin-API floor is below ours (>=2026.6.0),
50
+ // so proceeding would only fail cryptically at install time. Fail fast — but
51
+ // ONLY on a version we could parse and confirm is too old; a missing CLI or
52
+ // unrecognised output stays out of the way (the loader still guards it).
53
+ const verOut = which('openclaw') ? run('openclaw', ['--version'], { timeoutMs: 10_000 }) : null;
54
+ const parsed = verOut?.ok ? parseOpenClawVersion(verOut.stdout) : null;
55
+ if (parsed && !meetsFloor(parsed, OPENCLAW_FLOOR)) {
56
+ fail(`OpenClaw ${parsed.join('.')} is too old — ReefClaw needs ${OPENCLAW_FLOOR.join('.')} or newer.`);
57
+ info('Update OpenClaw, then re-run: npm i -g openclaw@latest');
58
+ process.exit(1);
59
+ }
32
60
  }
33
61
  /** Re-write openclaw.json: preserve the user's existing config, overlay the
34
62
  * ENTIRE plugins section the link wrote (the link can wipe other sections, so
@@ -91,6 +119,10 @@ function nextSteps() {
91
119
  async function main() {
92
120
  banner(bold(cyan('ReefClaw connect')) + dim(' — link your OpenClaw agent to ReefClaw (paper trading)'));
93
121
  preflight();
122
+ // Prevention: a host in a Binance-restricted region (HTTP 451) can't trade —
123
+ // not even paper, which uses live Binance prices. Advisory (never fatal); the
124
+ // dashboard readiness surface re-checks it live after connect.
125
+ const binanceReachable = await checkBinanceRegion();
94
126
  const pre = readConfig();
95
127
  const plugin = installPlugin();
96
128
  const merged = wireConfig(pre);
@@ -115,6 +147,9 @@ async function main() {
115
147
  if (!supervised) {
116
148
  warn('Connector supervision was not enabled — see the message above to finish it.');
117
149
  }
150
+ if (!binanceReachable) {
151
+ warn('This host looks geo-blocked by Binance (HTTP 451) — trading will not work until you run the agent from a Binance-permitted region (most EU / several Asia VPS regions).');
152
+ }
118
153
  nextSteps();
119
154
  }
120
155
  main().catch((err) => {
package/dist/validate.js CHANGED
@@ -1,19 +1,16 @@
1
1
  // Light post-install checks. We can only verify the LOCAL gateway here — the
2
2
  // relay handshake only succeeds once the user pastes their connect message
3
3
  // (that's the onboarding "auto-detect" step on the dashboard).
4
- import { step, ok, warn, info } from './ui.js';
4
+ import { step, ok, warn, info, fail } from './ui.js';
5
5
  /** Any HTTP response from the local gateway means OpenClaw is up and reachable.
6
6
  * A connection refused means OpenClaw isn't running. */
7
7
  export async function checkGateway(port) {
8
8
  step('Checking the local OpenClaw gateway');
9
9
  const url = `http://127.0.0.1:${port}/`;
10
+ const ctrl = new AbortController();
11
+ const t = setTimeout(() => ctrl.abort(), 3000);
10
12
  try {
11
- const ctrl = new AbortController();
12
- const t = setTimeout(() => ctrl.abort(), 3000);
13
- await fetch(url, { signal: ctrl.signal }).catch((e) => {
14
- throw e;
15
- });
16
- clearTimeout(t);
13
+ await fetch(url, { signal: ctrl.signal });
17
14
  ok(`gateway reachable on port ${port}`);
18
15
  return true;
19
16
  }
@@ -25,4 +22,39 @@ export async function checkGateway(port) {
25
22
  info('Make sure OpenClaw is running (the agent must be up for trading to work).');
26
23
  return false;
27
24
  }
25
+ finally {
26
+ clearTimeout(t);
27
+ }
28
+ }
29
+ /** Probe whether Binance USD-M Futures is reachable from THIS host. A `451` is
30
+ * Binance's geo-restriction signal — the host's region is blocked, and trading
31
+ * (even paper mode, which uses live Binance prices) cannot work from here. The
32
+ * probe is advisory: only a definitive `451` returns false; a network hiccup /
33
+ * timeout is NOT a geo-block, so we stay out of the way and let the dashboard
34
+ * readiness surface confirm a real, persistent problem later. Never fatal. */
35
+ export async function checkBinanceRegion() {
36
+ step('Checking Binance reachability from this host');
37
+ const url = 'https://fapi.binance.com/fapi/v1/ping';
38
+ const ctrl = new AbortController();
39
+ const t = setTimeout(() => ctrl.abort(), 5000);
40
+ try {
41
+ const res = await fetch(url, { signal: ctrl.signal });
42
+ if (res.status === 451) {
43
+ fail('Binance returned HTTP 451 — this host is in a Binance-restricted region.');
44
+ info('Trading (and paper mode, which uses live Binance prices) cannot work from here.');
45
+ info('Run the agent from a Binance-permitted region — most EU / several Asia VPS regions work.');
46
+ return false;
47
+ }
48
+ ok('Binance is reachable from this host');
49
+ return true;
50
+ }
51
+ catch {
52
+ // DNS / timeout / transient network — not a definitive geo-block. Don't
53
+ // block the install on a flaky probe.
54
+ info('Could not probe Binance reachability (network hiccup) — skipping; the dashboard verifies it after connect.');
55
+ return true;
56
+ }
57
+ finally {
58
+ clearTimeout(t);
59
+ }
28
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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
- };