solana-traderclaw 1.0.19
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/README.md +516 -0
- package/bin/gateway-persistence-linux.mjs +275 -0
- package/bin/installer-step-engine.mjs +1422 -0
- package/bin/llm-model-preference.mjs +136 -0
- package/bin/openclaw-trader.mjs +2624 -0
- package/bin/traderclaw.cjs +13 -0
- package/config/gateway-v1.json5 +121 -0
- package/dist/chunk-3UQIQJPQ.js +144 -0
- package/dist/chunk-3YPZOXWE.js +238 -0
- package/dist/chunk-RQZVD6TH.js +361 -0
- package/dist/chunk-T4YWGIIR.js +64 -0
- package/dist/index.js +2883 -0
- package/dist/src/alpha-buffer.js +6 -0
- package/dist/src/alpha-ws.js +6 -0
- package/dist/src/http-client.js +6 -0
- package/dist/src/session-manager.js +6 -0
- package/openclaw.plugin.json +104 -0
- package/package.json +60 -0
- package/skills/solana-trader/HEARTBEAT.md +51 -0
- package/skills/solana-trader/SKILL.md +2739 -0
- package/skills/solana-trader/bitquery-schema.md +303 -0
- package/skills/solana-trader/query-catalog.md +184 -0
- package/skills/solana-trader/refs/x-credentials.md +99 -0
- package/skills/solana-trader/websocket-streaming.md +265 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm `bin` entry for global `traderclaw`. Delegates to `openclaw-trader.mjs` with argv preserved.
|
|
4
|
+
* Uses .cjs so npm does not strip the bin link when `package.json` has `"type": "module"`.
|
|
5
|
+
*/
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const script = path.join(__dirname, "openclaw-trader.mjs");
|
|
9
|
+
const result = spawnSync(process.execPath, [script, ...process.argv.slice(2)], {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
env: process.env,
|
|
12
|
+
});
|
|
13
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// OpenClaw Gateway Configuration — TraderClaw V1 (Single Agent)
|
|
2
|
+
// Single "main" agent with 5-minute heartbeat + cron jobs for autonomous operation.
|
|
3
|
+
// Without this config, the agent ONLY runs when a user sends a message.
|
|
4
|
+
// With this config, the agent wakes every 5 minutes AND runs scheduled maintenance jobs.
|
|
5
|
+
// See: docs/scheduling-architecture.md
|
|
6
|
+
{
|
|
7
|
+
agents: {
|
|
8
|
+
list: [
|
|
9
|
+
{
|
|
10
|
+
id: "main",
|
|
11
|
+
default: true,
|
|
12
|
+
heartbeat: { every: "5m", target: "last" }
|
|
13
|
+
// Core trading loop — scan, analyze, decide, execute, monitor.
|
|
14
|
+
// Wakes every 5 minutes to check alpha buffer, positions, and market conditions.
|
|
15
|
+
// Also receives all cron jobs since V1 is a single-agent system.
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
cron: {
|
|
21
|
+
enabled: true,
|
|
22
|
+
maxConcurrentRuns: 2,
|
|
23
|
+
sessionRetention: "24h",
|
|
24
|
+
runLog: {
|
|
25
|
+
maxBytes: "2mb",
|
|
26
|
+
keepLines: 2000
|
|
27
|
+
},
|
|
28
|
+
jobs: [
|
|
29
|
+
// ── Strategy & Learning ───────────────────────────────────────
|
|
30
|
+
{
|
|
31
|
+
id: "strategy-evolution",
|
|
32
|
+
schedule: "0 */4 * * *", // Every 4 hours
|
|
33
|
+
agentId: "main",
|
|
34
|
+
message: "CRON_JOB: strategy_evolution — Review trade journal, compute weight adjustments, update strategy. Only update if sufficient closed trades have accumulated.",
|
|
35
|
+
enabled: true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "source-reputation",
|
|
39
|
+
schedule: "0 */3 * * *", // Every 3 hours
|
|
40
|
+
agentId: "main",
|
|
41
|
+
message: "CRON_JOB: source_reputation_recalc — Analyze which alpha signal sources led to wins vs losses. Update reputation tracking in memory.",
|
|
42
|
+
enabled: true
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// ── Risk & Audit ──────────────────────────────────────────────
|
|
46
|
+
{
|
|
47
|
+
id: "risk-audit",
|
|
48
|
+
schedule: "0 */2 * * *", // Every 2 hours
|
|
49
|
+
agentId: "main",
|
|
50
|
+
message: "CRON_JOB: portfolio_risk_audit — Portfolio stress tests, exposure checks, correlation analysis, drawdown monitoring.",
|
|
51
|
+
enabled: true
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// ── On-Chain Intelligence ─────────────────────────────────────
|
|
55
|
+
{
|
|
56
|
+
id: "meta-rotation",
|
|
57
|
+
schedule: "30 */3 * * *", // Every 3 hours at :30
|
|
58
|
+
agentId: "main",
|
|
59
|
+
message: "CRON_JOB: meta_rotation_analysis — Analyze narrative clusters across recent scans and trades. Identify hot vs cooling metas. Write observations to memory.",
|
|
60
|
+
enabled: true
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// ── Portfolio Maintenance ─────────────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
id: "dead-money-sweep",
|
|
66
|
+
schedule: "0 */2 * * *", // Every 2 hours
|
|
67
|
+
agentId: "main",
|
|
68
|
+
message: "CRON_JOB: dead_money_sweep — Check all open LOCAL_MANAGED positions for dead money. Exit stale positions. Tag as dead_money.",
|
|
69
|
+
enabled: true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "subscription-cleanup",
|
|
73
|
+
schedule: "0 * * * *", // Every hour
|
|
74
|
+
agentId: "main",
|
|
75
|
+
message: "CRON_JOB: subscription_cleanup — Check active Bitquery subscriptions. Unsubscribe from streams no longer needed (sold tokens, closed positions).",
|
|
76
|
+
enabled: true
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// ── Reporting ─────────────────────────────────────────────────
|
|
80
|
+
{
|
|
81
|
+
id: "daily-report",
|
|
82
|
+
schedule: "0 4 * * *", // Daily at 04:00 UTC
|
|
83
|
+
agentId: "main",
|
|
84
|
+
message: "CRON_JOB: daily_performance_report — Calculate daily PnL, aggregate win/loss stats, source reputation summary, write comprehensive memory entry.",
|
|
85
|
+
enabled: true
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// ── Whale / Smart Money Tracking ──────────────────────────────
|
|
89
|
+
{
|
|
90
|
+
id: "whale-watch",
|
|
91
|
+
schedule: "0 */2 * * *", // Every 2 hours
|
|
92
|
+
agentId: "main",
|
|
93
|
+
message: "CRON_JOB: whale_activity_scan — Scan for large wallet movements, deployer activity, and accumulation patterns across watched tokens.",
|
|
94
|
+
enabled: true
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
hooks: {
|
|
100
|
+
enabled: true,
|
|
101
|
+
token: "REPLACE_WITH_SECURE_TOKEN",
|
|
102
|
+
mappings: [
|
|
103
|
+
{
|
|
104
|
+
// High-priority alpha signals from SpyFly aggregator
|
|
105
|
+
// Triggers immediate agent turn outside heartbeat cadence
|
|
106
|
+
match: { path: "alpha-signal" },
|
|
107
|
+
action: "agent",
|
|
108
|
+
agentId: "main",
|
|
109
|
+
deliver: true
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
// Bitquery firehose alerts for on-chain discovery events
|
|
113
|
+
// Wakes agent for immediate analysis
|
|
114
|
+
match: { path: "firehose-alert" },
|
|
115
|
+
action: "agent",
|
|
116
|
+
agentId: "main",
|
|
117
|
+
deliver: true
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/alpha-buffer.ts
|
|
2
|
+
var MAX_BUFFER_SIZE = 200;
|
|
3
|
+
var SEEN_EVENT_TTL_MS = 60 * 60 * 1e3;
|
|
4
|
+
var MAX_SEEN_EVENTS = 500;
|
|
5
|
+
function dedupKey(signal) {
|
|
6
|
+
const minuteBucket = Math.floor(signal.ts / 6e4);
|
|
7
|
+
return `${signal.sourceName}|${signal.tokenAddress}|${signal.kind}|${minuteBucket}`;
|
|
8
|
+
}
|
|
9
|
+
var AlphaBuffer = class {
|
|
10
|
+
signals = [];
|
|
11
|
+
dedupSet = /* @__PURE__ */ new Set();
|
|
12
|
+
seenEventIds = /* @__PURE__ */ new Map();
|
|
13
|
+
sourceStats = /* @__PURE__ */ new Map();
|
|
14
|
+
tokenIndex = /* @__PURE__ */ new Map();
|
|
15
|
+
push(raw) {
|
|
16
|
+
if (raw.chain && raw.chain.toLowerCase() === "bsc") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const key = dedupKey(raw);
|
|
20
|
+
if (this.dedupSet.has(key)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const signal = {
|
|
24
|
+
...raw,
|
|
25
|
+
systemScore: raw.systemScore ?? 0,
|
|
26
|
+
_seen: false,
|
|
27
|
+
_ingestedAt: Date.now()
|
|
28
|
+
};
|
|
29
|
+
if (raw.eventId && this.seenEventIds.has(raw.eventId)) {
|
|
30
|
+
signal._seen = true;
|
|
31
|
+
}
|
|
32
|
+
this.dedupSet.add(key);
|
|
33
|
+
if (this.signals.length >= MAX_BUFFER_SIZE) {
|
|
34
|
+
const evicted = this.signals.shift();
|
|
35
|
+
const evictedKey = dedupKey(evicted);
|
|
36
|
+
this.dedupSet.delete(evictedKey);
|
|
37
|
+
this.decrementSourceStats(evicted);
|
|
38
|
+
this.rebuildTokenIndex();
|
|
39
|
+
}
|
|
40
|
+
const idx = this.signals.length;
|
|
41
|
+
this.signals.push(signal);
|
|
42
|
+
const tokenIdxList = this.tokenIndex.get(signal.tokenAddress) || [];
|
|
43
|
+
tokenIdxList.push(idx);
|
|
44
|
+
this.tokenIndex.set(signal.tokenAddress, tokenIdxList);
|
|
45
|
+
this.updateSourceStats(signal);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
getSignals(opts = {}) {
|
|
49
|
+
const { minScore, chain, kinds, unseen = true } = opts;
|
|
50
|
+
const results = [];
|
|
51
|
+
for (const signal of this.signals) {
|
|
52
|
+
if (unseen && signal._seen) continue;
|
|
53
|
+
if (minScore !== void 0 && signal.systemScore < minScore) continue;
|
|
54
|
+
if (chain && signal.chain.toLowerCase() !== chain.toLowerCase()) continue;
|
|
55
|
+
if (kinds && kinds.length > 0 && !kinds.includes(signal.kind)) continue;
|
|
56
|
+
results.push(signal);
|
|
57
|
+
}
|
|
58
|
+
if (unseen) {
|
|
59
|
+
for (const signal of results) {
|
|
60
|
+
signal._seen = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
getTokenHistory(tokenAddress) {
|
|
66
|
+
const indices = this.tokenIndex.get(tokenAddress);
|
|
67
|
+
if (!indices) return [];
|
|
68
|
+
return indices.filter((i) => i < this.signals.length).map((i) => this.signals[i]).filter((s) => s.tokenAddress === tokenAddress);
|
|
69
|
+
}
|
|
70
|
+
getSourceStatsAll() {
|
|
71
|
+
return Array.from(this.sourceStats.values());
|
|
72
|
+
}
|
|
73
|
+
markEventSeen(eventId) {
|
|
74
|
+
this.seenEventIds.set(eventId, Date.now());
|
|
75
|
+
this.pruneSeenEvents();
|
|
76
|
+
for (const signal of this.signals) {
|
|
77
|
+
if (signal.eventId === eventId) {
|
|
78
|
+
signal._seen = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
hasSeenEvent(eventId) {
|
|
83
|
+
return this.seenEventIds.has(eventId);
|
|
84
|
+
}
|
|
85
|
+
getBufferSize() {
|
|
86
|
+
return this.signals.length;
|
|
87
|
+
}
|
|
88
|
+
updateSourceStats(signal) {
|
|
89
|
+
const existing = this.sourceStats.get(signal.sourceName);
|
|
90
|
+
if (existing) {
|
|
91
|
+
existing.signalCount++;
|
|
92
|
+
existing.totalScore += signal.systemScore;
|
|
93
|
+
existing.avgScore = existing.totalScore / existing.signalCount;
|
|
94
|
+
} else {
|
|
95
|
+
this.sourceStats.set(signal.sourceName, {
|
|
96
|
+
name: signal.sourceName,
|
|
97
|
+
type: signal.sourceType,
|
|
98
|
+
signalCount: 1,
|
|
99
|
+
avgScore: signal.systemScore,
|
|
100
|
+
totalScore: signal.systemScore
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
decrementSourceStats(signal) {
|
|
105
|
+
const existing = this.sourceStats.get(signal.sourceName);
|
|
106
|
+
if (!existing) return;
|
|
107
|
+
existing.signalCount--;
|
|
108
|
+
existing.totalScore -= signal.systemScore;
|
|
109
|
+
if (existing.signalCount <= 0) {
|
|
110
|
+
this.sourceStats.delete(signal.sourceName);
|
|
111
|
+
} else {
|
|
112
|
+
existing.avgScore = existing.totalScore / existing.signalCount;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
rebuildTokenIndex() {
|
|
116
|
+
this.tokenIndex.clear();
|
|
117
|
+
for (let i = 0; i < this.signals.length; i++) {
|
|
118
|
+
const addr = this.signals[i].tokenAddress;
|
|
119
|
+
const list = this.tokenIndex.get(addr) || [];
|
|
120
|
+
list.push(i);
|
|
121
|
+
this.tokenIndex.set(addr, list);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
pruneSeenEvents() {
|
|
125
|
+
if (this.seenEventIds.size <= MAX_SEEN_EVENTS) return;
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
for (const [id, timestamp] of this.seenEventIds) {
|
|
128
|
+
if (now - timestamp > SEEN_EVENT_TTL_MS) {
|
|
129
|
+
this.seenEventIds.delete(id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (this.seenEventIds.size > MAX_SEEN_EVENTS) {
|
|
133
|
+
const sorted = Array.from(this.seenEventIds.entries()).sort((a, b) => a[1] - b[1]);
|
|
134
|
+
const toRemove = sorted.slice(0, sorted.length - MAX_SEEN_EVENTS);
|
|
135
|
+
for (const [id] of toRemove) {
|
|
136
|
+
this.seenEventIds.delete(id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
AlphaBuffer
|
|
144
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/alpha-ws.ts
|
|
2
|
+
var RECONNECT_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
|
|
3
|
+
var AlphaStreamManager = class {
|
|
4
|
+
config;
|
|
5
|
+
ws = null;
|
|
6
|
+
subscribed = false;
|
|
7
|
+
authenticated = false;
|
|
8
|
+
reconnectAttempt = 0;
|
|
9
|
+
reconnectTimer = null;
|
|
10
|
+
intentionalClose = false;
|
|
11
|
+
messageCount = 0;
|
|
12
|
+
lastEventTs = 0;
|
|
13
|
+
connectedAt = 0;
|
|
14
|
+
tier = "";
|
|
15
|
+
premiumAccess = false;
|
|
16
|
+
currentAccessToken = "";
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
async subscribe() {
|
|
21
|
+
if (this.subscribed && this.ws && this.ws.readyState === 1) {
|
|
22
|
+
return { subscribed: true, premiumAccess: this.premiumAccess, tier: this.tier };
|
|
23
|
+
}
|
|
24
|
+
this.intentionalClose = false;
|
|
25
|
+
await this.connect();
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const checkSubscribed = setInterval(() => {
|
|
28
|
+
if (this.subscribed) {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
clearInterval(checkSubscribed);
|
|
31
|
+
resolve({ subscribed: true, premiumAccess: this.premiumAccess, tier: this.tier });
|
|
32
|
+
}
|
|
33
|
+
}, 100);
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
clearInterval(checkSubscribed);
|
|
36
|
+
reject(new Error("Alpha stream subscription timed out after 15 seconds"));
|
|
37
|
+
}, 15e3);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async unsubscribe() {
|
|
41
|
+
this.intentionalClose = true;
|
|
42
|
+
this.subscribed = false;
|
|
43
|
+
if (this.reconnectTimer) {
|
|
44
|
+
clearTimeout(this.reconnectTimer);
|
|
45
|
+
this.reconnectTimer = null;
|
|
46
|
+
}
|
|
47
|
+
if (this.ws) {
|
|
48
|
+
try {
|
|
49
|
+
if (this.ws.readyState === 1) {
|
|
50
|
+
this.ws.send(JSON.stringify({ type: "alpha_stream_unsubscribe" }));
|
|
51
|
+
}
|
|
52
|
+
this.ws.close();
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
this.ws = null;
|
|
56
|
+
}
|
|
57
|
+
return { unsubscribed: true };
|
|
58
|
+
}
|
|
59
|
+
getAgentId() {
|
|
60
|
+
return this.config.agentId;
|
|
61
|
+
}
|
|
62
|
+
setAgentId(agentId) {
|
|
63
|
+
this.config.agentId = agentId;
|
|
64
|
+
}
|
|
65
|
+
setSubscriberType(subscriberType) {
|
|
66
|
+
this.config.subscriberType = subscriberType;
|
|
67
|
+
}
|
|
68
|
+
isSubscribed() {
|
|
69
|
+
return this.subscribed && this.ws !== null && this.ws.readyState === 1;
|
|
70
|
+
}
|
|
71
|
+
getStats() {
|
|
72
|
+
return {
|
|
73
|
+
subscribed: this.isSubscribed(),
|
|
74
|
+
messageCount: this.messageCount,
|
|
75
|
+
lastEventTs: this.lastEventTs,
|
|
76
|
+
connectedAt: this.connectedAt,
|
|
77
|
+
uptimeSeconds: this.connectedAt ? Math.floor((Date.now() - this.connectedAt) / 1e3) : 0
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
sendAlphaSubscribe() {
|
|
81
|
+
if (!this.ws || this.ws.readyState !== 1) return;
|
|
82
|
+
const subscribeMsg = { type: "alpha_stream_subscribe" };
|
|
83
|
+
if (this.config.agentId) {
|
|
84
|
+
subscribeMsg.agentId = this.config.agentId;
|
|
85
|
+
}
|
|
86
|
+
if (this.config.subscriberType) {
|
|
87
|
+
subscribeMsg.subscriberType = this.config.subscriberType;
|
|
88
|
+
} else if (this.config.agentId) {
|
|
89
|
+
subscribeMsg.subscriberType = "agent";
|
|
90
|
+
}
|
|
91
|
+
this.log("info", "Sending alpha_stream_subscribe");
|
|
92
|
+
this.ws.send(JSON.stringify(subscribeMsg));
|
|
93
|
+
}
|
|
94
|
+
async connect() {
|
|
95
|
+
const WebSocket = (await import("ws")).default;
|
|
96
|
+
this.currentAccessToken = await this.config.getAccessToken();
|
|
97
|
+
const url = `${this.config.wsUrl}?accessToken=${encodeURIComponent(this.currentAccessToken)}`;
|
|
98
|
+
this.authenticated = false;
|
|
99
|
+
this.log("info", `Connecting to alpha stream: ${this.config.wsUrl}`);
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
try {
|
|
102
|
+
this.ws = new WebSocket(url);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
reject(err);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const connectTimeout = setTimeout(() => {
|
|
108
|
+
if (this.ws && this.ws.readyState !== 1) {
|
|
109
|
+
this.ws.close();
|
|
110
|
+
reject(new Error("WebSocket connection timed out"));
|
|
111
|
+
}
|
|
112
|
+
}, 1e4);
|
|
113
|
+
this.ws.on("open", () => {
|
|
114
|
+
clearTimeout(connectTimeout);
|
|
115
|
+
this.connectedAt = Date.now();
|
|
116
|
+
this.reconnectAttempt = 0;
|
|
117
|
+
this.log("info", "WebSocket connected, waiting for server handshake...");
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
this.ws.on("message", (data) => {
|
|
121
|
+
try {
|
|
122
|
+
const msg = JSON.parse(data.toString());
|
|
123
|
+
this.handleMessage(msg);
|
|
124
|
+
} catch {
|
|
125
|
+
this.log("warn", "Failed to parse WebSocket message");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
this.ws.on("close", () => {
|
|
129
|
+
clearTimeout(connectTimeout);
|
|
130
|
+
this.subscribed = false;
|
|
131
|
+
this.authenticated = false;
|
|
132
|
+
this.log("info", "WebSocket closed");
|
|
133
|
+
if (!this.intentionalClose) {
|
|
134
|
+
this.scheduleReconnect();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
this.ws.on("error", (err) => {
|
|
138
|
+
clearTimeout(connectTimeout);
|
|
139
|
+
this.log("error", `WebSocket error: ${err.message}`);
|
|
140
|
+
if (this.ws && this.ws.readyState !== 1) {
|
|
141
|
+
reject(err);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
handleMessage(msg) {
|
|
147
|
+
switch (msg.type) {
|
|
148
|
+
case "connected":
|
|
149
|
+
if (!this.authenticated) {
|
|
150
|
+
this.log("info", "Server handshake received, sending auth...");
|
|
151
|
+
if (this.ws && this.ws.readyState === 1) {
|
|
152
|
+
this.ws.send(JSON.stringify({ type: "auth", accessToken: this.currentAccessToken }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case "authenticated":
|
|
157
|
+
this.tier = msg.tier || "";
|
|
158
|
+
if (!this.authenticated) {
|
|
159
|
+
this.authenticated = true;
|
|
160
|
+
this.log("info", `Authenticated: tier=${this.tier}`);
|
|
161
|
+
this.sendAlphaSubscribe();
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case "alpha_stream_subscribed":
|
|
165
|
+
this.subscribed = true;
|
|
166
|
+
this.tier = msg.tier || this.tier;
|
|
167
|
+
this.premiumAccess = msg.premiumAccess || false;
|
|
168
|
+
this.log("info", `Subscribed to alpha stream: tier=${this.tier}, premium=${this.premiumAccess}`);
|
|
169
|
+
break;
|
|
170
|
+
case "alpha_stream_unsubscribed":
|
|
171
|
+
this.subscribed = false;
|
|
172
|
+
this.log("info", "Unsubscribed from alpha stream");
|
|
173
|
+
break;
|
|
174
|
+
case "alpha_signal": {
|
|
175
|
+
this.messageCount++;
|
|
176
|
+
this.lastEventTs = Date.now();
|
|
177
|
+
const data = msg.data;
|
|
178
|
+
if (data) {
|
|
179
|
+
const signal = {
|
|
180
|
+
sourceName: data.sourceName || "",
|
|
181
|
+
sourceType: data.sourceType || "telegram",
|
|
182
|
+
externalRef: data.externalRef,
|
|
183
|
+
isPremium: data.isPremium || false,
|
|
184
|
+
tokenAddress: data.tokenAddress || "",
|
|
185
|
+
tokenName: data.tokenName || "",
|
|
186
|
+
tokenSymbol: data.tokenSymbol || "",
|
|
187
|
+
chain: data.chain || "solana",
|
|
188
|
+
marketCap: data.marketCap,
|
|
189
|
+
price: data.price,
|
|
190
|
+
kind: data.kind || "ca_drop",
|
|
191
|
+
signalStage: data.signalStage || "early",
|
|
192
|
+
summary: data.summary || "",
|
|
193
|
+
confidence: data.confidence || "low",
|
|
194
|
+
calledAgainCount: data.calledAgainCount ?? 0,
|
|
195
|
+
systemScore: data.systemScore ?? 0,
|
|
196
|
+
ts: msg.ts || Date.now(),
|
|
197
|
+
eventId: data.eventId
|
|
198
|
+
};
|
|
199
|
+
this.config.buffer.push(signal);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "error": {
|
|
204
|
+
const code = msg.code;
|
|
205
|
+
this.log("error", `WebSocket error: ${code} \u2014 ${msg.message || ""}`);
|
|
206
|
+
if (code === "WS_AUTH_REQUIRED" || code === "WS_AUTH_INVALID" || code === "WS_SESSION_INVALID" || code === "ACCESS_TOKEN_FORMAT_INVALID" || code === "ACCESS_TOKEN_EXPIRED") {
|
|
207
|
+
this.authenticated = false;
|
|
208
|
+
this.log("warn", "Auth error \u2014 closing and will reconnect with fresh token");
|
|
209
|
+
if (this.ws) this.ws.close();
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
scheduleReconnect() {
|
|
216
|
+
if (this.intentionalClose) return;
|
|
217
|
+
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
|
218
|
+
this.reconnectAttempt++;
|
|
219
|
+
this.log("info", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt})`);
|
|
220
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
221
|
+
try {
|
|
222
|
+
await this.connect();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
this.log("error", `Reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
225
|
+
this.scheduleReconnect();
|
|
226
|
+
}
|
|
227
|
+
}, delay);
|
|
228
|
+
}
|
|
229
|
+
log(level, msg) {
|
|
230
|
+
if (this.config.logger) {
|
|
231
|
+
this.config.logger[level](`[alpha-stream] ${msg}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
AlphaStreamManager
|
|
238
|
+
};
|