@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.
Files changed (45) hide show
  1. package/dist/abis-TXCLIR2K.js +37 -0
  2. package/dist/{addresses-5BITNIGX.js → addresses-J4NSZRGB.js} +2 -2
  3. package/dist/{agent-TB7VNLFH.js → agent-XBYSALOA.js} +1433 -662
  4. package/dist/agent-XBYSALOA.js.map +1 -0
  5. package/dist/{chat-CLBWW7PV.js → chat-EYCDCZUA.js} +30 -13
  6. package/dist/chat-EYCDCZUA.js.map +1 -0
  7. package/dist/{chunk-IQNYBIFE.js → chunk-363E34JI.js} +8 -8
  8. package/dist/{chunk-PHU6MI3J.js → chunk-5OMMVOR6.js} +5 -5
  9. package/dist/{chunk-B4AH26UI.js → chunk-DNX3P5YK.js} +150 -15
  10. package/dist/chunk-DNX3P5YK.js.map +1 -0
  11. package/dist/{chunk-JQBGEYAK.js → chunk-FCM6SSL6.js} +5 -5
  12. package/dist/{chunk-QZSKBYZ2.js → chunk-ITNGXYLB.js} +5 -5
  13. package/dist/{chunk-OPEVMNEY.js → chunk-KHTM4GB5.js} +5 -5
  14. package/dist/{chunk-OQUGUIYM.js → chunk-W3GXAODE.js} +111 -2
  15. package/dist/{chunk-OQUGUIYM.js.map → chunk-W3GXAODE.js.map} +1 -1
  16. package/dist/{chunk-RFTJBEZI.js → chunk-ZN6CUAX7.js} +4 -4
  17. package/dist/{chunk-RFTJBEZI.js.map → chunk-ZN6CUAX7.js.map} +1 -1
  18. package/dist/{eas-LVJEPWBY.js → eas-MUKSLDIP.js} +4 -4
  19. package/dist/{governor-HU3CPXMU.js → governor-WA74WFOS.js} +4 -4
  20. package/dist/index.js +33 -33
  21. package/dist/{price-OZRLJH3X.js → price-ELYQKRSN.js} +5 -5
  22. package/dist/{research-662HNMOG.js → research-4UA4UB5P.js} +2 -2
  23. package/dist/research-4UA4UB5P.js.map +1 -0
  24. package/dist/{research-5WEM4WZC.js → research-HXOXZA4N.js} +4 -4
  25. package/dist/{session-XRTKPC4Q.js → session-VEUMC4F6.js} +11 -11
  26. package/dist/{trade-HN4BXDVU.js → trade-WMPWF6RT.js} +10 -10
  27. package/dist/{xmtp-25RHTLIA.js → xmtp-7CZ5LB5O.js} +4 -4
  28. package/package.json +1 -1
  29. package/dist/agent-TB7VNLFH.js.map +0 -1
  30. package/dist/chat-CLBWW7PV.js.map +0 -1
  31. package/dist/chunk-B4AH26UI.js.map +0 -1
  32. /package/dist/{addresses-5BITNIGX.js.map → abis-TXCLIR2K.js.map} +0 -0
  33. /package/dist/{eas-LVJEPWBY.js.map → addresses-J4NSZRGB.js.map} +0 -0
  34. /package/dist/{chunk-IQNYBIFE.js.map → chunk-363E34JI.js.map} +0 -0
  35. /package/dist/{chunk-PHU6MI3J.js.map → chunk-5OMMVOR6.js.map} +0 -0
  36. /package/dist/{chunk-JQBGEYAK.js.map → chunk-FCM6SSL6.js.map} +0 -0
  37. /package/dist/{chunk-QZSKBYZ2.js.map → chunk-ITNGXYLB.js.map} +0 -0
  38. /package/dist/{chunk-OPEVMNEY.js.map → chunk-KHTM4GB5.js.map} +0 -0
  39. /package/dist/{governor-HU3CPXMU.js.map → eas-MUKSLDIP.js.map} +0 -0
  40. /package/dist/{research-662HNMOG.js.map → governor-WA74WFOS.js.map} +0 -0
  41. /package/dist/{price-OZRLJH3X.js.map → price-ELYQKRSN.js.map} +0 -0
  42. /package/dist/{research-5WEM4WZC.js.map → research-HXOXZA4N.js.map} +0 -0
  43. /package/dist/{session-XRTKPC4Q.js.map → session-VEUMC4F6.js.map} +0 -0
  44. /package/dist/{trade-HN4BXDVU.js.map → trade-WMPWF6RT.js.map} +0 -0
  45. /package/dist/{xmtp-25RHTLIA.js.map → xmtp-7CZ5LB5O.js.map} +0 -0
