@sherwoodagent/cli 0.29.1 → 0.38.3

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 (57) hide show
  1. package/dist/{addresses-NOWMJT4H.js → addresses-6GJP2WAY.js} +4 -4
  2. package/dist/{agent-U6OFXITU.js → agent-TB7VNLFH.js} +1085 -296
  3. package/dist/agent-TB7VNLFH.js.map +1 -0
  4. package/dist/{chat-SIB6GCJF.js → chat-36TB53HK.js} +9 -9
  5. package/dist/{chunk-7W6U6ZC6.js → chunk-4EG4TSRE.js} +6 -6
  6. package/dist/{chunk-UW6P2PMP.js → chunk-6SKMCZ7I.js} +6 -6
  7. package/dist/{chunk-EYI6JORK.js → chunk-B4AH26UI.js} +2 -2
  8. package/dist/{chunk-H32QA3NV.js → chunk-IJG4CTOI.js} +2 -2
  9. package/dist/{chunk-N2547R2L.js → chunk-OFR3LCYE.js} +3 -3
  10. package/dist/{chunk-7PDRYBHD.js → chunk-OPKMVIYJ.js} +4 -4
  11. package/dist/{chunk-7PDRYBHD.js.map → chunk-OPKMVIYJ.js.map} +1 -1
  12. package/dist/{chunk-PQ5ZX6ZT.js → chunk-OQUGUIYM.js} +14 -1
  13. package/dist/{chunk-PQ5ZX6ZT.js.map → chunk-OQUGUIYM.js.map} +1 -1
  14. package/dist/{chunk-OD5JECOR.js → chunk-PNUBECG2.js} +5 -5
  15. package/dist/{chunk-BVL3O7UH.js → chunk-SP2SI63D.js} +5 -5
  16. package/dist/{chunk-RGOTBJIN.js → chunk-WCBG44IQ.js} +9 -1
  17. package/dist/chunk-WCBG44IQ.js.map +1 -0
  18. package/dist/{chunk-C4SYHSY2.js → chunk-XKDB5D4E.js} +5 -5
  19. package/dist/{client-66VTYDRS.js → client-HKAG55PQ.js} +4 -4
  20. package/dist/{config-YXZKPSZB.js → config-LS7FKZL5.js} +4 -2
  21. package/dist/{eas-YTMCD3UN.js → eas-XNLD5PTV.js} +7 -7
  22. package/dist/{governor-2N3CPFOP.js → governor-ZOIXGEVS.js} +7 -7
  23. package/dist/index.js +67 -48
  24. package/dist/index.js.map +1 -1
  25. package/dist/{network-GJA2ZN3O.js → network-KSMKPKNL.js} +3 -3
  26. package/dist/{price-HGBIMFYN.js → price-WGV6NYK3.js} +6 -6
  27. package/dist/research-662HNMOG.js +14 -0
  28. package/dist/{research-ECOWBTFP.js → research-AYT6QDJP.js} +9 -9
  29. package/dist/{session-RTCMOPAQ.js → session-46SXKEKP.js} +11 -11
  30. package/dist/{trade-3HVCELTB.js → trade-LIRSGOV7.js} +17 -17
  31. package/dist/{xmtp-5FPJN5WE.js → xmtp-XQGWSO72.js} +21 -16
  32. package/dist/xmtp-XQGWSO72.js.map +1 -0
  33. package/package.json +7 -5
  34. package/dist/agent-U6OFXITU.js.map +0 -1
  35. package/dist/chunk-RGOTBJIN.js.map +0 -1
  36. package/dist/research-3GNTDKY4.js +0 -14
  37. package/dist/xmtp-5FPJN5WE.js.map +0 -1
  38. /package/dist/{addresses-NOWMJT4H.js.map → addresses-6GJP2WAY.js.map} +0 -0
  39. /package/dist/{chat-SIB6GCJF.js.map → chat-36TB53HK.js.map} +0 -0
  40. /package/dist/{chunk-7W6U6ZC6.js.map → chunk-4EG4TSRE.js.map} +0 -0
  41. /package/dist/{chunk-UW6P2PMP.js.map → chunk-6SKMCZ7I.js.map} +0 -0
  42. /package/dist/{chunk-EYI6JORK.js.map → chunk-B4AH26UI.js.map} +0 -0
  43. /package/dist/{chunk-H32QA3NV.js.map → chunk-IJG4CTOI.js.map} +0 -0
  44. /package/dist/{chunk-N2547R2L.js.map → chunk-OFR3LCYE.js.map} +0 -0
  45. /package/dist/{chunk-OD5JECOR.js.map → chunk-PNUBECG2.js.map} +0 -0
  46. /package/dist/{chunk-BVL3O7UH.js.map → chunk-SP2SI63D.js.map} +0 -0
  47. /package/dist/{chunk-C4SYHSY2.js.map → chunk-XKDB5D4E.js.map} +0 -0
  48. /package/dist/{client-66VTYDRS.js.map → client-HKAG55PQ.js.map} +0 -0
  49. /package/dist/{config-YXZKPSZB.js.map → config-LS7FKZL5.js.map} +0 -0
  50. /package/dist/{eas-YTMCD3UN.js.map → eas-XNLD5PTV.js.map} +0 -0
  51. /package/dist/{governor-2N3CPFOP.js.map → governor-ZOIXGEVS.js.map} +0 -0
  52. /package/dist/{network-GJA2ZN3O.js.map → network-KSMKPKNL.js.map} +0 -0
  53. /package/dist/{price-HGBIMFYN.js.map → price-WGV6NYK3.js.map} +0 -0
  54. /package/dist/{research-3GNTDKY4.js.map → research-662HNMOG.js.map} +0 -0
  55. /package/dist/{research-ECOWBTFP.js.map → research-AYT6QDJP.js.map} +0 -0
  56. /package/dist/{session-RTCMOPAQ.js.map → session-46SXKEKP.js.map} +0 -0
  57. /package/dist/{trade-3HVCELTB.js.map → trade-LIRSGOV7.js.map} +0 -0
@@ -1,21 +1,21 @@
1
1
  import {
2
2
  getResearchProvider
3
- } from "./chunk-EYI6JORK.js";
3
+ } from "./chunk-B4AH26UI.js";
4
4
  import {
5
5
  BASE_STRATEGY_ABI
6
- } from "./chunk-PQ5ZX6ZT.js";
7
- import "./chunk-N2547R2L.js";
6
+ } from "./chunk-OQUGUIYM.js";
7
+ import "./chunk-OFR3LCYE.js";
8
8
  import {
9
9
  hyperevm,
10
10
  hyperevmTestnet
11
- } from "./chunk-H32QA3NV.js";
12
- import "./chunk-RGOTBJIN.js";
11
+ } from "./chunk-IJG4CTOI.js";
12
+ import "./chunk-WCBG44IQ.js";
13
13
 
14
14
  // src/commands/agent.ts
15
15
  import chalk8 from "chalk";
16
- import { readFile as readFile10, writeFile as writeFile10, mkdir as mkdir11 } from "fs/promises";
17
- import { join as join11 } from "path";
18
- import { homedir as homedir11 } from "os";
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";
19
19
  import ora from "ora";
20
20
 
21
21
  // src/agent/index.ts
@@ -696,6 +696,40 @@ var DEFAULT_WEIGHTS = {
696
696
  fundamental: 0.1,
697
697
  event: 0.1
698
698
  };
699
+ var WEIGHT_PROFILES = {
700
+ 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 }
705
+ };
706
+ var MAJORS_TOKEN_IDS = /* @__PURE__ */ new Set(["bitcoin", "ethereum", "solana"]);
707
+ function profileForToken(tokenId, userProfile) {
708
+ if (userProfile && WEIGHT_PROFILES[userProfile]) {
709
+ return WEIGHT_PROFILES[userProfile];
710
+ }
711
+ if (MAJORS_TOKEN_IDS.has(tokenId.toLowerCase())) {
712
+ return WEIGHT_PROFILES["majors"];
713
+ }
714
+ return DEFAULT_WEIGHTS;
715
+ }
716
+ var DEFAULT_THRESHOLDS = {
717
+ strongBuy: 0.6,
718
+ buy: 0.3,
719
+ sell: -0.3,
720
+ strongSell: -0.6
721
+ };
722
+ var REGIME_THRESHOLDS = {
723
+ "trending-up": { strongBuy: 0.55, buy: 0.25, sell: -0.4, strongSell: -0.7 },
724
+ "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 },
726
+ "high-volatility": { strongBuy: 0.7, buy: 0.45, sell: -0.45, strongSell: -0.7 },
727
+ "low-volatility": { strongBuy: 0.6, buy: 0.3, sell: -0.3, strongSell: -0.6 }
728
+ };
729
+ function thresholdsForRegime(regime) {
730
+ if (!regime) return DEFAULT_THRESHOLDS;
731
+ return REGIME_THRESHOLDS[regime] ?? DEFAULT_THRESHOLDS;
732
+ }
699
733
  function scoreTechnical(signals) {
700
734
  let value = 0;
701
735
  const details = [];
@@ -949,8 +983,9 @@ function scoreEvent(data) {
949
983
  details: details.join("; ")
950
984
  };
951
985
  }
