@sherwoodagent/cli 0.29.1 → 0.38.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{addresses-NOWMJT4H.js → addresses-6GJP2WAY.js} +4 -4
- package/dist/{agent-U6OFXITU.js → agent-NUYY5PF7.js} +1084 -295
- package/dist/agent-NUYY5PF7.js.map +1 -0
- package/dist/{chat-SIB6GCJF.js → chat-ABL6Z5XZ.js} +8 -8
- package/dist/{chunk-OD5JECOR.js → chunk-3RVDBIXD.js} +4 -4
- package/dist/{chunk-UW6P2PMP.js → chunk-6X3FGJJO.js} +5 -5
- package/dist/{chunk-EYI6JORK.js → chunk-B4AH26UI.js} +2 -2
- package/dist/{chunk-H32QA3NV.js → chunk-IJG4CTOI.js} +2 -2
- package/dist/{chunk-7W6U6ZC6.js → chunk-LWIPZN5P.js} +5 -5
- package/dist/{chunk-BVL3O7UH.js → chunk-O2JHLTDB.js} +4 -4
- package/dist/{chunk-C4SYHSY2.js → chunk-ODB2GWA7.js} +4 -4
- package/dist/{chunk-N2547R2L.js → chunk-OFR3LCYE.js} +3 -3
- package/dist/{chunk-7PDRYBHD.js → chunk-OPKMVIYJ.js} +4 -4
- package/dist/{chunk-7PDRYBHD.js.map → chunk-OPKMVIYJ.js.map} +1 -1
- package/dist/{chunk-RGOTBJIN.js → chunk-WCBG44IQ.js} +9 -1
- package/dist/chunk-WCBG44IQ.js.map +1 -0
- package/dist/{client-66VTYDRS.js → client-HKAG55PQ.js} +4 -4
- package/dist/{config-YXZKPSZB.js → config-LS7FKZL5.js} +4 -2
- package/dist/{eas-YTMCD3UN.js → eas-KBFTHWSI.js} +6 -6
- package/dist/{governor-2N3CPFOP.js → governor-KEXLUOGB.js} +6 -6
- package/dist/index.js +66 -47
- package/dist/index.js.map +1 -1
- package/dist/{network-GJA2ZN3O.js → network-KSMKPKNL.js} +3 -3
- package/dist/{price-HGBIMFYN.js → price-M5H57DPY.js} +5 -5
- package/dist/research-662HNMOG.js +14 -0
- package/dist/{research-ECOWBTFP.js → research-W6QW2JJ3.js} +9 -9
- package/dist/{session-RTCMOPAQ.js → session-64BCMJIB.js} +10 -10
- package/dist/{trade-3HVCELTB.js → trade-7343J4LA.js} +16 -16
- package/dist/{xmtp-5FPJN5WE.js → xmtp-EB2CU2LE.js} +20 -15
- package/dist/xmtp-EB2CU2LE.js.map +1 -0
- package/package.json +7 -5
- package/dist/agent-U6OFXITU.js.map +0 -1
- package/dist/chunk-RGOTBJIN.js.map +0 -1
- package/dist/research-3GNTDKY4.js +0 -14
- package/dist/xmtp-5FPJN5WE.js.map +0 -1
- /package/dist/{addresses-NOWMJT4H.js.map → addresses-6GJP2WAY.js.map} +0 -0
- /package/dist/{chat-SIB6GCJF.js.map → chat-ABL6Z5XZ.js.map} +0 -0
- /package/dist/{chunk-OD5JECOR.js.map → chunk-3RVDBIXD.js.map} +0 -0
- /package/dist/{chunk-UW6P2PMP.js.map → chunk-6X3FGJJO.js.map} +0 -0
- /package/dist/{chunk-EYI6JORK.js.map → chunk-B4AH26UI.js.map} +0 -0
- /package/dist/{chunk-H32QA3NV.js.map → chunk-IJG4CTOI.js.map} +0 -0
- /package/dist/{chunk-7W6U6ZC6.js.map → chunk-LWIPZN5P.js.map} +0 -0
- /package/dist/{chunk-BVL3O7UH.js.map → chunk-O2JHLTDB.js.map} +0 -0
- /package/dist/{chunk-C4SYHSY2.js.map → chunk-ODB2GWA7.js.map} +0 -0
- /package/dist/{chunk-N2547R2L.js.map → chunk-OFR3LCYE.js.map} +0 -0
- /package/dist/{client-66VTYDRS.js.map → client-HKAG55PQ.js.map} +0 -0
- /package/dist/{config-YXZKPSZB.js.map → config-LS7FKZL5.js.map} +0 -0
- /package/dist/{eas-YTMCD3UN.js.map → eas-KBFTHWSI.js.map} +0 -0
- /package/dist/{governor-2N3CPFOP.js.map → governor-KEXLUOGB.js.map} +0 -0
- /package/dist/{network-GJA2ZN3O.js.map → network-KSMKPKNL.js.map} +0 -0
- /package/dist/{price-HGBIMFYN.js.map → price-M5H57DPY.js.map} +0 -0
- /package/dist/{research-3GNTDKY4.js.map → research-662HNMOG.js.map} +0 -0
- /package/dist/{research-ECOWBTFP.js.map → research-W6QW2JJ3.js.map} +0 -0
- /package/dist/{session-RTCMOPAQ.js.map → session-64BCMJIB.js.map} +0 -0
- /package/dist/{trade-3HVCELTB.js.map → trade-7343J4LA.js.map} +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getResearchProvider
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-B4AH26UI.js";
|
|
4
4
|
import {
|
|
5
5
|
BASE_STRATEGY_ABI
|
|
6
6
|
} from "./chunk-PQ5ZX6ZT.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-OFR3LCYE.js";
|
|
8
8
|
import {
|
|
9
9
|
hyperevm,
|
|
10
10
|
hyperevmTestnet
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
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
|
|
17
|
-
import { join as
|
|
18
|
-
import { homedir as
|
|
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
|
|
1005
|
-
else if (score
|
|
1006
|
-
else if (score
|
|
1007
|
-
else if (score
|
|
1008
|
-
else action = "
|
|
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 >
|
|
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 >
|
|
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 < -
|
|
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 < -
|
|
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
|
-
|
|
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 <
|
|
2691
|
-
batches.push(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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
|
-
|
|
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/
|
|
2885
|
-
import {
|
|
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 =
|
|
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 =
|
|
3047
|
-
const raw = await
|
|
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
|
|
3060
|
-
const cacheFile =
|
|
3061
|
-
await
|
|
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
|
|
3069
|
-
import { join as
|
|
3070
|
-
import { homedir as
|
|
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 =
|
|
3076
|
-
this.cacheFile =
|
|
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
|
|
3561
|
+
const data = await readFile6(this.cacheFile, "utf-8");
|
|
3337
3562
|
return JSON.parse(data);
|
|
3338
3563
|
}
|
|
3339
3564
|
async saveCache(analysis) {
|
|
3340
|
-
await
|
|
3565
|
+
await mkdir7(this.cacheDir, { recursive: true });
|
|
3341
3566
|
const cache = {
|
|
3342
3567
|
timestamp: Date.now(),
|
|
3343
3568
|
analysis
|
|
3344
3569
|
};
|
|
3345
|
-
await
|
|
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
|
|
3351
|
-
import { join as
|
|
3352
|
-
import { homedir as
|
|
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 =
|
|
3359
|
-
this.cacheFile =
|
|
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
|
|
3789
|
+
const data = await readFile7(this.cacheFile, "utf-8");
|
|
3565
3790
|
return JSON.parse(data);
|
|
3566
3791
|
}
|
|
3567
3792
|
async saveCache(btcStructure) {
|
|
3568
|
-
await
|
|
3793
|
+
await mkdir8(this.cacheDir, { recursive: true });
|
|
3569
3794
|
const cache = {
|
|
3570
3795
|
timestamp: Date.now(),
|
|
3571
3796
|
btcStructure
|
|
3572
3797
|
};
|
|
3573
|
-
await
|
|
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
|
|
3579
|
-
import { join as
|
|
3580
|
-
import { homedir as
|
|
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 =
|
|
3595
|
-
this.stateFile =
|
|
3596
|
-
this.configFile =
|
|
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
|
|
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
|
|
3861
|
-
await
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4442
|
+
resolvedWeights,
|
|
4205
4443
|
regimeAnalysis?.strategyAdjustments,
|
|
4206
|
-
correlationCheck
|
|
4444
|
+
correlationCheck,
|
|
4445
|
+
regimeAnalysis?.regime
|
|
4207
4446
|
);
|
|
4208
|
-
|
|
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
|
|
4327
|
-
import { join as
|
|
4328
|
-
import { homedir as
|
|
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 =
|
|
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 =
|
|
4595
|
-
const raw = await
|
|
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
|
|
4608
|
-
const cacheFile =
|
|
4609
|
-
await
|
|
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
|
|
4655
|
-
import { join as
|
|
4656
|
-
import { homedir as
|
|
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
|
|
4923
|
-
import { dirname, join as
|
|
4924
|
-
import { homedir as
|
|
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 =
|
|
4970
|
-
this.statePath =
|
|
4971
|
-
this.historyPath =
|
|
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
|
|
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
|
|
5242
|
+
await mkdir11(dirname2(this.statePath), { recursive: true });
|
|
5001
5243
|
const tmpPath = this.statePath + ".tmp";
|
|
5002
|
-
await
|
|
5003
|
-
await
|
|
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
|
|
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
|
|
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
|
|
5454
|
+
await mkdir11(dirname2(this.historyPath), { recursive: true });
|
|
5213
5455
|
const tmpPath = this.historyPath + ".tmp";
|
|
5214
|
-
await
|
|
5215
|
-
await
|
|
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 =
|
|
5620
|
-
await
|
|
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 ??
|
|
6083
|
+
const logPath = this.config.logPath ?? join11(homedir11(), ".sherwood", "agent", "cycles.jsonl");
|
|
5770
6084
|
try {
|
|
5771
|
-
await
|
|
5772
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
entryPrice
|
|
5962
|
-
|
|
5963
|
-
pnlPercent
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
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
|
|
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,
|
|
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 (
|
|
6841
|
+
if (decision.action === "HOLD") {
|
|
6447
6842
|
continue;
|
|
6448
6843
|
}
|
|
6449
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
|
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", "
|
|
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 =
|
|
6755
|
-
const data = await
|
|
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
|
|
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 =
|
|
6907
|
-
const configPath =
|
|
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
|
|
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
|
|
6916
|
-
await
|
|
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-
|
|
7876
|
+
//# sourceMappingURL=agent-NUYY5PF7.js.map
|