@@ -1,21 +1,22 @@
1
1
  import {
2
- getResearchProvider
3
- } from "./chunk-B4AH26UI.js";
4
- import {
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 readFile12, writeFile as writeFile11, mkdir as mkdir13 } from "fs/promises";
17
- import { join as join12 } from "path";
18
- import { homedir as homedir12 } from "os";
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.25,
693
- technical: 0.2,
694
- sentiment: 0.2,
695
- onchain: 0.15,
693
+ smartMoney: 0.15,
694
+ technical: 0.1,
695
+ sentiment: 0.4,
696
+ onchain: 0.2,
696
697
  fundamental: 0.1,
697
- event: 0.1
698
+ event: 0.05
698
699
  };
699
700
  var WEIGHT_PROFILES = {
700
701
  default: DEFAULT_WEIGHTS,
701
- majors: { smartMoney: 0.3, technical: 0.3, sentiment: 0.2, onchain: 0.2, fundamental: 0, event: 0 },
702
- altcoin: { smartMoney: 0.2, technical: 0.15, sentiment: 0.15, onchain: 0.15, fundamental: 0.2, event: 0.15 },
703
- sentHeavy: { smartMoney: 0.1, technical: 0.15, sentiment: 0.4, onchain: 0.15, fundamental: 0.1, event: 0.1 },
704
- techHeavy: { smartMoney: 0.1, technical: 0.4, sentiment: 0.15, onchain: 0.15, fundamental: 0.1, event: 0.1 }
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
- "ranging": { strongBuy: 0.65, buy: 0.4, sell: -0.4, strongSell: -0.65 },
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
- function computeTradeDecision(signals, weights, regimeAdjustments, correlationCheck, regime) {
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 weightMap = {
990
- technical: w.technical,
991
- sentiment: w.sentiment,
992
- onchain: w.onchain,
993
- fundamental: w.fundamental,
994
- event: w.event,
995
- smartMoney: w.smartMoney,
996
- // Strategy signal mappings to categories
997
- breakoutOnChain: w.technical,
998
- meanReversion: w.technical,
999
- multiTimeframe: w.technical,
1000
- dexFlow: w.onchain,
1001
- fundingRate: w.fundamental,
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
- const signalWeight = signal._weightOverride ?? weightMap[signal.name] ?? 0.1;
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 < 35) {
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
- new SmartMoneyStrategy(),
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
- new TvlMomentumStrategy(),
1937
+ // 95% fire rate
2275
1938
  new FundingRateStrategy(),
1939
+ // 52% fire rate — HL native
2276
1940
  new DexFlowStrategy(),
2277
- new MeanReversionStrategy(),
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 15 minutes old.
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 = 15 * 60 * 1e3;
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
- if (this.config.useX402) {
4273
- const [nansenResult, messariResult] = await Promise.allSettled([
4274
- // 4. Smart-money & on-chain data (via Nansen x402)
4275
- getResearchProvider("nansen").query({ type: "smart-money", target: tokenId }),
4276
- // 5. Fundamentals & events (via Messari x402)
4277
- getResearchProvider("messari").query({ type: "token", target: tokenId })
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 (nansenResult.status === "fulfilled") {
4280
- const smResult = nansenResult.value;
4281
- nansenData = smResult.data;
4282
- const flows = smResult.data.flows;
4283
- if (flows && flows.length > 0) {
4284
- const netflow = flows.reduce((sum, f) => sum + Number(f.netflow ?? f.net_flow ?? 0), 0);
4285
- signals.push(scoreOnChain({
4286
- exchangeNetFlow: netflow,
4287
- whaleAccumulating: netflow < 0
4288
- }));
4289
- } else {
4290
- signals.push(scoreOnChain({}));
4291
- }
4292
- console.error(chalk.dim(` x402 Nansen smart-money: cost ${smResult.costUsdc} USDC`));
4293
- } else {
4294
- console.error(chalk.dim(` x402 Nansen smart-money unavailable: ${nansenResult.reason} \u2014 using free data only`));
4295
- signals.push(scoreOnChain({}));
4296
- }
4297
- if (messariResult.status === "fulfilled") {
4298
- const tokenResult = messariResult.value;
4299
- messariData = tokenResult.data;
4300
- const d = tokenResult.data;
4301
- const metrics = d.metrics;
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
- signals.push(scoreFundamental({}));
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 Messari unavailable: ${messariResult.reason} \u2014 using free data only`));
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 results = [];
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
- results.push(result);
4256
+ const result = await this.analyzeToken(token, { skipX402: true });
4257
+ freeResults.push(result);
4468
4258
  }
4469
- return results;
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: 10,
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, 20);
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
- if (!existing) {
4919
- return {
4920
- success: false,
4921
- error: `No open position in ${tokenId} to sell`,
4922
- dryRun: this.config.dryRun
4923
- };
4924
- }
4925
- try {
4926
- const reason = decision.action === "STRONG_SELL" ? "Strong sell signal" : "Sell signal";
4927
- if (this.config.dryRun) {
4928
- console.error(chalk2.cyan(`[DRY RUN] Paper trade: SELL ${existing.quantity.toFixed(6)} ${tokenId} @ $${currentPrice.toFixed(4)}`));
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 stopLossDistance = currentPrice * 0.08;
4954
- const stopLossPrice = currentPrice - stopLossDistance;
4955
- const takeProfitPrice = currentPrice * (1 + 0.08 * 2.5);
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
- if (sizing.quantity <= 0 || sizing.sizeUsd <= 0) {
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 check = this.riskManager.canOpenPosition(tokenId, sizing.sizeUsd);
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: sizing.sizeUsd,
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: sizing.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
- console.error(chalk2.cyan(`[DRY RUN] Paper trade: BUY ${quantity.toFixed(6)} ${order.tokenId} @ $${currentPrice.toFixed(4)}`));
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 limitPx = priceToUint64(currentPrice * 1.01);
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
- [1, limitPx, sz, stopLossPx, stopLossSz]
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 pnlUsd = (exitPrice - pos.entryPrice) * pos.quantity;
5279
- const pnlPercent = (exitPrice - pos.entryPrice) / pos.entryPrice;
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
- pos.pnlUsd = (price - pos.entryPrice) * pos.quantity;
5317
- pos.pnlPercent = (price - pos.entryPrice) / pos.entryPrice;
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.12,
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
- canOpenPosition(token, sizeUsd) {
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
- return { allowed: false, reason: `Already have an open position in ${token}` };
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
- const riskUsd = portfolioValue * riskPct;
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
- const quantity = riskUsd / riskPerUnit;
5593
- const sizeUsd = quantity * entryPrice;
5591
+ let quantity = riskUsd / riskPerUnit;
5592
+ let sizeUsd = quantity * entryPrice;
5594
5593
  const maxSizeUsd = portfolioValue * this.config.maxSinglePosition;
5595
5594
  if (sizeUsd > maxSizeUsd) {
5596
- const cappedQuantity = maxSizeUsd / entryPrice;
5597
- return {
5598
- quantity: cappedQuantity,
5599
- sizeUsd: maxSizeUsd,
5600
- riskUsd: cappedQuantity * riskPerUnit
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 pnlPct = (pos.currentPrice - pos.entryPrice) / pos.entryPrice;
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 (this.config.breakevenTriggerPct > 0 && pnlPct >= this.config.breakevenTriggerPct) {
5664
- newStop = Math.max(newStop, pos.entryPrice);
5665
- }
5666
- if (this.config.profitLockSteps.length > 0) {
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
- if (this.config.trailingStopPct > 0) {
5675
- const trailStop = pos.currentPrice * (1 - this.config.trailingStopPct);
5676
- newStop = Math.max(newStop, trailStop);
5677
- }
5678
- if (newStop > pos.stopLoss) {
5679
- return { ...pos, stopLoss: newStop, trailingStop: newStop };
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 pnlPercent = (price - pos.entryPrice) / pos.entryPrice;
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
- if (price <= pos.stopLoss) {
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 && price <= pos.trailingStop) {
5723
- toClose.push(updatedPos);
5724
- reasons[pos.tokenId] = `Trailing stop hit at $${pos.trailingStop.toFixed(4)} (price: $${price.toFixed(4)})`;
5725
- continue;
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
- if (price >= pos.takeProfit) {
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 holdingDays = (Date.now() - pos.entryTimestamp) / (1e3 * 60 * 60 * 24);
5733
- if (holdingDays > 7 && pnlPercent < 0.02 && pnlPercent > -0.02) {
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 ${holdingDays.toFixed(0)} days with only ${(pnlPercent * 100).toFixed(1)}% PnL`;
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 CANDLE_BASED_STRATEGIES = [
6158
+ var BACKTEST_STRATEGIES = [
6126
6159
  "breakoutOnChain",
6127
- "meanReversion",
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
- const availableStrategies = [...CANDLE_BASED_STRATEGIES];
6344
+ console.log(chalk7.dim(` Using backtest strategies: ${BACKTEST_STRATEGIES.join(", ")} + momentum`));
6229
6345
  if (Object.keys(fearAndGreedData).length > 0) {
6230
- availableStrategies.push("sentimentContrarian");
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
- const technicals = getLatestSignals(windowCandles);
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 (this.config.useRegime) {
6274
- regime = MarketRegimeDetector.classifyFromCandles(windowCandles).regime;
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.length > 0 ? filtered : signals,
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
- highWaterMark: currentPrice
6478
+ extremePrice: currentPrice,
6479
+ // for shorts this becomes a low-water mark
6480
+ entryTimestamp: currentCandle.timestamp
6318
6481
  };
6319
6482
  } else if (position) {
6320
- if (currentPrice > position.highWaterMark) {
6321
- position.highWaterMark = currentPrice;
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 trailingStopHit = this.config.trailingStopPct !== void 0 && currentPrice <= position.highWaterMark * (1 - this.config.trailingStopPct);
6324
- const sellSignal = decision.action === "SELL" || decision.action === "STRONG_SELL";
6325
- if (trailingStopHit || sellSignal) {
6326
- const pnlPercent = (currentPrice - position.entryPrice) / position.entryPrice;
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 readFile11 } from "fs/promises";
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 readFile11(filePath, "utf-8");
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 = ["ethereum", "bitcoin", "solana", "aave", "uniswap"];
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 = join12(homedir12(), ".sherwood", "agent", "config.json");
7385
- const data = await readFile12(configPath, "utf-8");
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 = join12(homedir12(), ".sherwood", "agent");
7537
- const configPath = join12(configDir, "config.json");
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 readFile12(configPath, "utf-8");
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 mkdir13(configDir, { recursive: true });
7546
- await writeFile11(configPath, JSON.stringify(existingConfig, null, 2), "utf-8");
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 mkdir13(join12(homedir12(), ".sherwood", "agent"), { recursive: true });
7654
- await writeFile11(options.save, JSON.stringify(result, null, 2), "utf-8");
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 readFile12(options.baseline, "utf-8");
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-TB7VNLFH.js.map
8647
+ //# sourceMappingURL=agent-XBYSALOA.js.map