952
- function computeTradeDecision(signals, weights, regimeAdjustments, correlationCheck) {
986
+ function computeTradeDecision(signals, weights, regimeAdjustments, correlationCheck, regime) {
953
987
  const w = weights ?? DEFAULT_WEIGHTS;
988
+ const thresholds = thresholdsForRegime(regime);
954
989
  const weightMap = {
955
990
  technical: w.technical,
956
991
  sentiment: w.sentiment,
@@ -1001,11 +1036,11 @@ function computeTradeDecision(signals, weights, regimeAdjustments, correlationCh
1001
1036
  score = Math.max(-1, Math.min(1, score));
1002
1037
  }
1003
1038
  let action;
1004
- if (score > 0.6) action = "STRONG_BUY";
1005
- else if (score > 0.3) action = "BUY";
1006
- else if (score > -0.3) action = "HOLD";
1007
- else if (score > -0.6) action = "SELL";
1008
- else action = "STRONG_SELL";
1039
+ if (score >= thresholds.strongBuy) action = "STRONG_BUY";
1040
+ else if (score >= thresholds.buy) action = "BUY";
1041
+ else if (score <= thresholds.strongSell) action = "STRONG_SELL";
1042
+ else if (score <= thresholds.sell) action = "SELL";
1043
+ else action = "HOLD";
1009
1044
  const reasoning = signals.map((s) => `[${s.source}] ${s.details}`).join("\n");
1010
1045
  return {
1011
1046
  action,
@@ -1013,7 +1048,8 @@ function computeTradeDecision(signals, weights, regimeAdjustments, correlationCh
1013
1048
  signals,
1014
1049
  reasoning,
1015
1050
  confidence,
1016
- timestamp: Date.now()
1051
+ timestamp: Date.now(),
1052
+ thresholds
1017
1053
  };
1018
1054
  }
1019
1055
 
@@ -1522,29 +1558,29 @@ var FundingRateStrategy = class {
1522
1558
  const details = [];
1523
1559
  let value = 0;
1524
1560
  let confidence = 0.5;
1525
- if (rate > 5e-4) {
1561
+ if (rate > 2e-4) {
1526
1562
  value = -0.5;
1527
1563
  confidence = 0.6;
1528
1564
  details.push(
1529
1565
  `High positive funding ${ratePct}% (annualized ${annualizedPct}%): overleveraged longs \u2014 bearish contrarian on ${fundingData.exchange}`
1530
1566
  );
1531
- } else if (rate > 2e-4) {
1567
+ } else if (rate > 5e-5) {
1532
1568
  value = -0.3;
1533
1569
  confidence = 0.4;
1534
1570
  details.push(
1535
- `Mild positive funding ${ratePct}% (annualized ${annualizedPct}%): longs paying shorts \u2014 mildly bearish`
1571
+ `Mild positive funding ${ratePct}% (annualized ${annualizedPct}%): longs paying shorts \u2014 mildly bearish on ${fundingData.exchange}`
1536
1572
  );
1537
- } else if (rate < -5e-4) {
1573
+ } else if (rate < -2e-4) {
1538
1574
  value = 0.5;
1539
1575
  confidence = 0.6;
1540
1576
  details.push(
1541
1577
  `High negative funding ${ratePct}% (annualized ${annualizedPct}%): overleveraged shorts \u2014 bullish contrarian on ${fundingData.exchange}`
1542
1578
  );
1543
- } else if (rate < -2e-4) {
1579
+ } else if (rate < -5e-5) {
1544
1580
  value = 0.3;
1545
1581
  confidence = 0.4;
1546
1582
  details.push(
1547
- `Mild negative funding ${ratePct}% (annualized ${annualizedPct}%): shorts paying longs \u2014 mildly bullish`
1583
+ `Mild negative funding ${ratePct}% (annualized ${annualizedPct}%): shorts paying longs \u2014 mildly bullish on ${fundingData.exchange}`
1548
1584
  );
1549
1585
  } else {
1550
1586
  value = 0;
@@ -1895,146 +1931,6 @@ var MeanReversionStrategy = class {
1895
1931
  }
1896
1932
  };
1897
1933
 
1898
- // src/agent/strategies/twitter-sentiment.ts
1899
- var TwitterSentimentStrategy = class {
1900
- name = "twitterSentiment";
1901
- description = "Analyzes Twitter sentiment and mention volume for trading signals";
1902
- requiredData = ["twitterData"];
1903
- async analyze(ctx) {
1904
- const twitterData = ctx.twitterData;
1905
- if (!twitterData) {
1906
- return {
1907
- name: this.name,
1908
- value: 0,
1909
- confidence: 0,
1910
- source: "Twitter Sentiment",
1911
- details: "No Twitter sentiment data available \u2014 requires Twitter API credentials"
1912
- };
1913
- }
1914
- const {
1915
- mentionVolume,
1916
- sentimentScore,
1917
- engagementWeightedSentiment,
1918
- volumeSpike,
1919
- tweetCount,
1920
- llmSentiment,
1921
- llmConfidence,
1922
- llmBullishPercent,
1923
- llmBearishPercent
1924
- } = twitterData;
1925
- let value = 0;
1926
- const details = [];
1927
- let confidence = this.calculateConfidence(tweetCount, volumeSpike, llmConfidence);
1928
- const volumeSignal = this.analyzeVolumeSpike(volumeSpike, sentimentScore);
1929
- value += volumeSignal.value * 0.4;
1930
- if (volumeSignal.details) details.push(volumeSignal.details);
1931
- const sentimentSignal = this.analyzeSentiment(engagementWeightedSentiment, llmSentiment, llmBullishPercent, llmBearishPercent);
1932
- value += sentimentSignal.value * 0.4;
1933
- details.push(sentimentSignal.details);
1934
- const contrarianSignal = this.analyzeContrarian(sentimentScore, volumeSpike);
1935
- value += contrarianSignal.value * 0.2;
1936
- if (contrarianSignal.details) details.push(contrarianSignal.details);
1937
- details.push(`${tweetCount} tweets analyzed, ${volumeSpike.toFixed(2)}x volume spike`);
1938
- return {
1939
- name: this.name,
1940
- value: clamp(value),
1941
- confidence,
1942
- source: "Twitter Sentiment",
1943
- details: details.join("; ")
1944
- };
1945
- }
1946
- /** Analyze volume spike patterns. */
1947
- analyzeVolumeSpike(volumeSpike, sentimentScore) {
1948
- if (volumeSpike > 5) {
1949
- const signal = sentimentScore > 0 ? 0.5 : -0.5;
1950
- return {
1951
- value: signal,
1952
- details: `Extreme attention (${volumeSpike.toFixed(1)}x volume): ${signal > 0 ? "bullish momentum" : "bearish momentum"}`
1953
- };
1954
- } else if (volumeSpike > 3) {
1955
- const signal = sentimentScore > 0 ? 0.3 : -0.3;
1956
- return {
1957
- value: signal,
1958
- details: `High attention (${volumeSpike.toFixed(1)}x volume): ${signal > 0 ? "positive buzz" : "negative buzz"}`
1959
- };
1960
- } else if (volumeSpike > 1.5) {
1961
- const signal = sentimentScore > 0 ? 0.1 : -0.1;
1962
- return {
1963
- value: signal,
1964
- details: `Moderate buzz (${volumeSpike.toFixed(1)}x volume)`
1965
- };
1966
- }
1967
- return { value: 0 };
1968
- }
1969
- /** Analyze engagement-weighted sentiment. */
1970
- analyzeSentiment(engagementWeightedSentiment, llmSentiment, llmBullishPercent, llmBearishPercent) {
1971
- let finalSentiment;
1972
- let analysisMethod;
1973
- if (llmSentiment !== void 0) {
1974
- finalSentiment = llmSentiment * 0.6 + engagementWeightedSentiment * 0.4;
1975
- analysisMethod = "[LLM]";
1976
- } else {
1977
- finalSentiment = engagementWeightedSentiment;
1978
- analysisMethod = "";
1979
- }
1980
- const sentimentValue = finalSentiment * 0.4;
1981
- let description;
1982
- if (Math.abs(finalSentiment) < 0.1) {
1983
- description = "neutral sentiment";
1984
- } else if (finalSentiment > 0.7) {
1985
- description = "very bullish sentiment";
1986
- } else if (finalSentiment > 0.3) {
1987
- description = "bullish sentiment";
1988
- } else if (finalSentiment < -0.7) {
1989
- description = "very bearish sentiment";
1990
- } else if (finalSentiment < -0.3) {
1991
- description = "bearish sentiment";
1992
- } else {
1993
- description = finalSentiment > 0 ? "mildly bullish" : "mildly bearish";
1994
- }
1995
- let details = `${analysisMethod} Engagement-weighted ${description} (${finalSentiment > 0 ? "+" : ""}${finalSentiment.toFixed(2)})`;
1996
- if (llmBullishPercent !== void 0 && llmBearishPercent !== void 0) {
1997
- details += ` \u2014 ${llmBullishPercent.toFixed(0)}% bullish, ${llmBearishPercent.toFixed(0)}% bearish`;
1998
- }
1999
- return {
2000
- value: sentimentValue,
2001
- details
2002
- };
2003
- }
2004
- /** Apply contrarian analysis for extreme sentiment + high volume. */
2005
- analyzeContrarian(sentimentScore, volumeSpike) {
2006
- if (volumeSpike > 3) {
2007
- if (sentimentScore > 0.7) {
2008
- return {
2009
- value: -0.2,
2010
- details: "Contrarian warning: extreme bullishness may indicate local top"
2011
- };
2012
- } else if (sentimentScore < -0.7) {
2013
- return {
2014
- value: 0.2,
2015
- details: "Contrarian opportunity: extreme bearishness may indicate capitulation"
2016
- };
2017
- }
2018
- }
2019
- return { value: 0 };
2020
- }
2021
- /** Calculate confidence based on tweet count and volume spike. */
2022
- calculateConfidence(tweetCount, volumeSpike, llmConfidence) {
2023
- let confidence = 0.3;
2024
- if (tweetCount >= 100 && volumeSpike > 2) {
2025
- confidence = 0.8;
2026
- } else if (tweetCount >= 50) {
2027
- confidence = 0.7;
2028
- } else if (tweetCount >= 20) {
2029
- confidence = 0.5;
2030
- }
2031
- if (llmConfidence !== void 0 && llmConfidence > 70) {
2032
- confidence = Math.min(0.9, confidence + 0.2);
2033
- }
2034
- return confidence;
2035
- }
2036
- };
2037
-
2038
1934
  // src/agent/strategies/hyperliquid-flow.ts
2039
1935
  var HyperliquidFlowStrategy = class {
2040
1936
  name = "hyperliquidFlow";
@@ -2379,7 +2275,10 @@ var DEFAULT_STRATEGIES = [
2379
2275
  new FundingRateStrategy(),
2380
2276
  new DexFlowStrategy(),
2381
2277
  new MeanReversionStrategy(),
2382
- new TwitterSentimentStrategy(),
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.
2383
2282
  new HyperliquidFlowStrategy(),
2384
2283
  new MultiTimeframeStrategy()
2385
2284
  ];
@@ -2685,20 +2584,30 @@ var TwitterSentimentProvider = class {
2685
2584
  text: tweet.text,
2686
2585
  engagement: tweet.public_metrics.like_count + tweet.public_metrics.retweet_count + tweet.public_metrics.reply_count
2687
2586
  }));
2587
+ const llmTweets = [...tweetsWithEngagement].sort((a, b) => b.engagement - a.engagement).slice(0, 40);
2588
+ if (llmTweets.length === 0) {
2589
+ return null;
2590
+ }
2688
2591
  const batchSize = 20;
2689
2592
  const batches = [];
2690
- for (let i = 0; i < tweetsWithEngagement.length; i += batchSize) {
2691
- batches.push(tweetsWithEngagement.slice(i, i + batchSize));
2593
+ for (let i = 0; i < llmTweets.length; i += batchSize) {
2594
+ batches.push(llmTweets.slice(i, i + batchSize));
2692
2595
  }
2693
2596
  const allResults = [];
2597
+ let consecutiveBatchFailures = 0;
2598
+ const MAX_CONSECUTIVE_BATCH_FAILURES = 2;
2694
2599
  for (const batch of batches) {
2695
2600
  const sanitizeTweet = (text) => {
2696
2601
  return text.replace(/[\x00-\x1f\x7f]/g, "").replace(/[\u200B-\u200D\u202A-\u202E\u2066-\u2069\u00AD\uFEFF]/g, "").replace(/\n/g, " ").replace(/["\\]/g, (c) => `\\${c}`).slice(0, 280);
2697
2602
  };
2698
2603
  const tweetTexts = batch.map((tweet, idx) => `${idx + 1}. "${sanitizeTweet(tweet.text)}"`).join("\n");
2699
- const systemPrompt = `You are a crypto market sentiment analyzer. For each tweet, classify sentiment as BULLISH, BEARISH, or NEUTRAL with confidence 0-100. Consider sarcasm, irony, CT slang. Respond ONLY as a JSON array: [{"sentiment": "BULLISH", "confidence": 85}, ...]`;
2604
+ const systemPrompt = `You are a crypto market sentiment analyzer. For each tweet, classify sentiment as BULLISH, BEARISH, or NEUTRAL with confidence 0-100. Consider sarcasm, irony, and CT slang.
2605
+ Return ONLY valid JSON with this exact shape:
2606
+ {"results":[{"sentiment":"BULLISH|BEARISH|NEUTRAL","confidence":0-100}]}`;
2700
2607
  const userPrompt = `Analyze these ${batch.length} tweets:
2701
2608
  ${tweetTexts}`;
2609
+ const controller = new AbortController();
2610
+ const timeout = setTimeout(() => controller.abort(), 1e4);
2702
2611
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
2703
2612
  method: "POST",
2704
2613
  headers: {
@@ -2711,18 +2620,25 @@ ${tweetTexts}`;
2711
2620
  { role: "system", content: systemPrompt },
2712
2621
  { role: "user", content: userPrompt }
2713
2622
  ],
2623
+ response_format: { type: "json_object" },
2714
2624
  temperature: 0
2715
- })
2625
+ }),
2626
+ signal: controller.signal
2716
2627
  });
2628
+ clearTimeout(timeout);
2717
2629
  if (!response.ok) {
2718
2630
  console.warn(`OpenAI API error: ${response.status} ${response.statusText}`);
2719
- return null;
2631
+ consecutiveBatchFailures++;
2632
+ if (consecutiveBatchFailures >= MAX_CONSECUTIVE_BATCH_FAILURES) return null;
2633
+ continue;
2720
2634
  }
2721
2635
  const data = await response.json();
2722
2636
  const content = data.choices?.[0]?.message?.content;
2723
2637
  if (!content) {
2724
2638
  console.warn("OpenAI API returned no content");
2725
- return null;
2639
+ consecutiveBatchFailures++;
2640
+ if (consecutiveBatchFailures >= MAX_CONSECUTIVE_BATCH_FAILURES) return null;
2641
+ continue;
2726
2642
  }
2727
2643
  let batchResults;
2728
2644
  try {
@@ -2730,10 +2646,47 @@ ${tweetTexts}`;
2730
2646
  if (jsonStr.startsWith("```")) {
2731
2647
  jsonStr = jsonStr.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
2732
2648
  }
2733
- batchResults = JSON.parse(jsonStr);
2734
- if (!Array.isArray(batchResults) || batchResults.length !== batch.length) {
2735
- console.warn("OpenAI response format mismatch");
2736
- return null;
2649
+ const parsed = JSON.parse(jsonStr);
2650
+ let parsedResults = null;
2651
+ if (Array.isArray(parsed)) {
2652
+ parsedResults = parsed;
2653
+ } else if (parsed && typeof parsed === "object") {
2654
+ const obj = parsed;
2655
+ if (Array.isArray(obj.results)) {
2656
+ parsedResults = obj.results;
2657
+ } else if (Array.isArray(obj.tweets)) {
2658
+ parsedResults = obj.tweets;
2659
+ } else if (Array.isArray(obj.sentiments)) {
2660
+ parsedResults = obj.sentiments;
2661
+ }
2662
+ }
2663
+ if (!parsedResults) {
2664
+ const arrMatch = jsonStr.match(/\[[\s\S]*\]/);
2665
+ if (arrMatch) {
2666
+ const recovered = JSON.parse(arrMatch[0]);
2667
+ if (Array.isArray(recovered)) {
2668
+ parsedResults = recovered;
2669
+ }
2670
+ }
2671
+ }
2672
+ if (!parsedResults) {
2673
+ throw new Error("No parseable sentiment array");
2674
+ }
2675
+ batchResults = parsedResults.map((item) => {
2676
+ const row = item && typeof item === "object" ? item : {};
2677
+ const sentimentRaw = String(row.sentiment ?? row.label ?? row.classification ?? "NEUTRAL").toUpperCase();
2678
+ const confidenceRaw = Number(row.confidence ?? row.score ?? 50);
2679
+ return {
2680
+ sentiment: sentimentRaw,
2681
+ confidence: confidenceRaw
2682
+ };
2683
+ });
2684
+ if (batchResults.length > batch.length) {
2685
+ batchResults = batchResults.slice(0, batch.length);
2686
+ } else if (batchResults.length < batch.length) {
2687
+ while (batchResults.length < batch.length) {
2688
+ batchResults.push({ sentiment: "NEUTRAL", confidence: 50 });
2689
+ }
2737
2690
  }
2738
2691
  const VALID_SENTIMENTS = /* @__PURE__ */ new Set(["BULLISH", "BEARISH", "NEUTRAL"]);
2739
2692
  for (const r of batchResults) {
@@ -2741,9 +2694,12 @@ ${tweetTexts}`;
2741
2694
  if (typeof r.confidence !== "number" || !Number.isFinite(r.confidence)) r.confidence = 50;
2742
2695
  r.confidence = Math.max(0, Math.min(100, r.confidence));
2743
2696
  }
2697
+ consecutiveBatchFailures = 0;
2744
2698
  } catch (parseErr) {
2745
2699
  console.warn(`Failed to parse OpenAI response: ${parseErr}`);
2746
- return null;
2700
+ consecutiveBatchFailures++;
2701
+ if (consecutiveBatchFailures >= MAX_CONSECUTIVE_BATCH_FAILURES) return null;
2702
+ batchResults = batch.map(() => ({ sentiment: "NEUTRAL", confidence: 50 }));
2747
2703
  }
2748
2704
  for (let i = 0; i < batchResults.length; i++) {
2749
2705
  allResults.push({
@@ -2881,10 +2837,201 @@ ${tweetTexts}`;
2881
2837
  }
2882
2838
  };
2883
2839
 
2884
- // src/providers/data/hyperliquid.ts
2885
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2840
+ // src/agent/signal-logger.ts
2841
+ import { appendFile, stat, rename, mkdir as mkdir4 } from "fs/promises";
2886
2842
  import { join as join4 } from "path";
2887
2843
  import { homedir as homedir4 } from "os";
2844
+ var SIGNAL_DIR = join4(homedir4(), ".sherwood", "agent");
2845
+ var SIGNAL_FILE = join4(SIGNAL_DIR, "signal-history.jsonl");
2846
+ var MAX_FILE_SIZE = 10 * 1024 * 1024;
2847
+ function logSignal(analysis, price, weights) {
2848
+ _writeSignal(analysis, price, weights).catch((err) => {
2849
+ console.error(`[signal-logger] Warning: failed to log signal: ${err.message}`);
2850
+ });
2851
+ }
2852
+ async function _writeSignal(analysis, price, weights) {
2853
+ await mkdir4(SIGNAL_DIR, { recursive: true });
2854
+ try {
2855
+ const stats = await stat(SIGNAL_FILE);
2856
+ if (stats.size > MAX_FILE_SIZE) {
2857
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2858
+ const rotatedName = join4(SIGNAL_DIR, `signal-history-${dateStr}.jsonl`);
2859
+ await rename(SIGNAL_FILE, rotatedName);
2860
+ }
2861
+ } catch {
2862
+ }
2863
+ const entry = {
2864
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2865
+ tokenId: analysis.token,
2866
+ tokenSymbol: analysis.token.toUpperCase().slice(0, 6),
2867
+ price,
2868
+ decision: analysis.decision.action,
2869
+ score: analysis.decision.score,
2870
+ confidence: analysis.decision.confidence,
2871
+ signals: analysis.decision.signals.map((s) => ({
2872
+ name: s.name,
2873
+ value: s.value,
2874
+ confidence: s.confidence
2875
+ })),
2876
+ regime: analysis.regime?.regime ?? "unknown",
2877
+ btcCorrelation: analysis.correlation?.btcScore ?? 0,
2878
+ weights: {
2879
+ smartMoney: weights.smartMoney,
2880
+ technical: weights.technical,
2881
+ sentiment: weights.sentiment,
2882
+ onchain: weights.onchain,
2883
+ fundamental: weights.fundamental,
2884
+ event: weights.event
2885
+ }
2886
+ };
2887
+ const line = JSON.stringify(entry) + "\n";
2888
+ await appendFile(SIGNAL_FILE, line, "utf-8");
2889
+ }
2890
+ function getSignalFilePath() {
2891
+ return SIGNAL_FILE;
2892
+ }
2893
+
2894
+ // src/agent/signal-smoother.ts
2895
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
2896
+ import { dirname } from "path";
2897
+ var FAST_SIGNAL_NAMES = /* @__PURE__ */ new Set([
2898
+ "hyperliquidFlow",
2899
+ "smartMoney",
2900
+ "onchain",
2901
+ // legacy direct on-chain signal name
2902
+ "dexFlow",
2903
+ "fundingRate"
2904
+ ]);
2905
+ var DEFAULT_SMOOTHER_CONFIG = {
2906
+ windowSize: 3,
2907
+ maxAgeMs: 6 * 60 * 60 * 1e3,
2908
+ // 6 hours — drop stale readings
2909
+ agreementBoost: 1,
2910
+ maxConfidencePenalty: 0.5
2911
+ };
2912
+ var FileSmootherStorage = class {
2913
+ constructor(filePath) {
2914
+ this.filePath = filePath;
2915
+ }
2916
+ async load() {
2917
+ try {
2918
+ const raw = await readFile4(this.filePath, "utf-8");
2919
+ return JSON.parse(raw);
2920
+ } catch (err) {
2921
+ if (err.code === "ENOENT") return {};
2922
+ throw err;
2923
+ }
2924
+ }
2925
+ async save(cache) {
2926
+ await mkdir5(dirname(this.filePath), { recursive: true });
2927
+ const tmp = this.filePath + ".tmp";
2928
+ await writeFile4(tmp, JSON.stringify(cache), "utf-8");
2929
+ const { rename: rename3 } = await import("fs/promises");
2930
+ await rename3(tmp, this.filePath);
2931
+ }
2932
+ };
2933
+ var MemorySmootherStorage = class {
2934
+ cache = {};
2935
+ async load() {
2936
+ return this.cache;
2937
+ }
2938
+ async save(cache) {
2939
+ this.cache = cache;
2940
+ }
2941
+ };
2942
+ var SignalSmoother = class {
2943
+ constructor(storage, config = DEFAULT_SMOOTHER_CONFIG) {
2944
+ this.storage = storage;
2945
+ this.config = config;
2946
+ }
2947
+ /** In-process mutex chain — serializes load→mutate→save across tokens in
2948
+ * the same process. Does NOT protect against multiple processes racing
2949
+ * the same file; for that, the disk storage does atomic replace and
2950
+ * re-reads the freshest cache inside the mutex. See README / H1 below. */
2951
+ writeLock = Promise.resolve();
2952
+ /**
2953
+ * Smooth the given signals for a token. Fast signals get rolling-window
2954
+ * averages with confidence penalties for disagreement. Slow signals
2955
+ * pass through unchanged. Side effect: appends current readings to the
2956
+ * cache and persists.
2957
+ *
2958
+ * Serialized via writeLock: concurrent calls from within the same
2959
+ * process (e.g. parallel per-token smooths) queue on the shared mutex
2960
+ * so each sees the freshest cache. Cross-process concurrency (two agent
2961
+ * processes on the same host) is NOT fully safe — the second-to-save
2962
+ * wins. Document: run one agent per host, or use MemorySmootherStorage
2963
+ * in contexts with multiple simulation runs.
2964
+ */
2965
+ async smooth(tokenId, signals, nowMs = Date.now()) {
2966
+ const prev = this.writeLock;
2967
+ let release = () => {
2968
+ };
2969
+ this.writeLock = new Promise((resolve) => {
2970
+ release = resolve;
2971
+ });
2972
+ await prev;
2973
+ try {
2974
+ return await this.smoothUnlocked(tokenId, signals, nowMs);
2975
+ } finally {
2976
+ release();
2977
+ }
2978
+ }
2979
+ async smoothUnlocked(tokenId, signals, nowMs) {
2980
+ const cache = await this.storage.load();
2981
+ const cutoff = nowMs - this.config.maxAgeMs;
2982
+ for (const t of Object.keys(cache)) {
2983
+ if (t === tokenId) continue;
2984
+ const buffers = cache[t];
2985
+ if (!buffers) continue;
2986
+ const bufferList = Object.values(buffers);
2987
+ const anyFresh = bufferList.some((b) => b.length > 0 && b[b.length - 1].ts >= cutoff);
2988
+ if (!anyFresh) {
2989
+ delete cache[t];
2990
+ }
2991
+ }
2992
+ if (!cache[tokenId]) cache[tokenId] = {};
2993
+ const out = [];
2994
+ for (const sig of signals) {
2995
+ if (!FAST_SIGNAL_NAMES.has(sig.name)) {
2996
+ out.push(sig);
2997
+ continue;
2998
+ }
2999
+ const buffer = cache[tokenId][sig.name] ?? [];
3000
+ buffer.push({ ts: nowMs, value: sig.value, confidence: sig.confidence });
3001
+ const cutoff2 = nowMs - this.config.maxAgeMs;
3002
+ const fresh = buffer.filter((r) => r.ts >= cutoff2);
3003
+ const trimmed = fresh.slice(-this.config.windowSize);
3004
+ cache[tokenId][sig.name] = trimmed;
3005
+ const values = trimmed.map((r) => r.value);
3006
+ const meanValue = values.reduce((a, b) => a + b, 0) / values.length;
3007
+ const meanConfidence = trimmed.reduce((a, r) => a + r.confidence, 0) / trimmed.length;
3008
+ let smoothedConfidence = meanConfidence;
3009
+ if (trimmed.length > 1) {
3010
+ const variance = values.reduce((sum, v) => sum + (v - meanValue) ** 2, 0) / values.length;
3011
+ const stdDev = Math.sqrt(variance);
3012
+ const penalty = Math.min(this.config.maxConfidencePenalty, stdDev);
3013
+ smoothedConfidence = Math.max(0.05, meanConfidence * (1 - penalty));
3014
+ }
3015
+ out.push({
3016
+ ...sig,
3017
+ value: meanValue,
3018
+ confidence: smoothedConfidence,
3019
+ details: `${sig.details} [smoothed n=${trimmed.length}, raw=${sig.value.toFixed(2)}]`
3020
+ });
3021
+ }
3022
+ await this.storage.save(cache);
3023
+ return out;
3024
+ }
3025
+ };
3026
+
3027
+ // src/agent/index.ts
3028
+ import { join as joinPath } from "path";
3029
+ import { homedir as getHomedir } from "os";
3030
+
3031
+ // src/providers/data/hyperliquid.ts
3032
+ import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
3033
+ import { join as join5 } from "path";
3034
+ import { homedir as homedir5 } from "os";
2888
3035
  var HYPERLIQUID_BASE = "https://api.hyperliquid.xyz/info";
2889
3036
  var TOKEN_TO_COIN = {
2890
3037
  bitcoin: "BTC",
@@ -2920,7 +3067,7 @@ var HyperliquidProvider = class {
2920
3067
  oiCache = /* @__PURE__ */ new Map();
2921
3068
  // For tracking OI changes
2922
3069
  constructor() {
2923
- this.cacheDir = join4(homedir4(), ".sherwood", "agent", "cache");
3070
+ this.cacheDir = join5(homedir5(), ".sherwood", "agent", "cache");
2924
3071
  }
2925
3072
  /** Get comprehensive Hyperliquid data for a token. Returns null if token not supported or API failure. */
2926
3073
  async getHyperliquidData(tokenId) {
@@ -3043,8 +3190,8 @@ var HyperliquidProvider = class {
3043
3190
  /** Read cached Hyperliquid data. */
3044
3191
  async readCache(tokenId) {
3045
3192
  try {
3046
- const cacheFile = join4(this.cacheDir, `hl-${tokenId}.json`);
3047
- const raw = await readFile4(cacheFile, "utf-8");
3193
+ const cacheFile = join5(this.cacheDir, `hl-${tokenId}.json`);
3194
+ const raw = await readFile5(cacheFile, "utf-8");
3048
3195
  const cached = JSON.parse(raw);
3049
3196
  if (Date.now() - cached.ts < this.cacheTTL) {
3050
3197
  return cached.data;
@@ -3056,24 +3203,24 @@ var HyperliquidProvider = class {
3056
3203
  /** Write Hyperliquid data to cache. */
3057
3204
  async writeCache(tokenId, data) {
3058
3205
  try {
3059
- await mkdir4(this.cacheDir, { recursive: true });
3060
- const cacheFile = join4(this.cacheDir, `hl-${tokenId}.json`);
3061
- await writeFile4(cacheFile, JSON.stringify({ ts: Date.now(), data }), "utf-8");
3206
+ await mkdir6(this.cacheDir, { recursive: true });
3207
+ const cacheFile = join5(this.cacheDir, `hl-${tokenId}.json`);
3208
+ await writeFile5(cacheFile, JSON.stringify({ ts: Date.now(), data }), "utf-8");
3062
3209
  } catch {
3063
3210
  }
3064
3211
  }
3065
3212
  };
3066
3213
 
3067
3214
  // src/agent/regime.ts
3068
- import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
3069
- import { join as join5 } from "path";
3070
- import { homedir as homedir5 } from "os";
3071
- var MarketRegimeDetector = class {
3215
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
3216
+ import { join as join6 } from "path";
3217
+ import { homedir as homedir6 } from "os";
3218
+ var MarketRegimeDetector = class _MarketRegimeDetector {
3072
3219
  cacheDir;
3073
3220
  cacheFile;
3074
3221
  constructor() {
3075
- this.cacheDir = join5(homedir5(), ".sherwood", "agent", "cache");
3076
- this.cacheFile = join5(this.cacheDir, "regime.json");
3222
+ this.cacheDir = join6(homedir6(), ".sherwood", "agent", "cache");
3223
+ this.cacheFile = join6(this.cacheDir, "regime.json");
3077
3224
  }
3078
3225
  /**
3079
3226
  * Detect current market regime using BTC candles.
@@ -3097,6 +3244,84 @@ var MarketRegimeDetector = class {
3097
3244
  }
3098
3245
  return analysis;
3099
3246
  }
3247
+ /**
3248
+ * Pure regime classification from a candle window — no network calls,
3249
+ * no cache, no BTC dominance lookup. Use this from the backtester to
3250
+ * compute regime per-candle without look-ahead bias or external IO.
3251
+ */
3252
+ static classifyFromCandles(candles) {
3253
+ const detector = new _MarketRegimeDetector();
3254
+ return detector.classifySync(candles);
3255
+ }
3256
+ /** Synchronous candle-only analysis — same logic as analyzeBtc minus dominance/IO. */
3257
+ classifySync(candles) {
3258
+ if (candles.length < 200) {
3259
+ return this.fallbackAnalysis("Insufficient BTC data for regime analysis");
3260
+ }
3261
+ const closes = candles.map((c) => c.close);
3262
+ const currentPrice = closes[closes.length - 1];
3263
+ const ema50 = calculateEMA(closes, 50);
3264
+ const ema200 = calculateEMA(closes, 200);
3265
+ const currentEma50 = this.getLastValidValue(ema50);
3266
+ const currentEma200 = this.getLastValidValue(ema200);
3267
+ const bb = calculateBollingerBands(candles, 20, 2);
3268
+ const currentBbWidth = this.getLastValidValue(bb.width);
3269
+ const atr = calculateATR(candles, 14);
3270
+ const currentAtr = this.getLastValidValue(atr);
3271
+ const atrRatio = currentAtr / currentPrice;
3272
+ const adx = this.calculateADX(candles, 14);
3273
+ const currentAdx = this.getLastValidValue(adx);
3274
+ let btcTrend = "neutral";
3275
+ if (!isNaN(currentEma50) && !isNaN(currentEma200)) {
3276
+ if (currentPrice > currentEma50 && currentEma50 > currentEma200) {
3277
+ btcTrend = "up";
3278
+ } else if (currentPrice < currentEma50 && currentEma50 < currentEma200) {
3279
+ btcTrend = "down";
3280
+ }
3281
+ }
3282
+ let volatilityLevel = "normal";
3283
+ if (!isNaN(currentBbWidth)) {
3284
+ if (currentBbWidth < 0.04) volatilityLevel = "low";
3285
+ else if (currentBbWidth > 0.2) volatilityLevel = "extreme";
3286
+ else if (currentBbWidth > 0.1) volatilityLevel = "high";
3287
+ }
3288
+ let regime;
3289
+ let confidence;
3290
+ let details;
3291
+ if (volatilityLevel === "extreme") {
3292
+ regime = "high-volatility";
3293
+ confidence = 0.9;
3294
+ details = `Extreme volatility (BB ${currentBbWidth.toFixed(3)}, ATR ${atrRatio.toFixed(3)})`;
3295
+ } else if (volatilityLevel === "low" && currentAdx < 20) {
3296
+ regime = "low-volatility";
3297
+ confidence = 0.8;
3298
+ details = `Low volatility (BB ${currentBbWidth.toFixed(3)}, ADX ${currentAdx.toFixed(1)})`;
3299
+ } else {
3300
+ if (currentAdx > 25 && btcTrend !== "neutral") {
3301
+ if (btcTrend === "up") {
3302
+ regime = "trending-up";
3303
+ confidence = Math.min(0.9, 0.6 + (currentAdx - 25) * 0.01);
3304
+ details = `Strong uptrend (ADX ${currentAdx.toFixed(1)})`;
3305
+ } else {
3306
+ regime = "trending-down";
3307
+ confidence = Math.min(0.9, 0.6 + (currentAdx - 25) * 0.01);
3308
+ details = `Strong downtrend (ADX ${currentAdx.toFixed(1)})`;
3309
+ }
3310
+ } else {
3311
+ regime = "ranging";
3312
+ confidence = currentAdx < 20 ? 0.7 : 0.5;
3313
+ details = `Ranging (ADX ${currentAdx.toFixed(1)})`;
3314
+ }
3315
+ }
3316
+ return {
3317
+ regime,
3318
+ confidence,
3319
+ btcTrend,
3320
+ volatilityLevel,
3321
+ details,
3322
+ strategyAdjustments: this.getStrategyAdjustments(regime)
3323
+ };
3324
+ }
3100
3325
  async analyzeBtc(candles) {
3101
3326
  if (candles.length < 200) {
3102
3327
  return this.fallbackAnalysis("Insufficient BTC data for regime analysis");
@@ -3333,30 +3558,30 @@ var MarketRegimeDetector = class {
3333
3558
  };
3334
3559
  }
3335
3560
  async loadCache() {
3336
- const data = await readFile5(this.cacheFile, "utf-8");
3561
+ const data = await readFile6(this.cacheFile, "utf-8");
3337
3562
  return JSON.parse(data);
3338
3563
  }
3339
3564
  async saveCache(analysis) {
3340
- await mkdir5(this.cacheDir, { recursive: true });
3565
+ await mkdir7(this.cacheDir, { recursive: true });
3341
3566
  const cache = {
3342
3567
  timestamp: Date.now(),
3343
3568
  analysis
3344
3569
  };
3345
- await writeFile5(this.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
3570
+ await writeFile6(this.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
3346
3571
  }
3347
3572
  };
3348
3573
 
3349
3574
  // src/agent/correlation.ts
3350
- import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
3351
- import { join as join6 } from "path";
3352
- import { homedir as homedir6 } from "os";
3575
+ import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir8 } from "fs/promises";
3576
+ import { join as join7 } from "path";
3577
+ import { homedir as homedir7 } from "os";
3353
3578
  var CorrelationGuard = class {
3354
3579
  cacheDir;
3355
3580
  cacheFile;
3356
3581
  coingecko;
3357
3582
  constructor() {
3358
- this.cacheDir = join6(homedir6(), ".sherwood", "agent", "cache");
3359
- this.cacheFile = join6(this.cacheDir, "btc-correlation.json");
3583
+ this.cacheDir = join7(homedir7(), ".sherwood", "agent", "cache");
3584
+ this.cacheFile = join7(this.cacheDir, "btc-correlation.json");
3360
3585
  this.coingecko = new CoinGeckoProvider();
3361
3586
  }
3362
3587
  /**
@@ -3561,23 +3786,23 @@ var CorrelationGuard = class {
3561
3786
  return NaN;
3562
3787
  }
3563
3788
  async loadCache() {
3564
- const data = await readFile6(this.cacheFile, "utf-8");
3789
+ const data = await readFile7(this.cacheFile, "utf-8");
3565
3790
  return JSON.parse(data);
3566
3791
  }
3567
3792
  async saveCache(btcStructure) {
3568
- await mkdir6(this.cacheDir, { recursive: true });
3793
+ await mkdir8(this.cacheDir, { recursive: true });
3569
3794
  const cache = {
3570
3795
  timestamp: Date.now(),
3571
3796
  btcStructure
3572
3797
  };
3573
- await writeFile6(this.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
3798
+ await writeFile7(this.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
3574
3799
  }
3575
3800
  };
3576
3801
 
3577
3802
  // src/agent/alerts.ts
3578
- import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
3579
- import { join as join7 } from "path";
3580
- import { homedir as homedir7 } from "os";
3803
+ import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir9 } from "fs/promises";
3804
+ import { join as join8 } from "path";
3805
+ import { homedir as homedir8 } from "os";
3581
3806
  import { createHash as createHash2 } from "crypto";
3582
3807
  var DEFAULT_CONFIG = {
3583
3808
  enableRegimeAlerts: true,
@@ -3591,9 +3816,9 @@ var AlertSystem = class {
3591
3816
  configFile;
3592
3817
  config;
3593
3818
  constructor() {
3594
- this.stateDir = join7(homedir7(), ".sherwood", "agent");
3595
- this.stateFile = join7(this.stateDir, "alerts-state.json");
3596
- this.configFile = join7(this.stateDir, "alerts-config.json");
3819
+ this.stateDir = join8(homedir8(), ".sherwood", "agent");
3820
+ this.stateFile = join8(this.stateDir, "alerts-state.json");
3821
+ this.configFile = join8(this.stateDir, "alerts-config.json");
3597
3822
  this.config = DEFAULT_CONFIG;
3598
3823
  }
3599
3824
  /**
@@ -3840,7 +4065,7 @@ var AlertSystem = class {
3840
4065
  */
3841
4066
  async loadState() {
3842
4067
  try {
3843
- const data = await readFile7(this.stateFile, "utf-8");
4068
+ const data = await readFile8(this.stateFile, "utf-8");
3844
4069
  return JSON.parse(data);
3845
4070
  } catch {
3846
4071
  return {
@@ -3857,8 +4082,8 @@ var AlertSystem = class {
3857
4082
  */
3858
4083
  async saveState(state) {
3859
4084
  try {
3860
- await mkdir7(this.stateDir, { recursive: true });
3861
- await writeFile7(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
4085
+ await mkdir9(this.stateDir, { recursive: true });
4086
+ await writeFile8(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
3862
4087
  } catch (err) {
3863
4088
  console.error(`Failed to save alert state: ${err.message}`);
3864
4089
  }
@@ -3930,6 +4155,7 @@ var TradingAgent = class {
3930
4155
  regimeDetector;
3931
4156
  correlationGuard;
3932
4157
  alertSystem;
4158
+ smoother = null;
3933
4159
  constructor(config) {
3934
4160
  this.config = config;
3935
4161
  this.defillama = new DefiLlamaProvider();
@@ -4100,7 +4326,7 @@ var TradingAgent = class {
4100
4326
  }
4101
4327
  let marketData = void 0;
4102
4328
  try {
4103
- const [symbolResult, fundingRateResult, unlockResult, twitterResult] = await Promise.allSettled([
4329
+ const [symbolResult, fundingRateResult, unlockResult] = await Promise.allSettled([
4104
4330
  // Resolve token symbol + get market data for strategies
4105
4331
  this.coingecko.getCoinDetails(tokenId).then(async (coinDetails) => {
4106
4332
  const symbol = coinDetails?.symbol?.toUpperCase();
@@ -4110,9 +4336,7 @@ var TradingAgent = class {
4110
4336
  // Fetch funding rate data (free, from Binance)
4111
4337
  this.fundingRate.getFundingRate(tokenId),
4112
4338
  // Fetch token unlock estimates (free, from DefiLlama FDV)
4113
- this.tokenUnlocks.getUnlocks(tokenId),
4114
- // Fetch Twitter sentiment data (free tier with auth)
4115
- this.twitter.getSentiment(tokenId)
4339
+ this.tokenUnlocks.getUnlocks(tokenId)
4116
4340
  ]);
4117
4341
  let tokenSymbol;
4118
4342
  if (symbolResult.status === "fulfilled" && symbolResult.value) {
@@ -4120,7 +4344,12 @@ var TradingAgent = class {
4120
4344
  marketData = symbolResult.value.marketData;
4121
4345
  }
4122
4346
  let fundingRateData = void 0;
4123
- if (fundingRateResult.status === "fulfilled" && fundingRateResult.value) {
4347
+ if (hyperliquidData && typeof hyperliquidData.fundingRate === "number" && Number.isFinite(hyperliquidData.fundingRate)) {
4348
+ const rate1h = hyperliquidData.fundingRate;
4349
+ const rate8h = rate1h * 8;
4350
+ const annualizedRate = rate1h * 24 * 365;
4351
+ fundingRateData = { rate8h, annualizedRate, exchange: "hyperliquid" };
4352
+ } else if (fundingRateResult.status === "fulfilled" && fundingRateResult.value) {
4124
4353
  const fr = fundingRateResult.value;
4125
4354
  fundingRateData = { rate8h: fr.rate8h, annualizedRate: fr.annualizedRate, exchange: fr.exchange };
4126
4355
  }
@@ -4128,12 +4357,7 @@ var TradingAgent = class {
4128
4357
  if (unlockResult.status === "fulfilled" && unlockResult.value) {
4129
4358
  unlockData = unlockResult.value;
4130
4359
  }
4131
- let twitterData = void 0;
4132
- if (twitterResult.status === "fulfilled" && twitterResult.value) {
4133
- twitterData = twitterResult.value;
4134
- } else if (twitterResult.status === "rejected") {
4135
- console.error(chalk.dim(` Twitter sentiment failed: ${twitterResult.reason}`));
4136
- }
4360
+ const twitterData = void 0;
4137
4361
  const stratCtx = {
4138
4362
  tokenId,
4139
4363
  candles,
@@ -4161,7 +4385,20 @@ var TradingAgent = class {
4161
4385
  tokenSymbol
4162
4386
  // from phase 3
4163
4387
  };
4164
- const strategySignals = await runStrategies(stratCtx, this.config.strategyConfigs);
4388
+ let strategySignals = await runStrategies(stratCtx, this.config.strategyConfigs);
4389
+ if (this.config.smoothFastSignals) {
4390
+ if (!this.smoother) {
4391
+ this.smoother = new SignalSmoother(
4392
+ new FileSmootherStorage(joinPath(getHomedir(), ".sherwood", "agent", "signal-cache.json")),
4393
+ DEFAULT_SMOOTHER_CONFIG
4394
+ );
4395
+ }
4396
+ try {
4397
+ strategySignals = await this.smoother.smooth(tokenId, strategySignals);
4398
+ } catch (err) {
4399
+ console.error(chalk.dim(` Signal smoothing failed (using raw): ${err.message}`));
4400
+ }
4401
+ }
4165
4402
  for (const sig of strategySignals) {
4166
4403
  if (sig.confidence > 0.05) {
4167
4404
  signals.push(sig);
@@ -4199,19 +4436,24 @@ var TradingAgent = class {
4199
4436
  } catch (err) {
4200
4437
  console.error(chalk.dim(` Correlation check failed: ${err.message}`));
4201
4438
  }
4439
+ const resolvedWeights = this.config.weights ?? profileForToken(tokenId, this.config.weightProfile);
4202
4440
  const decision = computeTradeDecision(
4203
4441
  signals,
4204
- this.config.weights ?? DEFAULT_WEIGHTS,
4442
+ resolvedWeights,
4205
4443
  regimeAnalysis?.strategyAdjustments,
4206
- correlationCheck
4444
+ correlationCheck,
4445
+ regimeAnalysis?.regime
4207
4446
  );
4208
- return {
4447
+ const result = {
4209
4448
  token: tokenId,
4210
4449
  decision,
4211
4450
  data: { technicalSignals, fearAndGreed: fearAndGreedValue, tvl },
4212
4451
  regime: regimeAnalysis,
4213
4452
  correlation: correlationCheck
4214
4453
  };
4454
+ const currentPrice = hyperliquidData?.markPrice ?? (candles && candles.length > 0 ? candles[candles.length - 1].close : 0);
4455
+ logSignal(result, currentPrice, resolvedWeights);
4456
+ return result;
4215
4457
  }
4216
4458
  /** Update the token watchlist without recreating the agent (preserves caches). */
4217
4459
  updateTokens(tokens) {
@@ -4323,9 +4565,9 @@ var TradingAgent = class {
4323
4565
  };
4324
4566
 
4325
4567
  // src/agent/token-selector.ts
4326
- import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
4327
- import { join as join8 } from "path";
4328
- import { homedir as homedir8 } from "os";
4568
+ import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
4569
+ import { join as join9 } from "path";
4570
+ import { homedir as homedir9 } from "os";
4329
4571
  var HYPERLIQUID_BASE2 = "https://api.hyperliquid.xyz/info";
4330
4572
  var HYPERLIQUID_TO_COINGECKO = {
4331
4573
  BTC: "bitcoin",
@@ -4459,7 +4701,7 @@ var DynamicTokenSelector = class {
4459
4701
  cacheTTL = 30 * 60 * 1e3;
4460
4702
  // 30 minutes
4461
4703
  constructor() {
4462
- this.cacheDir = join8(homedir8(), ".sherwood", "agent", "cache");
4704
+ this.cacheDir = join9(homedir9(), ".sherwood", "agent", "cache");
4463
4705
  }
4464
4706
  /** Get dynamic token selection based on Hyperliquid market data. */
4465
4707
  async selectTokens(criteria) {
@@ -4591,8 +4833,8 @@ var DynamicTokenSelector = class {
4591
4833
  /** Read cached token selection. */
4592
4834
  async readCache() {
4593
4835
  try {
4594
- const cacheFile = join8(this.cacheDir, "token-selection.json");
4595
- const raw = await readFile8(cacheFile, "utf-8");
4836
+ const cacheFile = join9(this.cacheDir, "token-selection.json");
4837
+ const raw = await readFile9(cacheFile, "utf-8");
4596
4838
  const cached = JSON.parse(raw);
4597
4839
  if (Date.now() - cached.timestamp < this.cacheTTL) {
4598
4840
  return cached;
@@ -4604,9 +4846,9 @@ var DynamicTokenSelector = class {
4604
4846
  /** Write token selection to cache. */
4605
4847
  async writeCache(selection) {
4606
4848
  try {
4607
- await mkdir8(this.cacheDir, { recursive: true });
4608
- const cacheFile = join8(this.cacheDir, "token-selection.json");
4609
- await writeFile8(cacheFile, JSON.stringify(selection, null, 2), "utf-8");
4849
+ await mkdir10(this.cacheDir, { recursive: true });
4850
+ const cacheFile = join9(this.cacheDir, "token-selection.json");
4851
+ await writeFile9(cacheFile, JSON.stringify(selection, null, 2), "utf-8");
4610
4852
  } catch {
4611
4853
  }
4612
4854
  }
@@ -4651,9 +4893,9 @@ var DynamicTokenSelector = class {
4651
4893
 
4652
4894
  // src/agent/loop.ts
4653
4895
  import chalk6 from "chalk";
4654
- import { mkdir as mkdir10, appendFile } from "fs/promises";
4655
- import { join as join10 } from "path";
4656
- import { homedir as homedir10 } from "os";
4896
+ import { mkdir as mkdir12, appendFile as appendFile2 } from "fs/promises";
4897
+ import { join as join11 } from "path";
4898
+ import { homedir as homedir11 } from "os";
4657
4899
 
4658
4900
  // src/agent/executor.ts
4659
4901
  import chalk2 from "chalk";
@@ -4919,9 +5161,9 @@ async function getSzDecimals(assetIndex) {
4919
5161
  }
4920
5162
 
4921
5163
  // src/agent/portfolio.ts
4922
- import { mkdir as mkdir9, readFile as readFile9, writeFile as writeFile9, rename } from "fs/promises";
4923
- import { dirname, join as join9 } from "path";
4924
- import { homedir as homedir9 } from "os";
5164
+ import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile10, rename as rename2 } from "fs/promises";
5165
+ import { dirname as dirname2, join as join10 } from "path";
5166
+ import { homedir as homedir10 } from "os";
4925
5167
  import chalk3 from "chalk";
4926
5168
  function resetPnlCounters(state) {
4927
5169
  const now = Date.now();
@@ -4966,15 +5208,15 @@ var PortfolioTracker = class {
4966
5208
  historyPath;
4967
5209
  state;
4968
5210
  constructor() {
4969
- const base = join9(homedir9(), ".sherwood", "agent");
4970
- this.statePath = join9(base, "portfolio.json");
4971
- this.historyPath = join9(base, "trades.json");
5211
+ const base = join10(homedir10(), ".sherwood", "agent");
5212
+ this.statePath = join10(base, "portfolio.json");
5213
+ this.historyPath = join10(base, "trades.json");
4972
5214
  this.state = { ...DEFAULT_PORTFOLIO };
4973
5215
  }
4974
5216
  /** Load portfolio state from disk with validation */
4975
5217
  async load() {
4976
5218
  try {
4977
- const data = await readFile9(this.statePath, "utf-8");
5219
+ const data = await readFile10(this.statePath, "utf-8");
4978
5220
  const parsed = JSON.parse(data);
4979
5221
  if (!Number.isFinite(parsed.totalValue) || parsed.totalValue < 0 || !Number.isFinite(parsed.cash) || parsed.cash < 0 || !Number.isFinite(parsed.dailyPnl) || !Number.isFinite(parsed.weeklyPnl) || !Number.isFinite(parsed.monthlyPnl) || !Array.isArray(parsed.positions)) {
4980
5222
  console.error("Portfolio file has invalid data \u2014 resetting to defaults");
@@ -4997,10 +5239,10 @@ var PortfolioTracker = class {
4997
5239
  /** Save portfolio state to disk (atomic write to prevent corruption) */
4998
5240
  async save(state) {
4999
5241
  this.state = state;
5000
- await mkdir9(dirname(this.statePath), { recursive: true });
5242
+ await mkdir11(dirname2(this.statePath), { recursive: true });
5001
5243
  const tmpPath = this.statePath + ".tmp";
5002
- await writeFile9(tmpPath, JSON.stringify(state, null, 2), "utf-8");
5003
- await rename(tmpPath, this.statePath);
5244
+ await writeFile10(tmpPath, JSON.stringify(state, null, 2), "utf-8");
5245
+ await rename2(tmpPath, this.statePath);
5004
5246
  }
5005
5247
  /** Open a new position */
5006
5248
  async openPosition(position) {
@@ -5086,7 +5328,7 @@ var PortfolioTracker = class {
5086
5328
  async getHistory(days) {
5087
5329
  let records = [];
5088
5330
  try {
5089
- const data = await readFile9(this.historyPath, "utf-8");
5331
+ const data = await readFile10(this.historyPath, "utf-8");
5090
5332
  records = JSON.parse(data);
5091
5333
  } catch {
5092
5334
  return [];
@@ -5204,15 +5446,15 @@ var PortfolioTracker = class {
5204
5446
  async appendTradeRecord(record) {
5205
5447
  let records = [];
5206
5448
  try {
5207
- const data = await readFile9(this.historyPath, "utf-8");
5449
+ const data = await readFile10(this.historyPath, "utf-8");
5208
5450
  records = JSON.parse(data);
5209
5451
  } catch {
5210
5452
  }
5211
5453
  records.push(record);
5212
- await mkdir9(dirname(this.historyPath), { recursive: true });
5454
+ await mkdir11(dirname2(this.historyPath), { recursive: true });
5213
5455
  const tmpPath = this.historyPath + ".tmp";
5214
- await writeFile9(tmpPath, JSON.stringify(records, null, 2), "utf-8");
5215
- await rename(tmpPath, this.historyPath);
5456
+ await writeFile10(tmpPath, JSON.stringify(records, null, 2), "utf-8");
5457
+ await rename2(tmpPath, this.historyPath);
5216
5458
  }
5217
5459
  };
5218
5460
 
@@ -5225,6 +5467,12 @@ var DEFAULT_RISK_CONFIG = {
5225
5467
  maxConcurrentTrades: 8,
5226
5468
  hardStopPercent: 0.12,
5227
5469
  trailingStopAtr: 1.5,
5470
+ trailingStopPct: 0,
5471
+ // OFF — opt in via config
5472
+ breakevenTriggerPct: 0,
5473
+ // OFF — opt in via config
5474
+ profitLockSteps: [],
5475
+ // OFF — opt in via config
5228
5476
  dailyLossLimit: 0.05,
5229
5477
  weeklyLossLimit: 0.1,
5230
5478
  monthlyLossLimit: 0.15,
@@ -5386,6 +5634,53 @@ var RiskManager = class {
5386
5634
  }
5387
5635
  return { paused: false, level: null, message: "Within limits" };
5388
5636
  }
5637
+ /**
5638
+ * Ratchet stop-losses upward each cycle based on current prices.
5639
+ * Applies three independent mechanisms, whichever gives the highest stop:
5640
+ *
5641
+ * 1. Breakeven: if PnL% crosses +breakevenTriggerPct, move stop to entryPrice.
5642
+ * Locks in "no loss" once the trade is meaningfully in the money.
5643
+ *
5644
+ * 2. Profit-lock ratchet: stepped table (e.g. after +5% gain, lock in +2%).
5645
+ * Each triggered step moves stop to entryPrice × (1 + lock).
5646
+ * Prevents giving back earned profit on mean reversion.
5647
+ *
5648
+ * 3. Percent-trail: stop = max(currentStop, currentPrice × (1 - trailingStopPct)).
5649
+ * Continuous trail — tracks new highs as price runs.
5650
+ *
5651
+ * All mechanisms respect the "stops never move down" invariant.
5652
+ * Returns an array of updated positions (pure — does not mutate input).
5653
+ *
5654
+ * No ATR needed — works from price alone, so it can run every loop cycle
5655
+ * without re-computing technicals. Use updateStopLosses() when ATR is
5656
+ * already available (keeps tighter trails on volatile assets).
5657
+ */
5658
+ updateTrailingStops(positions) {
5659
+ return positions.map((pos) => {
5660
+ if (pos.currentPrice <= 0 || pos.entryPrice <= 0) return pos;
5661
+ const pnlPct = (pos.currentPrice - pos.entryPrice) / pos.entryPrice;
5662
+ 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
+ for (const step of this.config.profitLockSteps) {
5668
+ if (pnlPct >= step.trigger) {
5669
+ const lockedStop = pos.entryPrice * (1 + step.lock);
5670
+ newStop = Math.max(newStop, lockedStop);
5671
+ }
5672
+ }
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 };
5680
+ }
5681
+ return pos;
5682
+ });
5683
+ }
5389
5684
  /** Update trailing stop losses using ATR values */
5390
5685
  updateStopLosses(positions, atrValues) {
5391
5686
  return positions.map((pos) => {
@@ -5616,8 +5911,8 @@ var AgentLoop = class {
5616
5911
  /** Start the autonomous loop */
5617
5912
  async start() {
5618
5913
  this.running = true;
5619
- const stateDir = join10(homedir10(), ".sherwood", "agent");
5620
- await mkdir10(stateDir, { recursive: true });
5914
+ const stateDir = join11(homedir11(), ".sherwood", "agent");
5915
+ await mkdir12(stateDir, { recursive: true });
5621
5916
  const cfg = this.config.agent;
5622
5917
  console.log("");
5623
5918
  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"));
@@ -5685,7 +5980,26 @@ var AgentLoop = class {
5685
5980
  }
5686
5981
  }
5687
5982
  if (Object.keys(currentPrices).length > 0) {
5688
- await this.portfolio.updatePrices(currentPrices);
5983
+ const priceState = await this.portfolio.updatePrices(currentPrices);
5984
+ const tightened = this.riskManager.updateTrailingStops(priceState.positions);
5985
+ const anyChanged = tightened.some(
5986
+ (p, i) => p.stopLoss !== priceState.positions[i].stopLoss
5987
+ );
5988
+ if (anyChanged) {
5989
+ for (let i = 0; i < tightened.length; i++) {
5990
+ const before = priceState.positions[i];
5991
+ const after = tightened[i];
5992
+ if (after.stopLoss > before.stopLoss) {
5993
+ console.log(
5994
+ chalk6.dim(
5995
+ ` Stop tightened: ${after.symbol} $${before.stopLoss.toFixed(4)} \u2192 $${after.stopLoss.toFixed(4)}`
5996
+ )
5997
+ );
5998
+ }
5999
+ }
6000
+ priceState.positions = tightened;
6001
+ await this.portfolio.save(priceState);
6002
+ }
5689
6003
  const exits = await this.executor.processExits(currentPrices);
5690
6004
  exitsProcessed = exits.length;
5691
6005
  for (const exit of exits) {
@@ -5766,10 +6080,10 @@ var AgentLoop = class {
5766
6080
  }
5767
6081
  /** Log cycle result to a JSONL file */
5768
6082
  async logCycle(result) {
5769
- const logPath = this.config.logPath ?? join10(homedir10(), ".sherwood", "agent", "cycles.jsonl");
6083
+ const logPath = this.config.logPath ?? join11(homedir11(), ".sherwood", "agent", "cycles.jsonl");
5770
6084
  try {
5771
- await mkdir10(join10(homedir10(), ".sherwood", "agent"), { recursive: true });
5772
- await appendFile(logPath, JSON.stringify(result) + "\n", "utf-8");
6085
+ await mkdir12(join11(homedir11(), ".sherwood", "agent"), { recursive: true });
6086
+ await appendFile2(logPath, JSON.stringify(result) + "\n", "utf-8");
5773
6087
  } catch (err) {
5774
6088
  console.error(chalk6.dim(` Failed to write cycle log: ${err.message}`));
5775
6089
  }
@@ -5824,6 +6138,15 @@ var Backtester = class _Backtester {
5824
6138
  }
5825
6139
  /** Run backtest using historical data from CoinGecko. */
5826
6140
  async run() {
6141
+ const data = await this.fetchData();
6142
+ return this.simulate(data.candles, data.fearAndGreedData);
6143
+ }
6144
+ /**
6145
+ * Fetch historical OHLC + Fear & Greed data for the configured token/date range.
6146
+ * Extracted so callers (e.g. the calibrator) can fetch once and replay many
6147
+ * configurations against the same dataset without re-hitting CoinGecko.
6148
+ */
6149
+ async fetchData() {
5827
6150
  const startMs = new Date(this.config.startDate).getTime();
5828
6151
  const endMs = new Date(this.config.endDate).getTime();
5829
6152
  const totalDays = Math.ceil((endMs - startMs) / (24 * 60 * 60 * 1e3));
@@ -5881,10 +6204,19 @@ var Backtester = class _Backtester {
5881
6204
  console.log(chalk7.yellow(` Warning: Could not fetch Fear & Greed data: ${error.message}`));
5882
6205
  }
5883
6206
  }
6207
+ return { candles: allCandles, fearAndGreedData };
6208
+ }
6209
+ /**
6210
+ * Run the simulation loop on pre-fetched data. Pure of network IO.
6211
+ * The calibrator uses this directly after fetchData() to replay many
6212
+ * config permutations without re-fetching per permutation.
6213
+ */
6214
+ async simulate(allCandles, fearAndGreedData) {
5884
6215
  const stepSize = this.config.cycle === "1h" ? 1 : this.config.cycle === "4h" ? 4 : 24;
5885
6216
  const step = Math.max(1, Math.floor(stepSize / 24));
5886
6217
  let capital = this.config.initialCapital;
5887
6218
  let position = null;
6219
+ const smoother = this.config.smoothFastSignals ? new SignalSmoother(new MemorySmootherStorage(), DEFAULT_SMOOTHER_CONFIG) : null;
5888
6220
  const trades = [];
5889
6221
  const equityCurve = [];
5890
6222
  const returns = [];
@@ -5925,7 +6257,10 @@ var Backtester = class _Backtester {
5925
6257
  technicals,
5926
6258
  fearAndGreed
5927
6259
  };
5928
- const signals = await runStrategies(ctx);
6260
+ let signals = await runStrategies(ctx);
6261
+ if (smoother) {
6262
+ signals = await smoother.smooth(this.config.tokenId, signals, currentCandle.timestamp);
6263
+ }
5929
6264
  let filtered = signals;
5930
6265
  if (this.config.strategies.length > 0) {
5931
6266
  filtered = signals.filter((s) => this.config.strategies.some(
@@ -5934,13 +6269,42 @@ var Backtester = class _Backtester {
5934
6269
  } else {
5935
6270
  filtered = signals.filter((s) => CANDLE_BASED_STRATEGIES.includes(s.name) || fearAndGreed && s.name === "sentimentContrarian");
5936
6271
  }
5937
- decision = computeTradeDecision(filtered.length > 0 ? filtered : signals);
6272
+ let regime;
6273
+ if (this.config.useRegime) {
6274
+ regime = MarketRegimeDetector.classifyFromCandles(windowCandles).regime;
6275
+ }
6276
+ decision = computeTradeDecision(
6277
+ filtered.length > 0 ? filtered : signals,
6278
+ this.config.customWeights,
6279
+ void 0,
6280
+ void 0,
6281
+ regime
6282
+ );
6283
+ if (this.config.buyThreshold !== void 0 || this.config.sellThreshold !== void 0) {
6284
+ const buy = this.config.buyThreshold ?? decision.thresholds?.buy ?? 0.3;
6285
+ const sell = this.config.sellThreshold ?? decision.thresholds?.sell ?? -0.3;
6286
+ if (buy <= sell) {
6287
+ throw new Error(
6288
+ `Invalid threshold override: buyThreshold (${buy}) must be strictly greater than sellThreshold (${sell}). Collision would produce ambiguous actions at score == threshold.`
6289
+ );
6290
+ }
6291
+ const strongBuy = buy + 0.3;
6292
+ const strongSell = sell - 0.3;
6293
+ let action;
6294
+ if (decision.score >= strongBuy) action = "STRONG_BUY";
6295
+ else if (decision.score >= buy) action = "BUY";
6296
+ else if (decision.score <= strongSell) action = "STRONG_SELL";
6297
+ else if (decision.score <= sell) action = "SELL";
6298
+ else action = "HOLD";
6299
+ decision = { ...decision, action, thresholds: { strongBuy, buy, sell, strongSell } };
6300
+ }
5938
6301
  if (this.config.verbose) {
5939
6302
  const activeSignals = filtered.filter((s) => Math.abs(s.value) > 0.01);
5940
6303
  const signalSummary = activeSignals.map(
5941
6304
  (s) => `${s.name}:${s.value >= 0 ? "+" : ""}${s.value.toFixed(2)}`
5942
6305
  ).join(" ");
5943
- console.log(`${currentDate}: score=${decision.score.toFixed(2)} ${decision.action} (${signalSummary || "no signals"})`);
6306
+ const regimeTag = regime ? ` [${regime}]` : "";
6307
+ console.log(`${currentDate}: score=${decision.score.toFixed(2)} ${decision.action}${regimeTag} (${signalSummary || "no signals"})`);
5944
6308
  }
5945
6309
  } catch {
5946
6310
  continue;
@@ -5949,21 +6313,30 @@ var Backtester = class _Backtester {
5949
6313
  position = {
5950
6314
  entryPrice: currentPrice,
5951
6315
  entryDate: currentDate,
5952
- signal: decision.action
6316
+ signal: decision.action,
6317
+ highWaterMark: currentPrice
5953
6318
  };
5954
- } else if (position && (decision.action === "SELL" || decision.action === "STRONG_SELL")) {
5955
- const pnlPercent = (currentPrice - position.entryPrice) / position.entryPrice;
5956
- capital *= 1 + pnlPercent;
5957
- returns.push(pnlPercent);
5958
- trades.push({
5959
- entryDate: position.entryDate,
5960
- exitDate: currentDate,
5961
- entryPrice: position.entryPrice,
5962
- exitPrice: currentPrice,
5963
- pnlPercent,
5964
- signal: `${position.signal} \u2192 ${decision.action}`
5965
- });
5966
- position = null;
6319
+ } else if (position) {
6320
+ if (currentPrice > position.highWaterMark) {
6321
+ position.highWaterMark = currentPrice;
6322
+ }
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;
6327
+ capital *= 1 + pnlPercent;
6328
+ returns.push(pnlPercent);
6329
+ const exitReason = trailingStopHit ? `TRAILING_STOP (-${(this.config.trailingStopPct * 100).toFixed(1)}%)` : decision.action;
6330
+ trades.push({
6331
+ entryDate: position.entryDate,
6332
+ exitDate: currentDate,
6333
+ entryPrice: position.entryPrice,
6334
+ exitPrice: currentPrice,
6335
+ pnlPercent,
6336
+ signal: `${position.signal} \u2192 ${exitReason}`
6337
+ });
6338
+ position = null;
6339
+ }
5967
6340
  }
5968
6341
  }
5969
6342
  if (position && allCandles.length > 0) {
@@ -6042,8 +6415,11 @@ var Backtester = class _Backtester {
6042
6415
  initialCapital: config.capital,
6043
6416
  strategies: config.strategies,
6044
6417
  cycle: "1d",
6045
- verbose: false
6418
+ verbose: false,
6046
6419
  // Don't spam verbose output during walk-forward
6420
+ useRegime: config.useRegime,
6421
+ trailingStopPct: config.trailingStopPct,
6422
+ smoothFastSignals: config.smoothFastSignals
6047
6423
  });
6048
6424
  const trainResult = await trainBacktester.run();
6049
6425
  const testBacktester = new _Backtester({
@@ -6053,8 +6429,11 @@ var Backtester = class _Backtester {
6053
6429
  initialCapital: config.capital,
6054
6430
  strategies: config.strategies,
6055
6431
  cycle: "1d",
6056
- verbose: false
6432
+ verbose: false,
6057
6433
  // Don't spam verbose output during walk-forward
6434
+ useRegime: config.useRegime,
6435
+ trailingStopPct: config.trailingStopPct,
6436
+ smoothFastSignals: config.smoothFastSignals
6058
6437
  });
6059
6438
  const testResult = await testBacktester.run();
6060
6439
  const fold = {
@@ -6434,19 +6813,44 @@ ${alertText}`);
6434
6813
  };
6435
6814
 
6436
6815
  // src/agent/execution-pipeline.ts
6816
+ var DEFAULT_SCALED_SIZING = {
6817
+ enabled: false,
6818
+ floor: 0.3,
6819
+ ceiling: 0.6,
6820
+ minRiskPct: 5e-3,
6821
+ maxRiskPct: 0.02
6822
+ };
6437
6823
  var ExecutionPipeline = class {
6438
6824
  /**
6439
6825
  * Generate trade proposals from analysis results.
6440
- * Only creates proposals for high-confidence opportunities (score > 0.4, confidence > 50%).
6826
+ * Only creates proposals for high-confidence opportunities uses the
6827
+ * regime-conditional BUY/SELL threshold from the decision (defaults to
6828
+ * ±0.4 if the decision lacks regime context).
6829
+ *
6830
+ * Optional ProposalOptions.scaledSizing enables score-weighted sizing.
6831
+ * Previously this was a mutable static field — fixed per code review
6832
+ * to eliminate cross-invocation leakage in long-lived processes.
6441
6833
  */
6442
- static generateProposals(analyses, portfolioValueUsd = 1e4) {
6834
+ static generateProposals(analyses, portfolioValueOrOpts = 1e4, optsArg) {
6835
+ const opts = typeof portfolioValueOrOpts === "number" ? { portfolioValueUsd: portfolioValueOrOpts, ...optsArg ?? {} } : portfolioValueOrOpts ?? {};
6836
+ const portfolioValueUsd = opts.portfolioValueUsd ?? 1e4;
6837
+ const scaledSizing = opts.scaledSizing ?? DEFAULT_SCALED_SIZING;
6443
6838
  const proposals = [];
6444
6839
  for (const analysis of analyses) {
6445
6840
  const { decision, token, data } = analysis;
6446
- if (Math.abs(decision.score) <= 0.4 || decision.confidence <= 0.5) {
6841
+ if (decision.action === "HOLD") {
6447
6842
  continue;
6448
6843
  }
6449
- if (decision.action === "HOLD") {
6844
+ const baseBuyFloor = decision.thresholds?.buy ?? 0.4;
6845
+ const baseSellFloor = decision.thresholds?.sell ?? -0.4;
6846
+ const buyFloor = scaledSizing.enabled ? Math.min(baseBuyFloor, scaledSizing.floor) : baseBuyFloor;
6847
+ const sellFloor = scaledSizing.enabled ? Math.max(baseSellFloor, -scaledSizing.floor) : baseSellFloor;
6848
+ const clearsBuy = decision.score >= buyFloor;
6849
+ const clearsSell = decision.score <= sellFloor;
6850
+ if (!clearsBuy && !clearsSell) {
6851
+ continue;
6852
+ }
6853
+ if (decision.confidence <= 0.5) {
6450
6854
  continue;
6451
6855
  }
6452
6856
  const isLong = decision.score > 0;
@@ -6458,7 +6862,15 @@ var ExecutionPipeline = class {
6458
6862
  const stopLoss = isLong ? entryPrice - 2 * atr : entryPrice + 2 * atr;
6459
6863
  const riskAmount = Math.abs(entryPrice - stopLoss);
6460
6864
  const takeProfit = isLong ? entryPrice + 2.5 * riskAmount : entryPrice - 2.5 * riskAmount;
6461
- const riskPercentage = 0.02;
6865
+ let riskPercentage;
6866
+ if (scaledSizing.enabled) {
6867
+ const cfg = scaledSizing;
6868
+ const absScore = Math.abs(decision.score);
6869
+ const t = Math.min(1, Math.max(0, (absScore - cfg.floor) / (cfg.ceiling - cfg.floor)));
6870
+ riskPercentage = cfg.minRiskPct + t * (cfg.maxRiskPct - cfg.minRiskPct);
6871
+ } else {
6872
+ riskPercentage = 0.02;
6873
+ }
6462
6874
  const positionSizeUsd = portfolioValueUsd * riskPercentage / (Math.abs(entryPrice - stopLoss) / entryPrice);
6463
6875
  const maxAllocation = portfolioValueUsd * 0.1;
6464
6876
  const leverage = Math.min(5, positionSizeUsd / maxAllocation);
@@ -6568,6 +6980,221 @@ _Generated: ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}_`);
6568
6980
  }
6569
6981
  };
6570
6982
 
6983
+ // src/agent/signal-audit.ts
6984
+ import { readFile as readFile11 } from "fs/promises";
6985
+ var FIRE_EPSILON = 0.05;
6986
+ var SIGNAL_TO_CATEGORY = {
6987
+ // Direct categories
6988
+ technical: "technical",
6989
+ sentiment: "sentiment",
6990
+ onchain: "onchain",
6991
+ fundamental: "fundamental",
6992
+ event: "event",
6993
+ smartMoney: "smartMoney",
6994
+ // Strategy-level signals mapped to their category
6995
+ breakoutOnChain: "technical",
6996
+ meanReversion: "technical",
6997
+ multiTimeframe: "technical",
6998
+ dexFlow: "onchain",
6999
+ fundingRate: "fundamental",
7000
+ tvlMomentum: "fundamental",
7001
+ sentimentContrarian: "sentiment",
7002
+ twitterSentiment: "sentiment",
7003
+ tokenUnlock: "event",
7004
+ hyperliquidFlow: "onchain"
7005
+ };
7006
+ async function auditSignalHistory(filePath = getSignalFilePath()) {
7007
+ let raw;
7008
+ try {
7009
+ raw = await readFile11(filePath, "utf-8");
7010
+ } catch (err) {
7011
+ if (err.code === "ENOENT") {
7012
+ return {
7013
+ filePath,
7014
+ totalEntries: 0,
7015
+ dateRange: null,
7016
+ perSignal: [],
7017
+ perCategory: []
7018
+ };
7019
+ }
7020
+ throw err;
7021
+ }
7022
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
7023
+ const entries = [];
7024
+ for (const line of lines) {
7025
+ try {
7026
+ entries.push(JSON.parse(line));
7027
+ } catch {
7028
+ }
7029
+ }
7030
+ if (entries.length === 0) {
7031
+ return {
7032
+ filePath,
7033
+ totalEntries: 0,
7034
+ dateRange: null,
7035
+ perSignal: [],
7036
+ perCategory: []
7037
+ };
7038
+ }
7039
+ const bySignal = /* @__PURE__ */ new Map();
7040
+ for (const entry of entries) {
7041
+ for (const sig of entry.signals) {
7042
+ if (!bySignal.has(sig.name)) {
7043
+ bySignal.set(sig.name, { values: [], confidences: [] });
7044
+ }
7045
+ const bucket = bySignal.get(sig.name);
7046
+ bucket.values.push(sig.value);
7047
+ bucket.confidences.push(sig.confidence);
7048
+ }
7049
+ }
7050
+ const perSignal = [];
7051
+ for (const [name, bucket] of bySignal) {
7052
+ const obs = bucket.values.length;
7053
+ const fires = bucket.values.filter((v) => Math.abs(v) > FIRE_EPSILON).length;
7054
+ const meanAbsValue = bucket.values.reduce((a, v) => a + Math.abs(v), 0) / obs;
7055
+ const meanConfidence = bucket.confidences.reduce((a, c) => a + c, 0) / obs;
7056
+ const directionalBias = bucket.values.reduce((a, v) => a + v, 0) / obs;
7057
+ perSignal.push({
7058
+ name,
7059
+ category: SIGNAL_TO_CATEGORY[name] ?? "unknown",
7060
+ observations: obs,
7061
+ fireRate: fires / obs,
7062
+ meanAbsValue,
7063
+ meanConfidence,
7064
+ directionalBias
7065
+ });
7066
+ }
7067
+ perSignal.sort((a, b) => b.fireRate - a.fireRate);
7068
+ const byCategory = /* @__PURE__ */ new Map();
7069
+ for (const sig of perSignal) {
7070
+ if (!byCategory.has(sig.category)) {
7071
+ byCategory.set(sig.category, {
7072
+ signals: /* @__PURE__ */ new Set(),
7073
+ obs: 0,
7074
+ fires: 0,
7075
+ absSum: 0
7076
+ });
7077
+ }
7078
+ const bucket = byCategory.get(sig.category);
7079
+ bucket.signals.add(sig.name);
7080
+ bucket.obs += sig.observations;
7081
+ bucket.fires += sig.fireRate * sig.observations;
7082
+ bucket.absSum += sig.meanAbsValue * sig.observations;
7083
+ }
7084
+ const perCategory = [];
7085
+ for (const [category, bucket] of byCategory) {
7086
+ const fireRate = bucket.obs > 0 ? bucket.fires / bucket.obs : 0;
7087
+ const meanAbsValue = bucket.obs > 0 ? bucket.absSum / bucket.obs : 0;
7088
+ let recommendation;
7089
+ if (fireRate >= 0.4) recommendation = "keep";
7090
+ else if (fireRate < 0.1) recommendation = "drop";
7091
+ else recommendation = "review";
7092
+ perCategory.push({
7093
+ category,
7094
+ signalNames: [...bucket.signals],
7095
+ observations: bucket.obs,
7096
+ fireRate,
7097
+ meanAbsValue,
7098
+ recommendation
7099
+ });
7100
+ }
7101
+ perCategory.sort((a, b) => b.fireRate - a.fireRate);
7102
+ const timestamps = entries.map((e) => e.timestamp).sort();
7103
+ return {
7104
+ filePath,
7105
+ totalEntries: entries.length,
7106
+ dateRange: {
7107
+ from: timestamps[0],
7108
+ to: timestamps[timestamps.length - 1]
7109
+ },
7110
+ perSignal,
7111
+ perCategory
7112
+ };
7113
+ }
7114
+ var DELTA_EPSILON = 0.05;
7115
+ function classifyStatus(baselineRate, currentRate) {
7116
+ if (baselineRate === null) return "new";
7117
+ if (currentRate === null) return "removed";
7118
+ const delta = currentRate - baselineRate;
7119
+ if (Math.abs(delta) < DELTA_EPSILON) return "stable";
7120
+ return delta > 0 ? "improved" : "regressed";
7121
+ }
7122
+ function diffAudits(baseline, current) {
7123
+ const baseSignals = new Map(baseline.perSignal.map((s) => [s.name, s]));
7124
+ const currSignals = new Map(current.perSignal.map((s) => [s.name, s]));
7125
+ const allSignalNames = /* @__PURE__ */ new Set([...baseSignals.keys(), ...currSignals.keys()]);
7126
+ const perSignal = [];
7127
+ for (const name of allSignalNames) {
7128
+ const b = baseSignals.get(name);
7129
+ const c = currSignals.get(name);
7130
+ perSignal.push({
7131
+ name,
7132
+ category: (c ?? b)?.category ?? "unknown",
7133
+ baseline: {
7134
+ fireRate: b?.fireRate ?? 0,
7135
+ meanAbsValue: b?.meanAbsValue ?? 0,
7136
+ observations: b?.observations ?? 0
7137
+ },
7138
+ current: {
7139
+ fireRate: c?.fireRate ?? 0,
7140
+ meanAbsValue: c?.meanAbsValue ?? 0,
7141
+ observations: c?.observations ?? 0
7142
+ },
7143
+ fireRateDelta: (c?.fireRate ?? 0) - (b?.fireRate ?? 0),
7144
+ meanAbsValueDelta: (c?.meanAbsValue ?? 0) - (b?.meanAbsValue ?? 0),
7145
+ status: classifyStatus(b?.fireRate ?? null, c?.fireRate ?? null)
7146
+ });
7147
+ }
7148
+ perSignal.sort((a, b) => Math.abs(b.fireRateDelta) - Math.abs(a.fireRateDelta));
7149
+ const baseCats = new Map(baseline.perCategory.map((c) => [c.category, c]));
7150
+ const currCats = new Map(current.perCategory.map((c) => [c.category, c]));
7151
+ const allCats = /* @__PURE__ */ new Set([...baseCats.keys(), ...currCats.keys()]);
7152
+ const perCategory = [];
7153
+ for (const cat of allCats) {
7154
+ const b = baseCats.get(cat);
7155
+ const c = currCats.get(cat);
7156
+ perCategory.push({
7157
+ name: cat,
7158
+ category: cat,
7159
+ baseline: {
7160
+ fireRate: b?.fireRate ?? 0,
7161
+ meanAbsValue: b?.meanAbsValue ?? 0,
7162
+ observations: b?.observations ?? 0
7163
+ },
7164
+ current: {
7165
+ fireRate: c?.fireRate ?? 0,
7166
+ meanAbsValue: c?.meanAbsValue ?? 0,
7167
+ observations: c?.observations ?? 0
7168
+ },
7169
+ fireRateDelta: (c?.fireRate ?? 0) - (b?.fireRate ?? 0),
7170
+ meanAbsValueDelta: (c?.meanAbsValue ?? 0) - (b?.meanAbsValue ?? 0),
7171
+ status: classifyStatus(b?.fireRate ?? null, c?.fireRate ?? null)
7172
+ });
7173
+ }
7174
+ perCategory.sort((a, b) => Math.abs(b.fireRateDelta) - Math.abs(a.fireRateDelta));
7175
+ return {
7176
+ baselineDateRange: baseline.dateRange,
7177
+ currentDateRange: current.dateRange,
7178
+ perSignal,
7179
+ perCategory
7180
+ };
7181
+ }
7182
+ function suggestRenormalizedWeights(currentWeights, perCategory, dropThreshold = 0.1) {
7183
+ const keepCategories = new Set(
7184
+ perCategory.filter((c) => c.fireRate >= dropThreshold && c.category !== "unknown").map((c) => c.category)
7185
+ );
7186
+ let retainedSum = 0;
7187
+ for (const [cat, w] of Object.entries(currentWeights)) {
7188
+ if (keepCategories.has(cat)) retainedSum += w;
7189
+ }
7190
+ if (retainedSum === 0) return currentWeights;
7191
+ const out = {};
7192
+ for (const [cat, w] of Object.entries(currentWeights)) {
7193
+ out[cat] = keepCategories.has(cat) ? w / retainedSum : 0;
7194
+ }
7195
+ return out;
7196
+ }
7197
+
6571
7198
  // src/commands/agent.ts
6572
7199
  var DEFAULT_TOKENS = ["ethereum", "bitcoin", "solana", "aave", "uniswap"];
6573
7200
  function makeConfig(overrides) {
@@ -6577,12 +7204,14 @@ function makeConfig(overrides) {
6577
7204
  dryRun: true,
6578
7205
  maxPositionPct: 5,
6579
7206
  maxRiskPct: 20,
7207
+ useX402: true,
7208
+ // Paid signals (Nansen + Messari) on by default — opt out with --no-x402
6580
7209
  ...overrides
6581
7210
  };
6582
7211
  }
6583
7212
  function registerAgentCommands(program) {
6584
7213
  const agent = program.command("agent").description("Autonomous trading agent");
6585
- agent.command("analyze").description("Analyze token(s) using multi-signal scoring").argument("[tokens...]", "Token IDs to analyze (e.g., ethereum bitcoin)").option("--all", "Analyze full watchlist").option("--auto", "Use dynamic token selection from Hyperliquid market data").option("--json", "Output as JSON").option("--x402", "Include paid x402 data (Nansen smart-money, Messari fundamentals)").option("--telegram", "Output formatted summary for Telegram").option("--proposals", "Generate trade proposals for high-confidence opportunities").action(async (tokens, options) => {
7214
+ agent.command("analyze").description("Analyze token(s) using multi-signal scoring").argument("[tokens...]", "Token IDs to analyze (e.g., ethereum bitcoin)").option("--all", "Analyze full watchlist").option("--auto", "Use dynamic token selection from Hyperliquid market data").option("--json", "Output as JSON").option("--no-x402", "Skip paid x402 data (Nansen smart-money, Messari fundamentals) \u2014 on by default").option("--telegram", "Output formatted summary for Telegram").option("--proposals", "Generate trade proposals for high-confidence opportunities").option("--weight-profile <name>", "Weight profile: default | majors | altcoin | sentHeavy | techHeavy (auto: majors for BTC/ETH/SOL)").option("--scaled-sizing", "Score-weighted position sizing (smaller positions for marginal scores)").option("--smooth", "Smooth fast signals with rolling 3-reading window").action(async (tokens, options) => {
6586
7215
  let tokenList;
6587
7216
  let selectionSummary;
6588
7217
  if (options.auto) {
@@ -6604,7 +7233,8 @@ function registerAgentCommands(program) {
6604
7233
  } else {
6605
7234
  tokenList = options.all ? DEFAULT_TOKENS : tokens.length > 0 ? tokens : DEFAULT_TOKENS;
6606
7235
  }
6607
- const config = makeConfig({ tokens: tokenList, useX402: options.x402 ?? false });
7236
+ const scaledSizing = options.scaledSizing ? { ...DEFAULT_SCALED_SIZING, enabled: true } : void 0;
7237
+ const config = makeConfig({ tokens: tokenList, useX402: options.x402, weightProfile: options.weightProfile, smoothFastSignals: options.smooth });
6608
7238
  const tradingAgent = new TradingAgent(config);
6609
7239
  const spinner = ora("Analyzing tokens...").start();
6610
7240
  try {
@@ -6618,7 +7248,7 @@ function registerAgentCommands(program) {
6618
7248
  return;
6619
7249
  }
6620
7250
  if (options.proposals) {
6621
- const proposals = ExecutionPipeline.generateProposals(results);
7251
+ const proposals = ExecutionPipeline.generateProposals(results, { scaledSizing });
6622
7252
  const formatted = ExecutionPipeline.formatProposals(proposals);
6623
7253
  console.log(formatted);
6624
7254
  return;
@@ -6740,7 +7370,7 @@ function registerAgentCommands(program) {
6740
7370
  process.exitCode = 1;
6741
7371
  }
6742
7372
  });
6743
- agent.command("start").description("Start autonomous trading loop").option("--cycle <interval>", "Cycle interval (15m, 1h, 4h)", "4h").option("--dry-run", "Paper trading mode", true).option("--tokens <tokens>", "Comma-separated token list").option("--auto", "Use dynamic token selection from Hyperliquid market data").option("--log <path>", "Path to write cycle logs").option("--mode <mode>", "Execution mode: dry-run (default), hyperliquid-perp", "dry-run").option("--strategy-clone <address>", "Strategy clone address on HyperEVM (required for hyperliquid-perp)").option("--chain <chain>", "Chain for live execution (hyperevm, hyperevm-testnet)", "ethereum").option("--asset-index <n>", "HyperCore perp asset index (default: 3 = ETH)").option("--x402", "Include paid x402 data (Nansen smart-money, Messari fundamentals)").action(async (options) => {
7373
+ agent.command("start").description("Start autonomous trading loop").option("--cycle <interval>", "Cycle interval (15m, 1h, 4h)", "4h").option("--dry-run", "Paper trading mode", true).option("--tokens <tokens>", "Comma-separated token list").option("--auto", "Use dynamic token selection from Hyperliquid market data").option("--log <path>", "Path to write cycle logs").option("--mode <mode>", "Execution mode: dry-run (default), hyperliquid-perp", "dry-run").option("--strategy-clone <address>", "Strategy clone address on HyperEVM (required for hyperliquid-perp)").option("--chain <chain>", "Chain for live execution (hyperevm, hyperevm-testnet)", "ethereum").option("--asset-index <n>", "HyperCore perp asset index (default: 3 = ETH)").option("--no-x402", "Skip paid x402 data (Nansen smart-money, Messari fundamentals) \u2014 on by default").option("--weight-profile <name>", "Weight profile: default | majors | altcoin | sentHeavy | techHeavy (auto: majors for BTC/ETH/SOL)").option("--scaled-sizing", "Score-weighted position sizing (smaller positions for marginal scores)").option("--smooth", "Smooth fast signals with rolling 3-reading window").action(async (options) => {
6744
7374
  let tokenList;
6745
7375
  if (options.auto) {
6746
7376
  console.log(chalk8.dim(" Using dynamic token selection from Hyperliquid (refreshes every 30 min)"));
@@ -6751,8 +7381,8 @@ function registerAgentCommands(program) {
6751
7381
  const cycle = options.cycle ?? "4h";
6752
7382
  let savedRiskConfig = {};
6753
7383
  try {
6754
- const configPath = join11(homedir11(), ".sherwood", "agent", "config.json");
6755
- const data = await readFile10(configPath, "utf-8");
7384
+ const configPath = join12(homedir12(), ".sherwood", "agent", "config.json");
7385
+ const data = await readFile12(configPath, "utf-8");
6756
7386
  const parsed = JSON.parse(data);
6757
7387
  for (const [k, v] of Object.entries(parsed)) {
6758
7388
  if (k in DEFAULT_RISK_CONFIG && typeof v === "number" && Number.isFinite(v)) {
@@ -6777,7 +7407,7 @@ function registerAgentCommands(program) {
6777
7407
  return;
6778
7408
  }
6779
7409
  const loopConfig = {
6780
- agent: makeConfig({ tokens: tokenList, cycle, dryRun: !isLive, useX402: options.x402 ?? false }),
7410
+ agent: makeConfig({ tokens: tokenList, cycle, dryRun: !isLive, useX402: options.x402, weightProfile: options.weightProfile, smoothFastSignals: options.smooth }),
6781
7411
  execution: {
6782
7412
  dryRun: !isLive,
6783
7413
  mode: options.mode ?? "dry-run",
@@ -6903,17 +7533,17 @@ function registerAgentCommands(program) {
6903
7533
  }
6904
7534
  riskConfig[key] = numVal;
6905
7535
  console.log(chalk8.green(` Set ${key} = ${numVal}`));
6906
- const configDir = join11(homedir11(), ".sherwood", "agent");
6907
- const configPath = join11(configDir, "config.json");
7536
+ const configDir = join12(homedir12(), ".sherwood", "agent");
7537
+ const configPath = join12(configDir, "config.json");
6908
7538
  let existingConfig = {};
6909
7539
  try {
6910
- const data = await readFile10(configPath, "utf-8");
7540
+ const data = await readFile12(configPath, "utf-8");
6911
7541
  existingConfig = JSON.parse(data);
6912
7542
  } catch {
6913
7543
  }
6914
7544
  existingConfig[key] = numVal;
6915
- await mkdir11(configDir, { recursive: true });
6916
- await writeFile10(configPath, JSON.stringify(existingConfig, null, 2), "utf-8");
7545
+ await mkdir13(configDir, { recursive: true });
7546
+ await writeFile11(configPath, JSON.stringify(existingConfig, null, 2), "utf-8");
6917
7547
  console.log(chalk8.dim(` Saved to ${configPath}`));
6918
7548
  } else {
6919
7549
  console.log(chalk8.red(` Invalid value: ${value}`));
@@ -6944,9 +7574,15 @@ function registerAgentCommands(program) {
6944
7574
  console.log(` Max Slippage (small): ${(riskConfig.maxSlippage.small * 100).toFixed(1)}%`);
6945
7575
  console.log("");
6946
7576
  });
6947
- agent.command("backtest").description("Backtest strategies on historical data").argument("<token>", "Token ID (e.g., bitcoin, ethereum)").option("--from <date>", "Start date (YYYY-MM-DD)", "2024-01-01").option("--to <date>", "End date (YYYY-MM-DD)", "2024-12-31").option("--strategies <list>", "Comma-separated strategy names", "").option("--capital <amount>", "Initial capital in USD", "10000").option("--cycle <interval>", "Candle interval (1h, 4h, 1d)", "1d").option("--walk-forward", "Enable walk-forward optimization").option("--train <days>", "Training window in days for walk-forward", "90").option("--test <days>", "Test window in days for walk-forward", "30").option("--verbose", "Show detailed decision logs for each candle").action(async (token, options) => {
7577
+ agent.command("backtest").description("Backtest strategies on historical data").argument("<token>", "Token ID (e.g., bitcoin, ethereum)").option("--from <date>", "Start date (YYYY-MM-DD)", "2024-01-01").option("--to <date>", "End date (YYYY-MM-DD)", "2024-12-31").option("--strategies <list>", "Comma-separated strategy names", "").option("--capital <amount>", "Initial capital in USD", "10000").option("--cycle <interval>", "Candle interval (1h, 4h, 1d)", "1d").option("--walk-forward", "Enable walk-forward optimization").option("--train <days>", "Training window in days for walk-forward", "90").option("--test <days>", "Test window in days for walk-forward", "30").option("--verbose", "Show detailed decision logs for each candle").option("--regime", "Apply regime-conditional thresholds per-candle (matches live behavior)").option("--trailing-stop <pct>", "Trailing stop as decimal (e.g. 0.05 = 5%) \u2014 exits if price drops X% from high-water mark").option("--scaled-sizing", "Score-weighted position sizing (smaller positions for marginal scores)").option("--weight-profile <name>", "Named weight profile: default | majors | altcoin | sentHeavy | techHeavy").option("--smooth", "Smooth fast signals (HL flow, smartMoney, dexFlow, fundingRate) with rolling 3-reading window").action(async (token, options) => {
6948
7578
  const capital = parseFloat(options.capital);
6949
7579
  const strategies = options.strategies ? options.strategies.split(",").map((s) => s.trim()) : [];
7580
+ const trailingStopPct = options.trailingStop ? parseFloat(options.trailingStop) : void 0;
7581
+ if (trailingStopPct !== void 0 && (!Number.isFinite(trailingStopPct) || trailingStopPct <= 0 || trailingStopPct > 0.5)) {
7582
+ console.error(chalk8.red("--trailing-stop must be between 0 and 0.5 (e.g. 0.05 = 5%)"));
7583
+ process.exitCode = 1;
7584
+ return;
7585
+ }
6950
7586
  if (options.walkForward) {
6951
7587
  const trainWindow = parseInt(options.train, 10);
6952
7588
  const testWindow = parseInt(options.test, 10);
@@ -6959,7 +7595,10 @@ function registerAgentCommands(program) {
6959
7595
  testWindow,
6960
7596
  stepSize,
6961
7597
  capital,
6962
- strategies
7598
+ strategies,
7599
+ useRegime: options.regime,
7600
+ trailingStopPct,
7601
+ smoothFastSignals: options.smooth
6963
7602
  };
6964
7603
  const spinner = ora(`Walk-forward testing ${token} (${trainWindow}d train, ${testWindow}d test)...`).start();
6965
7604
  try {
@@ -6970,7 +7609,10 @@ function registerAgentCommands(program) {
6970
7609
  initialCapital: capital,
6971
7610
  strategies,
6972
7611
  cycle: options.cycle || "1d",
6973
- verbose: options.verbose
7612
+ verbose: options.verbose,
7613
+ useRegime: options.regime,
7614
+ trailingStopPct,
7615
+ smoothFastSignals: options.smooth
6974
7616
  });
6975
7617
  const result = await backtester.walkForwardTest(walkConfig);
6976
7618
  spinner.stop();
@@ -6987,7 +7629,10 @@ function registerAgentCommands(program) {
6987
7629
  initialCapital: capital,
6988
7630
  strategies,
6989
7631
  cycle: options.cycle || "1d",
6990
- verbose: options.verbose
7632
+ verbose: options.verbose,
7633
+ useRegime: options.regime,
7634
+ trailingStopPct,
7635
+ smoothFastSignals: options.smooth
6991
7636
  };
6992
7637
  const spinner = ora(`Backtesting ${token} from ${config.startDate} to ${config.endDate}...`).start();
6993
7638
  try {
@@ -7001,6 +7646,150 @@ function registerAgentCommands(program) {
7001
7646
  }
7002
7647
  }
7003
7648
  });
7649
+ 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
+ const dropThreshold = parseFloat(options.dropThreshold ?? "0.1");
7651
+ const result = await auditSignalHistory();
7652
+ if (options.save) {
7653
+ await mkdir13(join12(homedir12(), ".sherwood", "agent"), { recursive: true });
7654
+ await writeFile11(options.save, JSON.stringify(result, null, 2), "utf-8");
7655
+ console.log(chalk8.green(` Saved baseline snapshot to ${options.save}`));
7656
+ console.log(chalk8.dim(` ${result.totalEntries} entries, ${result.perSignal.length} signals`));
7657
+ return;
7658
+ }
7659
+ if (options.baseline) {
7660
+ let baseline;
7661
+ try {
7662
+ const raw = await readFile12(options.baseline, "utf-8");
7663
+ baseline = JSON.parse(raw);
7664
+ } catch (err) {
7665
+ console.error(chalk8.red(` Failed to load baseline: ${err.message}`));
7666
+ process.exitCode = 1;
7667
+ return;
7668
+ }
7669
+ const diff = diffAudits(baseline, result);
7670
+ if (options.json) {
7671
+ console.log(JSON.stringify(diff, null, 2));
7672
+ return;
7673
+ }
7674
+ console.log("");
7675
+ console.log(chalk8.bold(" Signal Audit Diff"));
7676
+ console.log(chalk8.dim(" " + "\u2550".repeat(72)));
7677
+ if (baseline.dateRange && result.dateRange) {
7678
+ console.log(chalk8.dim(` Baseline: ${baseline.dateRange.from.slice(0, 10)} \u2192 ${baseline.dateRange.to.slice(0, 10)} (${baseline.totalEntries} entries)`));
7679
+ console.log(chalk8.dim(` Current: ${result.dateRange.from.slice(0, 10)} \u2192 ${result.dateRange.to.slice(0, 10)} (${result.totalEntries} entries)`));
7680
+ }
7681
+ console.log("");
7682
+ console.log(chalk8.bold(" Per Category"));
7683
+ console.log(chalk8.dim(` ${"Category".padEnd(14)} ${"Was".padEnd(7)} ${"Now".padEnd(7)} ${"\u0394".padEnd(8)} Status`));
7684
+ console.log(chalk8.dim(" " + "\u2500".repeat(72)));
7685
+ for (const c of diff.perCategory) {
7686
+ const arrow = c.status === "improved" ? chalk8.green("\u2191") : c.status === "regressed" ? chalk8.red("\u2193") : c.status === "new" ? chalk8.cyan("\u2726") : c.status === "removed" ? chalk8.red("\u2717") : chalk8.dim("=");
7687
+ const deltaStr = (c.fireRateDelta >= 0 ? "+" : "") + (c.fireRateDelta * 100).toFixed(0) + "pp";
7688
+ const deltaColor = c.status === "improved" ? chalk8.green : c.status === "regressed" ? chalk8.red : chalk8.dim;
7689
+ console.log(
7690
+ ` ${c.category.padEnd(14)} ${(c.baseline.fireRate * 100).toFixed(0).padStart(5)}% ${(c.current.fireRate * 100).toFixed(0).padStart(5)}% ${deltaColor(deltaStr.padEnd(7))} ${arrow} ${c.status}`
7691
+ );
7692
+ }
7693
+ const changedSignals = diff.perSignal.filter((s) => s.status !== "stable");
7694
+ if (changedSignals.length > 0) {
7695
+ console.log("");
7696
+ console.log(chalk8.bold(" Changed Signals"));
7697
+ console.log(chalk8.dim(` ${"Signal".padEnd(22)} ${"Was".padEnd(7)} ${"Now".padEnd(7)} ${"\u0394".padEnd(8)} Status`));
7698
+ console.log(chalk8.dim(" " + "\u2500".repeat(72)));
7699
+ for (const s of changedSignals) {
7700
+ const arrow = s.status === "improved" ? chalk8.green("\u2191") : s.status === "regressed" ? chalk8.red("\u2193") : s.status === "new" ? chalk8.cyan("\u2726") : s.status === "removed" ? chalk8.red("\u2717") : chalk8.dim("=");
7701
+ const deltaStr = (s.fireRateDelta >= 0 ? "+" : "") + (s.fireRateDelta * 100).toFixed(0) + "pp";
7702
+ const deltaColor = s.status === "improved" ? chalk8.green : s.status === "regressed" ? chalk8.red : chalk8.dim;
7703
+ console.log(
7704
+ ` ${s.name.padEnd(22)} ${(s.baseline.fireRate * 100).toFixed(0).padStart(5)}% ${(s.current.fireRate * 100).toFixed(0).padStart(5)}% ${deltaColor(deltaStr.padEnd(7))} ${arrow} ${s.status}`
7705
+ );
7706
+ }
7707
+ } else {
7708
+ console.log("");
7709
+ console.log(chalk8.dim(" No per-signal changes above 5pp threshold."));
7710
+ }
7711
+ console.log("");
7712
+ return;
7713
+ }
7714
+ if (options.json) {
7715
+ console.log(JSON.stringify(result, null, 2));
7716
+ return;
7717
+ }
7718
+ console.log("");
7719
+ console.log(chalk8.bold(" Signal Activity Audit"));
7720
+ console.log(chalk8.dim(" " + "\u2550".repeat(72)));
7721
+ if (result.totalEntries === 0) {
7722
+ console.log(chalk8.dim(` No signal history yet at ${result.filePath}`));
7723
+ console.log(chalk8.dim(" Run 'sherwood agent analyze' a few times to populate it."));
7724
+ console.log("");
7725
+ return;
7726
+ }
7727
+ console.log(chalk8.dim(` Source: ${result.filePath}`));
7728
+ console.log(chalk8.dim(` Entries: ${result.totalEntries}`));
7729
+ if (result.dateRange) {
7730
+ console.log(
7731
+ chalk8.dim(
7732
+ ` Range: ${result.dateRange.from.slice(0, 10)} \u2192 ${result.dateRange.to.slice(0, 10)}`
7733
+ )
7734
+ );
7735
+ }
7736
+ console.log("");
7737
+ console.log(chalk8.bold(" Per Category"));
7738
+ console.log(
7739
+ chalk8.dim(
7740
+ ` ${"Category".padEnd(14)} ${"Fire%".padEnd(7)} ${"Obs".padEnd(6)} ${"Mean|v|".padEnd(9)} Signals`
7741
+ )
7742
+ );
7743
+ console.log(chalk8.dim(" " + "\u2500".repeat(72)));
7744
+ for (const c of result.perCategory) {
7745
+ const color = c.recommendation === "keep" ? chalk8.green : c.recommendation === "drop" ? chalk8.red : chalk8.yellow;
7746
+ const rec = `[${c.recommendation.toUpperCase()}]`;
7747
+ console.log(
7748
+ ` ${c.category.padEnd(14)} ${(c.fireRate * 100).toFixed(0).padStart(5)}% ${String(c.observations).padEnd(6)} ${c.meanAbsValue.toFixed(3).padEnd(9)} ${c.signalNames.join(", ")} ${color(rec)}`
7749
+ );
7750
+ }
7751
+ console.log("");
7752
+ console.log(chalk8.bold(" Per Signal"));
7753
+ console.log(
7754
+ chalk8.dim(
7755
+ ` ${"Signal".padEnd(22)} ${"Category".padEnd(12)} ${"Fire%".padEnd(7)} ${"Obs".padEnd(6)} ${"Mean|v|".padEnd(9)} ${"Bias".padEnd(8)} Conf`
7756
+ )
7757
+ );
7758
+ console.log(chalk8.dim(" " + "\u2500".repeat(72)));
7759
+ for (const s of result.perSignal) {
7760
+ const biasColor = s.directionalBias >= 0 ? chalk8.green : chalk8.red;
7761
+ console.log(
7762
+ ` ${s.name.padEnd(22)} ${s.category.padEnd(12)} ${(s.fireRate * 100).toFixed(0).padStart(5)}% ${String(s.observations).padEnd(6)} ${s.meanAbsValue.toFixed(3).padEnd(9)} ${biasColor((s.directionalBias >= 0 ? "+" : "") + s.directionalBias.toFixed(3)).padEnd(17)} ${s.meanConfidence.toFixed(2)}`
7763
+ );
7764
+ }
7765
+ const current = DEFAULT_WEIGHTS;
7766
+ const suggested = suggestRenormalizedWeights(current, result.perCategory, dropThreshold);
7767
+ const changed = Object.keys(current).some(
7768
+ (k) => Math.abs((suggested[k] ?? 0) - (current[k] ?? 0)) > 1e-3
7769
+ );
7770
+ if (changed) {
7771
+ console.log("");
7772
+ console.log(chalk8.bold(" Suggested Weight Renormalization"));
7773
+ console.log(chalk8.dim(` (Dropping categories below ${(dropThreshold * 100).toFixed(0)}% fire rate)`));
7774
+ console.log(chalk8.dim(" " + "\u2500".repeat(72)));
7775
+ console.log(
7776
+ chalk8.dim(
7777
+ ` ${"Category".padEnd(14)} ${"Current".padEnd(10)} ${"Suggested"}`
7778
+ )
7779
+ );
7780
+ for (const [cat, w] of Object.entries(current)) {
7781
+ const newW = suggested[cat] ?? 0;
7782
+ const tag = newW === 0 ? chalk8.red(" \u2190 drop") : newW > w ? chalk8.green(" \u2190 boost") : "";
7783
+ console.log(
7784
+ ` ${cat.padEnd(14)} ${w.toFixed(3).padEnd(10)} ${newW.toFixed(3)}${tag}`
7785
+ );
7786
+ }
7787
+ } else {
7788
+ console.log("");
7789
+ console.log(chalk8.dim(" All categories above drop threshold \u2014 no renormalization suggested."));
7790
+ }
7791
+ console.log("");
7792
+ });
7004
7793
  const alerts = agent.command("alerts").description("Manage trading alerts");
7005
7794
  alerts.command("list").alias("").description("Show recent alerts").option("--hours <n>", "Look back N hours", "24").action(async (options) => {
7006
7795
  try {
@@ -7084,4 +7873,4 @@ function formatFearGreed(value) {
7084
7873
  export {
7085
7874
  registerAgentCommands
7086
7875
  };
7087
- //# sourceMappingURL=agent-U6OFXITU.js.map
7876
+ //# sourceMappingURL=agent-TB7VNLFH.js.map