@sherwoodagent/cli 0.38.5 → 0.40.4
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/dist/abis-TXCLIR2K.js +37 -0
- package/dist/{addresses-5BITNIGX.js → addresses-J4NSZRGB.js} +2 -2
- package/dist/{agent-TB7VNLFH.js → agent-XBYSALOA.js} +1433 -662
- package/dist/agent-XBYSALOA.js.map +1 -0
- package/dist/{chat-CLBWW7PV.js → chat-EYCDCZUA.js} +30 -13
- package/dist/chat-EYCDCZUA.js.map +1 -0
- package/dist/{chunk-IQNYBIFE.js → chunk-363E34JI.js} +8 -8
- package/dist/{chunk-PHU6MI3J.js → chunk-5OMMVOR6.js} +5 -5
- package/dist/{chunk-B4AH26UI.js → chunk-DNX3P5YK.js} +150 -15
- package/dist/chunk-DNX3P5YK.js.map +1 -0
- package/dist/{chunk-JQBGEYAK.js → chunk-FCM6SSL6.js} +5 -5
- package/dist/{chunk-QZSKBYZ2.js → chunk-ITNGXYLB.js} +5 -5
- package/dist/{chunk-OPEVMNEY.js → chunk-KHTM4GB5.js} +5 -5
- package/dist/{chunk-OQUGUIYM.js → chunk-W3GXAODE.js} +111 -2
- package/dist/{chunk-OQUGUIYM.js.map → chunk-W3GXAODE.js.map} +1 -1
- package/dist/{chunk-RFTJBEZI.js → chunk-ZN6CUAX7.js} +4 -4
- package/dist/{chunk-RFTJBEZI.js.map → chunk-ZN6CUAX7.js.map} +1 -1
- package/dist/{eas-LVJEPWBY.js → eas-MUKSLDIP.js} +4 -4
- package/dist/{governor-HU3CPXMU.js → governor-WA74WFOS.js} +4 -4
- package/dist/index.js +33 -33
- package/dist/{price-OZRLJH3X.js → price-ELYQKRSN.js} +5 -5
- package/dist/{research-662HNMOG.js → research-4UA4UB5P.js} +2 -2
- package/dist/research-4UA4UB5P.js.map +1 -0
- package/dist/{research-5WEM4WZC.js → research-HXOXZA4N.js} +4 -4
- package/dist/{session-XRTKPC4Q.js → session-VEUMC4F6.js} +11 -11
- package/dist/{trade-HN4BXDVU.js → trade-WMPWF6RT.js} +10 -10
- package/dist/{xmtp-25RHTLIA.js → xmtp-7CZ5LB5O.js} +4 -4
- package/package.json +1 -1
- package/dist/agent-TB7VNLFH.js.map +0 -1
- package/dist/chat-CLBWW7PV.js.map +0 -1
- package/dist/chunk-B4AH26UI.js.map +0 -1
- /package/dist/{addresses-5BITNIGX.js.map → abis-TXCLIR2K.js.map} +0 -0
- /package/dist/{eas-LVJEPWBY.js.map → addresses-J4NSZRGB.js.map} +0 -0
- /package/dist/{chunk-IQNYBIFE.js.map → chunk-363E34JI.js.map} +0 -0
- /package/dist/{chunk-PHU6MI3J.js.map → chunk-5OMMVOR6.js.map} +0 -0
- /package/dist/{chunk-JQBGEYAK.js.map → chunk-FCM6SSL6.js.map} +0 -0
- /package/dist/{chunk-QZSKBYZ2.js.map → chunk-ITNGXYLB.js.map} +0 -0
- /package/dist/{chunk-OPEVMNEY.js.map → chunk-KHTM4GB5.js.map} +0 -0
- /package/dist/{governor-HU3CPXMU.js.map → eas-MUKSLDIP.js.map} +0 -0
- /package/dist/{research-662HNMOG.js.map → governor-WA74WFOS.js.map} +0 -0
- /package/dist/{price-OZRLJH3X.js.map → price-ELYQKRSN.js.map} +0 -0
- /package/dist/{research-5WEM4WZC.js.map → research-HXOXZA4N.js.map} +0 -0
- /package/dist/{session-XRTKPC4Q.js.map → session-VEUMC4F6.js.map} +0 -0
- /package/dist/{trade-HN4BXDVU.js.map → trade-WMPWF6RT.js.map} +0 -0
- /package/dist/{xmtp-25RHTLIA.js.map → xmtp-7CZ5LB5O.js.map} +0 -0
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getResearchProvider
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
BASE_STRATEGY_ABI
|
|
6
|
-
} from "./chunk-OQUGUIYM.js";
|
|
2
|
+
getResearchProvider,
|
|
3
|
+
isX402WalletFunded
|
|
4
|
+
} from "./chunk-DNX3P5YK.js";
|
|
7
5
|
import "./chunk-OFR3LCYE.js";
|
|
8
6
|
import {
|
|
9
7
|
hyperevm,
|
|
10
8
|
hyperevmTestnet
|
|
11
9
|
} from "./chunk-IJG4CTOI.js";
|
|
12
10
|
import "./chunk-WCBG44IQ.js";
|
|
11
|
+
import {
|
|
12
|
+
BASE_STRATEGY_ABI
|
|
13
|
+
} from "./chunk-W3GXAODE.js";
|
|
13
14
|
|
|
14
15
|
// src/commands/agent.ts
|
|
15
16
|
import chalk8 from "chalk";
|
|
16
|
-
import { readFile as
|
|
17
|
-
import { join as
|
|
18
|
-
import { homedir as
|
|
17
|
+
import { readFile as readFile13, writeFile as writeFile13, mkdir as mkdir15 } from "fs/promises";
|
|
18
|
+
import { join as join14 } from "path";
|
|
19
|
+
import { homedir as homedir14 } from "os";
|
|
19
20
|
import ora from "ora";
|
|
20
21
|
|
|
21
22
|
// src/agent/index.ts
|
|
@@ -689,19 +690,28 @@ function clamp(v, min = -1, max = 1) {
|
|
|
689
690
|
|
|
690
691
|
// src/agent/scoring.ts
|
|
691
692
|
var DEFAULT_WEIGHTS = {
|
|
692
|
-
smartMoney: 0.
|
|
693
|
-
technical: 0.
|
|
694
|
-
sentiment: 0.
|
|
695
|
-
onchain: 0.
|
|
693
|
+
smartMoney: 0.15,
|
|
694
|
+
technical: 0.1,
|
|
695
|
+
sentiment: 0.4,
|
|
696
|
+
onchain: 0.2,
|
|
696
697
|
fundamental: 0.1,
|
|
697
|
-
event: 0.
|
|
698
|
+
event: 0.05
|
|
698
699
|
};
|
|
699
700
|
var WEIGHT_PROFILES = {
|
|
700
701
|
default: DEFAULT_WEIGHTS,
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
702
|
+
// Majors (BTC/ETH/SOL): no TVL/event data. 4 active categories.
|
|
703
|
+
// Same rebalance rationale as DEFAULT_WEIGHTS — replay calibration showed
|
|
704
|
+
// sentiment-heavy profiles dominating production-stack data; this profile
|
|
705
|
+
// concentrates the freed-up weight into sentiment + onchain (the two
|
|
706
|
+
// categories that produce directional opinions on majors). Technical and
|
|
707
|
+
// smartMoney shrink because both are lagging or unfunded in practice.
|
|
708
|
+
majors: { smartMoney: 0.15, technical: 0.1, sentiment: 0.4, onchain: 0.35, fundamental: 0, event: 0 },
|
|
709
|
+
// Altcoins: keep fundamental (TVL data exists) and event. Sentiment lifted
|
|
710
|
+
// here too but less aggressively — altcoins respond more to TVL/flow than
|
|
711
|
+
// majors, and the sentimentContrarian extremes-only fix means sentiment
|
|
712
|
+
// weight is dormant most of the time.
|
|
713
|
+
altcoin: { smartMoney: 0.15, technical: 0.15, sentiment: 0.3, onchain: 0.2, fundamental: 0.1, event: 0.1 }
|
|
714
|
+
// sentHeavy/techHeavy removed — unvalidated, added parameter complexity without evidence of benefit
|
|
705
715
|
};
|
|
706
716
|
var MAJORS_TOKEN_IDS = /* @__PURE__ */ new Set(["bitcoin", "ethereum", "solana"]);
|
|
707
717
|
function profileForToken(tokenId, userProfile) {
|
|
@@ -722,7 +732,11 @@ var DEFAULT_THRESHOLDS = {
|
|
|
722
732
|
var REGIME_THRESHOLDS = {
|
|
723
733
|
"trending-up": { strongBuy: 0.55, buy: 0.25, sell: -0.4, strongSell: -0.7 },
|
|
724
734
|
"trending-down": { strongBuy: 0.7, buy: 0.4, sell: -0.25, strongSell: -0.55 },
|
|
725
|
-
|
|
735
|
+
// Ranging BUY lowered 0.30 → 0.25 to match trending-up. Production cycles
|
|
736
|
+
// showed scores capping at 0.27 for hours at a time during ranging regimes,
|
|
737
|
+
// missing real entries (BTC 0.272, DOGE 0.266, AAVE 0.266 all HOLD'd).
|
|
738
|
+
// Symmetric SELL lowered to -0.25 to keep the band balanced.
|
|
739
|
+
"ranging": { strongBuy: 0.55, buy: 0.25, sell: -0.25, strongSell: -0.55 },
|
|
726
740
|
"high-volatility": { strongBuy: 0.7, buy: 0.45, sell: -0.45, strongSell: -0.7 },
|
|
727
741
|
"low-volatility": { strongBuy: 0.6, buy: 0.3, sell: -0.3, strongSell: -0.6 }
|
|
728
742
|
};
|
|
@@ -983,33 +997,61 @@ function scoreEvent(data) {
|
|
|
983
997
|
details: details.join("; ")
|
|
984
998
|
};
|
|
985
999
|
}
|
|
986
|
-
|
|
1000
|
+
var LAGGING_TECHNICAL_SIGNALS = /* @__PURE__ */ new Set(["technical", "meanReversion"]);
|
|
1001
|
+
var SIGNAL_CATEGORY_MAP = {
|
|
1002
|
+
technical: "technical",
|
|
1003
|
+
sentiment: "sentiment",
|
|
1004
|
+
onchain: "onchain",
|
|
1005
|
+
fundamental: "fundamental",
|
|
1006
|
+
event: "event",
|
|
1007
|
+
smartMoney: "smartMoney",
|
|
1008
|
+
// Strategy signal mappings to categories
|
|
1009
|
+
// momentum intentionally has no key in getStrategyAdjustments() — it keeps
|
|
1010
|
+
// full weight during momentum overrides (not dampened like lagging indicators).
|
|
1011
|
+
momentum: "technical",
|
|
1012
|
+
breakoutOnChain: "technical",
|
|
1013
|
+
meanReversion: "technical",
|
|
1014
|
+
multiTimeframe: "technical",
|
|
1015
|
+
dexFlow: "onchain",
|
|
1016
|
+
fundingRate: "onchain",
|
|
1017
|
+
// FIX 2: was "fundamental" — wasted on majors (0.00 weight)
|
|
1018
|
+
tvlMomentum: "fundamental",
|
|
1019
|
+
sentimentContrarian: "sentiment",
|
|
1020
|
+
twitterSentiment: "sentiment",
|
|
1021
|
+
tokenUnlock: "event",
|
|
1022
|
+
hyperliquidFlow: "onchain"
|
|
1023
|
+
};
|
|
1024
|
+
var X402_CATEGORIES = /* @__PURE__ */ new Set(["smartMoney", "event"]);
|
|
1025
|
+
function computeTradeDecision(signals, weights, regimeAdjustments, correlationCheck, regime, x402Available) {
|
|
987
1026
|
const w = weights ?? DEFAULT_WEIGHTS;
|
|
988
1027
|
const thresholds = thresholdsForRegime(regime);
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
tvlMomentum: w.fundamental,
|
|
1003
|
-
sentimentContrarian: w.sentiment,
|
|
1004
|
-
twitterSentiment: w.sentiment,
|
|
1005
|
-
tokenUnlock: w.event,
|
|
1006
|
-
hyperliquidFlow: w.onchain
|
|
1007
|
-
};
|
|
1028
|
+
const excludedCategories = /* @__PURE__ */ new Set();
|
|
1029
|
+
if (x402Available === false) {
|
|
1030
|
+
for (const cat of X402_CATEGORIES) {
|
|
1031
|
+
if (w[cat] > 0) excludedCategories.add(cat);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const categoryCounts = {};
|
|
1035
|
+
for (const signal of signals) {
|
|
1036
|
+
if (signal._weightOverride !== void 0) continue;
|
|
1037
|
+
const category = SIGNAL_CATEGORY_MAP[signal.name];
|
|
1038
|
+
if (!category || excludedCategories.has(category)) continue;
|
|
1039
|
+
categoryCounts[category] = (categoryCounts[category] ?? 0) + 1;
|
|
1040
|
+
}
|
|
1008
1041
|
let totalWeight = 0;
|
|
1009
1042
|
let weightedSum = 0;
|
|
1010
1043
|
let weightedConfidence = 0;
|
|
1011
1044
|
for (const signal of signals) {
|
|
1012
|
-
|
|
1045
|
+
let signalWeight;
|
|
1046
|
+
if (signal._weightOverride !== void 0) {
|
|
1047
|
+
signalWeight = signal._weightOverride;
|
|
1048
|
+
} else {
|
|
1049
|
+
const category = SIGNAL_CATEGORY_MAP[signal.name];
|
|
1050
|
+
if (category && excludedCategories.has(category)) continue;
|
|
1051
|
+
const categoryWeight = category ? w[category] : 0.1;
|
|
1052
|
+
const count = category ? categoryCounts[category] ?? 1 : 1;
|
|
1053
|
+
signalWeight = categoryWeight / count;
|
|
1054
|
+
}
|
|
1013
1055
|
let adjustedValue = signal.value;
|
|
1014
1056
|
let adjustedConfidence = signal.confidence;
|
|
1015
1057
|
if (regimeAdjustments && signal.name in regimeAdjustments) {
|
|
@@ -1017,12 +1059,36 @@ function computeTradeDecision(signals, weights, regimeAdjustments, correlationCh
|
|
|
1017
1059
|
adjustedValue *= adjustment;
|
|
1018
1060
|
adjustedConfidence *= adjustment;
|
|
1019
1061
|
}
|
|
1062
|
+
if (regime && (regime === "trending-up" || regime === "trending-down") && LAGGING_TECHNICAL_SIGNALS.has(signal.name)) {
|
|
1063
|
+
signalWeight *= 0.5;
|
|
1064
|
+
}
|
|
1020
1065
|
weightedSum += adjustedValue * signalWeight;
|
|
1021
1066
|
weightedConfidence += adjustedConfidence * signalWeight;
|
|
1022
1067
|
totalWeight += signalWeight;
|
|
1023
1068
|
}
|
|
1024
1069
|
let score = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
1025
1070
|
const confidence = totalWeight > 0 ? weightedConfidence / totalWeight : 0;
|
|
1071
|
+
if (Math.abs(score) > 0.01) {
|
|
1072
|
+
const scoreSign = Math.sign(score);
|
|
1073
|
+
const categoryNetDirection = /* @__PURE__ */ new Map();
|
|
1074
|
+
for (const signal of signals) {
|
|
1075
|
+
const category = SIGNAL_CATEGORY_MAP[signal.name];
|
|
1076
|
+
if (!category || excludedCategories.has(category)) continue;
|
|
1077
|
+
if (signal._weightOverride !== void 0) continue;
|
|
1078
|
+
const current = categoryNetDirection.get(category) ?? 0;
|
|
1079
|
+
categoryNetDirection.set(category, current + signal.value);
|
|
1080
|
+
}
|
|
1081
|
+
let agreeing = 0;
|
|
1082
|
+
let totalActive = 0;
|
|
1083
|
+
for (const [, netValue] of categoryNetDirection) {
|
|
1084
|
+
totalActive++;
|
|
1085
|
+
if (Math.sign(netValue) === scoreSign) agreeing++;
|
|
1086
|
+
}
|
|
1087
|
+
if (agreeing >= 4 && totalActive >= 4) {
|
|
1088
|
+
const bonus = 1 + 0.15 * (agreeing - 3);
|
|
1089
|
+
score *= Math.min(bonus, 1.45);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1026
1092
|
if (correlationCheck) {
|
|
1027
1093
|
if (correlationCheck.btcBias === "bearish" && score > 0) {
|
|
1028
1094
|
score *= correlationCheck.suppressionFactor;
|
|
@@ -1035,6 +1101,7 @@ function computeTradeDecision(signals, weights, regimeAdjustments, correlationCh
|
|
|
1035
1101
|
}
|
|
1036
1102
|
score = Math.max(-1, Math.min(1, score));
|
|
1037
1103
|
}
|
|
1104
|
+
score = Math.max(-1, Math.min(1, score));
|
|
1038
1105
|
let action;
|
|
1039
1106
|
if (score >= thresholds.strongBuy) action = "STRONG_BUY";
|
|
1040
1107
|
else if (score >= thresholds.buy) action = "BUY";
|
|
@@ -1053,191 +1120,6 @@ function computeTradeDecision(signals, weights, regimeAdjustments, correlationCh
|
|
|
1053
1120
|
};
|
|
1054
1121
|
}
|
|
1055
1122
|
|
|
1056
|
-
// src/agent/strategies/smart-money.ts
|
|
1057
|
-
var SmartMoneyStrategy = class {
|
|
1058
|
-
name = "smartMoney";
|
|
1059
|
-
description = "Analyzes Nansen smart-money netflow to detect institutional accumulation or distribution";
|
|
1060
|
-
requiredData = ["nansenData"];
|
|
1061
|
-
async analyze(ctx) {
|
|
1062
|
-
const details = [];
|
|
1063
|
-
if (!ctx.nansenData) {
|
|
1064
|
-
return {
|
|
1065
|
-
name: this.name,
|
|
1066
|
-
value: 0,
|
|
1067
|
-
confidence: 0.1,
|
|
1068
|
-
source: "Smart Money Convergence",
|
|
1069
|
-
details: "No Nansen smart-money data available"
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
const data = ctx.nansenData;
|
|
1073
|
-
const flows = data.flows;
|
|
1074
|
-
if (!flows || flows.length === 0) {
|
|
1075
|
-
return {
|
|
1076
|
-
name: this.name,
|
|
1077
|
-
value: 0,
|
|
1078
|
-
confidence: 0.1,
|
|
1079
|
-
source: "Smart Money Convergence",
|
|
1080
|
-
details: "No smart-money flow data found"
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
const netflow = flows.reduce(
|
|
1084
|
-
(sum, f) => sum + Number(f.netflow ?? f.net_flow ?? 0),
|
|
1085
|
-
0
|
|
1086
|
-
);
|
|
1087
|
-
const accumulating = flows.filter(
|
|
1088
|
-
(f) => Number(f.netflow ?? f.net_flow ?? 0) < 0
|
|
1089
|
-
// negative = leaving exchanges = buying
|
|
1090
|
-
).length;
|
|
1091
|
-
const distributing = flows.length - accumulating;
|
|
1092
|
-
const convergenceRatio = Math.max(accumulating, distributing) / flows.length;
|
|
1093
|
-
const isConverging = convergenceRatio > 0.7;
|
|
1094
|
-
let value = 0;
|
|
1095
|
-
let confidence = 0.5;
|
|
1096
|
-
if (netflow < 0) {
|
|
1097
|
-
const magnitude = Math.min(Math.abs(netflow) / 1e6, 1);
|
|
1098
|
-
value = 0.5 + magnitude * 0.5;
|
|
1099
|
-
details.push(`Smart money buying: net outflow ${Math.abs(netflow).toFixed(0)} tokens`);
|
|
1100
|
-
confidence = 0.6;
|
|
1101
|
-
} else if (netflow > 0) {
|
|
1102
|
-
const magnitude = Math.min(Math.abs(netflow) / 1e6, 1);
|
|
1103
|
-
value = -(0.5 + magnitude * 0.5);
|
|
1104
|
-
details.push(`Smart money selling: net inflow ${netflow.toFixed(0)} tokens`);
|
|
1105
|
-
confidence = 0.6;
|
|
1106
|
-
} else {
|
|
1107
|
-
details.push("Smart money neutral: no significant flow");
|
|
1108
|
-
}
|
|
1109
|
-
if (isConverging && value !== 0) {
|
|
1110
|
-
const bonus = value > 0 ? 0.15 : -0.15;
|
|
1111
|
-
value = clamp(value + bonus);
|
|
1112
|
-
details.push(`${accumulating}/${flows.length} wallets accumulating (convergence signal)`);
|
|
1113
|
-
confidence += 0.15;
|
|
1114
|
-
} else {
|
|
1115
|
-
details.push(`Mixed signals: ${accumulating} accumulating, ${distributing} distributing`);
|
|
1116
|
-
}
|
|
1117
|
-
return {
|
|
1118
|
-
name: this.name,
|
|
1119
|
-
value: clamp(value),
|
|
1120
|
-
confidence: Math.min(confidence, 1),
|
|
1121
|
-
source: "Smart Money Convergence",
|
|
1122
|
-
details: details.join("; ")
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
// src/agent/strategies/token-unlock.ts
|
|
1128
|
-
function parseUnlocks(messariData) {
|
|
1129
|
-
const unlocks = [];
|
|
1130
|
-
const profile = messariData.profile;
|
|
1131
|
-
const tokenomics = profile?.economics ?? profile?.tokenomics ?? messariData.tokenomics;
|
|
1132
|
-
const schedule = tokenomics?.vesting_schedule ?? tokenomics?.unlock_schedule ?? messariData.unlock_schedule;
|
|
1133
|
-
if (!schedule || !Array.isArray(schedule)) return unlocks;
|
|
1134
|
-
const now = Date.now();
|
|
1135
|
-
for (const entry of schedule) {
|
|
1136
|
-
const unlockDate = entry.date ?? entry.unlock_date ?? entry.timestamp;
|
|
1137
|
-
if (!unlockDate) continue;
|
|
1138
|
-
const unlockTime = typeof unlockDate === "number" ? unlockDate : new Date(String(unlockDate)).getTime();
|
|
1139
|
-
const daysUntil = (unlockTime - now) / (1e3 * 60 * 60 * 24);
|
|
1140
|
-
if (daysUntil < 0 || daysUntil > 30) continue;
|
|
1141
|
-
const percentOfSupply = Number(entry.percent ?? entry.percent_of_supply ?? entry.amount_pct ?? 0);
|
|
1142
|
-
const typeRaw = String(entry.type ?? entry.category ?? entry.recipient ?? "unknown").toLowerCase();
|
|
1143
|
-
let type = "unknown";
|
|
1144
|
-
if (typeRaw.includes("team") || typeRaw.includes("founder")) type = "team";
|
|
1145
|
-
else if (typeRaw.includes("investor") || typeRaw.includes("vc") || typeRaw.includes("seed") || typeRaw.includes("private")) type = "investor";
|
|
1146
|
-
else if (typeRaw.includes("community") || typeRaw.includes("airdrop")) type = "community";
|
|
1147
|
-
else if (typeRaw.includes("ecosystem") || typeRaw.includes("treasury")) type = "ecosystem";
|
|
1148
|
-
unlocks.push({ percentOfSupply, daysUntil, type });
|
|
1149
|
-
}
|
|
1150
|
-
return unlocks;
|
|
1151
|
-
}
|
|
1152
|
-
var TokenUnlockStrategy = class {
|
|
1153
|
-
name = "tokenUnlock";
|
|
1154
|
-
description = "Detects upcoming token unlocks and signals potential selling pressure";
|
|
1155
|
-
requiredData = ["messariData"];
|
|
1156
|
-
async analyze(ctx) {
|
|
1157
|
-
if (!ctx.messariData && !ctx.unlockData) {
|
|
1158
|
-
return {
|
|
1159
|
-
name: this.name,
|
|
1160
|
-
value: 0,
|
|
1161
|
-
confidence: 0.1,
|
|
1162
|
-
source: "Token Unlock Frontrun",
|
|
1163
|
-
details: "No unlock data available"
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
if (!ctx.messariData && ctx.unlockData) {
|
|
1167
|
-
return this.analyzeFromUnlockData(ctx.unlockData);
|
|
1168
|
-
}
|
|
1169
|
-
const unlocks = parseUnlocks(ctx.messariData);
|
|
1170
|
-
if (unlocks.length === 0) {
|
|
1171
|
-
return {
|
|
1172
|
-
name: this.name,
|
|
1173
|
-
value: 0,
|
|
1174
|
-
confidence: 0.3,
|
|
1175
|
-
source: "Token Unlock Frontrun",
|
|
1176
|
-
details: "No upcoming token unlocks detected within 30 days"
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
let value = 0;
|
|
1180
|
-
let confidence = 0.5;
|
|
1181
|
-
const details = [];
|
|
1182
|
-
for (const unlock of unlocks) {
|
|
1183
|
-
let signal = 0;
|
|
1184
|
-
if (unlock.percentOfSupply > 5 && unlock.daysUntil <= 7) {
|
|
1185
|
-
signal = -(0.6 + Math.min((unlock.percentOfSupply - 5) / 15, 0.4));
|
|
1186
|
-
confidence = 0.8;
|
|
1187
|
-
} else if (unlock.percentOfSupply > 2 && unlock.daysUntil <= 14) {
|
|
1188
|
-
signal = -(0.3 + Math.min((unlock.percentOfSupply - 2) / 10, 0.5));
|
|
1189
|
-
confidence = Math.max(confidence, 0.6);
|
|
1190
|
-
} else if (unlock.percentOfSupply > 1) {
|
|
1191
|
-
signal = -0.2;
|
|
1192
|
-
}
|
|
1193
|
-
if (unlock.type === "team" || unlock.type === "investor") {
|
|
1194
|
-
signal = clamp(signal * 1.5);
|
|
1195
|
-
details.push(
|
|
1196
|
-
`${unlock.percentOfSupply.toFixed(1)}% supply unlocking in ${Math.ceil(unlock.daysUntil)} days (type: ${unlock.type}) \u2014 elevated sell pressure`
|
|
1197
|
-
);
|
|
1198
|
-
} else {
|
|
1199
|
-
details.push(
|
|
1200
|
-
`${unlock.percentOfSupply.toFixed(1)}% supply unlocking in ${Math.ceil(unlock.daysUntil)} days (type: ${unlock.type})`
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
if (signal < value) value = signal;
|
|
1204
|
-
}
|
|
1205
|
-
return {
|
|
1206
|
-
name: this.name,
|
|
1207
|
-
value: clamp(value),
|
|
1208
|
-
confidence: Math.min(confidence, 1),
|
|
1209
|
-
source: "Token Unlock Frontrun",
|
|
1210
|
-
details: details.join("; ")
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
/** Analyze from DefiLlama FDV-based unlock estimates (free fallback) */
|
|
1214
|
-
analyzeFromUnlockData(data) {
|
|
1215
|
-
if (data.upcomingUnlocks.length === 0 || data.totalUpcomingPercent < 0.5) {
|
|
1216
|
-
return {
|
|
1217
|
-
name: this.name,
|
|
1218
|
-
value: 0,
|
|
1219
|
-
confidence: 0.2,
|
|
1220
|
-
source: "Token Unlock Frontrun",
|
|
1221
|
-
details: "No significant upcoming unlocks detected"
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
let value = 0;
|
|
1225
|
-
const pct = data.totalUpcomingPercent;
|
|
1226
|
-
if (pct > 5) value = -0.6;
|
|
1227
|
-
else if (pct > 2) value = -0.4;
|
|
1228
|
-
else if (pct > 1) value = -0.2;
|
|
1229
|
-
const details = data.upcomingUnlocks.map((u) => u.description).join("; ");
|
|
1230
|
-
return {
|
|
1231
|
-
name: this.name,
|
|
1232
|
-
value: clamp(value),
|
|
1233
|
-
confidence: 0.4,
|
|
1234
|
-
// lower confidence since these are estimates
|
|
1235
|
-
source: "Token Unlock Frontrun",
|
|
1236
|
-
details: details || `~${pct.toFixed(1)}% estimated weekly vesting`
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
};
|
|
1240
|
-
|
|
1241
1123
|
// src/agent/strategies/sentiment-contrarian.ts
|
|
1242
1124
|
var SentimentContrarianStrategy = class {
|
|
1243
1125
|
name = "sentimentContrarian";
|
|
@@ -1261,22 +1143,15 @@ var SentimentContrarianStrategy = class {
|
|
|
1261
1143
|
value = 1 - fg / 20 * 0.4;
|
|
1262
1144
|
confidence = 0.9;
|
|
1263
1145
|
details.push(`Extreme fear (F&G=${fg}): strong contrarian buy signal`);
|
|
1264
|
-
} else if (fg
|
|
1265
|
-
value = 0.5 - (fg - 20) / 15 * 0.3;
|
|
1266
|
-
confidence = 0.7;
|
|
1267
|
-
details.push(`Fear (F&G=${fg}): contrarian buy signal`);
|
|
1268
|
-
} else if (fg <= 65) {
|
|
1269
|
-
value = 0;
|
|
1270
|
-
confidence = 0.3;
|
|
1271
|
-
details.push(`Neutral sentiment (F&G=${fg}): no contrarian edge`);
|
|
1272
|
-
} else if (fg <= 80) {
|
|
1273
|
-
value = -(0.2 + (fg - 65) / 15 * 0.3);
|
|
1274
|
-
confidence = 0.7;
|
|
1275
|
-
details.push(`Greed (F&G=${fg}): contrarian sell signal`);
|
|
1276
|
-
} else {
|
|
1146
|
+
} else if (fg > 80) {
|
|
1277
1147
|
value = -(0.6 + (fg - 80) / 20 * 0.4);
|
|
1278
1148
|
confidence = 0.9;
|
|
1279
1149
|
details.push(`Extreme greed (F&G=${fg}): strong contrarian sell signal`);
|
|
1150
|
+
} else {
|
|
1151
|
+
value = 0;
|
|
1152
|
+
confidence = 0.1;
|
|
1153
|
+
const label = fg < 35 ? "mild fear" : fg > 65 ? "mild greed" : "neutral";
|
|
1154
|
+
details.push(`${label} (F&G=${fg}): below extremes, no edge`);
|
|
1280
1155
|
}
|
|
1281
1156
|
if (ctx.sentimentZScore !== void 0) {
|
|
1282
1157
|
const z = ctx.sentimentZScore;
|
|
@@ -1418,120 +1293,6 @@ var BreakoutOnChainStrategy = class {
|
|
|
1418
1293
|
}
|
|
1419
1294
|
};
|
|
1420
1295
|
|
|
1421
|
-
// src/agent/strategies/tvl-momentum.ts
|
|
1422
|
-
function calculateTvlGrowthWoW(tvlData) {
|
|
1423
|
-
let history;
|
|
1424
|
-
if (Array.isArray(tvlData)) {
|
|
1425
|
-
history = tvlData;
|
|
1426
|
-
} else if (tvlData?.tvl && Array.isArray(tvlData.tvl)) {
|
|
1427
|
-
history = tvlData.tvl;
|
|
1428
|
-
} else if (tvlData?.chainTvls) {
|
|
1429
|
-
const combined = Object.values(tvlData.chainTvls).find((chain) => chain?.tvl && Array.isArray(chain.tvl));
|
|
1430
|
-
if (combined) history = combined.tvl;
|
|
1431
|
-
}
|
|
1432
|
-
if (!history || history.length < 8) return null;
|
|
1433
|
-
const sortedHistory = [...history].sort((a, b) => (a.date ?? 0) - (b.date ?? 0));
|
|
1434
|
-
const current = sortedHistory[sortedHistory.length - 1];
|
|
1435
|
-
const weekAgo = sortedHistory[Math.max(0, sortedHistory.length - 8)];
|
|
1436
|
-
const currentTvl = current?.totalLiquidityUSD ?? current?.tvl ?? 0;
|
|
1437
|
-
const weekAgoTvl = weekAgo?.totalLiquidityUSD ?? weekAgo?.tvl ?? 0;
|
|
1438
|
-
if (weekAgoTvl === 0) return null;
|
|
1439
|
-
return (currentTvl - weekAgoTvl) / weekAgoTvl;
|
|
1440
|
-
}
|
|
1441
|
-
function getCurrentTvl(tvlData) {
|
|
1442
|
-
if (typeof tvlData === "number") return tvlData;
|
|
1443
|
-
if (Array.isArray(tvlData) && tvlData.length > 0) {
|
|
1444
|
-
const last = tvlData[tvlData.length - 1];
|
|
1445
|
-
return last?.totalLiquidityUSD ?? last?.tvl ?? null;
|
|
1446
|
-
}
|
|
1447
|
-
if (tvlData?.tvl) {
|
|
1448
|
-
if (typeof tvlData.tvl === "number") return tvlData.tvl;
|
|
1449
|
-
if (Array.isArray(tvlData.tvl) && tvlData.tvl.length > 0) {
|
|
1450
|
-
const last = tvlData.tvl[tvlData.tvl.length - 1];
|
|
1451
|
-
return last?.totalLiquidityUSD ?? last?.tvl ?? null;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
return null;
|
|
1455
|
-
}
|
|
1456
|
-
var TvlMomentumStrategy = class {
|
|
1457
|
-
name = "tvlMomentum";
|
|
1458
|
-
description = "Analyzes TVL growth trends \u2014 bullish on rising TVL, bearish on declining";
|
|
1459
|
-
requiredData = ["tvlData"];
|
|
1460
|
-
async analyze(ctx) {
|
|
1461
|
-
if (!ctx.tvlData) {
|
|
1462
|
-
return {
|
|
1463
|
-
name: this.name,
|
|
1464
|
-
value: 0,
|
|
1465
|
-
confidence: 0.1,
|
|
1466
|
-
source: "TVL Momentum",
|
|
1467
|
-
details: "No TVL data available (token may not be a DeFi protocol)"
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
const details = [];
|
|
1471
|
-
let value = 0;
|
|
1472
|
-
let confidence = 0.4;
|
|
1473
|
-
const growthWoW = calculateTvlGrowthWoW(ctx.tvlData);
|
|
1474
|
-
if (growthWoW === null) {
|
|
1475
|
-
return {
|
|
1476
|
-
name: this.name,
|
|
1477
|
-
value: 0,
|
|
1478
|
-
confidence: 0.15,
|
|
1479
|
-
source: "TVL Momentum",
|
|
1480
|
-
details: "Insufficient TVL history for momentum calculation"
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
const growthPct = growthWoW * 100;
|
|
1484
|
-
if (growthWoW > 0.2) {
|
|
1485
|
-
value = 0.6 + Math.min((growthWoW - 0.2) / 0.3, 0.2);
|
|
1486
|
-
confidence = 0.5;
|
|
1487
|
-
details.push(`TVL surging +${growthPct.toFixed(1)}% WoW: strong bullish momentum`);
|
|
1488
|
-
} else if (growthWoW > 0.1) {
|
|
1489
|
-
value = 0.3 + (growthWoW - 0.1) / 0.1 * 0.3;
|
|
1490
|
-
confidence = 0.45;
|
|
1491
|
-
details.push(`TVL growing +${growthPct.toFixed(1)}% WoW: bullish momentum`);
|
|
1492
|
-
} else if (growthWoW >= -0.02 && growthWoW <= 0.02) {
|
|
1493
|
-
value = 0;
|
|
1494
|
-
confidence = 0.3;
|
|
1495
|
-
details.push(`TVL flat (${growthPct >= 0 ? "+" : ""}${growthPct.toFixed(1)}% WoW): neutral`);
|
|
1496
|
-
} else if (growthWoW > 0.02) {
|
|
1497
|
-
value = 0.1 + (growthWoW - 0.02) / 0.08 * 0.2;
|
|
1498
|
-
details.push(`TVL mildly growing +${growthPct.toFixed(1)}% WoW`);
|
|
1499
|
-
} else if (growthWoW >= -0.05) {
|
|
1500
|
-
value = -0.1 - (Math.abs(growthWoW) - 0.02) / 0.03 * 0.2;
|
|
1501
|
-
details.push(`TVL mildly declining ${growthPct.toFixed(1)}% WoW`);
|
|
1502
|
-
} else if (growthWoW >= -0.15) {
|
|
1503
|
-
value = -(0.3 + (Math.abs(growthWoW) - 0.05) / 0.1 * 0.3);
|
|
1504
|
-
confidence = 0.45;
|
|
1505
|
-
details.push(`TVL declining ${growthPct.toFixed(1)}% WoW: bearish momentum`);
|
|
1506
|
-
} else {
|
|
1507
|
-
value = -(0.7 + Math.min((Math.abs(growthWoW) - 0.15) / 0.15, 0.2));
|
|
1508
|
-
confidence = 0.5;
|
|
1509
|
-
details.push(`TVL crashing ${growthPct.toFixed(1)}% WoW: strong bearish momentum`);
|
|
1510
|
-
}
|
|
1511
|
-
if (ctx.marketData) {
|
|
1512
|
-
const mcap = ctx.marketData.market_cap?.usd ?? ctx.marketData.usd_market_cap;
|
|
1513
|
-
const currentTvl = getCurrentTvl(ctx.tvlData);
|
|
1514
|
-
if (mcap && currentTvl && currentTvl > 0) {
|
|
1515
|
-
const ratio = mcap / currentTvl;
|
|
1516
|
-
if (ratio < 1) {
|
|
1517
|
-
value = clamp(value + 0.15);
|
|
1518
|
-
details.push(`Mcap/TVL ratio ${ratio.toFixed(2)} < 1.0 (potentially undervalued)`);
|
|
1519
|
-
} else if (ratio > 10) {
|
|
1520
|
-
value = clamp(value - 0.1);
|
|
1521
|
-
details.push(`Mcap/TVL ratio ${ratio.toFixed(1)} (potentially overvalued)`);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
return {
|
|
1526
|
-
name: this.name,
|
|
1527
|
-
value: clamp(value),
|
|
1528
|
-
confidence: Math.min(confidence, 1),
|
|
1529
|
-
source: "TVL Momentum",
|
|
1530
|
-
details: details.join("; ")
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
};
|
|
1534
|
-
|
|
1535
1296
|
// src/agent/strategies/funding-rate.ts
|
|
1536
1297
|
function annualizedYieldFromFundingRate(rate8h) {
|
|
1537
1298
|
return rate8h * 3 * 365;
|
|
@@ -1833,104 +1594,6 @@ var DexFlowStrategy = class {
|
|
|
1833
1594
|
}
|
|
1834
1595
|
};
|
|
1835
1596
|
|
|
1836
|
-
// src/agent/strategies/mean-reversion.ts
|
|
1837
|
-
var MeanReversionStrategy = class {
|
|
1838
|
-
name = "meanReversion";
|
|
1839
|
-
description = "Mean reversion strategy for range-bound markets using BB + RSI + volume";
|
|
1840
|
-
requiredData = ["candles", "technicals"];
|
|
1841
|
-
async analyze(ctx) {
|
|
1842
|
-
if (!ctx.candles || !ctx.technicals || ctx.candles.length < 21) {
|
|
1843
|
-
return {
|
|
1844
|
-
name: this.name,
|
|
1845
|
-
value: 0,
|
|
1846
|
-
confidence: 0.1,
|
|
1847
|
-
source: "Mean Reversion",
|
|
1848
|
-
details: "Insufficient data for mean reversion analysis"
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
const tech = ctx.technicals;
|
|
1852
|
-
const details = [];
|
|
1853
|
-
let value = 0;
|
|
1854
|
-
let confidence = 0.6;
|
|
1855
|
-
const currentPrice = ctx.candles[ctx.candles.length - 1].close;
|
|
1856
|
-
if (tech.bb.squeeze) {
|
|
1857
|
-
return {
|
|
1858
|
-
name: this.name,
|
|
1859
|
-
value: 0,
|
|
1860
|
-
confidence: 0.2,
|
|
1861
|
-
source: "Mean Reversion",
|
|
1862
|
-
details: "BB squeeze active \u2014 awaiting breakout, mean reversion inactive"
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
|
-
const isTrending = tech.bb.width > 0.15;
|
|
1866
|
-
if (isTrending) {
|
|
1867
|
-
confidence -= 0.2;
|
|
1868
|
-
details.push(`BB width ${tech.bb.width.toFixed(4)} (trending market, reduced confidence)`);
|
|
1869
|
-
} else {
|
|
1870
|
-
details.push(`BB width ${tech.bb.width.toFixed(4)} (range-bound, favorable)`);
|
|
1871
|
-
}
|
|
1872
|
-
const ema50 = tech.ema.ema50;
|
|
1873
|
-
const ema200 = tech.ema.ema200;
|
|
1874
|
-
const bullishTrend = !isNaN(ema50) && !isNaN(ema200) && ema50 > ema200;
|
|
1875
|
-
const bearishTrend = !isNaN(ema50) && !isNaN(ema200) && ema50 < ema200;
|
|
1876
|
-
const volumeSpike = tech.volume.ratio > 1.5;
|
|
1877
|
-
if (currentPrice < tech.bb.lower && tech.rsi < 30) {
|
|
1878
|
-
value = 0.6;
|
|
1879
|
-
details.push(`Price $${currentPrice.toFixed(2)} below lower BB $${tech.bb.lower.toFixed(2)}`);
|
|
1880
|
-
details.push(`RSI ${tech.rsi.toFixed(1)} oversold`);
|
|
1881
|
-
if (volumeSpike) {
|
|
1882
|
-
value += 0.1;
|
|
1883
|
-
confidence += 0.1;
|
|
1884
|
-
details.push(`Volume spike ${tech.volume.ratio.toFixed(1)}x (capitulation confirmed)`);
|
|
1885
|
-
}
|
|
1886
|
-
if (bearishTrend) {
|
|
1887
|
-
confidence -= 0.15;
|
|
1888
|
-
details.push("Bearish EMA trend \u2014 reduced confidence on buy");
|
|
1889
|
-
} else if (bullishTrend) {
|
|
1890
|
-
confidence += 0.1;
|
|
1891
|
-
details.push("Bullish EMA trend supports mean reversion buy");
|
|
1892
|
-
}
|
|
1893
|
-
} else if (currentPrice > tech.bb.upper && tech.rsi > 70) {
|
|
1894
|
-
value = -0.6;
|
|
1895
|
-
details.push(`Price $${currentPrice.toFixed(2)} above upper BB $${tech.bb.upper.toFixed(2)}`);
|
|
1896
|
-
details.push(`RSI ${tech.rsi.toFixed(1)} overbought`);
|
|
1897
|
-
if (volumeSpike) {
|
|
1898
|
-
value -= 0.1;
|
|
1899
|
-
confidence += 0.1;
|
|
1900
|
-
details.push(`Volume spike ${tech.volume.ratio.toFixed(1)}x (euphoria confirmed)`);
|
|
1901
|
-
}
|
|
1902
|
-
if (bullishTrend) {
|
|
1903
|
-
confidence -= 0.15;
|
|
1904
|
-
details.push("Bullish EMA trend \u2014 reduced confidence on sell");
|
|
1905
|
-
} else if (bearishTrend) {
|
|
1906
|
-
confidence += 0.1;
|
|
1907
|
-
details.push("Bearish EMA trend supports mean reversion sell");
|
|
1908
|
-
}
|
|
1909
|
-
} else if (currentPrice < tech.bb.lower) {
|
|
1910
|
-
value = 0.2;
|
|
1911
|
-
details.push(`Price below lower BB but RSI ${tech.rsi.toFixed(1)} not oversold yet`);
|
|
1912
|
-
} else if (currentPrice > tech.bb.upper) {
|
|
1913
|
-
value = -0.2;
|
|
1914
|
-
details.push(`Price above upper BB but RSI ${tech.rsi.toFixed(1)} not overbought yet`);
|
|
1915
|
-
} else if (tech.rsi < 30) {
|
|
1916
|
-
value = 0.15;
|
|
1917
|
-
details.push(`RSI ${tech.rsi.toFixed(1)} oversold but price within BB range`);
|
|
1918
|
-
} else if (tech.rsi > 70) {
|
|
1919
|
-
value = -0.15;
|
|
1920
|
-
details.push(`RSI ${tech.rsi.toFixed(1)} overbought but price within BB range`);
|
|
1921
|
-
} else {
|
|
1922
|
-
details.push("No mean reversion signal \u2014 price within normal range");
|
|
1923
|
-
}
|
|
1924
|
-
return {
|
|
1925
|
-
name: this.name,
|
|
1926
|
-
value: clamp(value),
|
|
1927
|
-
confidence: Math.min(Math.max(confidence, 0.1), 1),
|
|
1928
|
-
source: "Mean Reversion",
|
|
1929
|
-
details: details.join("; ")
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
};
|
|
1933
|
-
|
|
1934
1597
|
// src/agent/strategies/hyperliquid-flow.ts
|
|
1935
1598
|
var HyperliquidFlowStrategy = class {
|
|
1936
1599
|
name = "hyperliquidFlow";
|
|
@@ -2267,20 +1930,25 @@ var MultiTimeframeStrategy = class {
|
|
|
2267
1930
|
|
|
2268
1931
|
// src/agent/strategies/index.ts
|
|
2269
1932
|
var DEFAULT_STRATEGIES = [
|
|
2270
|
-
|
|
2271
|
-
new TokenUnlockStrategy(),
|
|
1933
|
+
// ── Active signals (fire rate >25% in production) ──
|
|
2272
1934
|
new SentimentContrarianStrategy(),
|
|
1935
|
+
// 100% fire rate — F&G based
|
|
2273
1936
|
new BreakoutOnChainStrategy(),
|
|
2274
|
-
|
|
1937
|
+
// 95% fire rate
|
|
2275
1938
|
new FundingRateStrategy(),
|
|
1939
|
+
// 52% fire rate — HL native
|
|
2276
1940
|
new DexFlowStrategy(),
|
|
2277
|
-
|
|
2278
|
-
// TwitterSentimentStrategy disabled — Twitter API returns 402 (paid tier
|
|
2279
|
-
// required) for most token queries. Re-enable when we have an API path
|
|
2280
|
-
// that doesn't break. The class is still exported so it can be reinstated
|
|
2281
|
-
// by appending `new TwitterSentimentStrategy()` here.
|
|
1941
|
+
// 18% fire rate
|
|
2282
1942
|
new HyperliquidFlowStrategy(),
|
|
1943
|
+
// 57% fire rate
|
|
2283
1944
|
new MultiTimeframeStrategy()
|
|
1945
|
+
// 78% fire rate
|
|
1946
|
+
// ── Disabled: zero or near-zero fire rate in production ──
|
|
1947
|
+
// SmartMoneyStrategy — replaced by Nansen HL perp-trades in index.ts
|
|
1948
|
+
// TwitterSentimentStrategy — Twitter API returns 402
|
|
1949
|
+
// TokenUnlockStrategy — 0% fire rate, no data source
|
|
1950
|
+
// TvlMomentumStrategy — 0% fire rate for majors (no TVL on BTC/SOL)
|
|
1951
|
+
// MeanReversionStrategy — 0% fire rate, BB conditions never met
|
|
2284
1952
|
];
|
|
2285
1953
|
async function runStrategies(ctx, configs) {
|
|
2286
1954
|
const signals = [];
|
|
@@ -3224,14 +2892,14 @@ var MarketRegimeDetector = class _MarketRegimeDetector {
|
|
|
3224
2892
|
}
|
|
3225
2893
|
/**
|
|
3226
2894
|
* Detect current market regime using BTC candles.
|
|
3227
|
-
* Returns cached result if less than
|
|
2895
|
+
* Returns cached result if less than 5 minutes old.
|
|
3228
2896
|
*/
|
|
3229
2897
|
async detect(btcCandles) {
|
|
3230
2898
|
try {
|
|
3231
2899
|
const cached = await this.loadCache();
|
|
3232
2900
|
const now = Date.now();
|
|
3233
2901
|
const cacheAge = now - cached.timestamp;
|
|
3234
|
-
const CACHE_DURATION =
|
|
2902
|
+
const CACHE_DURATION = 5 * 60 * 1e3;
|
|
3235
2903
|
if (cacheAge < CACHE_DURATION) {
|
|
3236
2904
|
return cached.analysis;
|
|
3237
2905
|
}
|
|
@@ -3260,6 +2928,8 @@ var MarketRegimeDetector = class _MarketRegimeDetector {
|
|
|
3260
2928
|
}
|
|
3261
2929
|
const closes = candles.map((c) => c.close);
|
|
3262
2930
|
const currentPrice = closes[closes.length - 1];
|
|
2931
|
+
const momentumResult = this.checkMomentumOverride(candles, currentPrice);
|
|
2932
|
+
if (momentumResult) return momentumResult;
|
|
3263
2933
|
const ema50 = calculateEMA(closes, 50);
|
|
3264
2934
|
const ema200 = calculateEMA(closes, 200);
|
|
3265
2935
|
const currentEma50 = this.getLastValidValue(ema50);
|
|
@@ -3328,6 +2998,8 @@ var MarketRegimeDetector = class _MarketRegimeDetector {
|
|
|
3328
2998
|
}
|
|
3329
2999
|
const closes = candles.map((c) => c.close);
|
|
3330
3000
|
const currentPrice = closes[closes.length - 1];
|
|
3001
|
+
const momentumResult = this.checkMomentumOverride(candles, currentPrice);
|
|
3002
|
+
if (momentumResult) return momentumResult;
|
|
3331
3003
|
const ema50 = calculateEMA(closes, 50);
|
|
3332
3004
|
const ema200 = calculateEMA(closes, 200);
|
|
3333
3005
|
const currentEma50 = this.getLastValidValue(ema50);
|
|
@@ -3477,6 +3149,68 @@ var MarketRegimeDetector = class _MarketRegimeDetector {
|
|
|
3477
3149
|
return "neutral";
|
|
3478
3150
|
}
|
|
3479
3151
|
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Fast momentum check — catches intraday trends that EMA/ADX miss.
|
|
3154
|
+
* Returns a RegimeAnalysis if momentum override fires, or null to fall
|
|
3155
|
+
* through to the standard EMA/ADX classification.
|
|
3156
|
+
*
|
|
3157
|
+
* Threshold is volatility-adjusted: 1.5× the 14-candle ATR as a
|
|
3158
|
+
* percentage of current price, floored at 2% and capped at 8%.
|
|
3159
|
+
* - BTC (daily ATR ~2%): threshold ≈ 3% — same as before
|
|
3160
|
+
* - SOL (daily ATR ~5%): threshold ≈ 7.5% — avoids false triggers on normal alt vol
|
|
3161
|
+
* - FARTCOIN (daily ATR ~10%): threshold = 8% cap — only fires on genuinely unusual moves
|
|
3162
|
+
*
|
|
3163
|
+
* Requirements for trending-up: price above threshold AND in the upper
|
|
3164
|
+
* half of the range (prevents false positives on bounces within a larger
|
|
3165
|
+
* downtrend). Symmetric for trending-down.
|
|
3166
|
+
*/
|
|
3167
|
+
checkMomentumOverride(candles, currentPrice) {
|
|
3168
|
+
const MOMENTUM_LOOKBACK = 24;
|
|
3169
|
+
const MIN_THRESHOLD = 0.02;
|
|
3170
|
+
const MAX_THRESHOLD = 0.08;
|
|
3171
|
+
const ATR_MULTIPLIER = 1.5;
|
|
3172
|
+
const atrValues = calculateATR(candles, 14);
|
|
3173
|
+
const currentAtr = this.getLastValidValue(atrValues);
|
|
3174
|
+
const atrPct = currentPrice > 0 && !isNaN(currentAtr) ? currentAtr / currentPrice : 0.02;
|
|
3175
|
+
const threshold = Math.min(MAX_THRESHOLD, Math.max(MIN_THRESHOLD, atrPct * ATR_MULTIPLIER));
|
|
3176
|
+
const bb = calculateBollingerBands(candles, 20, 2);
|
|
3177
|
+
const currentBbWidth = this.getLastValidValue(bb.width);
|
|
3178
|
+
let volLevel = "normal";
|
|
3179
|
+
if (!isNaN(currentBbWidth)) {
|
|
3180
|
+
if (currentBbWidth < 0.04) volLevel = "low";
|
|
3181
|
+
else if (currentBbWidth > 0.2) volLevel = "extreme";
|
|
3182
|
+
else if (currentBbWidth > 0.1) volLevel = "high";
|
|
3183
|
+
}
|
|
3184
|
+
const recentCandles = candles.slice(-MOMENTUM_LOOKBACK);
|
|
3185
|
+
const recentLow = Math.min(...recentCandles.map((c) => c.low));
|
|
3186
|
+
const recentHigh = Math.max(...recentCandles.map((c) => c.high));
|
|
3187
|
+
const pctAboveLow = recentLow > 0 ? (currentPrice - recentLow) / recentLow : 0;
|
|
3188
|
+
const pctBelowHigh = recentHigh > 0 ? (recentHigh - currentPrice) / recentHigh : 0;
|
|
3189
|
+
const recentMid = (recentHigh + recentLow) / 2;
|
|
3190
|
+
const inUpperHalf = currentPrice >= recentMid;
|
|
3191
|
+
const inLowerHalf = currentPrice <= recentMid;
|
|
3192
|
+
if (pctAboveLow >= threshold && inUpperHalf) {
|
|
3193
|
+
return {
|
|
3194
|
+
regime: "trending-up",
|
|
3195
|
+
confidence: Math.min(0.85, 0.5 + pctAboveLow * 5),
|
|
3196
|
+
btcTrend: "up",
|
|
3197
|
+
volatilityLevel: volLevel,
|
|
3198
|
+
details: `Momentum override: price +${(pctAboveLow * 100).toFixed(1)}% above ${MOMENTUM_LOOKBACK}-candle low (threshold ${(threshold * 100).toFixed(1)}%, ATR-adjusted)`,
|
|
3199
|
+
strategyAdjustments: this.getStrategyAdjustments("trending-up")
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
if (pctBelowHigh >= threshold && inLowerHalf) {
|
|
3203
|
+
return {
|
|
3204
|
+
regime: "trending-down",
|
|
3205
|
+
confidence: Math.min(0.85, 0.5 + pctBelowHigh * 5),
|
|
3206
|
+
btcTrend: "down",
|
|
3207
|
+
volatilityLevel: volLevel,
|
|
3208
|
+
details: `Momentum override: price -${(pctBelowHigh * 100).toFixed(1)}% below ${MOMENTUM_LOOKBACK}-candle high (threshold ${(threshold * 100).toFixed(1)}%, ATR-adjusted)`,
|
|
3209
|
+
strategyAdjustments: this.getStrategyAdjustments("trending-down")
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
return null;
|
|
3213
|
+
}
|
|
3480
3214
|
getStrategyAdjustments(regime) {
|
|
3481
3215
|
const baseMultipliers = {
|
|
3482
3216
|
"trending-up": {
|
|
@@ -4171,7 +3905,7 @@ var TradingAgent = class {
|
|
|
4171
3905
|
this.alertSystem = new AlertSystem();
|
|
4172
3906
|
}
|
|
4173
3907
|
/** Analyze a single token — gather all data and score. */
|
|
4174
|
-
async analyzeToken(tokenId) {
|
|
3908
|
+
async analyzeToken(tokenId, opts) {
|
|
4175
3909
|
const signals = [];
|
|
4176
3910
|
let technicalSignals;
|
|
4177
3911
|
let fearAndGreedValue;
|
|
@@ -4225,6 +3959,36 @@ var TradingAgent = class {
|
|
|
4225
3959
|
candles = ohlcResult.value;
|
|
4226
3960
|
technicalSignals = getLatestSignals(candles);
|
|
4227
3961
|
signals.push(scoreTechnical(technicalSignals));
|
|
3962
|
+
if (candles.length >= 24) {
|
|
3963
|
+
const recent = candles.slice(-24);
|
|
3964
|
+
const currentClose = recent[recent.length - 1].close;
|
|
3965
|
+
const recentLow = Math.min(...recent.map((c) => c.low));
|
|
3966
|
+
const recentHigh = Math.max(...recent.map((c) => c.high));
|
|
3967
|
+
const pctFromLow = recentLow > 0 ? (currentClose - recentLow) / recentLow : 0;
|
|
3968
|
+
const pctFromHigh = recentHigh > 0 ? (recentHigh - currentClose) / recentHigh : 0;
|
|
3969
|
+
let momentumValue = 0;
|
|
3970
|
+
let momentumDetails = "";
|
|
3971
|
+
if (pctFromHigh < 0.01) {
|
|
3972
|
+
momentumValue = Math.min(0.6, pctFromLow * 5);
|
|
3973
|
+
momentumDetails = `Price +${(pctFromLow * 100).toFixed(1)}% from 24-candle low, near highs`;
|
|
3974
|
+
} else if (pctFromLow < 0.01) {
|
|
3975
|
+
momentumValue = -Math.min(0.6, pctFromHigh * 5);
|
|
3976
|
+
momentumDetails = `Price -${(pctFromHigh * 100).toFixed(1)}% from 24-candle high, near lows`;
|
|
3977
|
+
} else if (recentHigh > recentLow) {
|
|
3978
|
+
const rangePosition = (currentClose - recentLow) / (recentHigh - recentLow);
|
|
3979
|
+
momentumValue = (rangePosition - 0.5) * 0.4;
|
|
3980
|
+
momentumDetails = `Mid-range (${(rangePosition * 100).toFixed(0)}th percentile of 24-candle range)`;
|
|
3981
|
+
}
|
|
3982
|
+
if (Math.abs(momentumValue) > 0.02) {
|
|
3983
|
+
signals.push({
|
|
3984
|
+
name: "momentum",
|
|
3985
|
+
value: momentumValue,
|
|
3986
|
+
confidence: Math.min(0.7, 0.3 + Math.abs(momentumValue)),
|
|
3987
|
+
source: "Price Momentum",
|
|
3988
|
+
details: momentumDetails
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
4228
3992
|
} else if (ohlcResult.status === "rejected") {
|
|
4229
3993
|
console.error(chalk.dim(` Technical analysis failed for ${tokenId}: ${ohlcResult.reason}`));
|
|
4230
3994
|
}
|
|
@@ -4233,7 +3997,6 @@ var TradingAgent = class {
|
|
|
4233
3997
|
fearAndGreedValue = fgData[0].value;
|
|
4234
3998
|
const values = fgData.map((d) => d.value);
|
|
4235
3999
|
sentimentZScore = this.sentiment.computeSentimentZScore(values);
|
|
4236
|
-
signals.push(scoreSentiment(fearAndGreedValue, sentimentZScore));
|
|
4237
4000
|
} else if (fearGreedResult.status === "rejected") {
|
|
4238
4001
|
console.error(chalk.dim(` Sentiment data failed: ${fearGreedResult.reason}`));
|
|
4239
4002
|
}
|
|
@@ -4269,58 +4032,65 @@ var TradingAgent = class {
|
|
|
4269
4032
|
}
|
|
4270
4033
|
let nansenData = void 0;
|
|
4271
4034
|
let messariData = void 0;
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4035
|
+
const x402Enabled = this.config.useX402 && !opts?.skipX402;
|
|
4036
|
+
let x402Available = x402Enabled ? false : void 0;
|
|
4037
|
+
if (x402Enabled) {
|
|
4038
|
+
x402Available = await isX402WalletFunded();
|
|
4039
|
+
if (!x402Available) {
|
|
4040
|
+
console.error(chalk.yellow(` x402 wallet has insufficient USDC \u2014 skipping paid signals (smartMoney, event) for this cycle`));
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
if (x402Enabled && x402Available) {
|
|
4044
|
+
const hlSymbolMap = {
|
|
4045
|
+
bitcoin: "BTC",
|
|
4046
|
+
ethereum: "ETH",
|
|
4047
|
+
solana: "SOL",
|
|
4048
|
+
arbitrum: "ARB",
|
|
4049
|
+
aave: "AAVE",
|
|
4050
|
+
uniswap: "UNI",
|
|
4051
|
+
dogecoin: "DOGE",
|
|
4052
|
+
ripple: "XRP",
|
|
4053
|
+
hyperliquid: "HYPE",
|
|
4054
|
+
"worldcoin-wld": "WLD",
|
|
4055
|
+
bittensor: "TAO",
|
|
4056
|
+
zcash: "ZEC",
|
|
4057
|
+
fartcoin: "FARTCOIN",
|
|
4058
|
+
pepe: "PEPE",
|
|
4059
|
+
polkadot: "DOT"
|
|
4060
|
+
};
|
|
4061
|
+
const hlSymbol = hlSymbolMap[tokenId] ?? tokenId.toUpperCase();
|
|
4062
|
+
const nansenProvider = getResearchProvider("nansen");
|
|
4063
|
+
const [hlPerpResult] = await Promise.allSettled([
|
|
4064
|
+
nansenProvider.queryHyperliquidSmartMoney(hlSymbol)
|
|
4278
4065
|
]);
|
|
4279
|
-
if (
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
const profile = d.profile;
|
|
4303
|
-
const mcapToTvl = metrics ? Number(metrics.mcap_to_tvl ?? 0) || void 0 : void 0;
|
|
4304
|
-
const existingFundIdx = signals.findIndex((s) => s.name === "fundamental");
|
|
4305
|
-
if (existingFundIdx >= 0) signals.splice(existingFundIdx, 1);
|
|
4306
|
-
if (mcapToTvl || metrics) {
|
|
4307
|
-
signals.push(scoreFundamental({ mcapToTvl }));
|
|
4066
|
+
if (hlPerpResult.status === "fulfilled") {
|
|
4067
|
+
const hlResult = hlPerpResult.value;
|
|
4068
|
+
const trades = hlResult.data.trades;
|
|
4069
|
+
if (trades && trades.length > 0) {
|
|
4070
|
+
let longValueUsd = 0;
|
|
4071
|
+
let shortValueUsd = 0;
|
|
4072
|
+
for (const t of trades) {
|
|
4073
|
+
const val = Number(t.value_usd ?? 0);
|
|
4074
|
+
const side = String(t.side ?? "").toLowerCase();
|
|
4075
|
+
if (side === "long") longValueUsd += val;
|
|
4076
|
+
else if (side === "short") shortValueUsd += val;
|
|
4077
|
+
}
|
|
4078
|
+
const totalValue = longValueUsd + shortValueUsd;
|
|
4079
|
+
const longRatio = totalValue > 0 ? longValueUsd / totalValue : 0.5;
|
|
4080
|
+
const smBias = (longRatio - 0.5) * 2;
|
|
4081
|
+
signals.push({
|
|
4082
|
+
name: "smartMoney",
|
|
4083
|
+
value: smBias * 0.5,
|
|
4084
|
+
confidence: Math.min(0.7, 0.3 + trades.length / 30 * 0.4),
|
|
4085
|
+
source: "Nansen HL Smart Money",
|
|
4086
|
+
details: `${trades.length} trades: $${(longValueUsd / 1e6).toFixed(1)}M long / $${(shortValueUsd / 1e6).toFixed(1)}M short (${(longRatio * 100).toFixed(0)}% long)`
|
|
4087
|
+
});
|
|
4088
|
+
console.error(chalk.dim(` x402 Nansen HL perps: ${trades.length} trades, ${(longRatio * 100).toFixed(0)}% long bias, cost ${hlResult.costUsdc} USDC`));
|
|
4308
4089
|
} else {
|
|
4309
|
-
|
|
4090
|
+
console.error(chalk.dim(` x402 Nansen HL perps: no recent trades for ${hlSymbol}`));
|
|
4310
4091
|
}
|
|
4311
|
-
const hasPositiveCatalyst = profile ? Boolean(profile.is_verified || profile.tag_names) : false;
|
|
4312
|
-
signals.push(scoreEvent({ positiveEvent: hasPositiveCatalyst || void 0 }));
|
|
4313
|
-
console.error(chalk.dim(` x402 Messari token: cost ${tokenResult.costUsdc} USDC`));
|
|
4314
4092
|
} else {
|
|
4315
|
-
console.error(chalk.dim(` x402
|
|
4316
|
-
if (!signals.some((s) => s.name === "fundamental")) {
|
|
4317
|
-
signals.push(scoreFundamental({}));
|
|
4318
|
-
}
|
|
4319
|
-
signals.push(scoreEvent({}));
|
|
4320
|
-
}
|
|
4321
|
-
} else {
|
|
4322
|
-
if (!signals.some((s) => s.name === "fundamental")) {
|
|
4323
|
-
signals.push(scoreFundamental({}));
|
|
4093
|
+
console.error(chalk.dim(` x402 Nansen HL perps unavailable: ${hlPerpResult.reason}`));
|
|
4324
4094
|
}
|
|
4325
4095
|
signals.push(scoreEvent({}));
|
|
4326
4096
|
}
|
|
@@ -4442,7 +4212,8 @@ var TradingAgent = class {
|
|
|
4442
4212
|
resolvedWeights,
|
|
4443
4213
|
regimeAnalysis?.strategyAdjustments,
|
|
4444
4214
|
correlationCheck,
|
|
4445
|
-
regimeAnalysis?.regime
|
|
4215
|
+
regimeAnalysis?.regime,
|
|
4216
|
+
x402Available
|
|
4446
4217
|
);
|
|
4447
4218
|
const result = {
|
|
4448
4219
|
token: tokenId,
|
|
@@ -4459,14 +4230,43 @@ var TradingAgent = class {
|
|
|
4459
4230
|
updateTokens(tokens) {
|
|
4460
4231
|
this.config.tokens = tokens;
|
|
4461
4232
|
}
|
|
4462
|
-
/** Analyze all watchlist tokens.
|
|
4233
|
+
/** Analyze all watchlist tokens.
|
|
4234
|
+
*
|
|
4235
|
+
* When `x402TopN` is set and x402 is enabled, uses a two-pass approach:
|
|
4236
|
+
* 1. Score ALL tokens with free signals only (no x402 cost)
|
|
4237
|
+
* 2. Re-score the top N tokens by free-signal score WITH x402 paid data
|
|
4238
|
+
*
|
|
4239
|
+
* This reduces x402 cost from ~$0.16×10 = $1.60/run to ~$0.16×3 = $0.48/run
|
|
4240
|
+
* while concentrating paid data on the tokens most likely to fire trades.
|
|
4241
|
+
*/
|
|
4463
4242
|
async analyzeAll() {
|
|
4464
|
-
const
|
|
4243
|
+
const topN = this.config.x402TopN;
|
|
4244
|
+
const shouldTwoPass = this.config.useX402 && topN && topN > 0 && topN < this.config.tokens.length;
|
|
4245
|
+
if (!shouldTwoPass) {
|
|
4246
|
+
const results = [];
|
|
4247
|
+
for (const token of this.config.tokens) {
|
|
4248
|
+
const result = await this.analyzeToken(token);
|
|
4249
|
+
results.push(result);
|
|
4250
|
+
}
|
|
4251
|
+
return results;
|
|
4252
|
+
}
|
|
4253
|
+
console.error(chalk.dim(` x402 top-${topN} mode: scoring all ${this.config.tokens.length} tokens with free signals first...`));
|
|
4254
|
+
const freeResults = [];
|
|
4465
4255
|
for (const token of this.config.tokens) {
|
|
4466
|
-
const result = await this.analyzeToken(token);
|
|
4467
|
-
|
|
4256
|
+
const result = await this.analyzeToken(token, { skipX402: true });
|
|
4257
|
+
freeResults.push(result);
|
|
4468
4258
|
}
|
|
4469
|
-
|
|
4259
|
+
const ranked = [...freeResults].sort(
|
|
4260
|
+
(a, b) => Math.abs(b.decision.score) - Math.abs(a.decision.score)
|
|
4261
|
+
);
|
|
4262
|
+
const topTokens = new Set(ranked.slice(0, topN).map((r) => r.token));
|
|
4263
|
+
console.error(chalk.dim(` x402 enriching top ${topN}: ${[...topTokens].join(", ")}`));
|
|
4264
|
+
const enrichedMap = /* @__PURE__ */ new Map();
|
|
4265
|
+
for (const token of topTokens) {
|
|
4266
|
+
const enriched = await this.analyzeToken(token);
|
|
4267
|
+
enrichedMap.set(token, enriched);
|
|
4268
|
+
}
|
|
4269
|
+
return freeResults.map((r) => enrichedMap.get(r.token) ?? r);
|
|
4470
4270
|
}
|
|
4471
4271
|
/** Analyze all tokens and generate alerts for state changes. */
|
|
4472
4272
|
async analyzeAllWithAlerts() {
|
|
@@ -4708,7 +4508,8 @@ var DynamicTokenSelector = class {
|
|
|
4708
4508
|
const config = {
|
|
4709
4509
|
minVolume24h: 5e6,
|
|
4710
4510
|
// $5M minimum
|
|
4711
|
-
topByVolume:
|
|
4511
|
+
topByVolume: 18,
|
|
4512
|
+
// bumped 10 → 18 for more shots on goal per cycle
|
|
4712
4513
|
fundingExtremes: 5,
|
|
4713
4514
|
minFundingRate: 3e-4,
|
|
4714
4515
|
// 0.03%
|
|
@@ -4807,7 +4608,7 @@ var DynamicTokenSelector = class {
|
|
|
4807
4608
|
}
|
|
4808
4609
|
}
|
|
4809
4610
|
}
|
|
4810
|
-
const tokens = Array.from(selectedTokens).slice(0,
|
|
4611
|
+
const tokens = Array.from(selectedTokens).slice(0, 25);
|
|
4811
4612
|
return {
|
|
4812
4613
|
tokens,
|
|
4813
4614
|
reason: Object.fromEntries(
|
|
@@ -4901,7 +4702,7 @@ import { homedir as homedir11 } from "os";
|
|
|
4901
4702
|
import chalk2 from "chalk";
|
|
4902
4703
|
import { createWalletClient, http, encodeFunctionData, encodeAbiParameters } from "viem";
|
|
4903
4704
|
import { privateKeyToAccount } from "viem/accounts";
|
|
4904
|
-
var TradeExecutor = class {
|
|
4705
|
+
var TradeExecutor = class _TradeExecutor {
|
|
4905
4706
|
config;
|
|
4906
4707
|
riskManager;
|
|
4907
4708
|
portfolio;
|
|
@@ -4910,62 +4711,118 @@ var TradeExecutor = class {
|
|
|
4910
4711
|
this.riskManager = riskManager;
|
|
4911
4712
|
this.portfolio = portfolio;
|
|
4912
4713
|
}
|
|
4714
|
+
/** CoinGecko token ID → Hyperliquid perp asset index.
|
|
4715
|
+
* Used by multi-asset execution to resolve which HL perp to trade
|
|
4716
|
+
* when a signal fires. One strategy clone can trade any of these. */
|
|
4717
|
+
static TOKEN_TO_ASSET_INDEX = {
|
|
4718
|
+
bitcoin: 0,
|
|
4719
|
+
ethereum: 1,
|
|
4720
|
+
solana: 2,
|
|
4721
|
+
arbitrum: 4,
|
|
4722
|
+
dogecoin: 6,
|
|
4723
|
+
chainlink: 8,
|
|
4724
|
+
aave: 10,
|
|
4725
|
+
uniswap: 11,
|
|
4726
|
+
ripple: 23,
|
|
4727
|
+
polkadot: 28,
|
|
4728
|
+
avalanche: 35,
|
|
4729
|
+
hyperliquid: 131,
|
|
4730
|
+
zcash: 144,
|
|
4731
|
+
bittensor: 148,
|
|
4732
|
+
"worldcoin-wld": 265,
|
|
4733
|
+
fartcoin: 343,
|
|
4734
|
+
"fetch-ai": 373,
|
|
4735
|
+
pepe: 166,
|
|
4736
|
+
pendle: 249,
|
|
4737
|
+
sui: 116,
|
|
4738
|
+
near: 71,
|
|
4739
|
+
aptos: 83
|
|
4740
|
+
};
|
|
4741
|
+
/** Resolve a CoinGecko token ID to its HL perp asset index. */
|
|
4742
|
+
static resolveAssetIndex(tokenId) {
|
|
4743
|
+
return _TradeExecutor.TOKEN_TO_ASSET_INDEX[tokenId];
|
|
4744
|
+
}
|
|
4913
4745
|
/** Execute a trade based on a decision */
|
|
4914
4746
|
async execute(decision, tokenId, currentPrice) {
|
|
4915
|
-
if (decision.action === "SELL" || decision.action === "STRONG_SELL") {
|
|
4747
|
+
if ((decision.action === "SELL" || decision.action === "STRONG_SELL") && this.config.mode !== "hyperliquid-perp") {
|
|
4916
4748
|
const sellState = await this.portfolio.load();
|
|
4917
4749
|
const existing = sellState.positions.find((p) => p.tokenId === tokenId);
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4750
|
+
const existingSide = existing?.side ?? "long";
|
|
4751
|
+
if (existing && existingSide === "long") {
|
|
4752
|
+
try {
|
|
4753
|
+
const reason = decision.action === "STRONG_SELL" ? "Strong sell signal" : "Sell signal";
|
|
4754
|
+
if (this.config.dryRun) {
|
|
4755
|
+
console.error(chalk2.cyan(`[DRY RUN] Paper trade: SELL ${existing.quantity.toFixed(6)} ${tokenId} @ $${currentPrice.toFixed(4)}`));
|
|
4756
|
+
}
|
|
4757
|
+
const closeResult = await this.portfolio.closePosition(tokenId, currentPrice, reason);
|
|
4758
|
+
return {
|
|
4759
|
+
success: true,
|
|
4760
|
+
position: { ...existing, currentPrice, pnlUsd: closeResult.pnl, pnlPercent: closeResult.pnlPercent },
|
|
4761
|
+
dryRun: this.config.dryRun
|
|
4762
|
+
};
|
|
4763
|
+
} catch (err) {
|
|
4764
|
+
return {
|
|
4765
|
+
success: false,
|
|
4766
|
+
error: `Failed to close position: ${err.message}`,
|
|
4767
|
+
dryRun: this.config.dryRun
|
|
4768
|
+
};
|
|
4929
4769
|
}
|
|
4930
|
-
const closeResult = await this.portfolio.closePosition(tokenId, currentPrice, reason);
|
|
4931
|
-
return {
|
|
4932
|
-
success: true,
|
|
4933
|
-
position: { ...existing, currentPrice, pnlUsd: closeResult.pnl, pnlPercent: closeResult.pnlPercent },
|
|
4934
|
-
dryRun: this.config.dryRun
|
|
4935
|
-
};
|
|
4936
|
-
} catch (err) {
|
|
4937
|
-
return {
|
|
4938
|
-
success: false,
|
|
4939
|
-
error: `Failed to close position: ${err.message}`,
|
|
4940
|
-
dryRun: this.config.dryRun
|
|
4941
|
-
};
|
|
4942
4770
|
}
|
|
4943
4771
|
}
|
|
4944
|
-
if (decision.action !== "BUY" && decision.action !== "STRONG_BUY") {
|
|
4772
|
+
if (decision.action !== "BUY" && decision.action !== "STRONG_BUY" && decision.action !== "SELL" && decision.action !== "STRONG_SELL") {
|
|
4945
4773
|
return {
|
|
4946
4774
|
success: false,
|
|
4947
4775
|
error: `Action ${decision.action} does not trigger execution`,
|
|
4948
4776
|
dryRun: this.config.dryRun
|
|
4949
4777
|
};
|
|
4950
4778
|
}
|
|
4779
|
+
if (this.config.mode === "hyperliquid-perp") {
|
|
4780
|
+
const resolvedIndex = _TradeExecutor.resolveAssetIndex(tokenId);
|
|
4781
|
+
if (resolvedIndex === void 0) {
|
|
4782
|
+
return {
|
|
4783
|
+
success: false,
|
|
4784
|
+
error: `Token ${tokenId} has no known Hyperliquid perp asset index \u2014 skipping execution`,
|
|
4785
|
+
dryRun: this.config.dryRun
|
|
4786
|
+
};
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4951
4789
|
const state = await this.portfolio.load();
|
|
4952
4790
|
this.riskManager.updatePortfolio(state);
|
|
4953
|
-
const
|
|
4954
|
-
const
|
|
4955
|
-
const
|
|
4791
|
+
const isShort = decision.action === "SELL" || decision.action === "STRONG_SELL";
|
|
4792
|
+
const direction = isShort ? "short" : "long";
|
|
4793
|
+
const STOP_LOSS_PCT2 = 0.03;
|
|
4794
|
+
const RR_RATIO = 2;
|
|
4795
|
+
const stopLossDistance = currentPrice * STOP_LOSS_PCT2;
|
|
4796
|
+
const stopLossPrice = isShort ? currentPrice + stopLossDistance : currentPrice - stopLossDistance;
|
|
4797
|
+
const takeProfitPrice = isShort ? currentPrice * (1 - STOP_LOSS_PCT2 * RR_RATIO) : currentPrice * (1 + STOP_LOSS_PCT2 * RR_RATIO);
|
|
4798
|
+
const existingSameSide = state.positions.find(
|
|
4799
|
+
(p) => p.tokenId === tokenId && (p.side ?? "long") === direction
|
|
4800
|
+
);
|
|
4801
|
+
const isPyramid = existingSameSide !== void 0;
|
|
4802
|
+
const sizeMultiplier = isPyramid ? 0.5 ** ((existingSameSide.addCount ?? 0) + 1) : 1;
|
|
4956
4803
|
const sizing = this.riskManager.calculatePositionSize(
|
|
4957
4804
|
currentPrice,
|
|
4958
4805
|
stopLossPrice,
|
|
4959
4806
|
state.totalValue
|
|
4960
4807
|
);
|
|
4961
|
-
|
|
4808
|
+
const pyramidQuantity = sizing.quantity * sizeMultiplier;
|
|
4809
|
+
const pyramidSizeUsd = sizing.sizeUsd * sizeMultiplier;
|
|
4810
|
+
if (pyramidQuantity <= 0 || pyramidSizeUsd <= 0) {
|
|
4962
4811
|
return {
|
|
4963
4812
|
success: false,
|
|
4964
4813
|
error: "Position sizing returned zero \u2014 check portfolio value and stop distance",
|
|
4965
4814
|
dryRun: this.config.dryRun
|
|
4966
4815
|
};
|
|
4967
4816
|
}
|
|
4968
|
-
const
|
|
4817
|
+
const MIN_PYRAMID_USD = 15;
|
|
4818
|
+
if (isPyramid && pyramidSizeUsd < MIN_PYRAMID_USD) {
|
|
4819
|
+
return {
|
|
4820
|
+
success: false,
|
|
4821
|
+
error: `Pyramid add #${(existingSameSide.addCount ?? 0) + 1} would be $${pyramidSizeUsd.toFixed(2)} \u2014 below $${MIN_PYRAMID_USD} HL minimum notional. Skipping.`,
|
|
4822
|
+
dryRun: this.config.dryRun
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
const check = this.riskManager.canOpenPosition(tokenId, pyramidSizeUsd, direction);
|
|
4969
4826
|
if (!check.allowed) {
|
|
4970
4827
|
return {
|
|
4971
4828
|
success: false,
|
|
@@ -4975,15 +4832,15 @@ var TradeExecutor = class {
|
|
|
4975
4832
|
}
|
|
4976
4833
|
const order = {
|
|
4977
4834
|
tokenId,
|
|
4978
|
-
side: "buy",
|
|
4979
|
-
amountUsd:
|
|
4835
|
+
side: isShort ? "sell" : "buy",
|
|
4836
|
+
amountUsd: pyramidSizeUsd,
|
|
4980
4837
|
maxSlippage: 0.015,
|
|
4981
4838
|
stopLoss: stopLossPrice,
|
|
4982
4839
|
takeProfit: takeProfitPrice
|
|
4983
4840
|
};
|
|
4984
4841
|
if (this.config.dryRun) {
|
|
4985
4842
|
try {
|
|
4986
|
-
const position = await this.executeDryRun(order, currentPrice);
|
|
4843
|
+
const position = await this.executeDryRun(order, currentPrice, isPyramid);
|
|
4987
4844
|
return { success: true, position, dryRun: true };
|
|
4988
4845
|
} catch (err) {
|
|
4989
4846
|
return {
|
|
@@ -4995,12 +4852,13 @@ var TradeExecutor = class {
|
|
|
4995
4852
|
} else {
|
|
4996
4853
|
try {
|
|
4997
4854
|
const result = await this.executeLive(order, currentPrice);
|
|
4998
|
-
const position = await this.portfolio.openPosition({
|
|
4855
|
+
const position = isPyramid ? await this.portfolio.addToPosition(tokenId, result.executedPrice, pyramidQuantity, direction) : await this.portfolio.openPosition({
|
|
4999
4856
|
tokenId,
|
|
5000
4857
|
symbol: tokenId.toUpperCase(),
|
|
4858
|
+
side: direction,
|
|
5001
4859
|
entryPrice: result.executedPrice,
|
|
5002
4860
|
currentPrice: result.executedPrice,
|
|
5003
|
-
quantity:
|
|
4861
|
+
quantity: pyramidQuantity,
|
|
5004
4862
|
entryTimestamp: Date.now(),
|
|
5005
4863
|
stopLoss: order.stopLoss,
|
|
5006
4864
|
takeProfit: order.takeProfit,
|
|
@@ -5040,13 +4898,19 @@ var TradeExecutor = class {
|
|
|
5040
4898
|
return results;
|
|
5041
4899
|
}
|
|
5042
4900
|
/** Dry-run execution — paper trade */
|
|
5043
|
-
async executeDryRun(order, currentPrice) {
|
|
4901
|
+
async executeDryRun(order, currentPrice, isPyramid = false) {
|
|
5044
4902
|
const quantity = order.amountUsd / currentPrice;
|
|
5045
|
-
|
|
4903
|
+
const direction = order.side === "sell" ? "short" : "long";
|
|
4904
|
+
const sideLabel = isPyramid ? direction === "short" ? "PYRAMID SHORT" : "PYRAMID BUY" : direction === "short" ? "SHORT" : "BUY";
|
|
4905
|
+
console.error(chalk2.cyan(`[DRY RUN] Paper trade: ${sideLabel} ${quantity.toFixed(6)} ${order.tokenId} @ $${currentPrice.toFixed(4)}`));
|
|
5046
4906
|
console.error(chalk2.cyan(` Size: $${order.amountUsd.toFixed(2)} | SL: $${order.stopLoss.toFixed(4)} | TP: $${order.takeProfit.toFixed(4)}`));
|
|
4907
|
+
if (isPyramid) {
|
|
4908
|
+
return this.portfolio.addToPosition(order.tokenId, currentPrice, quantity, direction);
|
|
4909
|
+
}
|
|
5047
4910
|
const position = await this.portfolio.openPosition({
|
|
5048
4911
|
tokenId: order.tokenId,
|
|
5049
4912
|
symbol: order.tokenId.toUpperCase(),
|
|
4913
|
+
side: direction,
|
|
5050
4914
|
entryPrice: currentPrice,
|
|
5051
4915
|
currentPrice,
|
|
5052
4916
|
quantity,
|
|
@@ -5078,16 +4942,18 @@ var TradeExecutor = class {
|
|
|
5078
4942
|
chain,
|
|
5079
4943
|
transport: http()
|
|
5080
4944
|
});
|
|
5081
|
-
const
|
|
4945
|
+
const assetIndex = _TradeExecutor.resolveAssetIndex(order.tokenId) ?? this.config.assetIndex ?? 3;
|
|
4946
|
+
const isShort = order.side === "sell";
|
|
4947
|
+
const limitPx = isShort ? priceToUint64(currentPrice * 0.99) : priceToUint64(currentPrice * 1.01);
|
|
5082
4948
|
const quantity = order.amountUsd / currentPrice;
|
|
5083
|
-
const assetIndex = this.config.assetIndex ?? 3;
|
|
5084
4949
|
const szDec = await getSzDecimals(assetIndex);
|
|
5085
4950
|
const sz = sizeToUint64(quantity, szDec);
|
|
5086
4951
|
const stopLossPx = priceToUint64(order.stopLoss);
|
|
5087
4952
|
const stopLossSz = sz;
|
|
4953
|
+
const action = isShort ? 7 : 6;
|
|
5088
4954
|
const actionData = encodeAbiParameters(
|
|
5089
|
-
[{ type: "uint8" }, { type: "uint64" }, { type: "uint64" }, { type: "uint64" }, { type: "uint64" }],
|
|
5090
|
-
[
|
|
4955
|
+
[{ type: "uint8" }, { type: "uint32" }, { type: "uint64" }, { type: "uint64" }, { type: "uint64" }, { type: "uint64" }],
|
|
4956
|
+
[action, assetIndex, limitPx, sz, stopLossPx, stopLossSz]
|
|
5091
4957
|
);
|
|
5092
4958
|
const txData = encodeFunctionData({
|
|
5093
4959
|
abi: BASE_STRATEGY_ABI,
|
|
@@ -5213,6 +5079,75 @@ var PortfolioTracker = class {
|
|
|
5213
5079
|
this.historyPath = join10(base, "trades.json");
|
|
5214
5080
|
this.state = { ...DEFAULT_PORTFOLIO };
|
|
5215
5081
|
}
|
|
5082
|
+
/**
|
|
5083
|
+
* Initialize portfolio from on-chain vault balance instead of the
|
|
5084
|
+
* hardcoded $10k default. Reads totalAssets() from the vault that
|
|
5085
|
+
* the strategy clone is deployed to.
|
|
5086
|
+
*
|
|
5087
|
+
* Call once on startup (before the first cycle) when no persisted
|
|
5088
|
+
* portfolio.json exists. If a portfolio already exists on disk,
|
|
5089
|
+
* this is a no-op — we trust the persisted state.
|
|
5090
|
+
*
|
|
5091
|
+
* @param strategyClone - HyperliquidPerpStrategy clone address
|
|
5092
|
+
* @param chain - 'hyperevm' | 'hyperevm-testnet'
|
|
5093
|
+
*/
|
|
5094
|
+
async initFromOnChain(strategyClone, chain) {
|
|
5095
|
+
try {
|
|
5096
|
+
await readFile10(this.statePath, "utf-8");
|
|
5097
|
+
return;
|
|
5098
|
+
} catch {
|
|
5099
|
+
}
|
|
5100
|
+
try {
|
|
5101
|
+
const { createPublicClient, http: http2 } = await import("viem");
|
|
5102
|
+
const { hyperevm: hyperevm2, hyperevmTestnet: hyperevmTestnet2 } = await import("./network-KSMKPKNL.js");
|
|
5103
|
+
const { BASE_STRATEGY_ABI: BASE_STRATEGY_ABI2, SYNDICATE_VAULT_ABI } = await import("./abis-TXCLIR2K.js");
|
|
5104
|
+
const selectedChain = chain === "hyperevm-testnet" ? hyperevmTestnet2 : hyperevm2;
|
|
5105
|
+
const client = createPublicClient({
|
|
5106
|
+
chain: selectedChain,
|
|
5107
|
+
transport: http2()
|
|
5108
|
+
});
|
|
5109
|
+
const vaultAddr = await client.readContract({
|
|
5110
|
+
address: strategyClone,
|
|
5111
|
+
abi: BASE_STRATEGY_ABI2,
|
|
5112
|
+
functionName: "vault"
|
|
5113
|
+
});
|
|
5114
|
+
if (!vaultAddr || vaultAddr === "0x0000000000000000000000000000000000000000") {
|
|
5115
|
+
console.error("[portfolio] Strategy clone has no vault \u2014 using default portfolio");
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
5118
|
+
const totalAssets = await client.readContract({
|
|
5119
|
+
address: vaultAddr,
|
|
5120
|
+
abi: SYNDICATE_VAULT_ABI,
|
|
5121
|
+
functionName: "totalAssets"
|
|
5122
|
+
});
|
|
5123
|
+
const vaultValueUsd = Number(totalAssets) / 1e6;
|
|
5124
|
+
if (vaultValueUsd > 0) {
|
|
5125
|
+
this.state = {
|
|
5126
|
+
...DEFAULT_PORTFOLIO,
|
|
5127
|
+
totalValue: vaultValueUsd,
|
|
5128
|
+
cash: vaultValueUsd
|
|
5129
|
+
};
|
|
5130
|
+
await this.save(this.state);
|
|
5131
|
+
console.error(`[portfolio] Initialized from on-chain vault: $${vaultValueUsd.toFixed(2)} USDC`);
|
|
5132
|
+
} else {
|
|
5133
|
+
console.error("[portfolio] Vault has 0 assets \u2014 using default portfolio");
|
|
5134
|
+
}
|
|
5135
|
+
} catch (err) {
|
|
5136
|
+
console.error(`[portfolio] Failed to read on-chain balance: ${err.message} \u2014 using default`);
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
/**
|
|
5140
|
+
* Force-refresh portfolio value from on-chain vault balance.
|
|
5141
|
+
* Unlike initFromOnChain(), this runs even if portfolio.json exists.
|
|
5142
|
+
* Call via `sherwood agent config --sync-vault` or on explicit user request.
|
|
5143
|
+
*/
|
|
5144
|
+
async syncFromOnChain(strategyClone, chain) {
|
|
5145
|
+
try {
|
|
5146
|
+
await import("fs/promises").then((fs) => fs.unlink(this.statePath));
|
|
5147
|
+
} catch {
|
|
5148
|
+
}
|
|
5149
|
+
await this.initFromOnChain(strategyClone, chain);
|
|
5150
|
+
}
|
|
5216
5151
|
/** Load portfolio state from disk with validation */
|
|
5217
5152
|
async load() {
|
|
5218
5153
|
try {
|
|
@@ -5255,6 +5190,8 @@ var PortfolioTracker = class {
|
|
|
5255
5190
|
await this.load();
|
|
5256
5191
|
const fullPosition = {
|
|
5257
5192
|
...position,
|
|
5193
|
+
addCount: position.addCount ?? 0,
|
|
5194
|
+
lastAddTimestamp: position.lastAddTimestamp ?? position.entryTimestamp,
|
|
5258
5195
|
pnlPercent: 0,
|
|
5259
5196
|
pnlUsd: 0
|
|
5260
5197
|
};
|
|
@@ -5267,6 +5204,47 @@ var PortfolioTracker = class {
|
|
|
5267
5204
|
await this.save(this.state);
|
|
5268
5205
|
return fullPosition;
|
|
5269
5206
|
}
|
|
5207
|
+
/**
|
|
5208
|
+
* Pyramid into an existing position.
|
|
5209
|
+
* Computes a quantity-weighted average entry price and increments `addCount`.
|
|
5210
|
+
* Cash is debited at the new fill price. Stops/take-profits stay anchored to
|
|
5211
|
+
* the prior values (caller is free to widen them after).
|
|
5212
|
+
*
|
|
5213
|
+
* Throws if no existing position exists or the side disagrees.
|
|
5214
|
+
*/
|
|
5215
|
+
async addToPosition(tokenId, addPrice, addQuantity, side) {
|
|
5216
|
+
if (addQuantity <= 0) throw new Error(`Invalid add quantity: ${addQuantity}`);
|
|
5217
|
+
if (addPrice <= 0) throw new Error(`Invalid add price: ${addPrice}`);
|
|
5218
|
+
await this.load();
|
|
5219
|
+
const idx = this.state.positions.findIndex((p) => p.tokenId === tokenId);
|
|
5220
|
+
if (idx === -1) throw new Error(`No open position for ${tokenId} to pyramid into`);
|
|
5221
|
+
const pos = this.state.positions[idx];
|
|
5222
|
+
const existingSide = pos.side ?? "long";
|
|
5223
|
+
if (existingSide !== side) {
|
|
5224
|
+
throw new Error(`Cannot pyramid ${side} on existing ${existingSide} position in ${tokenId}`);
|
|
5225
|
+
}
|
|
5226
|
+
const newQty = pos.quantity + addQuantity;
|
|
5227
|
+
const weightedEntry = (pos.entryPrice * pos.quantity + addPrice * addQuantity) / newQty;
|
|
5228
|
+
const updated = {
|
|
5229
|
+
...pos,
|
|
5230
|
+
entryPrice: weightedEntry,
|
|
5231
|
+
quantity: newQty,
|
|
5232
|
+
currentPrice: addPrice,
|
|
5233
|
+
addCount: (pos.addCount ?? 0) + 1,
|
|
5234
|
+
lastAddTimestamp: Date.now(),
|
|
5235
|
+
pnlPercent: 0,
|
|
5236
|
+
// recomputed on next price update
|
|
5237
|
+
pnlUsd: 0
|
|
5238
|
+
};
|
|
5239
|
+
this.state.positions[idx] = updated;
|
|
5240
|
+
this.state.cash -= addPrice * addQuantity;
|
|
5241
|
+
this.state.totalValue = this.state.cash + this.state.positions.reduce(
|
|
5242
|
+
(sum, p) => sum + p.quantity * p.currentPrice,
|
|
5243
|
+
0
|
|
5244
|
+
);
|
|
5245
|
+
await this.save(this.state);
|
|
5246
|
+
return updated;
|
|
5247
|
+
}
|
|
5270
5248
|
/** Close a position and record trade */
|
|
5271
5249
|
async closePosition(tokenId, exitPrice, reason) {
|
|
5272
5250
|
await this.load();
|
|
@@ -5275,13 +5253,14 @@ var PortfolioTracker = class {
|
|
|
5275
5253
|
throw new Error(`No open position for ${tokenId}`);
|
|
5276
5254
|
}
|
|
5277
5255
|
const pos = this.state.positions[idx];
|
|
5278
|
-
const
|
|
5279
|
-
const
|
|
5256
|
+
const isShort = pos.side === "short";
|
|
5257
|
+
const pnlUsd = isShort ? (pos.entryPrice - exitPrice) * pos.quantity : (exitPrice - pos.entryPrice) * pos.quantity;
|
|
5258
|
+
const pnlPercent = isShort ? (pos.entryPrice - exitPrice) / (pos.entryPrice || 1) : (exitPrice - pos.entryPrice) / (pos.entryPrice || 1);
|
|
5280
5259
|
const duration = Math.floor((Date.now() - pos.entryTimestamp) / 1e3);
|
|
5281
5260
|
const record = {
|
|
5282
5261
|
tokenId: pos.tokenId,
|
|
5283
5262
|
symbol: pos.symbol,
|
|
5284
|
-
side: "long",
|
|
5263
|
+
side: pos.side ?? "long",
|
|
5285
5264
|
entryPrice: pos.entryPrice,
|
|
5286
5265
|
exitPrice,
|
|
5287
5266
|
quantity: pos.quantity,
|
|
@@ -5313,8 +5292,9 @@ var PortfolioTracker = class {
|
|
|
5313
5292
|
const price = prices[pos.tokenId];
|
|
5314
5293
|
if (price !== void 0) {
|
|
5315
5294
|
pos.currentPrice = price;
|
|
5316
|
-
|
|
5317
|
-
pos.
|
|
5295
|
+
const isShort = pos.side === "short";
|
|
5296
|
+
pos.pnlUsd = isShort ? (pos.entryPrice - price) * pos.quantity : (price - pos.entryPrice) * pos.quantity;
|
|
5297
|
+
pos.pnlPercent = isShort ? (pos.entryPrice - price) / (pos.entryPrice || 1) : (price - pos.entryPrice) / (pos.entryPrice || 1);
|
|
5318
5298
|
}
|
|
5319
5299
|
}
|
|
5320
5300
|
this.state.totalValue = this.state.cash + this.state.positions.reduce(
|
|
@@ -5465,7 +5445,8 @@ var DEFAULT_RISK_CONFIG = {
|
|
|
5465
5445
|
maxSinglePosition: 0.1,
|
|
5466
5446
|
maxCorrelatedExposure: 0.2,
|
|
5467
5447
|
maxConcurrentTrades: 8,
|
|
5468
|
-
hardStopPercent: 0.
|
|
5448
|
+
hardStopPercent: 0.05,
|
|
5449
|
+
// 5% hard stop — short-term trades shouldn't bleed past this
|
|
5469
5450
|
trailingStopAtr: 1.5,
|
|
5470
5451
|
trailingStopPct: 0,
|
|
5471
5452
|
// OFF — opt in via config
|
|
@@ -5479,6 +5460,8 @@ var DEFAULT_RISK_CONFIG = {
|
|
|
5479
5460
|
maxSlippage: { large: 5e-3, mid: 0.015, small: 0.03 },
|
|
5480
5461
|
riskPerTrade: 0.02
|
|
5481
5462
|
};
|
|
5463
|
+
var MAX_PYRAMID_ADDS = 2;
|
|
5464
|
+
var PYRAMID_MIN_SPACING_MS = 4 * 60 * 60 * 1e3;
|
|
5482
5465
|
var EMPTY_PORTFOLIO = {
|
|
5483
5466
|
totalValue: 0,
|
|
5484
5467
|
positions: [],
|
|
@@ -5494,8 +5477,11 @@ var RiskManager = class {
|
|
|
5494
5477
|
this.config = { ...DEFAULT_RISK_CONFIG, ...config };
|
|
5495
5478
|
this.portfolio = { ...EMPTY_PORTFOLIO };
|
|
5496
5479
|
}
|
|
5497
|
-
/** Check if we can open a new position
|
|
5498
|
-
|
|
5480
|
+
/** Check if we can open a new position (or pyramid into an existing one).
|
|
5481
|
+
* When `direction` is omitted, defaults to 'long' for backward compatibility.
|
|
5482
|
+
* An existing position with a different direction always rejects (no flips
|
|
5483
|
+
* via pyramid — flips must explicitly close the prior position first). */
|
|
5484
|
+
canOpenPosition(token, sizeUsd, direction = "long") {
|
|
5499
5485
|
if (this.portfolio.positions.length >= this.config.maxConcurrentTrades) {
|
|
5500
5486
|
return { allowed: false, reason: `Max concurrent trades (${this.config.maxConcurrentTrades}) reached` };
|
|
5501
5487
|
}
|
|
@@ -5528,7 +5514,20 @@ var RiskManager = class {
|
|
|
5528
5514
|
}
|
|
5529
5515
|
const existing = this.portfolio.positions.find((p) => p.tokenId === token);
|
|
5530
5516
|
if (existing) {
|
|
5531
|
-
|
|
5517
|
+
const existingSide = existing.side ?? "long";
|
|
5518
|
+
if (existingSide !== direction) {
|
|
5519
|
+
return { allowed: false, reason: `Conflicting position in ${token} (existing ${existingSide}, signal ${direction}) \u2014 close first` };
|
|
5520
|
+
}
|
|
5521
|
+
const addCount = existing.addCount ?? 0;
|
|
5522
|
+
if (addCount >= MAX_PYRAMID_ADDS) {
|
|
5523
|
+
return { allowed: false, reason: `Pyramid cap reached for ${token} (${MAX_PYRAMID_ADDS} adds)` };
|
|
5524
|
+
}
|
|
5525
|
+
const lastAdd = existing.lastAddTimestamp ?? existing.entryTimestamp;
|
|
5526
|
+
const elapsed = Date.now() - lastAdd;
|
|
5527
|
+
if (elapsed < PYRAMID_MIN_SPACING_MS) {
|
|
5528
|
+
const remainHrs = ((PYRAMID_MIN_SPACING_MS - elapsed) / 36e5).toFixed(1);
|
|
5529
|
+
return { allowed: false, reason: `Pyramid spacing not met for ${token} (next add in ${remainHrs}h)` };
|
|
5530
|
+
}
|
|
5532
5531
|
}
|
|
5533
5532
|
if (sizeUsd > this.portfolio.cash) {
|
|
5534
5533
|
return { allowed: false, reason: `Insufficient cash: need $${sizeUsd.toFixed(2)}, have $${this.portfolio.cash.toFixed(2)}` };
|
|
@@ -5584,21 +5583,25 @@ var RiskManager = class {
|
|
|
5584
5583
|
return { quantity: 0, sizeUsd: 0, riskUsd: 0 };
|
|
5585
5584
|
}
|
|
5586
5585
|
const riskPct = maxRiskPercent ?? this.config.riskPerTrade;
|
|
5587
|
-
|
|
5586
|
+
let riskUsd = portfolioValue * riskPct;
|
|
5588
5587
|
const riskPerUnit = Math.abs(entryPrice - stopLossPrice);
|
|
5589
5588
|
if (riskPerUnit <= 0) {
|
|
5590
5589
|
return { quantity: 0, sizeUsd: 0, riskUsd: 0 };
|
|
5591
5590
|
}
|
|
5592
|
-
|
|
5593
|
-
|
|
5591
|
+
let quantity = riskUsd / riskPerUnit;
|
|
5592
|
+
let sizeUsd = quantity * entryPrice;
|
|
5594
5593
|
const maxSizeUsd = portfolioValue * this.config.maxSinglePosition;
|
|
5595
5594
|
if (sizeUsd > maxSizeUsd) {
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5595
|
+
sizeUsd = maxSizeUsd;
|
|
5596
|
+
quantity = maxSizeUsd / entryPrice;
|
|
5597
|
+
riskUsd = quantity * riskPerUnit;
|
|
5598
|
+
}
|
|
5599
|
+
const MIN_POSITION_USD = 15;
|
|
5600
|
+
const effectiveFloor = Math.min(MIN_POSITION_USD, maxSizeUsd);
|
|
5601
|
+
if (sizeUsd < effectiveFloor && portfolioValue >= MIN_POSITION_USD * 2) {
|
|
5602
|
+
sizeUsd = effectiveFloor;
|
|
5603
|
+
quantity = sizeUsd / entryPrice;
|
|
5604
|
+
riskUsd = quantity * riskPerUnit;
|
|
5602
5605
|
}
|
|
5603
5606
|
return { quantity, sizeUsd, riskUsd };
|
|
5604
5607
|
}
|
|
@@ -5658,25 +5661,43 @@ var RiskManager = class {
|
|
|
5658
5661
|
updateTrailingStops(positions) {
|
|
5659
5662
|
return positions.map((pos) => {
|
|
5660
5663
|
if (pos.currentPrice <= 0 || pos.entryPrice <= 0) return pos;
|
|
5661
|
-
const
|
|
5664
|
+
const isShort = pos.side === "short";
|
|
5665
|
+
const pnlPct = isShort ? (pos.entryPrice - pos.currentPrice) / (pos.entryPrice || 1) : (pos.currentPrice - pos.entryPrice) / (pos.entryPrice || 1);
|
|
5662
5666
|
let newStop = pos.stopLoss;
|
|
5663
|
-
if (
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
+
if (isShort) {
|
|
5668
|
+
if (this.config.breakevenTriggerPct > 0 && pnlPct >= this.config.breakevenTriggerPct) {
|
|
5669
|
+
newStop = Math.min(newStop, pos.entryPrice);
|
|
5670
|
+
}
|
|
5671
|
+
for (const step of this.config.profitLockSteps) {
|
|
5672
|
+
if (pnlPct >= step.trigger) {
|
|
5673
|
+
const lockedStop = pos.entryPrice * (1 - step.lock);
|
|
5674
|
+
newStop = Math.min(newStop, lockedStop);
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
if (this.config.trailingStopPct > 0) {
|
|
5678
|
+
const trailStop = pos.currentPrice * (1 + this.config.trailingStopPct);
|
|
5679
|
+
newStop = Math.min(newStop, trailStop);
|
|
5680
|
+
}
|
|
5681
|
+
if (newStop < pos.stopLoss) {
|
|
5682
|
+
return { ...pos, stopLoss: newStop, trailingStop: newStop };
|
|
5683
|
+
}
|
|
5684
|
+
} else {
|
|
5685
|
+
if (this.config.breakevenTriggerPct > 0 && pnlPct >= this.config.breakevenTriggerPct) {
|
|
5686
|
+
newStop = Math.max(newStop, pos.entryPrice);
|
|
5687
|
+
}
|
|
5667
5688
|
for (const step of this.config.profitLockSteps) {
|
|
5668
5689
|
if (pnlPct >= step.trigger) {
|
|
5669
5690
|
const lockedStop = pos.entryPrice * (1 + step.lock);
|
|
5670
5691
|
newStop = Math.max(newStop, lockedStop);
|
|
5671
5692
|
}
|
|
5672
5693
|
}
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5694
|
+
if (this.config.trailingStopPct > 0) {
|
|
5695
|
+
const trailStop = pos.currentPrice * (1 - this.config.trailingStopPct);
|
|
5696
|
+
newStop = Math.max(newStop, trailStop);
|
|
5697
|
+
}
|
|
5698
|
+
if (newStop > pos.stopLoss) {
|
|
5699
|
+
return { ...pos, stopLoss: newStop, trailingStop: newStop };
|
|
5700
|
+
}
|
|
5680
5701
|
}
|
|
5681
5702
|
return pos;
|
|
5682
5703
|
});
|
|
@@ -5708,31 +5729,37 @@ var RiskManager = class {
|
|
|
5708
5729
|
const price = currentPrices[pos.tokenId];
|
|
5709
5730
|
if (price === void 0) continue;
|
|
5710
5731
|
const updatedPos = { ...pos, currentPrice: price };
|
|
5711
|
-
const
|
|
5732
|
+
const isShort = pos.side === "short";
|
|
5733
|
+
const pnlPercent = isShort ? (pos.entryPrice - price) / (pos.entryPrice || 1) : (price - pos.entryPrice) / (pos.entryPrice || 1);
|
|
5712
5734
|
if (pnlPercent <= -this.config.hardStopPercent) {
|
|
5713
5735
|
toClose.push(updatedPos);
|
|
5714
5736
|
reasons[pos.tokenId] = `Hard stop hit: ${(pnlPercent * 100).toFixed(1)}% loss (limit: -${(this.config.hardStopPercent * 100).toFixed(0)}%)`;
|
|
5715
5737
|
continue;
|
|
5716
5738
|
}
|
|
5717
|
-
|
|
5739
|
+
const stopHit = isShort ? price >= pos.stopLoss : price <= pos.stopLoss;
|
|
5740
|
+
if (stopHit) {
|
|
5718
5741
|
toClose.push(updatedPos);
|
|
5719
5742
|
reasons[pos.tokenId] = `Stop loss hit at $${pos.stopLoss.toFixed(4)} (price: $${price.toFixed(4)})`;
|
|
5720
5743
|
continue;
|
|
5721
5744
|
}
|
|
5722
|
-
if (pos.trailingStop !== void 0
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5745
|
+
if (pos.trailingStop !== void 0) {
|
|
5746
|
+
const trailHit = isShort ? price >= pos.trailingStop : price <= pos.trailingStop;
|
|
5747
|
+
if (trailHit) {
|
|
5748
|
+
toClose.push(updatedPos);
|
|
5749
|
+
reasons[pos.tokenId] = `Trailing stop hit at $${pos.trailingStop.toFixed(4)} (price: $${price.toFixed(4)})`;
|
|
5750
|
+
continue;
|
|
5751
|
+
}
|
|
5726
5752
|
}
|
|
5727
|
-
|
|
5753
|
+
const tpHit = isShort ? price <= pos.takeProfit : price >= pos.takeProfit;
|
|
5754
|
+
if (tpHit) {
|
|
5728
5755
|
toClose.push(updatedPos);
|
|
5729
5756
|
reasons[pos.tokenId] = `Take profit hit at $${pos.takeProfit.toFixed(4)} (price: $${price.toFixed(4)})`;
|
|
5730
5757
|
continue;
|
|
5731
5758
|
}
|
|
5732
|
-
const
|
|
5733
|
-
if (
|
|
5759
|
+
const holdingHours = (Date.now() - pos.entryTimestamp) / (1e3 * 60 * 60);
|
|
5760
|
+
if (holdingHours > 48 && pnlPercent < 0.01 && pnlPercent > -0.01) {
|
|
5734
5761
|
toClose.push(updatedPos);
|
|
5735
|
-
reasons[pos.tokenId] = `Time stop: held ${
|
|
5762
|
+
reasons[pos.tokenId] = `Time stop: held ${(holdingHours / 24).toFixed(1)} days with only ${(pnlPercent * 100).toFixed(1)}% PnL`;
|
|
5736
5763
|
continue;
|
|
5737
5764
|
}
|
|
5738
5765
|
}
|
|
@@ -5913,6 +5940,12 @@ var AgentLoop = class {
|
|
|
5913
5940
|
this.running = true;
|
|
5914
5941
|
const stateDir = join11(homedir11(), ".sherwood", "agent");
|
|
5915
5942
|
await mkdir12(stateDir, { recursive: true });
|
|
5943
|
+
if (this.config.execution.strategyClone && this.config.execution.chain) {
|
|
5944
|
+
await this.portfolio.initFromOnChain(
|
|
5945
|
+
this.config.execution.strategyClone,
|
|
5946
|
+
this.config.execution.chain
|
|
5947
|
+
);
|
|
5948
|
+
}
|
|
5916
5949
|
const cfg = this.config.agent;
|
|
5917
5950
|
console.log("");
|
|
5918
5951
|
console.log(chalk6.bold(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
@@ -6032,7 +6065,7 @@ var AgentLoop = class {
|
|
|
6032
6065
|
const signals = [];
|
|
6033
6066
|
for (const result of results) {
|
|
6034
6067
|
const { action, score } = result.decision;
|
|
6035
|
-
signals.push({ token: result.token, score, action });
|
|
6068
|
+
signals.push({ token: result.token, score, action, regime: result.regime?.regime });
|
|
6036
6069
|
if (action === "STRONG_BUY" || action === "BUY" || action === "SELL" || action === "STRONG_SELL") {
|
|
6037
6070
|
try {
|
|
6038
6071
|
const priceData = await this.coingecko.getPrice([result.token], ["usd"]);
|
|
@@ -6122,10 +6155,21 @@ function parseCycleInterval(cycle) {
|
|
|
6122
6155
|
|
|
6123
6156
|
// src/agent/backtest.ts
|
|
6124
6157
|
import chalk7 from "chalk";
|
|
6125
|
-
var
|
|
6158
|
+
var BACKTEST_STRATEGIES = [
|
|
6126
6159
|
"breakoutOnChain",
|
|
6127
|
-
|
|
6128
|
-
"multiTimeframe"
|
|
6160
|
+
// 95% fire rate — candle-based
|
|
6161
|
+
"multiTimeframe",
|
|
6162
|
+
// 78% fire rate — candle-based
|
|
6163
|
+
"sentimentContrarian",
|
|
6164
|
+
// 100% fire rate — uses fearAndGreed from context
|
|
6165
|
+
"momentum",
|
|
6166
|
+
// computed from candles directly in the backtest loop
|
|
6167
|
+
"fundingRate",
|
|
6168
|
+
// fires when ctx.fundingRateData is set (won't fire in backtest — ok)
|
|
6169
|
+
"dexFlow",
|
|
6170
|
+
// fires when ctx.dexData is set (won't fire in backtest — ok)
|
|
6171
|
+
"hyperliquidFlow"
|
|
6172
|
+
// fires when ctx.hyperliquidData is set (won't fire — ok)
|
|
6129
6173
|
];
|
|
6130
6174
|
var Backtester = class _Backtester {
|
|
6131
6175
|
config;
|
|
@@ -6206,12 +6250,84 @@ var Backtester = class _Backtester {
|
|
|
6206
6250
|
}
|
|
6207
6251
|
return { candles: allCandles, fearAndGreedData };
|
|
6208
6252
|
}
|
|
6253
|
+
/**
|
|
6254
|
+
* Pre-compute per-candle strategy signals and regime classification.
|
|
6255
|
+
* Called once per token by the calibrator; result is fed back into
|
|
6256
|
+
* `simulate(..., precomputed)` to skip the network-bound strategy stack.
|
|
6257
|
+
*
|
|
6258
|
+
* Filters signals to BACKTEST_STRATEGIES (the calibrator does not vary
|
|
6259
|
+
* `config.strategies`). Regime is always computed and returned —
|
|
6260
|
+
* `simulate()` will only consume it when `config.useRegime` is set.
|
|
6261
|
+
*/
|
|
6262
|
+
async precomputeSignals(allCandles, fearAndGreedData) {
|
|
6263
|
+
const stepSize = this.config.cycle === "1h" ? 1 : this.config.cycle === "4h" ? 4 : 24;
|
|
6264
|
+
const step = Math.max(1, Math.floor(stepSize / 24));
|
|
6265
|
+
const windowSize = 30;
|
|
6266
|
+
const out = new Array(allCandles.length).fill(null);
|
|
6267
|
+
for (let i = windowSize; i < allCandles.length; i += step) {
|
|
6268
|
+
const windowCandles = allCandles.slice(Math.max(0, i - 200), i);
|
|
6269
|
+
if (windowCandles.length < windowSize) continue;
|
|
6270
|
+
const currentCandle = allCandles[i];
|
|
6271
|
+
const currentDate = new Date(currentCandle.timestamp).toISOString().split("T")[0];
|
|
6272
|
+
const currentPrice = currentCandle.close;
|
|
6273
|
+
try {
|
|
6274
|
+
const technicals = getLatestSignals(windowCandles);
|
|
6275
|
+
let fearAndGreed;
|
|
6276
|
+
const fgValue = fearAndGreedData[currentDate];
|
|
6277
|
+
if (fgValue !== void 0) {
|
|
6278
|
+
const classification = fgValue < 25 ? "Extreme Fear" : fgValue < 40 ? "Fear" : fgValue < 60 ? "Neutral" : fgValue < 75 ? "Greed" : "Extreme Greed";
|
|
6279
|
+
fearAndGreed = { value: fgValue, classification };
|
|
6280
|
+
}
|
|
6281
|
+
const ctx = {
|
|
6282
|
+
tokenId: this.config.tokenId,
|
|
6283
|
+
candles: windowCandles,
|
|
6284
|
+
technicals,
|
|
6285
|
+
fearAndGreed
|
|
6286
|
+
};
|
|
6287
|
+
const rawSignals = await runStrategies(ctx);
|
|
6288
|
+
if (windowCandles.length >= 24) {
|
|
6289
|
+
const recent = windowCandles.slice(-24);
|
|
6290
|
+
const recentLow = Math.min(...recent.map((c) => c.low));
|
|
6291
|
+
const recentHigh = Math.max(...recent.map((c) => c.high));
|
|
6292
|
+
const pctFromLow = recentLow > 0 ? (currentPrice - recentLow) / recentLow : 0;
|
|
6293
|
+
const pctFromHigh = recentHigh > 0 ? (recentHigh - currentPrice) / recentHigh : 0;
|
|
6294
|
+
let momentumValue = 0;
|
|
6295
|
+
if (pctFromHigh < 0.01) momentumValue = Math.min(0.6, pctFromLow * 5);
|
|
6296
|
+
else if (pctFromLow < 0.01) momentumValue = -Math.min(0.6, pctFromHigh * 5);
|
|
6297
|
+
else if (recentHigh > recentLow) {
|
|
6298
|
+
const rangePosition = (currentPrice - recentLow) / (recentHigh - recentLow);
|
|
6299
|
+
momentumValue = (rangePosition - 0.5) * 0.4;
|
|
6300
|
+
}
|
|
6301
|
+
if (Math.abs(momentumValue) > 0.02) {
|
|
6302
|
+
rawSignals.push({
|
|
6303
|
+
name: "momentum",
|
|
6304
|
+
value: momentumValue,
|
|
6305
|
+
confidence: Math.min(0.7, 0.3 + Math.abs(momentumValue)),
|
|
6306
|
+
source: "Price Momentum",
|
|
6307
|
+
details: `Backtest momentum`
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
const filtered = rawSignals.filter((s) => BACKTEST_STRATEGIES.includes(s.name));
|
|
6312
|
+
const regime = MarketRegimeDetector.classifyFromCandles(windowCandles).regime;
|
|
6313
|
+
out[i] = { signals: filtered.length > 0 ? filtered : rawSignals, regime };
|
|
6314
|
+
} catch {
|
|
6315
|
+
out[i] = null;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
return out;
|
|
6319
|
+
}
|
|
6209
6320
|
/**
|
|
6210
6321
|
* Run the simulation loop on pre-fetched data. Pure of network IO.
|
|
6211
6322
|
* The calibrator uses this directly after fetchData() to replay many
|
|
6212
6323
|
* config permutations without re-fetching per permutation.
|
|
6324
|
+
*
|
|
6325
|
+
* When `precomputed` is supplied, the inner per-candle strategy stack
|
|
6326
|
+
* (network-bound) is skipped and the cached signals/regime are used
|
|
6327
|
+
* instead. Smoothing is also skipped on the precomputed path — the
|
|
6328
|
+
* caller is responsible for applying it before precomputing if needed.
|
|
6213
6329
|
*/
|
|
6214
|
-
async simulate(allCandles, fearAndGreedData) {
|
|
6330
|
+
async simulate(allCandles, fearAndGreedData, precomputed) {
|
|
6215
6331
|
const stepSize = this.config.cycle === "1h" ? 1 : this.config.cycle === "4h" ? 4 : 24;
|
|
6216
6332
|
const step = Math.max(1, Math.floor(stepSize / 24));
|
|
6217
6333
|
let capital = this.config.initialCapital;
|
|
@@ -6225,12 +6341,10 @@ var Backtester = class _Backtester {
|
|
|
6225
6341
|
if (this.config.strategies.length > 0) {
|
|
6226
6342
|
console.log(chalk7.dim(` Using user-specified strategies: ${this.config.strategies.join(", ")}`));
|
|
6227
6343
|
} else {
|
|
6228
|
-
|
|
6344
|
+
console.log(chalk7.dim(` Using backtest strategies: ${BACKTEST_STRATEGIES.join(", ")} + momentum`));
|
|
6229
6345
|
if (Object.keys(fearAndGreedData).length > 0) {
|
|
6230
|
-
|
|
6346
|
+
console.log(chalk7.dim(` Fear & Greed data available \u2014 sentimentContrarian active`));
|
|
6231
6347
|
}
|
|
6232
|
-
console.log(chalk7.dim(` Using candle-based strategies: ${availableStrategies.join(", ")}`));
|
|
6233
|
-
console.log(chalk7.dim(` Filtered out external data strategies to prevent zero-signal noise`));
|
|
6234
6348
|
}
|
|
6235
6349
|
console.log();
|
|
6236
6350
|
}
|
|
@@ -6240,41 +6354,74 @@ var Backtester = class _Backtester {
|
|
|
6240
6354
|
const currentDate = new Date(currentCandle.timestamp).toISOString().split("T")[0];
|
|
6241
6355
|
const currentPrice = currentCandle.close;
|
|
6242
6356
|
if (windowCandles.length < windowSize) continue;
|
|
6243
|
-
const equity = position ? capital * (currentPrice / position.entryPrice) : capital;
|
|
6357
|
+
const equity = position ? capital * (1 + (position.side === "short" ? (position.entryPrice - currentPrice) / position.entryPrice : (currentPrice - position.entryPrice) / position.entryPrice)) : capital;
|
|
6244
6358
|
equityCurve.push({ date: currentDate, value: equity });
|
|
6245
6359
|
let decision;
|
|
6246
6360
|
try {
|
|
6247
|
-
|
|
6248
|
-
let fearAndGreed;
|
|
6249
|
-
const fgValue = fearAndGreedData[currentDate];
|
|
6250
|
-
if (fgValue !== void 0) {
|
|
6251
|
-
const classification = fgValue < 25 ? "Extreme Fear" : fgValue < 40 ? "Fear" : fgValue < 60 ? "Neutral" : fgValue < 75 ? "Greed" : "Extreme Greed";
|
|
6252
|
-
fearAndGreed = { value: fgValue, classification };
|
|
6253
|
-
}
|
|
6254
|
-
const ctx = {
|
|
6255
|
-
tokenId: this.config.tokenId,
|
|
6256
|
-
candles: windowCandles,
|
|
6257
|
-
technicals,
|
|
6258
|
-
fearAndGreed
|
|
6259
|
-
};
|
|
6260
|
-
let signals = await runStrategies(ctx);
|
|
6261
|
-
if (smoother) {
|
|
6262
|
-
signals = await smoother.smooth(this.config.tokenId, signals, currentCandle.timestamp);
|
|
6263
|
-
}
|
|
6264
|
-
let filtered = signals;
|
|
6265
|
-
if (this.config.strategies.length > 0) {
|
|
6266
|
-
filtered = signals.filter((s) => this.config.strategies.some(
|
|
6267
|
-
(name) => s.name.toLowerCase().includes(name.toLowerCase())
|
|
6268
|
-
));
|
|
6269
|
-
} else {
|
|
6270
|
-
filtered = signals.filter((s) => CANDLE_BASED_STRATEGIES.includes(s.name) || fearAndGreed && s.name === "sentimentContrarian");
|
|
6271
|
-
}
|
|
6361
|
+
let filtered;
|
|
6272
6362
|
let regime;
|
|
6273
|
-
if (
|
|
6274
|
-
|
|
6363
|
+
if (precomputed) {
|
|
6364
|
+
const entry = precomputed[i];
|
|
6365
|
+
if (!entry) continue;
|
|
6366
|
+
filtered = entry.signals;
|
|
6367
|
+
regime = this.config.useRegime ? entry.regime : void 0;
|
|
6368
|
+
} else {
|
|
6369
|
+
const technicals = getLatestSignals(windowCandles);
|
|
6370
|
+
let fearAndGreed;
|
|
6371
|
+
const fgValue = fearAndGreedData[currentDate];
|
|
6372
|
+
if (fgValue !== void 0) {
|
|
6373
|
+
const classification = fgValue < 25 ? "Extreme Fear" : fgValue < 40 ? "Fear" : fgValue < 60 ? "Neutral" : fgValue < 75 ? "Greed" : "Extreme Greed";
|
|
6374
|
+
fearAndGreed = { value: fgValue, classification };
|
|
6375
|
+
}
|
|
6376
|
+
const ctx = {
|
|
6377
|
+
tokenId: this.config.tokenId,
|
|
6378
|
+
candles: windowCandles,
|
|
6379
|
+
technicals,
|
|
6380
|
+
fearAndGreed
|
|
6381
|
+
};
|
|
6382
|
+
let signals = await runStrategies(ctx);
|
|
6383
|
+
if (windowCandles.length >= 24) {
|
|
6384
|
+
const recent = windowCandles.slice(-24);
|
|
6385
|
+
const recentLow = Math.min(...recent.map((c) => c.low));
|
|
6386
|
+
const recentHigh = Math.max(...recent.map((c) => c.high));
|
|
6387
|
+
const pctFromLow = recentLow > 0 ? (currentPrice - recentLow) / recentLow : 0;
|
|
6388
|
+
const pctFromHigh = recentHigh > 0 ? (recentHigh - currentPrice) / recentHigh : 0;
|
|
6389
|
+
let momentumValue = 0;
|
|
6390
|
+
if (pctFromHigh < 0.01) {
|
|
6391
|
+
momentumValue = Math.min(0.6, pctFromLow * 5);
|
|
6392
|
+
} else if (pctFromLow < 0.01) {
|
|
6393
|
+
momentumValue = -Math.min(0.6, pctFromHigh * 5);
|
|
6394
|
+
} else if (recentHigh > recentLow) {
|
|
6395
|
+
const rangePosition = (currentPrice - recentLow) / (recentHigh - recentLow);
|
|
6396
|
+
momentumValue = (rangePosition - 0.5) * 0.4;
|
|
6397
|
+
}
|
|
6398
|
+
if (Math.abs(momentumValue) > 0.02) {
|
|
6399
|
+
signals.push({
|
|
6400
|
+
name: "momentum",
|
|
6401
|
+
value: momentumValue,
|
|
6402
|
+
confidence: Math.min(0.7, 0.3 + Math.abs(momentumValue)),
|
|
6403
|
+
source: "Price Momentum",
|
|
6404
|
+
details: `Backtest momentum`
|
|
6405
|
+
});
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
if (smoother) {
|
|
6409
|
+
signals = await smoother.smooth(this.config.tokenId, signals, currentCandle.timestamp);
|
|
6410
|
+
}
|
|
6411
|
+
if (this.config.strategies.length > 0) {
|
|
6412
|
+
filtered = signals.filter((s) => this.config.strategies.some(
|
|
6413
|
+
(name) => s.name.toLowerCase().includes(name.toLowerCase())
|
|
6414
|
+
));
|
|
6415
|
+
} else {
|
|
6416
|
+
filtered = signals.filter((s) => BACKTEST_STRATEGIES.includes(s.name));
|
|
6417
|
+
}
|
|
6418
|
+
if (filtered.length === 0) filtered = signals;
|
|
6419
|
+
if (this.config.useRegime) {
|
|
6420
|
+
regime = MarketRegimeDetector.classifyFromCandles(windowCandles).regime;
|
|
6421
|
+
}
|
|
6275
6422
|
}
|
|
6276
6423
|
decision = computeTradeDecision(
|
|
6277
|
-
filtered
|
|
6424
|
+
filtered,
|
|
6278
6425
|
this.config.customWeights,
|
|
6279
6426
|
void 0,
|
|
6280
6427
|
void 0,
|
|
@@ -6309,31 +6456,60 @@ var Backtester = class _Backtester {
|
|
|
6309
6456
|
} catch {
|
|
6310
6457
|
continue;
|
|
6311
6458
|
}
|
|
6459
|
+
const STOP_LOSS_PCT2 = 0.03;
|
|
6460
|
+
const TAKE_PROFIT_PCT2 = 0.06;
|
|
6461
|
+
const TIME_STOP_HOURS2 = 48;
|
|
6462
|
+
const TRAIL_PCT2 = this.config.trailingStopPct ?? 0.025;
|
|
6312
6463
|
if (!position && (decision.action === "BUY" || decision.action === "STRONG_BUY")) {
|
|
6313
6464
|
position = {
|
|
6465
|
+
side: "long",
|
|
6466
|
+
entryPrice: currentPrice,
|
|
6467
|
+
entryDate: currentDate,
|
|
6468
|
+
signal: decision.action,
|
|
6469
|
+
extremePrice: currentPrice,
|
|
6470
|
+
entryTimestamp: currentCandle.timestamp
|
|
6471
|
+
};
|
|
6472
|
+
} else if (!position && (decision.action === "SELL" || decision.action === "STRONG_SELL")) {
|
|
6473
|
+
position = {
|
|
6474
|
+
side: "short",
|
|
6314
6475
|
entryPrice: currentPrice,
|
|
6315
6476
|
entryDate: currentDate,
|
|
6316
6477
|
signal: decision.action,
|
|
6317
|
-
|
|
6478
|
+
extremePrice: currentPrice,
|
|
6479
|
+
// for shorts this becomes a low-water mark
|
|
6480
|
+
entryTimestamp: currentCandle.timestamp
|
|
6318
6481
|
};
|
|
6319
6482
|
} else if (position) {
|
|
6320
|
-
|
|
6321
|
-
|
|
6483
|
+
const isShort = position.side === "short";
|
|
6484
|
+
if (isShort) {
|
|
6485
|
+
if (currentPrice < position.extremePrice) position.extremePrice = currentPrice;
|
|
6486
|
+
} else {
|
|
6487
|
+
if (currentPrice > position.extremePrice) position.extremePrice = currentPrice;
|
|
6322
6488
|
}
|
|
6323
|
-
const
|
|
6324
|
-
const
|
|
6325
|
-
|
|
6326
|
-
|
|
6489
|
+
const pnlPercent = isShort ? (position.entryPrice - currentPrice) / position.entryPrice : (currentPrice - position.entryPrice) / position.entryPrice;
|
|
6490
|
+
const holdingHours = (currentCandle.timestamp - position.entryTimestamp) / (1e3 * 60 * 60);
|
|
6491
|
+
let exitReason = null;
|
|
6492
|
+
if (pnlPercent <= -STOP_LOSS_PCT2) {
|
|
6493
|
+
exitReason = `STOP_LOSS (${(pnlPercent * 100).toFixed(1)}%)`;
|
|
6494
|
+
} else if (pnlPercent >= TAKE_PROFIT_PCT2) {
|
|
6495
|
+
exitReason = `TAKE_PROFIT (+${(pnlPercent * 100).toFixed(1)}%)`;
|
|
6496
|
+
} else if (TRAIL_PCT2 > 0 && (isShort ? currentPrice >= position.extremePrice * (1 + TRAIL_PCT2) : currentPrice <= position.extremePrice * (1 - TRAIL_PCT2))) {
|
|
6497
|
+
exitReason = `TRAILING_STOP (extreme $${position.extremePrice.toFixed(2)}, ${isShort ? "+" : "-"}${(TRAIL_PCT2 * 100).toFixed(1)}%)`;
|
|
6498
|
+
} else if (holdingHours > TIME_STOP_HOURS2 && Math.abs(pnlPercent) < 0.01) {
|
|
6499
|
+
exitReason = `TIME_STOP (${(holdingHours / 24).toFixed(1)}d, ${(pnlPercent * 100).toFixed(1)}%)`;
|
|
6500
|
+
} else if (isShort ? decision.action === "BUY" || decision.action === "STRONG_BUY" : decision.action === "SELL" || decision.action === "STRONG_SELL") {
|
|
6501
|
+
exitReason = decision.action;
|
|
6502
|
+
}
|
|
6503
|
+
if (exitReason) {
|
|
6327
6504
|
capital *= 1 + pnlPercent;
|
|
6328
6505
|
returns.push(pnlPercent);
|
|
6329
|
-
const exitReason = trailingStopHit ? `TRAILING_STOP (-${(this.config.trailingStopPct * 100).toFixed(1)}%)` : decision.action;
|
|
6330
6506
|
trades.push({
|
|
6331
6507
|
entryDate: position.entryDate,
|
|
6332
6508
|
exitDate: currentDate,
|
|
6333
6509
|
entryPrice: position.entryPrice,
|
|
6334
6510
|
exitPrice: currentPrice,
|
|
6335
6511
|
pnlPercent,
|
|
6336
|
-
signal: `${position.signal} \u2192 ${exitReason}`
|
|
6512
|
+
signal: `${position.signal}${isShort ? " [SHORT]" : ""} \u2192 ${exitReason}`
|
|
6337
6513
|
});
|
|
6338
6514
|
position = null;
|
|
6339
6515
|
}
|
|
@@ -6343,7 +6519,7 @@ var Backtester = class _Backtester {
|
|
|
6343
6519
|
const lastCandle = allCandles[allCandles.length - 1];
|
|
6344
6520
|
const lastPrice = lastCandle.close;
|
|
6345
6521
|
const lastDate = new Date(lastCandle.timestamp).toISOString().split("T")[0];
|
|
6346
|
-
const pnlPercent = (lastPrice - position.entryPrice) / position.entryPrice;
|
|
6522
|
+
const pnlPercent = position.side === "short" ? (position.entryPrice - lastPrice) / position.entryPrice : (lastPrice - position.entryPrice) / position.entryPrice;
|
|
6347
6523
|
capital *= 1 + pnlPercent;
|
|
6348
6524
|
returns.push(pnlPercent);
|
|
6349
6525
|
trades.push({
|
|
@@ -6352,7 +6528,7 @@ var Backtester = class _Backtester {
|
|
|
6352
6528
|
entryPrice: position.entryPrice,
|
|
6353
6529
|
exitPrice: lastPrice,
|
|
6354
6530
|
pnlPercent,
|
|
6355
|
-
signal: `${position.signal} \u2192 CLOSE`
|
|
6531
|
+
signal: `${position.signal}${position.side === "short" ? " [SHORT]" : ""} \u2192 CLOSE`
|
|
6356
6532
|
});
|
|
6357
6533
|
}
|
|
6358
6534
|
const totalReturn = capital - this.config.initialCapital;
|
|
@@ -6692,6 +6868,508 @@ var Backtester = class _Backtester {
|
|
|
6692
6868
|
}
|
|
6693
6869
|
};
|
|
6694
6870
|
|
|
6871
|
+
// src/agent/calibrator.ts
|
|
6872
|
+
import { mkdir as mkdir13, writeFile as writeFile11 } from "fs/promises";
|
|
6873
|
+
import { join as join12 } from "path";
|
|
6874
|
+
import { homedir as homedir12 } from "os";
|
|
6875
|
+
var WEIGHT_PROFILES2 = {
|
|
6876
|
+
// ── Live production profiles (from scoring.ts; keep in sync) ──
|
|
6877
|
+
"default-live": { smartMoney: 0.15, technical: 0.1, sentiment: 0.4, onchain: 0.2, fundamental: 0.1, event: 0.05 },
|
|
6878
|
+
"majors-live": { smartMoney: 0.15, technical: 0.1, sentiment: 0.4, onchain: 0.35, fundamental: 0, event: 0 },
|
|
6879
|
+
"altcoin-live": { smartMoney: 0.15, technical: 0.15, sentiment: 0.3, onchain: 0.2, fundamental: 0.1, event: 0.1 },
|
|
6880
|
+
// ── Exploratory profiles (historical baselines + variants) ──
|
|
6881
|
+
default: { smartMoney: 0.25, technical: 0.2, sentiment: 0.2, onchain: 0.15, fundamental: 0.1, event: 0.1 },
|
|
6882
|
+
techHeavy: { smartMoney: 0.1, technical: 0.4, sentiment: 0.15, onchain: 0.15, fundamental: 0.1, event: 0.1 },
|
|
6883
|
+
sentHeavy: { smartMoney: 0.1, technical: 0.15, sentiment: 0.4, onchain: 0.15, fundamental: 0.1, event: 0.1 },
|
|
6884
|
+
onchainHvy: { smartMoney: 0.1, technical: 0.2, sentiment: 0.15, onchain: 0.35, fundamental: 0.1, event: 0.1 },
|
|
6885
|
+
balanced: { smartMoney: 0.17, technical: 0.17, sentiment: 0.17, onchain: 0.17, fundamental: 0.16, event: 0.16 },
|
|
6886
|
+
momentum: { smartMoney: 0.15, technical: 0.35, sentiment: 0.1, onchain: 0.25, fundamental: 0.1, event: 0.05 },
|
|
6887
|
+
contrarian: { smartMoney: 0.1, technical: 0.1, sentiment: 0.4, onchain: 0.1, fundamental: 0.15, event: 0.15 },
|
|
6888
|
+
flowBased: { smartMoney: 0.2, technical: 0.15, sentiment: 0.1, onchain: 0.35, fundamental: 0.1, event: 0.1 }
|
|
6889
|
+
};
|
|
6890
|
+
var BUY_THRESHOLDS = [0.2, 0.25, 0.3, 0.4, 0.5];
|
|
6891
|
+
var SELL_THRESHOLDS = [-0.2, -0.25, -0.3, -0.4, -0.5];
|
|
6892
|
+
var DEFAULT_CALIBRATION_TOKENS = [
|
|
6893
|
+
"bitcoin",
|
|
6894
|
+
"ethereum",
|
|
6895
|
+
"solana",
|
|
6896
|
+
"aave",
|
|
6897
|
+
"uniswap",
|
|
6898
|
+
"chainlink",
|
|
6899
|
+
"arbitrum"
|
|
6900
|
+
];
|
|
6901
|
+
function buildCalibrationConfigs() {
|
|
6902
|
+
const configs = [];
|
|
6903
|
+
for (const [profileName, weights] of Object.entries(WEIGHT_PROFILES2)) {
|
|
6904
|
+
for (const buyThreshold of BUY_THRESHOLDS) {
|
|
6905
|
+
for (const sellThreshold of SELL_THRESHOLDS) {
|
|
6906
|
+
configs.push({ profileName, weights, buyThreshold, sellThreshold });
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
return configs;
|
|
6911
|
+
}
|
|
6912
|
+
async function runCalibration(opts) {
|
|
6913
|
+
const { tokens, days, capital, onProgress } = opts;
|
|
6914
|
+
const configs = buildCalibrationConfigs();
|
|
6915
|
+
const log = onProgress ?? (() => {
|
|
6916
|
+
});
|
|
6917
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
6918
|
+
endDate.setDate(endDate.getDate() - 1);
|
|
6919
|
+
const startDate = new Date(endDate);
|
|
6920
|
+
startDate.setDate(startDate.getDate() - days);
|
|
6921
|
+
const startStr = startDate.toISOString().slice(0, 10);
|
|
6922
|
+
const endStr = endDate.toISOString().slice(0, 10);
|
|
6923
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
6924
|
+
for (const cfg of configs) {
|
|
6925
|
+
const key = configKey(cfg);
|
|
6926
|
+
resultMap.set(key, {
|
|
6927
|
+
config: cfg,
|
|
6928
|
+
tokenResults: [],
|
|
6929
|
+
avgReturn: 0,
|
|
6930
|
+
avgSharpe: 0,
|
|
6931
|
+
worstDrawdown: 0,
|
|
6932
|
+
totalTrades: 0
|
|
6933
|
+
});
|
|
6934
|
+
}
|
|
6935
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
6936
|
+
const tokenId = tokens[ti];
|
|
6937
|
+
log(`[${ti + 1}/${tokens.length}] Fetching data for ${tokenId}...`);
|
|
6938
|
+
const baseBt = new Backtester({
|
|
6939
|
+
tokenId,
|
|
6940
|
+
startDate: startStr,
|
|
6941
|
+
endDate: endStr,
|
|
6942
|
+
initialCapital: capital,
|
|
6943
|
+
strategies: [],
|
|
6944
|
+
cycle: "1d",
|
|
6945
|
+
verbose: false
|
|
6946
|
+
});
|
|
6947
|
+
let candles;
|
|
6948
|
+
let fearAndGreedData;
|
|
6949
|
+
try {
|
|
6950
|
+
const data = await baseBt.fetchData();
|
|
6951
|
+
candles = data.candles;
|
|
6952
|
+
fearAndGreedData = data.fearAndGreedData;
|
|
6953
|
+
log(` Got ${candles.length} candles + ${Object.keys(fearAndGreedData).length} F&G points`);
|
|
6954
|
+
} catch (err) {
|
|
6955
|
+
log(` SKIP ${tokenId}: ${err.message}`);
|
|
6956
|
+
for (const cfg of configs) {
|
|
6957
|
+
const key = configKey(cfg);
|
|
6958
|
+
resultMap.get(key).tokenResults.push({
|
|
6959
|
+
tokenId,
|
|
6960
|
+
totalReturn: 0,
|
|
6961
|
+
sharpeRatio: 0,
|
|
6962
|
+
maxDrawdown: 0,
|
|
6963
|
+
winRate: 0,
|
|
6964
|
+
numTrades: 0
|
|
6965
|
+
});
|
|
6966
|
+
}
|
|
6967
|
+
continue;
|
|
6968
|
+
}
|
|
6969
|
+
log(` Precomputing signals for ${candles.length} candles...`);
|
|
6970
|
+
let precomputed;
|
|
6971
|
+
try {
|
|
6972
|
+
precomputed = await baseBt.precomputeSignals(candles, fearAndGreedData);
|
|
6973
|
+
const valid = precomputed.filter((p) => p !== null).length;
|
|
6974
|
+
log(` Precomputed ${valid} candle signal sets`);
|
|
6975
|
+
} catch (err) {
|
|
6976
|
+
log(` SKIP ${tokenId}: precompute failed: ${err.message}`);
|
|
6977
|
+
for (const cfg of configs) {
|
|
6978
|
+
const key = configKey(cfg);
|
|
6979
|
+
resultMap.get(key).tokenResults.push({
|
|
6980
|
+
tokenId,
|
|
6981
|
+
totalReturn: 0,
|
|
6982
|
+
sharpeRatio: 0,
|
|
6983
|
+
maxDrawdown: 0,
|
|
6984
|
+
winRate: 0,
|
|
6985
|
+
numTrades: 0
|
|
6986
|
+
});
|
|
6987
|
+
}
|
|
6988
|
+
continue;
|
|
6989
|
+
}
|
|
6990
|
+
log(` Running ${configs.length} configs on ${tokenId}...`);
|
|
6991
|
+
for (let ci = 0; ci < configs.length; ci++) {
|
|
6992
|
+
const cfg = configs[ci];
|
|
6993
|
+
const key = configKey(cfg);
|
|
6994
|
+
try {
|
|
6995
|
+
const bt = new Backtester({
|
|
6996
|
+
tokenId,
|
|
6997
|
+
startDate: startStr,
|
|
6998
|
+
endDate: endStr,
|
|
6999
|
+
initialCapital: capital,
|
|
7000
|
+
strategies: [],
|
|
7001
|
+
cycle: "1d",
|
|
7002
|
+
verbose: false,
|
|
7003
|
+
customWeights: cfg.weights,
|
|
7004
|
+
buyThreshold: cfg.buyThreshold,
|
|
7005
|
+
sellThreshold: cfg.sellThreshold
|
|
7006
|
+
});
|
|
7007
|
+
const result = await bt.simulate(candles, fearAndGreedData, precomputed);
|
|
7008
|
+
resultMap.get(key).tokenResults.push({
|
|
7009
|
+
tokenId,
|
|
7010
|
+
totalReturn: result.totalReturnPercent,
|
|
7011
|
+
sharpeRatio: result.sharpeRatio,
|
|
7012
|
+
maxDrawdown: result.maxDrawdown,
|
|
7013
|
+
winRate: result.winRate,
|
|
7014
|
+
numTrades: result.totalTrades
|
|
7015
|
+
});
|
|
7016
|
+
} catch {
|
|
7017
|
+
resultMap.get(key).tokenResults.push({
|
|
7018
|
+
tokenId,
|
|
7019
|
+
totalReturn: 0,
|
|
7020
|
+
sharpeRatio: 0,
|
|
7021
|
+
maxDrawdown: 0,
|
|
7022
|
+
winRate: 0,
|
|
7023
|
+
numTrades: 0
|
|
7024
|
+
});
|
|
7025
|
+
}
|
|
7026
|
+
if ((ci + 1) % 50 === 0) {
|
|
7027
|
+
log(` ${tokenId}: ${ci + 1}/${configs.length} configs done`);
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
log(` ${tokenId} complete`);
|
|
7031
|
+
if (ti < tokens.length - 1) {
|
|
7032
|
+
log(` Pausing 5s for CoinGecko rate limits...`);
|
|
7033
|
+
await sleep(5e3);
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
const results = [];
|
|
7037
|
+
for (const entry of resultMap.values()) {
|
|
7038
|
+
const tr = entry.tokenResults;
|
|
7039
|
+
if (tr.length === 0) continue;
|
|
7040
|
+
entry.avgReturn = tr.reduce((s, r) => s + r.totalReturn, 0) / tr.length;
|
|
7041
|
+
entry.avgSharpe = tr.reduce((s, r) => s + r.sharpeRatio, 0) / tr.length;
|
|
7042
|
+
entry.worstDrawdown = Math.max(...tr.map((r) => r.maxDrawdown));
|
|
7043
|
+
entry.totalTrades = tr.reduce((s, r) => s + r.numTrades, 0);
|
|
7044
|
+
results.push(entry);
|
|
7045
|
+
}
|
|
7046
|
+
results.sort((a, b) => {
|
|
7047
|
+
const aZero = a.totalTrades === 0;
|
|
7048
|
+
const bZero = b.totalTrades === 0;
|
|
7049
|
+
if (aZero !== bZero) return aZero ? 1 : -1;
|
|
7050
|
+
if (Math.abs(b.avgSharpe - a.avgSharpe) > 1e-3) return b.avgSharpe - a.avgSharpe;
|
|
7051
|
+
return b.avgReturn - a.avgReturn;
|
|
7052
|
+
});
|
|
7053
|
+
await saveResults(results);
|
|
7054
|
+
return results;
|
|
7055
|
+
}
|
|
7056
|
+
function configKey(cfg) {
|
|
7057
|
+
return `${cfg.profileName}|buy=${cfg.buyThreshold}|sell=${cfg.sellThreshold}`;
|
|
7058
|
+
}
|
|
7059
|
+
function sleep(ms) {
|
|
7060
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7061
|
+
}
|
|
7062
|
+
async function saveResults(results) {
|
|
7063
|
+
const dir = join12(homedir12(), ".sherwood", "agent");
|
|
7064
|
+
await mkdir13(dir, { recursive: true });
|
|
7065
|
+
const path = join12(dir, "calibration-results.json");
|
|
7066
|
+
const payload = {
|
|
7067
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7068
|
+
totalConfigs: results.length,
|
|
7069
|
+
results: results.map((r, i) => ({
|
|
7070
|
+
rank: i + 1,
|
|
7071
|
+
profile: r.config.profileName,
|
|
7072
|
+
buyThreshold: r.config.buyThreshold,
|
|
7073
|
+
sellThreshold: r.config.sellThreshold,
|
|
7074
|
+
weights: r.config.weights,
|
|
7075
|
+
avgReturn: r.avgReturn,
|
|
7076
|
+
avgSharpe: r.avgSharpe,
|
|
7077
|
+
worstDrawdown: r.worstDrawdown,
|
|
7078
|
+
totalTrades: r.totalTrades,
|
|
7079
|
+
tokenResults: r.tokenResults
|
|
7080
|
+
}))
|
|
7081
|
+
};
|
|
7082
|
+
await writeFile11(path, JSON.stringify(payload, null, 2), "utf-8");
|
|
7083
|
+
}
|
|
7084
|
+
function formatCalibrationTable(results, topN = 20) {
|
|
7085
|
+
const lines = [];
|
|
7086
|
+
lines.push("");
|
|
7087
|
+
lines.push(" Calibration Results (ranked by Sharpe ratio)");
|
|
7088
|
+
lines.push(" " + "=".repeat(110));
|
|
7089
|
+
lines.push(
|
|
7090
|
+
" " + "Rank".padEnd(6) + "Profile".padEnd(13) + "Buy".padEnd(7) + "Sell".padEnd(7) + "Avg Return".padEnd(13) + "Avg Sharpe".padEnd(13) + "Worst DD".padEnd(11) + "Trades".padEnd(9) + "Tokens"
|
|
7091
|
+
);
|
|
7092
|
+
lines.push(" " + "-".repeat(110));
|
|
7093
|
+
const top = results.slice(0, topN);
|
|
7094
|
+
for (let i = 0; i < top.length; i++) {
|
|
7095
|
+
const r = top[i];
|
|
7096
|
+
const rank = String(i + 1).padEnd(6);
|
|
7097
|
+
const profile = r.config.profileName.padEnd(13);
|
|
7098
|
+
const buy = String(r.config.buyThreshold).padEnd(7);
|
|
7099
|
+
const sell = String(r.config.sellThreshold).padEnd(7);
|
|
7100
|
+
const avgRet = (r.avgReturn.toFixed(2) + "%").padEnd(13);
|
|
7101
|
+
const avgSharpe = r.avgSharpe.toFixed(3).padEnd(13);
|
|
7102
|
+
const worstDD = (r.worstDrawdown.toFixed(2) + "%").padEnd(11);
|
|
7103
|
+
const trades = String(r.totalTrades).padEnd(9);
|
|
7104
|
+
const tokenCount = String(r.tokenResults.length);
|
|
7105
|
+
lines.push(" " + rank + profile + buy + sell + avgRet + avgSharpe + worstDD + trades + tokenCount);
|
|
7106
|
+
}
|
|
7107
|
+
lines.push(" " + "-".repeat(110));
|
|
7108
|
+
if (results.length > 0) {
|
|
7109
|
+
const best = results[0];
|
|
7110
|
+
lines.push("");
|
|
7111
|
+
lines.push(" BEST CONFIGURATION:");
|
|
7112
|
+
lines.push(` Profile: ${best.config.profileName}`);
|
|
7113
|
+
lines.push(` Buy threshold: ${best.config.buyThreshold}`);
|
|
7114
|
+
lines.push(` Sell threshold: ${best.config.sellThreshold}`);
|
|
7115
|
+
lines.push(` Weights: smartMoney=${best.config.weights.smartMoney} technical=${best.config.weights.technical} sentiment=${best.config.weights.sentiment} onchain=${best.config.weights.onchain} fundamental=${best.config.weights.fundamental} event=${best.config.weights.event}`);
|
|
7116
|
+
lines.push(` Avg return: ${best.avgReturn.toFixed(2)}%`);
|
|
7117
|
+
lines.push(` Avg Sharpe: ${best.avgSharpe.toFixed(3)}`);
|
|
7118
|
+
lines.push(` Worst drawdown: ${best.worstDrawdown.toFixed(2)}%`);
|
|
7119
|
+
lines.push("");
|
|
7120
|
+
lines.push(" Per-token breakdown (best config):");
|
|
7121
|
+
for (const tr of best.tokenResults) {
|
|
7122
|
+
lines.push(` ${tr.tokenId.padEnd(12)} return: ${tr.totalReturn.toFixed(2).padStart(8)}% sharpe: ${tr.sharpeRatio.toFixed(3).padStart(7)} trades: ${tr.numTrades}`);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
lines.push("");
|
|
7126
|
+
const savePath = join12(homedir12(), ".sherwood", "agent", "calibration-results.json");
|
|
7127
|
+
lines.push(` Full results saved to ${savePath}`);
|
|
7128
|
+
lines.push("");
|
|
7129
|
+
return lines.join("\n");
|
|
7130
|
+
}
|
|
7131
|
+
|
|
7132
|
+
// src/agent/replay-calibrator.ts
|
|
7133
|
+
import { readFile as readFile11, mkdir as mkdir14, writeFile as writeFile12 } from "fs/promises";
|
|
7134
|
+
import { join as join13 } from "path";
|
|
7135
|
+
import { homedir as homedir13 } from "os";
|
|
7136
|
+
var STOP_LOSS_PCT = 0.03;
|
|
7137
|
+
var TAKE_PROFIT_PCT = 0.06;
|
|
7138
|
+
var TRAIL_PCT = 0.025;
|
|
7139
|
+
var TIME_STOP_HOURS = 48;
|
|
7140
|
+
async function loadSignalHistory(path) {
|
|
7141
|
+
const raw = await readFile11(path, "utf-8");
|
|
7142
|
+
const rows = [];
|
|
7143
|
+
for (const line of raw.split("\n")) {
|
|
7144
|
+
if (!line.trim()) continue;
|
|
7145
|
+
try {
|
|
7146
|
+
rows.push(JSON.parse(line));
|
|
7147
|
+
} catch {
|
|
7148
|
+
}
|
|
7149
|
+
}
|
|
7150
|
+
return rows;
|
|
7151
|
+
}
|
|
7152
|
+
function groupByToken(rows) {
|
|
7153
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
7154
|
+
for (const r of rows) {
|
|
7155
|
+
if (!grouped.has(r.tokenId)) grouped.set(r.tokenId, []);
|
|
7156
|
+
grouped.get(r.tokenId).push(r);
|
|
7157
|
+
}
|
|
7158
|
+
for (const list of grouped.values()) {
|
|
7159
|
+
list.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
7160
|
+
}
|
|
7161
|
+
return grouped;
|
|
7162
|
+
}
|
|
7163
|
+
function replayToken(tokenRows, config, useRegime) {
|
|
7164
|
+
let position = null;
|
|
7165
|
+
const returns = [];
|
|
7166
|
+
const trades = [];
|
|
7167
|
+
const equityCurve = [1];
|
|
7168
|
+
let equity = 1;
|
|
7169
|
+
for (let i = 0; i < tokenRows.length; i++) {
|
|
7170
|
+
const row = tokenRows[i];
|
|
7171
|
+
const ts = Date.parse(row.timestamp);
|
|
7172
|
+
const price = row.price;
|
|
7173
|
+
if (!Number.isFinite(price) || price <= 0) continue;
|
|
7174
|
+
const signals = row.signals.map((s) => ({
|
|
7175
|
+
name: s.name,
|
|
7176
|
+
value: s.value,
|
|
7177
|
+
confidence: s.confidence,
|
|
7178
|
+
source: "",
|
|
7179
|
+
details: ""
|
|
7180
|
+
}));
|
|
7181
|
+
const regime = useRegime ? row.regime && row.regime !== "unknown" ? row.regime : void 0 : void 0;
|
|
7182
|
+
let decision = computeTradeDecision(
|
|
7183
|
+
signals,
|
|
7184
|
+
config.weights,
|
|
7185
|
+
void 0,
|
|
7186
|
+
void 0,
|
|
7187
|
+
regime
|
|
7188
|
+
);
|
|
7189
|
+
const buy = config.buyThreshold;
|
|
7190
|
+
const sell = config.sellThreshold;
|
|
7191
|
+
if (buy <= sell) continue;
|
|
7192
|
+
const strongBuy = buy + 0.3;
|
|
7193
|
+
const strongSell = sell - 0.3;
|
|
7194
|
+
let action;
|
|
7195
|
+
if (decision.score >= strongBuy) action = "STRONG_BUY";
|
|
7196
|
+
else if (decision.score >= buy) action = "BUY";
|
|
7197
|
+
else if (decision.score <= strongSell) action = "STRONG_SELL";
|
|
7198
|
+
else if (decision.score <= sell) action = "SELL";
|
|
7199
|
+
else action = "HOLD";
|
|
7200
|
+
decision = { ...decision, action };
|
|
7201
|
+
if (position) {
|
|
7202
|
+
const isShort = position.side === "short";
|
|
7203
|
+
if (isShort) {
|
|
7204
|
+
if (price < position.extremePrice) position.extremePrice = price;
|
|
7205
|
+
} else {
|
|
7206
|
+
if (price > position.extremePrice) position.extremePrice = price;
|
|
7207
|
+
}
|
|
7208
|
+
const pnlPercent = isShort ? (position.entryPrice - price) / position.entryPrice : (price - position.entryPrice) / position.entryPrice;
|
|
7209
|
+
const holdingHours = (ts - position.entryTimestamp) / (1e3 * 60 * 60);
|
|
7210
|
+
let exitReason = null;
|
|
7211
|
+
if (pnlPercent <= -STOP_LOSS_PCT) exitReason = "STOP";
|
|
7212
|
+
else if (pnlPercent >= TAKE_PROFIT_PCT) exitReason = "TP";
|
|
7213
|
+
else if (TRAIL_PCT > 0 && (isShort ? price >= position.extremePrice * (1 + TRAIL_PCT) : price <= position.extremePrice * (1 - TRAIL_PCT))) {
|
|
7214
|
+
exitReason = "TRAIL";
|
|
7215
|
+
} else if (holdingHours > TIME_STOP_HOURS && Math.abs(pnlPercent) < 0.01) {
|
|
7216
|
+
exitReason = "TIME";
|
|
7217
|
+
} else if (isShort ? action === "BUY" || action === "STRONG_BUY" : action === "SELL" || action === "STRONG_SELL") {
|
|
7218
|
+
exitReason = "FLIP";
|
|
7219
|
+
}
|
|
7220
|
+
if (exitReason) {
|
|
7221
|
+
equity *= 1 + pnlPercent;
|
|
7222
|
+
equityCurve.push(equity);
|
|
7223
|
+
returns.push(pnlPercent);
|
|
7224
|
+
trades.push({ pnlPercent, durationMs: ts - position.entryTimestamp });
|
|
7225
|
+
position = null;
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
if (!position) {
|
|
7229
|
+
if (action === "BUY" || action === "STRONG_BUY") {
|
|
7230
|
+
position = { side: "long", entryPrice: price, entryTimestamp: ts, extremePrice: price, signal: action };
|
|
7231
|
+
} else if (action === "SELL" || action === "STRONG_SELL") {
|
|
7232
|
+
position = { side: "short", entryPrice: price, entryTimestamp: ts, extremePrice: price, signal: action };
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
if (position) {
|
|
7237
|
+
let lastValidPrice = null;
|
|
7238
|
+
for (let i = tokenRows.length - 1; i >= 0; i--) {
|
|
7239
|
+
const p = tokenRows[i].price;
|
|
7240
|
+
if (Number.isFinite(p) && p > 0) {
|
|
7241
|
+
lastValidPrice = p;
|
|
7242
|
+
break;
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
if (lastValidPrice !== null) {
|
|
7246
|
+
const pnlPercent = position.side === "short" ? (position.entryPrice - lastValidPrice) / position.entryPrice : (lastValidPrice - position.entryPrice) / position.entryPrice;
|
|
7247
|
+
const lastTs = Date.parse(tokenRows[tokenRows.length - 1].timestamp);
|
|
7248
|
+
equity *= 1 + pnlPercent;
|
|
7249
|
+
equityCurve.push(equity);
|
|
7250
|
+
returns.push(pnlPercent);
|
|
7251
|
+
trades.push({ pnlPercent, durationMs: lastTs - position.entryTimestamp });
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
const totalReturn = equity - 1;
|
|
7255
|
+
const winRate = returns.length > 0 ? returns.filter((r) => r > 0).length / returns.length : 0;
|
|
7256
|
+
let sharpeRatio = 0;
|
|
7257
|
+
if (returns.length > 1) {
|
|
7258
|
+
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
|
7259
|
+
const variance = returns.reduce((s, r) => s + (r - mean) ** 2, 0) / (returns.length - 1);
|
|
7260
|
+
const stdDev = Math.sqrt(variance);
|
|
7261
|
+
if (stdDev > 0) {
|
|
7262
|
+
const avgHoldDays = trades.length > 0 ? trades.reduce((s, t) => s + t.durationMs, 0) / trades.length / (1e3 * 60 * 60 * 24) : 1;
|
|
7263
|
+
const tradesPerYear = Math.max(252 / Math.max(avgHoldDays, 1), 1);
|
|
7264
|
+
sharpeRatio = mean / stdDev * Math.sqrt(tradesPerYear);
|
|
7265
|
+
}
|
|
7266
|
+
}
|
|
7267
|
+
let peak = equityCurve[0];
|
|
7268
|
+
let maxDrawdown = 0;
|
|
7269
|
+
for (const v of equityCurve) {
|
|
7270
|
+
if (v > peak) peak = v;
|
|
7271
|
+
const dd = (peak - v) / peak;
|
|
7272
|
+
if (dd > maxDrawdown) maxDrawdown = dd;
|
|
7273
|
+
}
|
|
7274
|
+
return {
|
|
7275
|
+
tokenId: tokenRows[0]?.tokenId ?? "",
|
|
7276
|
+
totalReturn,
|
|
7277
|
+
sharpeRatio,
|
|
7278
|
+
maxDrawdown,
|
|
7279
|
+
winRate,
|
|
7280
|
+
numTrades: returns.length
|
|
7281
|
+
};
|
|
7282
|
+
}
|
|
7283
|
+
async function runHistoryReplay(options = {}) {
|
|
7284
|
+
const historyPath = options.historyPath ?? join13(homedir13(), ".sherwood", "agent", "signal-history.jsonl");
|
|
7285
|
+
const log = options.onProgress ?? (() => {
|
|
7286
|
+
});
|
|
7287
|
+
const useRegime = options.useRegime ?? true;
|
|
7288
|
+
log(`Loading ${historyPath}...`);
|
|
7289
|
+
let allRows = await loadSignalHistory(historyPath);
|
|
7290
|
+
log(`Loaded ${allRows.length} rows`);
|
|
7291
|
+
if (options.lastDays !== void 0 && options.lastDays > 0) {
|
|
7292
|
+
const cutoff = Date.now() - options.lastDays * 24 * 60 * 60 * 1e3;
|
|
7293
|
+
const before = allRows.length;
|
|
7294
|
+
allRows = allRows.filter((r) => Date.parse(r.timestamp) >= cutoff);
|
|
7295
|
+
log(`Filtered to last ${options.lastDays}d: ${allRows.length} rows (dropped ${before - allRows.length})`);
|
|
7296
|
+
}
|
|
7297
|
+
const grouped = groupByToken(allRows);
|
|
7298
|
+
let tokens = options.tokens ?? Array.from(grouped.keys());
|
|
7299
|
+
const MIN_ROWS = 5;
|
|
7300
|
+
tokens = tokens.filter((t) => (grouped.get(t)?.length ?? 0) >= MIN_ROWS);
|
|
7301
|
+
log(`Replaying ${tokens.length} tokens (>=${MIN_ROWS} observations each)`);
|
|
7302
|
+
const configs = [];
|
|
7303
|
+
for (const [profileName, weights] of Object.entries(WEIGHT_PROFILES2)) {
|
|
7304
|
+
for (const buyThreshold of BUY_THRESHOLDS) {
|
|
7305
|
+
for (const sellThreshold of SELL_THRESHOLDS) {
|
|
7306
|
+
if (buyThreshold <= sellThreshold) continue;
|
|
7307
|
+
configs.push({ profileName, weights, buyThreshold, sellThreshold });
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
log(`Sweeping ${configs.length} configs across ${tokens.length} tokens`);
|
|
7312
|
+
const results = configs.map((config) => ({
|
|
7313
|
+
config,
|
|
7314
|
+
tokenResults: [],
|
|
7315
|
+
avgReturn: 0,
|
|
7316
|
+
avgSharpe: 0,
|
|
7317
|
+
worstDrawdown: 0,
|
|
7318
|
+
totalTrades: 0
|
|
7319
|
+
}));
|
|
7320
|
+
let processed = 0;
|
|
7321
|
+
for (const token of tokens) {
|
|
7322
|
+
const rows = grouped.get(token);
|
|
7323
|
+
for (let ci = 0; ci < configs.length; ci++) {
|
|
7324
|
+
const cfg = configs[ci];
|
|
7325
|
+
const tokenResult = replayToken(rows, cfg, useRegime);
|
|
7326
|
+
results[ci].tokenResults.push(tokenResult);
|
|
7327
|
+
}
|
|
7328
|
+
processed++;
|
|
7329
|
+
log(` ${token}: ${rows.length} rows, ${configs.length} configs done (${processed}/${tokens.length})`);
|
|
7330
|
+
}
|
|
7331
|
+
for (const r of results) {
|
|
7332
|
+
const tr = r.tokenResults;
|
|
7333
|
+
if (tr.length === 0) continue;
|
|
7334
|
+
r.avgReturn = tr.reduce((s, x) => s + x.totalReturn, 0) / tr.length;
|
|
7335
|
+
r.avgSharpe = tr.reduce((s, x) => s + x.sharpeRatio, 0) / tr.length;
|
|
7336
|
+
r.worstDrawdown = Math.max(...tr.map((x) => x.maxDrawdown));
|
|
7337
|
+
r.totalTrades = tr.reduce((s, x) => s + x.numTrades, 0);
|
|
7338
|
+
}
|
|
7339
|
+
results.sort((a, b) => {
|
|
7340
|
+
const aZero = a.totalTrades === 0;
|
|
7341
|
+
const bZero = b.totalTrades === 0;
|
|
7342
|
+
if (aZero !== bZero) return aZero ? 1 : -1;
|
|
7343
|
+
if (Math.abs(b.avgSharpe - a.avgSharpe) > 1e-3) return b.avgSharpe - a.avgSharpe;
|
|
7344
|
+
return b.avgReturn - a.avgReturn;
|
|
7345
|
+
});
|
|
7346
|
+
await saveResults2(results);
|
|
7347
|
+
return results;
|
|
7348
|
+
}
|
|
7349
|
+
async function saveResults2(results) {
|
|
7350
|
+
const dir = join13(homedir13(), ".sherwood", "agent");
|
|
7351
|
+
await mkdir14(dir, { recursive: true });
|
|
7352
|
+
const path = join13(dir, "replay-calibration-results.json");
|
|
7353
|
+
const payload = {
|
|
7354
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7355
|
+
totalConfigs: results.length,
|
|
7356
|
+
source: "signal-history.jsonl replay",
|
|
7357
|
+
results: results.map((r, i) => ({
|
|
7358
|
+
rank: i + 1,
|
|
7359
|
+
profile: r.config.profileName,
|
|
7360
|
+
buyThreshold: r.config.buyThreshold,
|
|
7361
|
+
sellThreshold: r.config.sellThreshold,
|
|
7362
|
+
weights: r.config.weights,
|
|
7363
|
+
avgReturn: r.avgReturn,
|
|
7364
|
+
avgSharpe: r.avgSharpe,
|
|
7365
|
+
worstDrawdown: r.worstDrawdown,
|
|
7366
|
+
totalTrades: r.totalTrades,
|
|
7367
|
+
tokenResults: r.tokenResults
|
|
7368
|
+
}))
|
|
7369
|
+
};
|
|
7370
|
+
await writeFile12(path, JSON.stringify(payload, null, 2), "utf-8");
|
|
7371
|
+
}
|
|
7372
|
+
|
|
6695
7373
|
// src/agent/alert-formatter.ts
|
|
6696
7374
|
var AlertFormatter = class {
|
|
6697
7375
|
/**
|
|
@@ -6981,7 +7659,7 @@ _Generated: ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}_`);
|
|
|
6981
7659
|
};
|
|
6982
7660
|
|
|
6983
7661
|
// src/agent/signal-audit.ts
|
|
6984
|
-
import { readFile as
|
|
7662
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6985
7663
|
var FIRE_EPSILON = 0.05;
|
|
6986
7664
|
var SIGNAL_TO_CATEGORY = {
|
|
6987
7665
|
// Direct categories
|
|
@@ -7006,7 +7684,7 @@ var SIGNAL_TO_CATEGORY = {
|
|
|
7006
7684
|
async function auditSignalHistory(filePath = getSignalFilePath()) {
|
|
7007
7685
|
let raw;
|
|
7008
7686
|
try {
|
|
7009
|
-
raw = await
|
|
7687
|
+
raw = await readFile12(filePath, "utf-8");
|
|
7010
7688
|
} catch (err) {
|
|
7011
7689
|
if (err.code === "ENOENT") {
|
|
7012
7690
|
return {
|
|
@@ -7196,7 +7874,28 @@ function suggestRenormalizedWeights(currentWeights, perCategory, dropThreshold =
|
|
|
7196
7874
|
}
|
|
7197
7875
|
|
|
7198
7876
|
// src/commands/agent.ts
|
|
7199
|
-
var DEFAULT_TOKENS = [
|
|
7877
|
+
var DEFAULT_TOKENS = [
|
|
7878
|
+
"bitcoin",
|
|
7879
|
+
"ethereum",
|
|
7880
|
+
"solana",
|
|
7881
|
+
// majors
|
|
7882
|
+
"ripple",
|
|
7883
|
+
"dogecoin",
|
|
7884
|
+
"avalanche-2",
|
|
7885
|
+
"polkadot",
|
|
7886
|
+
"near",
|
|
7887
|
+
// L1s
|
|
7888
|
+
"arbitrum",
|
|
7889
|
+
// L2
|
|
7890
|
+
"aave",
|
|
7891
|
+
"uniswap",
|
|
7892
|
+
"chainlink",
|
|
7893
|
+
// DeFi
|
|
7894
|
+
"hyperliquid",
|
|
7895
|
+
"bittensor",
|
|
7896
|
+
"fartcoin"
|
|
7897
|
+
// HL natives / themes
|
|
7898
|
+
];
|
|
7200
7899
|
function makeConfig(overrides) {
|
|
7201
7900
|
return {
|
|
7202
7901
|
tokens: DEFAULT_TOKENS,
|
|
@@ -7206,6 +7905,10 @@ function makeConfig(overrides) {
|
|
|
7206
7905
|
maxRiskPct: 20,
|
|
7207
7906
|
useX402: true,
|
|
7208
7907
|
// Paid signals (Nansen + Messari) on by default — opt out with --no-x402
|
|
7908
|
+
x402TopN: 3,
|
|
7909
|
+
// Only run x402 on top 3 tokens by free-signal score — saves ~$1.12/run
|
|
7910
|
+
smoothFastSignals: true,
|
|
7911
|
+
// Rolling-window smoothing for noisy signals — prevents single-scan flicker
|
|
7209
7912
|
...overrides
|
|
7210
7913
|
};
|
|
7211
7914
|
}
|
|
@@ -7381,8 +8084,8 @@ function registerAgentCommands(program) {
|
|
|
7381
8084
|
const cycle = options.cycle ?? "4h";
|
|
7382
8085
|
let savedRiskConfig = {};
|
|
7383
8086
|
try {
|
|
7384
|
-
const configPath =
|
|
7385
|
-
const data = await
|
|
8087
|
+
const configPath = join14(homedir14(), ".sherwood", "agent", "config.json");
|
|
8088
|
+
const data = await readFile13(configPath, "utf-8");
|
|
7386
8089
|
const parsed = JSON.parse(data);
|
|
7387
8090
|
for (const [k, v] of Object.entries(parsed)) {
|
|
7388
8091
|
if (k in DEFAULT_RISK_CONFIG && typeof v === "number" && Number.isFinite(v)) {
|
|
@@ -7533,17 +8236,17 @@ function registerAgentCommands(program) {
|
|
|
7533
8236
|
}
|
|
7534
8237
|
riskConfig[key] = numVal;
|
|
7535
8238
|
console.log(chalk8.green(` Set ${key} = ${numVal}`));
|
|
7536
|
-
const configDir =
|
|
7537
|
-
const configPath =
|
|
8239
|
+
const configDir = join14(homedir14(), ".sherwood", "agent");
|
|
8240
|
+
const configPath = join14(configDir, "config.json");
|
|
7538
8241
|
let existingConfig = {};
|
|
7539
8242
|
try {
|
|
7540
|
-
const data = await
|
|
8243
|
+
const data = await readFile13(configPath, "utf-8");
|
|
7541
8244
|
existingConfig = JSON.parse(data);
|
|
7542
8245
|
} catch {
|
|
7543
8246
|
}
|
|
7544
8247
|
existingConfig[key] = numVal;
|
|
7545
|
-
await
|
|
7546
|
-
await
|
|
8248
|
+
await mkdir15(configDir, { recursive: true });
|
|
8249
|
+
await writeFile13(configPath, JSON.stringify(existingConfig, null, 2), "utf-8");
|
|
7547
8250
|
console.log(chalk8.dim(` Saved to ${configPath}`));
|
|
7548
8251
|
} else {
|
|
7549
8252
|
console.log(chalk8.red(` Invalid value: ${value}`));
|
|
@@ -7646,12 +8349,80 @@ function registerAgentCommands(program) {
|
|
|
7646
8349
|
}
|
|
7647
8350
|
}
|
|
7648
8351
|
});
|
|
8352
|
+
agent.command("calibrate").description("Sweep weight profiles \xD7 BUY/SELL thresholds against historical data; ranks by Sharpe").option("--tokens <list>", "Comma-separated CoinGecko IDs (default: bitcoin,ethereum,solana,aave,uniswap,chainlink,arbitrum)").option("--days <n>", "Lookback window in days", "60").option("--capital <amount>", "Initial capital per token in USD", "10000").option("--top <n>", "Show top N configs in the ranked table", "20").option("--from-history [path]", "Replay against signal-history.jsonl (true production signal stack including HL/funding/dexFlow). Path defaults to ~/.sherwood/agent/signal-history.jsonl").option("--last <days>", "Replay only rows captured within the last N days (use after a scoring change to ignore stale captures). Replay path only.").option("--no-regime", "When using --from-history, ignore the regime field on each row (use DEFAULT_THRESHOLDS instead)").action(async (options) => {
|
|
8353
|
+
const topN = parseInt(options.top, 10);
|
|
8354
|
+
if (options.fromHistory) {
|
|
8355
|
+
const historyPath = typeof options.fromHistory === "string" ? options.fromHistory : void 0;
|
|
8356
|
+
const tokens2 = options.tokens ? options.tokens.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
|
|
8357
|
+
let lastDays;
|
|
8358
|
+
if (options.last !== void 0) {
|
|
8359
|
+
lastDays = parseFloat(options.last);
|
|
8360
|
+
if (!Number.isFinite(lastDays) || lastDays <= 0) {
|
|
8361
|
+
console.error(chalk8.red(`Invalid --last: ${options.last} (expected positive number of days)`));
|
|
8362
|
+
process.exitCode = 1;
|
|
8363
|
+
return;
|
|
8364
|
+
}
|
|
8365
|
+
}
|
|
8366
|
+
console.log(chalk8.bold(`
|
|
8367
|
+
Replaying calibration against signal-history.jsonl${lastDays ? ` (last ${lastDays}d)` : ""}...
|
|
8368
|
+
`));
|
|
8369
|
+
try {
|
|
8370
|
+
const results = await runHistoryReplay({
|
|
8371
|
+
historyPath,
|
|
8372
|
+
tokens: tokens2,
|
|
8373
|
+
lastDays,
|
|
8374
|
+
useRegime: options.regime,
|
|
8375
|
+
onProgress: (msg) => console.log(chalk8.dim(` ${msg}`))
|
|
8376
|
+
});
|
|
8377
|
+
console.log(formatCalibrationTable(results, topN));
|
|
8378
|
+
console.log(chalk8.dim(` Source: signal-history replay (production signal stack)`));
|
|
8379
|
+
console.log(chalk8.dim(` Saved: ~/.sherwood/agent/replay-calibration-results.json
|
|
8380
|
+
`));
|
|
8381
|
+
} catch (err) {
|
|
8382
|
+
console.error(chalk8.red(`
|
|
8383
|
+
Replay calibration failed: ${err.message}
|
|
8384
|
+
`));
|
|
8385
|
+
process.exitCode = 1;
|
|
8386
|
+
}
|
|
8387
|
+
return;
|
|
8388
|
+
}
|
|
8389
|
+
const tokens = options.tokens ? options.tokens.split(",").map((t) => t.trim()).filter(Boolean) : DEFAULT_CALIBRATION_TOKENS;
|
|
8390
|
+
const days = parseInt(options.days, 10);
|
|
8391
|
+
const capital = parseFloat(options.capital);
|
|
8392
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
8393
|
+
console.error(chalk8.red(`Invalid --days: ${options.days}`));
|
|
8394
|
+
process.exitCode = 1;
|
|
8395
|
+
return;
|
|
8396
|
+
}
|
|
8397
|
+
if (!Number.isFinite(capital) || capital <= 0) {
|
|
8398
|
+
console.error(chalk8.red(`Invalid --capital: ${options.capital}`));
|
|
8399
|
+
process.exitCode = 1;
|
|
8400
|
+
return;
|
|
8401
|
+
}
|
|
8402
|
+
console.log(chalk8.bold(`
|
|
8403
|
+
Calibrating ${tokens.length} tokens over ${days}d (candle-based, 200 configs per token)...
|
|
8404
|
+
`));
|
|
8405
|
+
try {
|
|
8406
|
+
const results = await runCalibration({
|
|
8407
|
+
tokens,
|
|
8408
|
+
days,
|
|
8409
|
+
capital,
|
|
8410
|
+
onProgress: (msg) => console.log(chalk8.dim(` ${msg}`))
|
|
8411
|
+
});
|
|
8412
|
+
console.log(formatCalibrationTable(results, topN));
|
|
8413
|
+
} catch (err) {
|
|
8414
|
+
console.error(chalk8.red(`
|
|
8415
|
+
Calibration failed: ${err.message}
|
|
8416
|
+
`));
|
|
8417
|
+
process.exitCode = 1;
|
|
8418
|
+
}
|
|
8419
|
+
});
|
|
7649
8420
|
agent.command("signal-audit").description("Audit signal history \u2014 which signal categories actually fire?").option("--drop-threshold <rate>", "Fire-rate below which a category is flagged for dropping", "0.1").option("--json", "Emit raw JSON instead of formatted table").option("--save <path>", "Save current audit as a baseline JSON snapshot").option("--baseline <path>", "Diff current audit against a saved baseline").action(async (options) => {
|
|
7650
8421
|
const dropThreshold = parseFloat(options.dropThreshold ?? "0.1");
|
|
7651
8422
|
const result = await auditSignalHistory();
|
|
7652
8423
|
if (options.save) {
|
|
7653
|
-
await
|
|
7654
|
-
await
|
|
8424
|
+
await mkdir15(join14(homedir14(), ".sherwood", "agent"), { recursive: true });
|
|
8425
|
+
await writeFile13(options.save, JSON.stringify(result, null, 2), "utf-8");
|
|
7655
8426
|
console.log(chalk8.green(` Saved baseline snapshot to ${options.save}`));
|
|
7656
8427
|
console.log(chalk8.dim(` ${result.totalEntries} entries, ${result.perSignal.length} signals`));
|
|
7657
8428
|
return;
|
|
@@ -7659,7 +8430,7 @@ function registerAgentCommands(program) {
|
|
|
7659
8430
|
if (options.baseline) {
|
|
7660
8431
|
let baseline;
|
|
7661
8432
|
try {
|
|
7662
|
-
const raw = await
|
|
8433
|
+
const raw = await readFile13(options.baseline, "utf-8");
|
|
7663
8434
|
baseline = JSON.parse(raw);
|
|
7664
8435
|
} catch (err) {
|
|
7665
8436
|
console.error(chalk8.red(` Failed to load baseline: ${err.message}`));
|
|
@@ -7873,4 +8644,4 @@ function formatFearGreed(value) {
|
|
|
7873
8644
|
export {
|
|
7874
8645
|
registerAgentCommands
|
|
7875
8646
|
};
|
|
7876
|
-
//# sourceMappingURL=agent-
|
|
8647
|
+
//# sourceMappingURL=agent-XBYSALOA.js.map
|