@reefclaw/connect 0.1.6 → 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.
- package/assets/bridge/bridge.js +46 -3
- package/assets/bridge/utils/skill-version.d.ts +9 -0
- package/assets/bridge/utils/skill-version.js +45 -1
- package/assets/plugin/ccxt/binance-ban-gate.js +3 -1
- package/assets/plugin/ccxt/binance-public.d.ts +15 -0
- package/assets/plugin/ccxt/binance-public.js +34 -1
- package/assets/plugin/index.js +14 -0
- package/assets/shared/index.d.ts +2 -0
- package/assets/shared/index.js +1 -0
- package/assets/skill/SKILL.md +6 -0
- package/dist/cli.js +36 -1
- package/dist/validate.js +33 -1
- 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
- package/dist/daemon.js +0 -104
package/assets/bridge/bridge.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { logger } from './logger.js';
|
|
8
8
|
import { Connector } from './connector.js';
|
|
9
|
-
import { readLocalSkillVersion, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
9
|
+
import { readLocalSkillVersion, readAgentVisibleSkillVersions, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
10
10
|
import { verifySkillSignature, signatureRequired, readLastAppliedSignedAtMs, recordAppliedSignedAt, } from './utils/skill-signing.js';
|
|
11
11
|
import { isTradingMode, validateModeTransition, redactTokens } from '@reefclaw/shared';
|
|
12
12
|
import { OPERATOR_WRITE_METHODS } from './types.js';
|
|
@@ -837,6 +837,37 @@ export class Bridge {
|
|
|
837
837
|
logger.warn(TAG, `Failed to update workspace skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
838
838
|
}
|
|
839
839
|
}
|
|
840
|
+
// 2d. Also write to the plugin-extension bundled skill directory — the
|
|
841
|
+
// chat-channel install layout (`/plugins install clawhub:...`) loads the
|
|
842
|
+
// agent-visible skill from ~/.openclaw/extensions/<plugin>/skills/, and a
|
|
843
|
+
// plugin (re)install resets that copy to the bundled bootstrap while the
|
|
844
|
+
// workspace copy above (our version tracker) survives. Without this
|
|
845
|
+
// write the agent stays on the bootstrap forever while the pull reports
|
|
846
|
+
// "already up to date" (observed live 2026-07-11).
|
|
847
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
848
|
+
if (existsSync(extensionsDir)) {
|
|
849
|
+
try {
|
|
850
|
+
for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
851
|
+
if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
|
|
852
|
+
continue;
|
|
853
|
+
const extSkillsDir = join(extensionsDir, ext.name, 'skills');
|
|
854
|
+
if (!existsSync(extSkillsDir))
|
|
855
|
+
continue;
|
|
856
|
+
for (const skillEntry of readdirSync(extSkillsDir, { withFileTypes: true })) {
|
|
857
|
+
if (!skillEntry.isDirectory() || !skillEntry.name.includes('reefclaw'))
|
|
858
|
+
continue;
|
|
859
|
+
const extSkillMd = join(extSkillsDir, skillEntry.name, 'SKILL.md');
|
|
860
|
+
if (existsSync(extSkillMd)) {
|
|
861
|
+
writeFileSync(extSkillMd, content, 'utf-8');
|
|
862
|
+
logger.info(TAG, `Updated extension skill SKILL.md: ${extSkillMd}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (scanErr) {
|
|
868
|
+
logger.warn(TAG, `Failed to update extension skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
840
871
|
// 3. Invalidate skillsSnapshot cache WITHOUT wiping sessions.json
|
|
841
872
|
// Previously we wrote '{}' to sessions.json which destroyed OpenClaw chat history.
|
|
842
873
|
// Now we surgically remove only the skillsSnapshot key from each session entry,
|
|
@@ -979,12 +1010,24 @@ export class Bridge {
|
|
|
979
1010
|
return;
|
|
980
1011
|
}
|
|
981
1012
|
this.currentSkillVersion = readLocalSkillVersion();
|
|
982
|
-
|
|
1013
|
+
// The workspace tracker alone is NOT proof the agent has the full skill:
|
|
1014
|
+
// a plugin (re)install resets the agent-visible extension copy to the
|
|
1015
|
+
// bundled bootstrap while the workspace copy survives (observed live
|
|
1016
|
+
// 2026-07-11 — agent stranded on 0.0.5 with the tracker at 2.20.4). If
|
|
1017
|
+
// ANY agent-visible copy is behind the webapp version, re-apply anyway;
|
|
1018
|
+
// applySkillUpdate is idempotent across all copies.
|
|
1019
|
+
const staleAgentCopies = readAgentVisibleSkillVersions().filter((v) => compareSemver(body.version, v) > 0);
|
|
1020
|
+
if (compareSemver(body.version, this.currentSkillVersion) <= 0 && staleAgentCopies.length === 0) {
|
|
983
1021
|
logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} already >= webapp v${body.version} — nothing to do`);
|
|
984
1022
|
this.skillPullInFlight = false;
|
|
985
1023
|
return;
|
|
986
1024
|
}
|
|
987
|
-
|
|
1025
|
+
if (staleAgentCopies.length > 0 && compareSemver(body.version, this.currentSkillVersion) <= 0) {
|
|
1026
|
+
logger.info(TAG, `SKILL.md webapp pull: agent-visible skill copy at v${staleAgentCopies[0]} behind webapp v${body.version} (plugin reinstall reset it) — re-applying`);
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
|
|
1030
|
+
}
|
|
988
1031
|
const result = await this.applySkillUpdate({ version: body.version, content: body.content });
|
|
989
1032
|
this.emit('agent_state', 'skill_update_applied', {
|
|
990
1033
|
event: 'skill_update_applied',
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
export declare function parseSkillVersion(content: string): string | null;
|
|
3
3
|
/** Read current SKILL.md version from disk */
|
|
4
4
|
export declare function readLocalSkillVersion(): string | null;
|
|
5
|
+
/** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
|
|
6
|
+
* plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
|
|
7
|
+
* — what OpenClaw's skill loader actually reads on chat-channel installs.
|
|
8
|
+
* A plugin (re)install resets these to the bundled bootstrap while the
|
|
9
|
+
* workspace copy (the version tracker above) survives, so the tracker alone
|
|
10
|
+
* cannot be trusted for "already up to date" (observed live 2026-07-11: agent
|
|
11
|
+
* stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
|
|
12
|
+
* unreadable dirs/files are skipped; no extensions dir → empty list. */
|
|
13
|
+
export declare function readAgentVisibleSkillVersions(): string[];
|
|
5
14
|
/**
|
|
6
15
|
* Compare two semver strings.
|
|
7
16
|
* Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SKILL.md version parsing utility for OTA updates
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
2
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
const SKILL_MD_PATH = join(homedir(), '.openclaw', 'workspace', 'SKILL.md');
|
|
@@ -18,6 +18,50 @@ export function readLocalSkillVersion() {
|
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
|
|
22
|
+
* plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
|
|
23
|
+
* — what OpenClaw's skill loader actually reads on chat-channel installs.
|
|
24
|
+
* A plugin (re)install resets these to the bundled bootstrap while the
|
|
25
|
+
* workspace copy (the version tracker above) survives, so the tracker alone
|
|
26
|
+
* cannot be trusted for "already up to date" (observed live 2026-07-11: agent
|
|
27
|
+
* stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
|
|
28
|
+
* unreadable dirs/files are skipped; no extensions dir → empty list. */
|
|
29
|
+
export function readAgentVisibleSkillVersions() {
|
|
30
|
+
const versions = [];
|
|
31
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
32
|
+
let extEntries;
|
|
33
|
+
try {
|
|
34
|
+
extEntries = readdirSync(extensionsDir, { withFileTypes: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return versions;
|
|
38
|
+
}
|
|
39
|
+
for (const ext of extEntries) {
|
|
40
|
+
if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
|
|
41
|
+
continue;
|
|
42
|
+
const skillsDir = join(extensionsDir, ext.name, 'skills');
|
|
43
|
+
let skillEntries;
|
|
44
|
+
try {
|
|
45
|
+
skillEntries = readdirSync(skillsDir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const s of skillEntries) {
|
|
51
|
+
if (!s.isDirectory() || !s.name.includes('reefclaw'))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const v = parseSkillVersion(readFileSync(join(skillsDir, s.name, 'SKILL.md'), 'utf-8'));
|
|
55
|
+
if (v)
|
|
56
|
+
versions.push(v);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// unreadable/absent copy — nothing to report for this entry
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return versions;
|
|
64
|
+
}
|
|
21
65
|
// 120KB — the full SKILL.md crossed 100KB at v2.20.0 (103.7KB), which silently
|
|
22
66
|
// broke every OTA apply against the old 100KB cap. Keep comfortably under the
|
|
23
67
|
// 128KiB Cloudflare Durable-Object per-value hard limit the relay stores into.
|
|
@@ -128,6 +128,8 @@ const CONTEXT_WEIGHT = {
|
|
|
128
128
|
fetchOHLCV: 2,
|
|
129
129
|
fetchFundingRate: 1,
|
|
130
130
|
fetchOpenInterest: 1,
|
|
131
|
+
// Readiness gate's host→Binance reachability probe (fapiPublicGetTime, wt 1).
|
|
132
|
+
reachabilityProbe: 1,
|
|
131
133
|
// ExchangeInfoCache boot load — ccxt loadMarkets() → GET /fapi/v1/exchangeInfo,
|
|
132
134
|
// IP weight 1 (doc-verified developers.binance.com 2026-07-10). Was the
|
|
133
135
|
// known "ungated_ip" ccxt-internal call named in the window-summary comment.
|
|
@@ -200,7 +202,7 @@ const NEVER_PACE = new Set([
|
|
|
200
202
|
const SHED_FIRST = new Set([
|
|
201
203
|
'fetchTodayIncomeBreakdown', 'fetchTransfers', 'fetchRecentTradedSymbols',
|
|
202
204
|
'fetchTicker', 'fetchTickerRaw', 'fetchOHLCV', 'fetchOrderBook',
|
|
203
|
-
'fetchFundingRate', 'fetchOpenInterest',
|
|
205
|
+
'fetchFundingRate', 'fetchOpenInterest', 'reachabilityProbe',
|
|
204
206
|
]);
|
|
205
207
|
let weightWindowStart = 0;
|
|
206
208
|
let weightUsed = 0;
|
|
@@ -15,4 +15,19 @@ export declare class BinancePublicApi {
|
|
|
15
15
|
fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
|
|
16
16
|
/** Fetch OHLCV candles. Returns null on error. */
|
|
17
17
|
fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
|
|
18
|
+
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
19
|
+
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
20
|
+
* `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
|
|
21
|
+
* `api.binance.com`, since the public client doesn't set defaultType:'future').
|
|
22
|
+
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
23
|
+
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
24
|
+
* noteBinanceError). Outcomes:
|
|
25
|
+
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
26
|
+
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
27
|
+
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
28
|
+
* - 'unreachable' network / DNS / timeout / other error */
|
|
29
|
+
probeReachability(): Promise<{
|
|
30
|
+
outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
|
|
31
|
+
driftMs: number | null;
|
|
32
|
+
}>;
|
|
18
33
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// No API keys required — only uses public market data.
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { logger } from '../logger.js';
|
|
5
|
-
import { assertNotBanned, noteBinanceError, noteSuccess } from './binance-ban-gate.js';
|
|
5
|
+
import { assertNotBanned, noteBinanceError, noteSuccess, BinanceBannedError } from './binance-ban-gate.js';
|
|
6
6
|
const TAG = 'binance-public';
|
|
7
7
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -144,4 +144,37 @@ export class BinancePublicApi {
|
|
|
144
144
|
return null;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
/** Probe Binance USD-M FUTURES reachability from this host — the readiness
|
|
148
|
+
* gate's core signal. Calls the futures-explicit implicit method so it hits
|
|
149
|
+
* `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
|
|
150
|
+
* `api.binance.com`, since the public client doesn't set defaultType:'future').
|
|
151
|
+
* HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
|
|
152
|
+
* we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
|
|
153
|
+
* noteBinanceError). Outcomes:
|
|
154
|
+
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
155
|
+
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
156
|
+
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
157
|
+
* - 'unreachable' network / DNS / timeout / other error */
|
|
158
|
+
async probeReachability() {
|
|
159
|
+
try {
|
|
160
|
+
assertNotBanned('reachabilityProbe');
|
|
161
|
+
const r = await this.exchange.fapiPublicGetTime({});
|
|
162
|
+
noteSuccess();
|
|
163
|
+
const serverTime = Number(r?.serverTime);
|
|
164
|
+
const driftMs = Number.isFinite(serverTime) ? serverTime - Date.now() : null;
|
|
165
|
+
return { outcome: 'reachable', driftMs };
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
noteBinanceError(err);
|
|
169
|
+
if (err instanceof BinanceBannedError) {
|
|
170
|
+
// Gate paused us (418/429 backoff or weight pacer) — we didn't actually
|
|
171
|
+
// reach the host, so the result is UNKNOWN, not a reachability failure.
|
|
172
|
+
return { outcome: 'unknown', driftMs: null };
|
|
173
|
+
}
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
const geo = msg.includes('451');
|
|
176
|
+
logger.warn(TAG, `probeReachability failed${geo ? ' (HTTP 451 geo-block)' : ''}: ${msg}`);
|
|
177
|
+
return { outcome: geo ? 'geo_blocked' : 'unreachable', driftMs: null };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
147
180
|
}
|
package/assets/plugin/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { PositionStateStore } from './live/position-state-store.js';
|
|
|
29
29
|
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
30
30
|
import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
|
|
31
31
|
import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
|
|
32
|
+
import { startReadinessReporter } from './ingest/readiness-reporter.js';
|
|
32
33
|
import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
|
|
33
34
|
import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
|
|
34
35
|
import { getMyRecentReviewsTool } from './tools/get-my-recent-reviews.js';
|
|
@@ -2020,6 +2021,19 @@ const paperTradingPlugin = {
|
|
|
2020
2021
|
pluginToolNames = toolNames;
|
|
2021
2022
|
pluginInitialised = true;
|
|
2022
2023
|
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
2024
|
+
// Agent-readiness reporter (docs/AGENT_READINESS_GATE_PLAN.md Phase 1):
|
|
2025
|
+
// probe host→Binance reachability (HTTP 451 geo-block) + clock drift on the
|
|
2026
|
+
// host and POST a plain-English report to the webapp, so a silently-broken
|
|
2027
|
+
// agent shows an actionable dashboard alert instead of a false green.
|
|
2028
|
+
// Advisory + fire-and-forget; no token → no-op. Runs here once (guarded by
|
|
2029
|
+
// the pluginInitialised early-return → once per process) + on an unref'd
|
|
2030
|
+
// interval inside the reporter.
|
|
2031
|
+
startReadinessReporter({
|
|
2032
|
+
apiBaseUrl,
|
|
2033
|
+
token: resolveIngestToken({ connectionToken }),
|
|
2034
|
+
binanceApi,
|
|
2035
|
+
toolCount: toolNames.length,
|
|
2036
|
+
});
|
|
2023
2037
|
maybeStartConnectorSupervisor();
|
|
2024
2038
|
},
|
|
2025
2039
|
};
|
package/assets/shared/index.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export type { FillEvent, FillSource } from './fills.js';
|
|
|
8
8
|
export { redactTokens, redactTokensInPayload, REDACTED_TOKEN } from './redact.js';
|
|
9
9
|
export type { Direction, OhlcvBar, TradeFlowBucket, GlobalMarketContext, MarketContext, SignalCondition, StrategyEvaluation, StrategyDefinition, SignalEvent, StrategyState, SignalSnapshot, } from './signals/types.js';
|
|
10
10
|
export type { ConditionResult, ConditionContext, ConditionFn, ConditionConfig, EntryRuleConfig, StopRuleConfig, DirectionRule, PrimaryTimeframe, StrategyConfig, } from './signals/conditions/types.js';
|
|
11
|
+
export type { ReadinessStatus, ReadinessPhase, ReadinessCheckId, ReadinessCheck, ReadinessReport, } from './readiness.js';
|
|
12
|
+
export { READINESS_CHECK_COPY, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
|
package/assets/shared/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { VALID_CHANNELS, VALID_EMERGENCY_ACTIONS } from './protocol.js';
|
|
|
3
3
|
export { logger, setLogLevel, formatError } from './logger.js';
|
|
4
4
|
export { VALID_TRADING_MODES, isTradingMode, validateModeTransition, modeRequiresCredentials, } from './trading-mode.js';
|
|
5
5
|
export { redactTokens, redactTokensInPayload, REDACTED_TOKEN } from './redact.js';
|
|
6
|
+
export { READINESS_CHECK_COPY, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -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,7 +1,7 @@
|
|
|
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) {
|
|
@@ -26,3 +26,35 @@ export async function checkGateway(port) {
|
|
|
26
26
|
clearTimeout(t);
|
|
27
27
|
}
|
|
28
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
|
+
}
|
|
60
|
+
}
|
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
|
-
};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
// Shared indicator computation helpers.
|
|
2
|
-
// Used by both regime feature engineering and signal condition evaluation.
|
|
3
|
-
export function computeEMA(data, period) {
|
|
4
|
-
if (data.length < period)
|
|
5
|
-
return data[data.length - 1] ?? 0;
|
|
6
|
-
const k = 2 / (period + 1);
|
|
7
|
-
let ema = mean(data.slice(0, period));
|
|
8
|
-
for (let i = period; i < data.length; i++) {
|
|
9
|
-
ema = data[i] * k + ema * (1 - k);
|
|
10
|
-
}
|
|
11
|
-
return ema;
|
|
12
|
-
}
|
|
13
|
-
export function computeATR(highs, lows, closes, period) {
|
|
14
|
-
const series = computeATRSeries(highs, lows, closes, period);
|
|
15
|
-
return series[series.length - 1] ?? 0;
|
|
16
|
-
}
|
|
17
|
-
export function computeATRSeries(highs, lows, closes, period) {
|
|
18
|
-
if (highs.length < 2)
|
|
19
|
-
return [];
|
|
20
|
-
const tr = [];
|
|
21
|
-
for (let i = 1; i < highs.length; i++) {
|
|
22
|
-
tr.push(Math.max(highs[i] - lows[i], Math.abs(highs[i] - closes[i - 1]), Math.abs(lows[i] - closes[i - 1])));
|
|
23
|
-
}
|
|
24
|
-
const atr = [];
|
|
25
|
-
atr[0] = mean(tr.slice(0, period));
|
|
26
|
-
for (let i = 1; i < tr.length; i++) {
|
|
27
|
-
atr[i] = (atr[i - 1] * (period - 1) + tr[i]) / period;
|
|
28
|
-
}
|
|
29
|
-
return atr;
|
|
30
|
-
}
|
|
31
|
-
export function computeRSI(closes, period) {
|
|
32
|
-
if (closes.length < period + 1)
|
|
33
|
-
return 50;
|
|
34
|
-
const changes = [];
|
|
35
|
-
for (let i = 1; i < closes.length; i++) {
|
|
36
|
-
changes.push(closes[i] - closes[i - 1]);
|
|
37
|
-
}
|
|
38
|
-
let avgGain = 0;
|
|
39
|
-
let avgLoss = 0;
|
|
40
|
-
for (let i = 0; i < period; i++) {
|
|
41
|
-
if (changes[i] > 0)
|
|
42
|
-
avgGain += changes[i];
|
|
43
|
-
else
|
|
44
|
-
avgLoss -= changes[i];
|
|
45
|
-
}
|
|
46
|
-
avgGain /= period;
|
|
47
|
-
avgLoss /= period;
|
|
48
|
-
for (let i = period; i < changes.length; i++) {
|
|
49
|
-
const gain = changes[i] > 0 ? changes[i] : 0;
|
|
50
|
-
const loss = changes[i] < 0 ? -changes[i] : 0;
|
|
51
|
-
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
52
|
-
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
53
|
-
}
|
|
54
|
-
if (avgLoss === 0)
|
|
55
|
-
return 100;
|
|
56
|
-
const rs = avgGain / avgLoss;
|
|
57
|
-
return 100 - 100 / (1 + rs);
|
|
58
|
-
}
|
|
59
|
-
export function linearSlope(y, x) {
|
|
60
|
-
const n = y.length;
|
|
61
|
-
if (n < 2)
|
|
62
|
-
return 0;
|
|
63
|
-
const xs = x ?? Array.from({ length: n }, (_, i) => i);
|
|
64
|
-
const mx = mean(xs);
|
|
65
|
-
const my = mean(y);
|
|
66
|
-
let num = 0;
|
|
67
|
-
let den = 0;
|
|
68
|
-
for (let i = 0; i < n; i++) {
|
|
69
|
-
num += (xs[i] - mx) * (y[i] - my);
|
|
70
|
-
den += (xs[i] - mx) ** 2;
|
|
71
|
-
}
|
|
72
|
-
return den > 0 ? num / den : 0;
|
|
73
|
-
}
|
|
74
|
-
export function computeStd(data) {
|
|
75
|
-
if (data.length < 2)
|
|
76
|
-
return 0;
|
|
77
|
-
const avg = mean(data);
|
|
78
|
-
const variance = data.reduce((s, v) => s + (v - avg) ** 2, 0) / (data.length - 1);
|
|
79
|
-
return Math.sqrt(variance);
|
|
80
|
-
}
|
|
81
|
-
export function mean(data) {
|
|
82
|
-
if (data.length === 0)
|
|
83
|
-
return 0;
|
|
84
|
-
return data.reduce((s, v) => s + v, 0) / data.length;
|
|
85
|
-
}
|
|
86
|
-
/** Find swing highs and lows from OHLCV bars (simple pivot-point method). */
|
|
87
|
-
export function findSwingPoints(bars, lookback = 5) {
|
|
88
|
-
const highs = [];
|
|
89
|
-
const lows = [];
|
|
90
|
-
for (let i = lookback; i < bars.length - lookback; i++) {
|
|
91
|
-
let isHigh = true;
|
|
92
|
-
let isLow = true;
|
|
93
|
-
for (let j = i - lookback; j <= i + lookback; j++) {
|
|
94
|
-
if (j === i)
|
|
95
|
-
continue;
|
|
96
|
-
if (bars[j].high >= bars[i].high)
|
|
97
|
-
isHigh = false;
|
|
98
|
-
if (bars[j].low <= bars[i].low)
|
|
99
|
-
isLow = false;
|
|
100
|
-
}
|
|
101
|
-
if (isHigh)
|
|
102
|
-
highs.push(bars[i].high);
|
|
103
|
-
if (isLow)
|
|
104
|
-
lows.push(bars[i].low);
|
|
105
|
-
}
|
|
106
|
-
return { highs, lows };
|
|
107
|
-
}
|
package/dist/daemon.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// Keep the bridge running across reboots. Linux/systemd-user is implemented
|
|
2
|
-
// fully; macOS and Windows fall back to printing the manual run command (a
|
|
3
|
-
// launchd/Task-Scheduler unit is a follow-up). The bridge reads its config from
|
|
4
|
-
// ~/.openclaw/openclaw.json, so until the user pastes their connect message the
|
|
5
|
-
// service will start, find no token, and restart — harmless; it connects within
|
|
6
|
-
// seconds of the agent writing the config.
|
|
7
|
-
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { homedir, userInfo } from 'node:os';
|
|
10
|
-
import { BRIDGE_DIR } from './paths.js';
|
|
11
|
-
import { run, which } from './exec.js';
|
|
12
|
-
import { step, ok, info, warn } from './ui.js';
|
|
13
|
-
const SERVICE_NAME = 'reefclaw-bridge';
|
|
14
|
-
const NODE = process.execPath; // absolute path to the node running the installer
|
|
15
|
-
/**
|
|
16
|
-
* Build the systemd user-unit text. Pure + exported so the path-quoting is
|
|
17
|
-
* unit-testable. Both `node` (process.execPath) and `bridgeDir` (under the
|
|
18
|
-
* user's home) can contain spaces. Quoting rules differ per directive:
|
|
19
|
-
* - ExecStart= is parsed with shell-like word splitting, so an unquoted
|
|
20
|
-
* `ExecStart=/home/a b/node …` reads the binary as `/home/a` — QUOTE both
|
|
21
|
-
* the binary and the script path.
|
|
22
|
-
* - WorkingDirectory= takes the raw value after `=` as a single path (no word
|
|
23
|
-
* splitting) — spaces are safe UNQUOTED, and quotes are treated as literal
|
|
24
|
-
* characters, failing the unit with "path is not absolute" (verified live
|
|
25
|
-
* on systemd 255 / Ubuntu 24.04). Do NOT quote it.
|
|
26
|
-
*/
|
|
27
|
-
export function buildSystemdUnit(node, bridgeDir) {
|
|
28
|
-
const indexJs = join(bridgeDir, 'index.js');
|
|
29
|
-
return `[Unit]
|
|
30
|
-
Description=ReefClaw connector - bridges OpenClaw to the ReefClaw dashboard
|
|
31
|
-
After=network-online.target
|
|
32
|
-
Wants=network-online.target
|
|
33
|
-
|
|
34
|
-
[Service]
|
|
35
|
-
Type=simple
|
|
36
|
-
WorkingDirectory=${bridgeDir}
|
|
37
|
-
ExecStart="${node}" "${indexJs}" --provider gateway --log-level info
|
|
38
|
-
Restart=always
|
|
39
|
-
RestartSec=5s
|
|
40
|
-
|
|
41
|
-
[Install]
|
|
42
|
-
WantedBy=default.target
|
|
43
|
-
`;
|
|
44
|
-
}
|
|
45
|
-
function manualHint() {
|
|
46
|
-
warn('Could not set up an auto-start service on this OS yet.');
|
|
47
|
-
info('Keep the connector running with this command (leave it open / use your own service manager):');
|
|
48
|
-
info(` "${NODE}" "${join(BRIDGE_DIR, 'index.js')}" --provider gateway`);
|
|
49
|
-
}
|
|
50
|
-
function installSystemd() {
|
|
51
|
-
if (!which('systemctl'))
|
|
52
|
-
return false;
|
|
53
|
-
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
54
|
-
mkdirSync(unitDir, { recursive: true });
|
|
55
|
-
const unit = buildSystemdUnit(NODE, BRIDGE_DIR);
|
|
56
|
-
writeFileSync(join(unitDir, `${SERVICE_NAME}.service`), unit, 'utf-8');
|
|
57
|
-
run('systemctl', ['--user', 'daemon-reload']);
|
|
58
|
-
const enabled = run('systemctl', ['--user', 'enable', '--now', `${SERVICE_NAME}.service`]);
|
|
59
|
-
if (!enabled.ok) {
|
|
60
|
-
warn('systemd --user enable/start did not succeed:');
|
|
61
|
-
if (enabled.stderr.trim())
|
|
62
|
-
info(enabled.stderr.trim().split('\n').slice(-2).join('\n'));
|
|
63
|
-
info(`Try: systemctl --user enable --now ${SERVICE_NAME}.service`);
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
// `enable --now` can exit 0 while the unit failed to load (e.g. a bad unit
|
|
67
|
-
// file setting) — verify the unit actually came up before claiming ✓.
|
|
68
|
-
// 'active' = running; 'activating' = the expected pre-token restart loop
|
|
69
|
-
// (the bridge exits until the user pastes their connect message, and
|
|
70
|
-
// Restart=always re-launches it). Anything else (inactive/failed) means the
|
|
71
|
-
// unit never loaded.
|
|
72
|
-
const active = run('systemctl', ['--user', 'is-active', `${SERVICE_NAME}.service`]);
|
|
73
|
-
const state = active.stdout.trim();
|
|
74
|
-
if (state !== 'active' && state !== 'activating') {
|
|
75
|
-
warn(`the service did not come up (state: ${state || 'unknown'}).`);
|
|
76
|
-
info(`Inspect: systemctl --user status ${SERVICE_NAME}.service`);
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
// Linger lets the user service run without an active login session (servers).
|
|
80
|
-
// Best-effort: needs privileges; non-fatal if it fails.
|
|
81
|
-
const linger = run('loginctl', ['enable-linger', userInfo().username]);
|
|
82
|
-
if (linger.ok) {
|
|
83
|
-
info('enabled linger (service survives logout / reboot)');
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
info('note: run `sudo loginctl enable-linger $USER` so the connector survives logout.');
|
|
87
|
-
}
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
export function installDaemon() {
|
|
91
|
-
step('Starting the connector as a background service');
|
|
92
|
-
if (process.platform === 'linux') {
|
|
93
|
-
if (installSystemd()) {
|
|
94
|
-
ok(`connector running as a systemd user service (${SERVICE_NAME})`);
|
|
95
|
-
info(`logs: journalctl --user -u ${SERVICE_NAME} -f`);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
manualHint();
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
// macOS / Windows: manual for now (launchd / Task Scheduler unit is a follow-up).
|
|
102
|
-
manualHint();
|
|
103
|
-
return false;
|
|
104
|
-
}
|