@sherwoodagent/cli 0.51.1 → 0.53.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.
Files changed (40) hide show
  1. package/dist/{addresses-DOWLGXGE.js → addresses-CKPOWDDV.js} +2 -2
  2. package/dist/{agent-E5GHURKJ.js → agent-ER5GYBVQ.js} +2 -2
  3. package/dist/{chat-TD6V5LFD.js → chat-3IW4XDZJ.js} +5 -5
  4. package/dist/{chunk-T464VNRC.js → chunk-4VXAOLRC.js} +10 -8
  5. package/dist/chunk-4VXAOLRC.js.map +1 -0
  6. package/dist/{chunk-UYL6Q7BS.js → chunk-D2D6BPBM.js} +2 -2
  7. package/dist/{chunk-OMW47WOO.js → chunk-DBOINZYU.js} +4 -2
  8. package/dist/chunk-DBOINZYU.js.map +1 -0
  9. package/dist/{chunk-4OF4IZ6M.js → chunk-ENIUCMDV.js} +2 -2
  10. package/dist/{chunk-ME6MUKGZ.js → chunk-JEKXXHI5.js} +2 -2
  11. package/dist/{chunk-FWC57PT7.js → chunk-LXUNJSB6.js} +9 -5
  12. package/dist/chunk-LXUNJSB6.js.map +1 -0
  13. package/dist/{chunk-2HNUJ7XH.js → chunk-ZRU43SUB.js} +2 -2
  14. package/dist/{eas-LHJF2UCG.js → eas-PFIAGWAB.js} +3 -3
  15. package/dist/{governor-JD4KZXDA.js → governor-HZW5TEP2.js} +3 -3
  16. package/dist/index.js +1337 -104
  17. package/dist/index.js.map +1 -1
  18. package/dist/{price-2PNKWY4D.js → price-UC2W6HYQ.js} +2 -2
  19. package/dist/{research-WBZU4QNU.js → research-2GOILSS7.js} +3 -3
  20. package/dist/{session-IYT3N7MG.js → session-3QAL5VEV.js} +5 -5
  21. package/dist/{trade-PTYLNJBU.js → trade-UXQOCNLJ.js} +6 -6
  22. package/dist/{xmtp-ZACWHUI5.js → xmtp-KEFA7F24.js} +3 -3
  23. package/package.json +1 -1
  24. package/dist/chunk-FWC57PT7.js.map +0 -1
  25. package/dist/chunk-OMW47WOO.js.map +0 -1
  26. package/dist/chunk-T464VNRC.js.map +0 -1
  27. /package/dist/{addresses-DOWLGXGE.js.map → addresses-CKPOWDDV.js.map} +0 -0
  28. /package/dist/{agent-E5GHURKJ.js.map → agent-ER5GYBVQ.js.map} +0 -0
  29. /package/dist/{chat-TD6V5LFD.js.map → chat-3IW4XDZJ.js.map} +0 -0
  30. /package/dist/{chunk-UYL6Q7BS.js.map → chunk-D2D6BPBM.js.map} +0 -0
  31. /package/dist/{chunk-4OF4IZ6M.js.map → chunk-ENIUCMDV.js.map} +0 -0
  32. /package/dist/{chunk-ME6MUKGZ.js.map → chunk-JEKXXHI5.js.map} +0 -0
  33. /package/dist/{chunk-2HNUJ7XH.js.map → chunk-ZRU43SUB.js.map} +0 -0
  34. /package/dist/{eas-LHJF2UCG.js.map → eas-PFIAGWAB.js.map} +0 -0
  35. /package/dist/{governor-JD4KZXDA.js.map → governor-HZW5TEP2.js.map} +0 -0
  36. /package/dist/{price-2PNKWY4D.js.map → price-UC2W6HYQ.js.map} +0 -0
  37. /package/dist/{research-WBZU4QNU.js.map → research-2GOILSS7.js.map} +0 -0
  38. /package/dist/{session-IYT3N7MG.js.map → session-3QAL5VEV.js.map} +0 -0
  39. /package/dist/{trade-PTYLNJBU.js.map → trade-UXQOCNLJ.js.map} +0 -0
  40. /package/dist/{xmtp-ZACWHUI5.js.map → xmtp-KEFA7F24.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  HyperliquidProvider,
4
+ TOKEN_TO_COIN,
5
+ calculateATR,
4
6
  getLatestSignals,
5
7
  hlCancelAllOrders,
6
8
  hlGetMeta,
7
9
  hlPlaceLimitOrder,
8
10
  resolveHLCoin
9
- } from "./chunk-OMW47WOO.js";
11
+ } from "./chunk-DBOINZYU.js";
10
12
  import {
11
13
  UniswapProvider,
12
14
  applySlippage,
13
15
  encodeSwapPath,
14
16
  getMultiHopQuote,
15
17
  getQuote
16
- } from "./chunk-2HNUJ7XH.js";
18
+ } from "./chunk-ZRU43SUB.js";
17
19
  import {
18
20
  chatCompletion,
19
21
  checkApiKeyValid,
@@ -30,14 +32,14 @@ import {
30
32
  queryJoinRequests,
31
33
  revokeJoinRequest,
32
34
  stripReferrerPrefix
33
- } from "./chunk-ME6MUKGZ.js";
35
+ } from "./chunk-JEKXXHI5.js";
34
36
  import {
35
37
  createSyndicate,
36
38
  getActiveSyndicates,
37
39
  getSyndicate,
38
40
  subdomainExists,
39
41
  updateMetadata
40
- } from "./chunk-UYL6Q7BS.js";
42
+ } from "./chunk-D2D6BPBM.js";
41
43
  import {
42
44
  approveDepositor,
43
45
  deposit,
@@ -61,7 +63,7 @@ import {
61
63
  searchSyndicateAcrossChains,
62
64
  setTextRecord,
63
65
  setVaultAddress
64
- } from "./chunk-4OF4IZ6M.js";
66
+ } from "./chunk-ENIUCMDV.js";
65
67
  import {
66
68
  PROPOSAL_STATE,
67
69
  PROPOSAL_STATES,
@@ -93,7 +95,7 @@ import {
93
95
  setVotingPeriod,
94
96
  settleProposal,
95
97
  vote
96
- } from "./chunk-T464VNRC.js";
98
+ } from "./chunk-4VXAOLRC.js";
97
99
  import {
98
100
  AERODROME,
99
101
  AGENT_REGISTRY,
@@ -105,7 +107,7 @@ import {
105
107
  TOKENS,
106
108
  UNISWAP,
107
109
  VENICE
108
- } from "./chunk-FWC57PT7.js";
110
+ } from "./chunk-LXUNJSB6.js";
109
111
  import {
110
112
  formatContractError,
111
113
  getAccount,
@@ -208,7 +210,7 @@ var MoonwellProvider = class {
208
210
  };
209
211
 
210
212
  // src/commands/strategy-template.ts
211
- import { parseUnits, formatUnits, isAddress, erc20Abi, encodeAbiParameters as encodeAbiParameters8, decodeAbiParameters } from "viem";
213
+ import { parseUnits, formatUnits, isAddress, erc20Abi, encodeAbiParameters as encodeAbiParameters9, decodeAbiParameters } from "viem";
212
214
  import chalk from "chalk";
213
215
  import ora from "ora";
214
216
  import { writeFileSync, mkdirSync } from "fs";
@@ -650,6 +652,65 @@ function buildSettleCalls7(clone) {
650
652
  ];
651
653
  }
652
654
 
655
+ // src/strategies/hyperliquid-grid-template.ts
656
+ import { encodeAbiParameters as encodeAbiParameters8, encodeFunctionData as encodeFunctionData8, maxUint256 as maxUint2563 } from "viem";
657
+ function buildInitData8(asset, depositAmount, leverage, maxOrderSize, maxOrdersPerTick, assetIndices) {
658
+ return encodeAbiParameters8(
659
+ [
660
+ { type: "address" },
661
+ { type: "uint256" },
662
+ { type: "uint32" },
663
+ { type: "uint256" },
664
+ { type: "uint32" },
665
+ { type: "uint32[]" }
666
+ ],
667
+ [asset, depositAmount, leverage, maxOrderSize, maxOrdersPerTick, assetIndices]
668
+ );
669
+ }
670
+ function buildExecuteCalls8(clone, asset, amount) {
671
+ const approveAmount = amount === 0n ? maxUint2563 : amount;
672
+ return [
673
+ {
674
+ target: asset,
675
+ data: encodeFunctionData8({
676
+ abi: ERC20_ABI,
677
+ functionName: "approve",
678
+ args: [clone, approveAmount]
679
+ }),
680
+ value: 0n
681
+ },
682
+ {
683
+ target: clone,
684
+ data: encodeFunctionData8({
685
+ abi: BASE_STRATEGY_ABI,
686
+ functionName: "execute"
687
+ }),
688
+ value: 0n
689
+ },
690
+ {
691
+ target: asset,
692
+ data: encodeFunctionData8({
693
+ abi: ERC20_ABI,
694
+ functionName: "approve",
695
+ args: [clone, 0n]
696
+ }),
697
+ value: 0n
698
+ }
699
+ ];
700
+ }
701
+ function buildSettleCalls8(clone) {
702
+ return [
703
+ {
704
+ target: clone,
705
+ data: encodeFunctionData8({
706
+ abi: BASE_STRATEGY_ABI,
707
+ functionName: "settle"
708
+ }),
709
+ value: 0n
710
+ }
711
+ ];
712
+ }
713
+
653
714
  // src/commands/strategy-template.ts
654
715
  import { concat as concat2, numberToHex } from "viem";
655
716
  var ZERO = "0x0000000000000000000000000000000000000000";
@@ -666,14 +727,14 @@ function encodeV3Path(tokens, fees) {
666
727
  }
667
728
  function buildSwapExtraData(network, tokenIn, tokenOut, feeTier, hop) {
668
729
  if (network === "robinhood-testnet") {
669
- return encodeAbiParameters8([{ type: "uint24" }], [feeTier]);
730
+ return encodeAbiParameters9([{ type: "uint24" }], [feeTier]);
670
731
  }
671
732
  if (hop) {
672
733
  const path = encodeV3Path([tokenIn, hop.via, tokenOut], [hop.feeIn, hop.feeOut]);
673
- const encoded2 = encodeAbiParameters8([{ type: "bytes" }], [path]);
734
+ const encoded2 = encodeAbiParameters9([{ type: "bytes" }], [path]);
674
735
  return `0x01${encoded2.slice(2)}`;
675
736
  }
676
- const encoded = encodeAbiParameters8([{ type: "uint24" }], [feeTier]);
737
+ const encoded = encodeAbiParameters9([{ type: "uint24" }], [feeTier]);
677
738
  return `0x00${encoded.slice(2)}`;
678
739
  }
679
740
  async function detectSwapRoute(asset, token, preferredFeeTier) {
@@ -812,6 +873,12 @@ var TEMPLATES = [
812
873
  key: "hyperliquid-perp",
813
874
  description: "Leveraged perp trading on Hyperliquid via HyperEVM precompiles",
814
875
  addressKey: "HYPERLIQUID_PERP"
876
+ },
877
+ {
878
+ name: "Hyperliquid Grid",
879
+ key: "hyperliquid-grid",
880
+ description: "ATR-based grid trading on Hyperliquid via HyperEVM precompiles",
881
+ addressKey: "HYPERLIQUID_GRID"
815
882
  }
816
883
  ];
817
884
  function resolveTemplate(key) {
@@ -1079,6 +1146,21 @@ async function buildInitDataForTemplate(templateKey, opts, vault) {
1079
1146
  assetAmount: depositAmount
1080
1147
  };
1081
1148
  }
1149
+ if (templateKey === "hyperliquid-grid") {
1150
+ const token = opts.token || "USDC";
1151
+ const asset = resolveToken(token);
1152
+ const decimals = token.toUpperCase() === "USDC" ? 6 : 18;
1153
+ const depositAmount = opts.amount ? parseUnits(opts.amount, decimals) : 0n;
1154
+ const leverage = Number(opts.leverage || "5");
1155
+ const maxOrderSize = parseUnits(opts.maxOrderSize || "10000", decimals);
1156
+ const maxOrdersPerTick = Number(opts.maxOrdersPerTick || "20");
1157
+ const assetIndices = (opts.assetIndices || "0").split(",").map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n));
1158
+ return {
1159
+ initData: buildInitData8(asset, depositAmount, leverage, maxOrderSize, maxOrdersPerTick, assetIndices),
1160
+ asset,
1161
+ assetAmount: depositAmount
1162
+ };
1163
+ }
1082
1164
  throw new Error(`No init builder for template: ${templateKey}`);
1083
1165
  }
1084
1166
  function buildCallsForTemplate(templateKey, clone, asset, assetAmount, extraApprovals) {
@@ -1126,6 +1208,12 @@ function buildCallsForTemplate(templateKey, clone, asset, assetAmount, extraAppr
1126
1208
  settleCalls: buildSettleCalls7(clone)
1127
1209
  };
1128
1210
  }
1211
+ if (templateKey === "hyperliquid-grid") {
1212
+ return {
1213
+ executeCalls: buildExecuteCalls8(clone, asset, assetAmount),
1214
+ settleCalls: buildSettleCalls8(clone)
1215
+ };
1216
+ }
1129
1217
  throw new Error(`No call builder for template: ${templateKey}`);
1130
1218
  }
1131
1219
  function resolveSwapAdapter() {
@@ -1220,7 +1308,7 @@ function registerStrategyTemplateCommands(strategy2) {
1220
1308
  console.log(chalk.dim("Full proposal: sherwood strategy propose <template> --vault <addr> ..."));
1221
1309
  console.log();
1222
1310
  });
1223
- strategy2.command("clone").description("Clone a strategy template and initialize it").argument("<template>", "Template: moonwell-supply, aerodrome-lp, venice-inference, wsteth-moonwell, mamo-yield, portfolio, hyperliquid-perp").requiredOption("--vault <address>", "Vault address").option("--amount <n>", "Asset amount to deploy").option("--min-redeem <n>", "Min asset on settlement (Moonwell)").option("--token <symbol>", "Asset token symbol (default: USDC)").option("--asset <symbol>", "Asset token (USDC, VVV, or address)").option("--agent <address>", "Agent wallet (Venice, default: your wallet)").option("--min-vvv <n>", "Min VVV from swap (Venice)").option("--single-hop", "Single-hop Aerodrome swap (Venice)").option("--token-a <address>", "Token A (Aerodrome)").option("--token-b <address>", "Token B (Aerodrome)").option("--amount-a <n>", "Token A amount (Aerodrome)").option("--amount-b <n>", "Token B amount (Aerodrome)").option("--stable", "Stable pool (Aerodrome)").option("--gauge <address>", "Gauge address (Aerodrome)").option("--lp-token <address>", "LP token address (Aerodrome)").option("--min-a-out <n>", "Min token A on settle (Aerodrome)").option("--min-b-out <n>", "Min token B on settle (Aerodrome)").option("--slippage <bps>", "Slippage tolerance in bps (wstETH, default: 500 = 5%)").option("--mamo-factory <address>", "Mamo StrategyFactory address (Mamo)").option("--tokens <list>", "Comma-separated token addresses or symbols (Portfolio)").option("--weights <list>", "Comma-separated weights in bps, must sum to 10000 (Portfolio)").option("--max-slippage <bps>", "Max slippage bps (Portfolio, default: 500)").option("--fee-tier <n>", "Pool fee tier (Portfolio, default: 3000)").option("--swap-adapter <address>", "Swap adapter address (Portfolio)").option("--leverage <number>", "Leverage multiplier (Hyperliquid Perp, default: 10)").option("--asset-index <number>", "Perp asset index (Hyperliquid Perp, default: 0 for BTC)").option("--max-position <amount>", "Max position size in USD (Hyperliquid Perp, default: 100000)").option("--max-trades-per-day <n>", "Max trades per day (Hyperliquid Perp, default: 50)").action(async (templateKey, opts) => {
1311
+ strategy2.command("clone").description("Clone a strategy template and initialize it").argument("<template>", "Template: moonwell-supply, aerodrome-lp, venice-inference, wsteth-moonwell, mamo-yield, portfolio, hyperliquid-perp, hyperliquid-grid").requiredOption("--vault <address>", "Vault address").option("--amount <n>", "Asset amount to deploy").option("--min-redeem <n>", "Min asset on settlement (Moonwell)").option("--token <symbol>", "Asset token symbol (default: USDC)").option("--asset <symbol>", "Asset token (USDC, VVV, or address)").option("--agent <address>", "Agent wallet (Venice, default: your wallet)").option("--min-vvv <n>", "Min VVV from swap (Venice)").option("--single-hop", "Single-hop Aerodrome swap (Venice)").option("--token-a <address>", "Token A (Aerodrome)").option("--token-b <address>", "Token B (Aerodrome)").option("--amount-a <n>", "Token A amount (Aerodrome)").option("--amount-b <n>", "Token B amount (Aerodrome)").option("--stable", "Stable pool (Aerodrome)").option("--gauge <address>", "Gauge address (Aerodrome)").option("--lp-token <address>", "LP token address (Aerodrome)").option("--min-a-out <n>", "Min token A on settle (Aerodrome)").option("--min-b-out <n>", "Min token B on settle (Aerodrome)").option("--slippage <bps>", "Slippage tolerance in bps (wstETH, default: 500 = 5%)").option("--mamo-factory <address>", "Mamo StrategyFactory address (Mamo)").option("--tokens <list>", "Comma-separated token addresses or symbols (Portfolio)").option("--weights <list>", "Comma-separated weights in bps, must sum to 10000 (Portfolio)").option("--max-slippage <bps>", "Max slippage bps (Portfolio, default: 500)").option("--fee-tier <n>", "Pool fee tier (Portfolio, default: 3000)").option("--swap-adapter <address>", "Swap adapter address (Portfolio)").option("--leverage <number>", "Leverage multiplier (Hyperliquid Perp, default: 10)").option("--asset-index <number>", "Perp asset index (Hyperliquid Perp, default: 0 for BTC)").option("--max-position <amount>", "Max position size in USD (Hyperliquid Perp, default: 100000)").option("--max-trades-per-day <n>", "Max trades per day (Hyperliquid Perp, default: 50)").option("--max-order-size <amount>", "Max single-order size in asset units (Hyperliquid Grid, default: 10000)").option("--max-orders-per-tick <n>", "Max orders per tick (Hyperliquid Grid, default: 20)").option("--asset-indices <list>", "Comma-separated perp asset indices (Hyperliquid Grid, default: 0)").action(async (templateKey, opts) => {
1224
1312
  const vault = opts.vault;
1225
1313
  if (!isAddress(vault)) {
1226
1314
  console.error(chalk.red("Invalid vault address"));
@@ -1268,7 +1356,7 @@ function registerStrategyTemplateCommands(strategy2) {
1268
1356
  console.log(chalk.dim("Use this address in your proposal batch calls."));
1269
1357
  console.log();
1270
1358
  });
1271
- strategy2.command("init").description("Initialize an already-deployed but uninitialized strategy clone").argument("<template>", "Template: moonwell-supply, aerodrome-lp, venice-inference, wsteth-moonwell, mamo-yield, portfolio, hyperliquid-perp").requiredOption("--clone <address>", "Clone address to initialize").requiredOption("--vault <address>", "Vault address").option("--amount <n>", "Asset amount to deploy").option("--min-redeem <n>", "Min asset on settlement (Moonwell)").option("--token <symbol>", "Asset token symbol (default: USDC)").option("--asset <symbol>", "Asset token (USDC, VVV, or address)").option("--agent <address>", "Agent wallet (Venice, default: your wallet)").option("--min-vvv <n>", "Min VVV from swap (Venice)").option("--single-hop", "Single-hop Aerodrome swap (Venice)").option("--token-a <address>", "Token A (Aerodrome)").option("--token-b <address>", "Token B (Aerodrome)").option("--amount-a <n>", "Token A amount (Aerodrome)").option("--amount-b <n>", "Token B amount (Aerodrome)").option("--stable", "Stable pool (Aerodrome)").option("--gauge <address>", "Gauge address (Aerodrome)").option("--lp-token <address>", "LP token address (Aerodrome)").option("--min-a-out <n>", "Min token A on settle (Aerodrome)").option("--min-b-out <n>", "Min token B on settle (Aerodrome)").option("--slippage <bps>", "Slippage tolerance in bps (wstETH, default: 500 = 5%)").option("--mamo-factory <address>", "Mamo StrategyFactory address (Mamo)").option("--tokens <list>", "Comma-separated token addresses or symbols (Portfolio)").option("--weights <list>", "Comma-separated weights in bps, must sum to 10000 (Portfolio)").option("--max-slippage <bps>", "Max slippage bps (Portfolio, default: 500)").option("--fee-tier <n>", "Pool fee tier (Portfolio, default: 3000)").option("--swap-adapter <address>", "Swap adapter address (Portfolio)").option("--leverage <number>", "Leverage multiplier (Hyperliquid Perp, default: 10)").option("--asset-index <number>", "Perp asset index (Hyperliquid Perp, default: 0 for BTC)").option("--max-position <amount>", "Max position size in USD (Hyperliquid Perp, default: 100000)").option("--max-trades-per-day <n>", "Max trades per day (Hyperliquid Perp, default: 50)").action(async (templateKey, opts) => {
1359
+ strategy2.command("init").description("Initialize an already-deployed but uninitialized strategy clone").argument("<template>", "Template: moonwell-supply, aerodrome-lp, venice-inference, wsteth-moonwell, mamo-yield, portfolio, hyperliquid-perp, hyperliquid-grid").requiredOption("--clone <address>", "Clone address to initialize").requiredOption("--vault <address>", "Vault address").option("--amount <n>", "Asset amount to deploy").option("--min-redeem <n>", "Min asset on settlement (Moonwell)").option("--token <symbol>", "Asset token symbol (default: USDC)").option("--asset <symbol>", "Asset token (USDC, VVV, or address)").option("--agent <address>", "Agent wallet (Venice, default: your wallet)").option("--min-vvv <n>", "Min VVV from swap (Venice)").option("--single-hop", "Single-hop Aerodrome swap (Venice)").option("--token-a <address>", "Token A (Aerodrome)").option("--token-b <address>", "Token B (Aerodrome)").option("--amount-a <n>", "Token A amount (Aerodrome)").option("--amount-b <n>", "Token B amount (Aerodrome)").option("--stable", "Stable pool (Aerodrome)").option("--gauge <address>", "Gauge address (Aerodrome)").option("--lp-token <address>", "LP token address (Aerodrome)").option("--min-a-out <n>", "Min token A on settle (Aerodrome)").option("--min-b-out <n>", "Min token B on settle (Aerodrome)").option("--slippage <bps>", "Slippage tolerance in bps (wstETH, default: 500 = 5%)").option("--mamo-factory <address>", "Mamo StrategyFactory address (Mamo)").option("--tokens <list>", "Comma-separated token addresses or symbols (Portfolio)").option("--weights <list>", "Comma-separated weights in bps, must sum to 10000 (Portfolio)").option("--max-slippage <bps>", "Max slippage bps (Portfolio, default: 500)").option("--fee-tier <n>", "Pool fee tier (Portfolio, default: 3000)").option("--swap-adapter <address>", "Swap adapter address (Portfolio)").option("--leverage <number>", "Leverage multiplier (Hyperliquid Perp, default: 10)").option("--asset-index <number>", "Perp asset index (Hyperliquid Perp, default: 0 for BTC)").option("--max-position <amount>", "Max position size in USD (Hyperliquid Perp, default: 100000)").option("--max-trades-per-day <n>", "Max trades per day (Hyperliquid Perp, default: 50)").option("--max-order-size <amount>", "Max single-order size in asset units (Hyperliquid Grid, default: 10000)").option("--max-orders-per-tick <n>", "Max orders per tick (Hyperliquid Grid, default: 20)").option("--asset-indices <list>", "Comma-separated perp asset indices (Hyperliquid Grid, default: 0)").action(async (templateKey, opts) => {
1272
1360
  const clone = opts.clone;
1273
1361
  const vault = opts.vault;
1274
1362
  if (!isAddress(clone)) {
@@ -1324,7 +1412,7 @@ function registerStrategyTemplateCommands(strategy2) {
1324
1412
  console.log(` Proposer: ${chalk.green(getAccount().address)}`);
1325
1413
  console.log();
1326
1414
  });
1327
- strategy2.command("propose").description("Clone + init + build calls + submit governance proposal (all-in-one)").argument("<template>", "Template: moonwell-supply, aerodrome-lp, venice-inference, wsteth-moonwell, mamo-yield, portfolio, hyperliquid-perp").requiredOption("--vault <address>", "Vault address").option("--write-calls <dir>", "Write execute/settle JSON to directory (skip proposal submission)").option("--name <name>", "Proposal name").option("--description <text>", "Proposal description").option("--performance-fee <bps>", "Agent fee in bps").option("--duration <duration>", "Strategy duration (7d, 24h, etc.)").option("--amount <n>", "Asset amount to deploy").option("--min-redeem <n>", "Min asset on settlement (Moonwell)").option("--token <symbol>", "Asset token symbol (default: USDC)").option("--asset <symbol>", "Asset token (USDC, VVV, or address)").option("--agent <address>", "Agent wallet (Venice, default: your wallet)").option("--min-vvv <n>", "Min VVV from swap (Venice)").option("--single-hop", "Single-hop Aerodrome swap (Venice)").option("--token-a <address>", "Token A (Aerodrome)").option("--token-b <address>", "Token B (Aerodrome)").option("--amount-a <n>", "Token A amount (Aerodrome)").option("--amount-b <n>", "Token B amount (Aerodrome)").option("--stable", "Stable pool (Aerodrome)").option("--gauge <address>", "Gauge address (Aerodrome)").option("--lp-token <address>", "LP token address (Aerodrome)").option("--min-a-out <n>", "Min token A on settle (Aerodrome)").option("--min-b-out <n>", "Min token B on settle (Aerodrome)").option("--slippage <bps>", "Slippage tolerance in bps (wstETH, default: 500 = 5%)").option("--mamo-factory <address>", "Mamo StrategyFactory address (Mamo)").option("--tokens <list>", "Comma-separated token addresses or symbols (Portfolio)").option("--weights <list>", "Comma-separated weights in bps, must sum to 10000 (Portfolio)").option("--max-slippage <bps>", "Max slippage bps (Portfolio, default: 500)").option("--fee-tier <n>", "Pool fee tier (Portfolio, default: 3000)").option("--swap-adapter <address>", "Swap adapter address (Portfolio)").option("--leverage <number>", "Leverage multiplier (Hyperliquid Perp, default: 10)").option("--asset-index <number>", "Perp asset index (Hyperliquid Perp, default: 0 for BTC)").option("--max-position <amount>", "Max position size in USD (Hyperliquid Perp, default: 100000)").option("--max-trades-per-day <n>", "Max trades per day (Hyperliquid Perp, default: 50)").action(async (templateKey, opts) => {
1415
+ strategy2.command("propose").description("Clone + init + build calls + submit governance proposal (all-in-one)").argument("<template>", "Template: moonwell-supply, aerodrome-lp, venice-inference, wsteth-moonwell, mamo-yield, portfolio, hyperliquid-perp, hyperliquid-grid").requiredOption("--vault <address>", "Vault address").option("--write-calls <dir>", "Write execute/settle JSON to directory (skip proposal submission)").option("--name <name>", "Proposal name").option("--description <text>", "Proposal description").option("--performance-fee <bps>", "Agent fee in bps").option("--duration <duration>", "Strategy duration (7d, 24h, etc.)").option("--amount <n>", "Asset amount to deploy").option("--min-redeem <n>", "Min asset on settlement (Moonwell)").option("--token <symbol>", "Asset token symbol (default: USDC)").option("--asset <symbol>", "Asset token (USDC, VVV, or address)").option("--agent <address>", "Agent wallet (Venice, default: your wallet)").option("--min-vvv <n>", "Min VVV from swap (Venice)").option("--single-hop", "Single-hop Aerodrome swap (Venice)").option("--token-a <address>", "Token A (Aerodrome)").option("--token-b <address>", "Token B (Aerodrome)").option("--amount-a <n>", "Token A amount (Aerodrome)").option("--amount-b <n>", "Token B amount (Aerodrome)").option("--stable", "Stable pool (Aerodrome)").option("--gauge <address>", "Gauge address (Aerodrome)").option("--lp-token <address>", "LP token address (Aerodrome)").option("--min-a-out <n>", "Min token A on settle (Aerodrome)").option("--min-b-out <n>", "Min token B on settle (Aerodrome)").option("--slippage <bps>", "Slippage tolerance in bps (wstETH, default: 500 = 5%)").option("--mamo-factory <address>", "Mamo StrategyFactory address (Mamo)").option("--tokens <list>", "Comma-separated token addresses or symbols (Portfolio)").option("--weights <list>", "Comma-separated weights in bps, must sum to 10000 (Portfolio)").option("--max-slippage <bps>", "Max slippage bps (Portfolio, default: 500)").option("--fee-tier <n>", "Pool fee tier (Portfolio, default: 3000)").option("--swap-adapter <address>", "Swap adapter address (Portfolio)").option("--leverage <number>", "Leverage multiplier (Hyperliquid Perp, default: 10)").option("--asset-index <number>", "Perp asset index (Hyperliquid Perp, default: 0 for BTC)").option("--max-position <amount>", "Max position size in USD (Hyperliquid Perp, default: 100000)").option("--max-trades-per-day <n>", "Max trades per day (Hyperliquid Perp, default: 50)").option("--max-order-size <amount>", "Max single-order size in asset units (Hyperliquid Grid, default: 10000)").option("--max-orders-per-tick <n>", "Max orders per tick (Hyperliquid Grid, default: 20)").option("--asset-indices <list>", "Comma-separated perp asset indices (Hyperliquid Grid, default: 0)").action(async (templateKey, opts) => {
1328
1416
  const vault = opts.vault;
1329
1417
  if (!isAddress(vault)) {
1330
1418
  console.error(chalk.red("Invalid vault address"));
@@ -1450,9 +1538,9 @@ function registerStrategyTemplateCommands(strategy2) {
1450
1538
  console.error(chalk.red("Missing --name, --performance-fee, or --duration. Use --write-calls to skip proposal submission."));
1451
1539
  process.exit(1);
1452
1540
  }
1453
- const { propose: propose2 } = await import("./governor-JD4KZXDA.js");
1541
+ const { propose: propose2 } = await import("./governor-HZW5TEP2.js");
1454
1542
  const { pinJSON } = await import("./ipfs-IGXLLJCF.js");
1455
- const { parseDuration: parseDuration2 } = await import("./governor-JD4KZXDA.js");
1543
+ const { parseDuration: parseDuration2 } = await import("./governor-HZW5TEP2.js");
1456
1544
  const performanceFeeBps = BigInt(opts.performanceFee);
1457
1545
  if (performanceFeeBps < 0n || performanceFeeBps > 10000n) {
1458
1546
  console.error(chalk.red("--performance-fee must be 0-10000 (basis points)"));
@@ -1559,7 +1647,7 @@ function registerStrategyTemplateCommands(strategy2) {
1559
1647
  let prices = [];
1560
1648
  let priceSource = null;
1561
1649
  try {
1562
- const { getTokenPricesInAsset } = await import("./price-2PNKWY4D.js");
1650
+ const { getTokenPricesInAsset } = await import("./price-UC2W6HYQ.js");
1563
1651
  prices = await getTokenPricesInAsset({
1564
1652
  tokens: allocations.map((a, i) => ({
1565
1653
  token: a.token,
@@ -1808,7 +1896,7 @@ function registerStrategyTemplateCommands(strategy2) {
1808
1896
  });
1809
1897
  const updateSpinner = ora("Updating target weights...").start();
1810
1898
  try {
1811
- const innerData = encodeAbiParameters8(
1899
+ const innerData = encodeAbiParameters9(
1812
1900
  [{ type: "uint256[]" }, { type: "uint256" }, { type: "bytes[]" }],
1813
1901
  [newWeightsBps.map((w) => BigInt(w)), BigInt(maxSlip), swapData]
1814
1902
  );
@@ -1989,7 +2077,7 @@ function registerVeniceCommands(program2) {
1989
2077
  console.log(chalk2.dim(` Key: ${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`));
1990
2078
  console.log(chalk2.dim(" Saved to ~/.sherwood/config.json"));
1991
2079
  try {
1992
- const { createVeniceProvisionAttestation, getEasScanUrl: getEasScanUrl2 } = await import("./eas-LHJF2UCG.js");
2080
+ const { createVeniceProvisionAttestation, getEasScanUrl: getEasScanUrl2 } = await import("./eas-PFIAGWAB.js");
1993
2081
  const { uid } = await createVeniceProvisionAttestation(account.address);
1994
2082
  if (uid !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
1995
2083
  console.log(chalk2.dim(` Attested: ${getEasScanUrl2(uid)}`));
@@ -2345,7 +2433,7 @@ ${opts.prompt}`;
2345
2433
  console.log(chalk2.dim(`Model: ${result.model} | Tokens: ${result.usage.promptTokens} in, ${result.usage.completionTokens} out, ${result.usage.totalTokens} total`));
2346
2434
  }
2347
2435
  try {
2348
- const { createVeniceInferenceAttestation, getEasScanUrl: getEasScanUrl2 } = await import("./eas-LHJF2UCG.js");
2436
+ const { createVeniceInferenceAttestation, getEasScanUrl: getEasScanUrl2 } = await import("./eas-PFIAGWAB.js");
2349
2437
  const { keccak256: keccak2562, toHex, isAddress: isAddr } = await import("viem");
2350
2438
  const { getChainContracts: getChainContracts2 } = await import("./config-REASKDIK.js");
2351
2439
  const { getChain: getActiveChain } = await import("./network-3ZU7UBDU.js");
@@ -2377,7 +2465,7 @@ import chalk3 from "chalk";
2377
2465
  import ora3 from "ora";
2378
2466
 
2379
2467
  // src/strategies/allowance-disburse.ts
2380
- import { encodeFunctionData as encodeFunctionData8, parseUnits as parseUnits3 } from "viem";
2468
+ import { encodeFunctionData as encodeFunctionData9, parseUnits as parseUnits3 } from "viem";
2381
2469
  function buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDecimals, minUsdc, swapPath) {
2382
2470
  const assetAmount = parseUnits3(config.amount, assetDecimals);
2383
2471
  const isUsdc = assetAddress.toLowerCase() === TOKENS().USDC.toLowerCase();
@@ -2386,7 +2474,7 @@ function buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDec
2386
2474
  if (!isUsdc) {
2387
2475
  calls.push({
2388
2476
  target: assetAddress,
2389
- data: encodeFunctionData8({
2477
+ data: encodeFunctionData9({
2390
2478
  abi: ERC20_ABI,
2391
2479
  functionName: "approve",
2392
2480
  args: [UNISWAP().SWAP_ROUTER, assetAmount]
@@ -2396,7 +2484,7 @@ function buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDec
2396
2484
  if (isWeth) {
2397
2485
  calls.push({
2398
2486
  target: UNISWAP().SWAP_ROUTER,
2399
- data: encodeFunctionData8({
2487
+ data: encodeFunctionData9({
2400
2488
  abi: SWAP_ROUTER_EXACT_INPUT_SINGLE_ABI,
2401
2489
  functionName: "exactInputSingle",
2402
2490
  args: [
@@ -2416,7 +2504,7 @@ function buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDec
2416
2504
  } else {
2417
2505
  calls.push({
2418
2506
  target: UNISWAP().SWAP_ROUTER,
2419
- data: encodeFunctionData8({
2507
+ data: encodeFunctionData9({
2420
2508
  abi: SWAP_ROUTER_ABI,
2421
2509
  functionName: "exactInput",
2422
2510
  args: [
@@ -2436,7 +2524,7 @@ function buildDisburseBatch(config, vaultAddress, agents, assetAddress, assetDec
2436
2524
  for (const agent of agents) {
2437
2525
  calls.push({
2438
2526
  target: TOKENS().USDC,
2439
- data: encodeFunctionData8({
2527
+ data: encodeFunctionData9({
2440
2528
  abi: ERC20_ABI,
2441
2529
  functionName: "transfer",
2442
2530
  args: [agent, perAgent]
@@ -2843,7 +2931,7 @@ function registerIdentityCommands(program2) {
2843
2931
  }
2844
2932
 
2845
2933
  // src/commands/proposal.ts
2846
- import { isAddress as isAddress4, encodeFunctionData as encodeFunctionData10 } from "viem";
2934
+ import { isAddress as isAddress4, encodeFunctionData as encodeFunctionData11 } from "viem";
2847
2935
  import chalk6 from "chalk";
2848
2936
  import ora5 from "ora";
2849
2937
  import { readFileSync as readFileSync2 } from "fs";
@@ -2879,7 +2967,7 @@ function parseBigIntArg(value, name) {
2879
2967
  }
2880
2968
 
2881
2969
  // src/lib/simulate.ts
2882
- import { decodeFunctionData, encodeFunctionData as encodeFunctionData9 } from "viem";
2970
+ import { decodeFunctionData, encodeFunctionData as encodeFunctionData10 } from "viem";
2883
2971
  import chalk5 from "chalk";
2884
2972
  var G = chalk5.green;
2885
2973
  var W = chalk5.white;
@@ -3398,7 +3486,7 @@ async function simulateViaApi(vault, calls) {
3398
3486
  async function simulateViaEthCall(vault, calls) {
3399
3487
  const governor = getGovernorAddress();
3400
3488
  const client = getPublicClient();
3401
- const input2 = encodeFunctionData9({
3489
+ const input2 = encodeFunctionData10({
3402
3490
  abi: SYNDICATE_VAULT_ABI,
3403
3491
  functionName: "executeGovernorBatch",
3404
3492
  args: [calls.map((c) => ({ target: c.target, data: c.data, value: c.value }))]
@@ -3521,7 +3609,7 @@ function printSimulationResult(result, callType) {
3521
3609
  }
3522
3610
  async function sendSimulationAlert(subdomain, proposalId, vault, execResult, settleResult) {
3523
3611
  try {
3524
- const xmtp = await import("./xmtp-ZACWHUI5.js");
3612
+ const xmtp = await import("./xmtp-KEFA7F24.js");
3525
3613
  const { getAccount: getAccount2 } = await import("./client-UHHCB6EZ.js");
3526
3614
  const group = await xmtp.getGroup("", subdomain);
3527
3615
  const allRisks = [...execResult.risks, ...settleResult?.risks ?? []];
@@ -4030,7 +4118,7 @@ function registerProposalCommands(program2) {
4030
4118
  process.exit(1);
4031
4119
  }
4032
4120
  const asset = await getAssetAddress();
4033
- const noopCalldata = encodeFunctionData10({
4121
+ const noopCalldata = encodeFunctionData11({
4034
4122
  abi: ERC20_ABI,
4035
4123
  functionName: "balanceOf",
4036
4124
  args: [vault]
@@ -4676,7 +4764,9 @@ var GridPortfolio = class {
4676
4764
  allocation: allocation * (config.tokenSplit[token] ?? 1 / config.tokens.length),
4677
4765
  stats: emptyStats(),
4678
4766
  centerPrice: 0,
4679
- atr: 0
4767
+ atr: 0,
4768
+ trend: 0,
4769
+ lastTrendRefreshAt: 0
4680
4770
  }));
4681
4771
  const state = {
4682
4772
  totalAllocation: allocation,
@@ -4692,10 +4782,10 @@ var GridPortfolio = class {
4692
4782
  getState() {
4693
4783
  return this.state;
4694
4784
  }
4695
- /** Reset daily counters if UTC day boundary crossed. */
4696
- resetDailyStats(state) {
4697
- const now = Date.now();
4698
- const todayMidnight = /* @__PURE__ */ new Date();
4785
+ /** Reset daily counters if UTC day boundary crossed. `now` is injectable
4786
+ * so the backtester can drive resets off backtest time, not wall-clock. */
4787
+ resetDailyStats(state, now = Date.now()) {
4788
+ const todayMidnight = new Date(now);
4699
4789
  todayMidnight.setUTCHours(0, 0, 0, 0);
4700
4790
  const todayMs = todayMidnight.getTime();
4701
4791
  let changed = false;
@@ -4709,16 +4799,25 @@ var GridPortfolio = class {
4709
4799
  }
4710
4800
  return changed;
4711
4801
  }
4712
- /** Check if grid should be paused (pool dropped below threshold). */
4713
- checkPauseThreshold(state, config) {
4802
+ /** Check if grid should be paused or resumed (pool equity dropped/recovered).
4803
+ * `prices` is required to mark open buys to market. Hysteresis: pause at
4804
+ * `pauseThresholdPct` drop, resume only when drop recovers below
4805
+ * `unpauseRecoveryPct`. Returns true iff state.paused was changed. */
4806
+ checkPauseThreshold(state, config, prices) {
4714
4807
  const currentValue = state.grids.reduce((sum, g) => {
4715
- const openFillValue = g.openFills.filter((f) => !f.closed).reduce((s, f) => s + f.quantity * f.buyPrice, 0);
4716
- return sum + g.allocation + openFillValue;
4808
+ const price = prices[g.token];
4809
+ const unrealized = price && price > 0 ? g.openFills.filter((f) => !f.closed).reduce((s, f) => s + (price - f.buyPrice) * f.quantity, 0) : 0;
4810
+ return sum + g.allocation + unrealized;
4717
4811
  }, 0);
4718
4812
  const dropPct = 1 - currentValue / state.totalAllocation;
4719
- if (dropPct >= config.pauseThresholdPct) {
4813
+ if (!state.paused && dropPct >= config.pauseThresholdPct) {
4720
4814
  state.paused = true;
4721
- state.pauseReason = `Grid pool dropped ${(dropPct * 100).toFixed(1)}% (threshold: ${(config.pauseThresholdPct * 100).toFixed(0)}%)`;
4815
+ state.pauseReason = `Grid pool dropped ${(dropPct * 100).toFixed(1)}% (pause threshold: ${(config.pauseThresholdPct * 100).toFixed(0)}%, resume when \u2264 ${(config.unpauseRecoveryPct * 100).toFixed(0)}%)`;
4816
+ return true;
4817
+ }
4818
+ if (state.paused && dropPct <= config.unpauseRecoveryPct) {
4819
+ state.paused = false;
4820
+ state.pauseReason = "";
4722
4821
  return true;
4723
4822
  }
4724
4823
  return false;
@@ -4735,14 +4834,24 @@ var GridPortfolio = class {
4735
4834
  };
4736
4835
 
4737
4836
  // src/grid/manager.ts
4837
+ var defaultFillDetector = (level, currentPrice) => level.side === "buy" ? currentPrice <= level.price : currentPrice >= level.price;
4838
+ var defaultCloseFillDetector = (openFill, currentPrice) => currentPrice >= openFill.targetSellPrice;
4738
4839
  var GridManager = class {
4739
4840
  config;
4740
4841
  portfolio;
4741
4842
  hl;
4742
- constructor(config) {
4843
+ candleFetcher;
4844
+ fillDetector;
4845
+ closeFillDetector;
4846
+ nowProvider;
4847
+ constructor(config, candleFetcher, fillDetector, closeFillDetector, portfolio, nowProvider) {
4743
4848
  this.config = config;
4744
- this.portfolio = new GridPortfolio();
4849
+ this.portfolio = portfolio ?? new GridPortfolio();
4745
4850
  this.hl = new HyperliquidProvider();
4851
+ this.candleFetcher = candleFetcher ?? ((tokenId, interval, lookbackMs) => this.hl.getCandles(tokenId, interval, lookbackMs));
4852
+ this.fillDetector = fillDetector ?? defaultFillDetector;
4853
+ this.closeFillDetector = closeFillDetector ?? defaultCloseFillDetector;
4854
+ this.nowProvider = nowProvider ?? (() => Date.now());
4746
4855
  }
4747
4856
  /** Load or initialize grid state.
4748
4857
  * Also syncs new tokens added to config after initial grid setup. */
@@ -4764,7 +4873,9 @@ var GridManager = class {
4764
4873
  allocation: alloc,
4765
4874
  stats: { totalRoundTrips: 0, totalPnlUsd: 0, todayPnlUsd: 0, totalFills: 0, todayFills: 0, lastDailyReset: 0, lastRebalanceAt: 0 },
4766
4875
  centerPrice: 0,
4767
- atr: 0
4876
+ atr: 0,
4877
+ trend: 0,
4878
+ lastTrendRefreshAt: 0
4768
4879
  });
4769
4880
  addedAllocation += alloc;
4770
4881
  console.error(` [grid] Added new token grid: ${token} ($${alloc.toFixed(0)} allocation)`);
@@ -4785,8 +4896,15 @@ var GridManager = class {
4785
4896
  */
4786
4897
  async tick(prices, regime) {
4787
4898
  const state = await this.portfolio.load();
4788
- if (!state || state.paused || !this.config.enabled) {
4789
- return { fills: 0, roundTrips: 0, pnlUsd: 0, paused: state?.paused ?? false };
4899
+ if (!state || !this.config.enabled) {
4900
+ return { fills: 0, roundTrips: 0, pnlUsd: 0, paused: false };
4901
+ }
4902
+ if (state.paused) {
4903
+ const resumed = this.portfolio.checkPauseThreshold(state, this.config, prices);
4904
+ if (resumed) {
4905
+ await this.portfolio.save(state);
4906
+ }
4907
+ return { fills: 0, roundTrips: 0, pnlUsd: 0, paused: state.paused };
4790
4908
  }
4791
4909
  if (regime === "high-volatility") {
4792
4910
  console.error(chalk9.dim(` [grid] Paused: high-volatility regime \u2014 too risky for grid`));
@@ -4799,6 +4917,7 @@ var GridManager = class {
4799
4917
  for (const grid of state.grids) {
4800
4918
  const price = prices[grid.token];
4801
4919
  if (!price || price <= 0) continue;
4920
+ await this.maybeRefreshTrend(grid);
4802
4921
  if (grid.levels.length === 0 || this.needsFullRebuild(grid)) {
4803
4922
  await this.buildGrid(grid, price);
4804
4923
  }
@@ -4810,13 +4929,29 @@ var GridManager = class {
4810
4929
  await this.shiftGrid(grid, price);
4811
4930
  }
4812
4931
  }
4813
- this.portfolio.checkPauseThreshold(state, this.config);
4932
+ this.portfolio.checkPauseThreshold(state, this.config, prices);
4814
4933
  await this.portfolio.save(state);
4815
4934
  return { fills: totalFills, roundTrips: totalRoundTrips, pnlUsd: totalPnl, paused: false };
4816
4935
  }
4936
+ /** Hourly refresh of `grid.trend` from recent 4h candles. Called from
4937
+ * tick() so the downtrend filter sees fresh data between buildGrid runs. */
4938
+ async maybeRefreshTrend(grid) {
4939
+ const now = this.nowProvider();
4940
+ const ONE_HOUR = 60 * 60 * 1e3;
4941
+ if (now - grid.lastTrendRefreshAt < ONE_HOUR) return;
4942
+ const candles = await this.candleFetcher(grid.token, "4h", 14 * 4 * 60 * 60 * 1e3);
4943
+ if (!candles || candles.length < 2) return;
4944
+ const TREND_LOOKBACK_BARS = 14;
4945
+ const sliceStart = Math.max(0, candles.length - TREND_LOOKBACK_BARS);
4946
+ const first = candles[sliceStart].close;
4947
+ const last = candles[candles.length - 1].close;
4948
+ if (first <= 0) return;
4949
+ grid.trend = (last - first) / first;
4950
+ grid.lastTrendRefreshAt = now;
4951
+ }
4817
4952
  /** Build a fresh grid centered on the current price using ATR. */
4818
4953
  async buildGrid(grid, currentPrice) {
4819
- const candles = await this.hl.getCandles(grid.token, "4h", 14 * 24 * 60 * 60 * 1e3);
4954
+ const candles = await this.candleFetcher(grid.token, "4h", 14 * 24 * 60 * 60 * 1e3);
4820
4955
  if (!candles || candles.length < this.config.atrPeriod) {
4821
4956
  console.error(chalk9.dim(` [grid] Cannot build grid for ${grid.token}: insufficient candle data`));
4822
4957
  return;
@@ -4827,12 +4962,24 @@ var GridManager = class {
4827
4962
  console.error(chalk9.dim(` [grid] Cannot build grid for ${grid.token}: invalid ATR (${atr})`));
4828
4963
  return;
4829
4964
  }
4965
+ const TREND_LOOKBACK_BARS = 14;
4966
+ let trend = 0;
4967
+ if (candles.length >= 2) {
4968
+ const sliceStart = Math.max(0, candles.length - TREND_LOOKBACK_BARS);
4969
+ const first = candles[sliceStart].close;
4970
+ const last = candles[candles.length - 1].close;
4971
+ if (first > 0) {
4972
+ trend = (last - first) / first;
4973
+ }
4974
+ }
4830
4975
  const range = atr * this.config.atrMultiplier;
4831
4976
  const spacing = range / this.config.levelsPerSide;
4832
4977
  const effectiveCapital = grid.allocation * this.config.leverage;
4833
4978
  const quantity = effectiveCapital / (this.config.levelsPerSide * currentPrice);
4834
4979
  const levels = [];
4835
- for (let i = 1; i <= this.config.levelsPerSide; i++) {
4980
+ const inDowntrend = this.config.downtrendBlockPct > 0 && trend < -this.config.downtrendBlockPct;
4981
+ const buyCount = inDowntrend ? 1 : this.config.levelsPerSide;
4982
+ for (let i = 1; i <= buyCount; i++) {
4836
4983
  levels.push({
4837
4984
  price: currentPrice - spacing * i,
4838
4985
  side: "buy",
@@ -4853,9 +5000,10 @@ var GridManager = class {
4853
5000
  grid.levels = levels;
4854
5001
  grid.centerPrice = currentPrice;
4855
5002
  grid.atr = atr;
5003
+ grid.trend = trend;
4856
5004
  grid.stats.lastRebalanceAt = Date.now();
4857
5005
  console.error(chalk9.dim(
4858
- ` [grid] Built ${grid.token} grid: center=$${currentPrice.toFixed(2)} ATR=$${atr.toFixed(2)} range=$${(currentPrice - range).toFixed(2)}-$${(currentPrice + range).toFixed(2)} spacing=$${spacing.toFixed(2)} qty=${quantity.toFixed(6)}`
5006
+ ` [grid] Built ${grid.token} grid: center=$${currentPrice.toFixed(2)} ATR=$${atr.toFixed(2)} range=$${(currentPrice - range).toFixed(2)}-$${(currentPrice + range).toFixed(2)} spacing=$${spacing.toFixed(2)} qty=${quantity.toFixed(6)} trend=${(trend * 100).toFixed(1)}%${inDowntrend ? " [DOWNTREND: 1 buy / " + this.config.levelsPerSide + " sells]" : ""}`
4859
5007
  ));
4860
5008
  }
4861
5009
  /** Simulate order fills against the current price. */
@@ -4866,9 +5014,19 @@ var GridManager = class {
4866
5014
  const now = Date.now();
4867
5015
  const spacing = grid.atr > 0 ? grid.atr * this.config.atrMultiplier / this.config.levelsPerSide : 0;
4868
5016
  if (spacing <= 0) return { fills: 0, roundTrips: 0, pnlUsd: 0 };
5017
+ const exposureCap = grid.allocation * this.config.leverage * this.config.maxOpenNotionalMultiple;
5018
+ const currentOpenNotional = grid.openFills.filter((f) => !f.closed).reduce((s, f) => s + f.quantity * f.buyPrice, 0);
5019
+ const buyExposureFull = currentOpenNotional >= exposureCap;
5020
+ const inDowntrend = this.config.downtrendBlockPct > 0 && grid.trend < -this.config.downtrendBlockPct;
4869
5021
  for (const level of grid.levels) {
4870
5022
  if (level.filled) continue;
4871
- if (level.side === "buy" && currentPrice <= level.price) {
5023
+ if (level.side === "buy" && this.fillDetector(level, currentPrice)) {
5024
+ if (buyExposureFull) {
5025
+ continue;
5026
+ }
5027
+ if (inDowntrend) {
5028
+ continue;
5029
+ }
4872
5030
  level.filled = true;
4873
5031
  level.filledAt = now;
4874
5032
  fills++;
@@ -4888,7 +5046,7 @@ var GridManager = class {
4888
5046
  ` [grid] BUY fill ${grid.token} @ $${level.price.toFixed(2)} qty=${level.quantity.toFixed(6)}`
4889
5047
  ));
4890
5048
  }
4891
- if (level.side === "sell" && currentPrice >= level.price) {
5049
+ if (level.side === "sell" && this.fillDetector(level, currentPrice)) {
4892
5050
  level.filled = true;
4893
5051
  level.filledAt = now;
4894
5052
  fills++;
@@ -4901,8 +5059,8 @@ var GridManager = class {
4901
5059
  }
4902
5060
  for (const openFill of grid.openFills) {
4903
5061
  if (openFill.closed) continue;
4904
- if (currentPrice >= openFill.targetSellPrice) {
4905
- const profit = (currentPrice - openFill.buyPrice) * openFill.quantity * this.config.leverage;
5062
+ if (this.closeFillDetector(openFill, currentPrice)) {
5063
+ const profit = (currentPrice - openFill.buyPrice) * openFill.quantity;
4906
5064
  if (profit >= this.config.minProfitPerFillUsd) {
4907
5065
  openFill.closed = true;
4908
5066
  openFill.pnlUsd = profit;
@@ -4919,6 +5077,25 @@ var GridManager = class {
4919
5077
  }
4920
5078
  }
4921
5079
  }
5080
+ if (this.config.stopLossPct > 0) {
5081
+ for (const openFill of grid.openFills) {
5082
+ if (openFill.closed) continue;
5083
+ const stopPrice = openFill.buyPrice * (1 - this.config.stopLossPct);
5084
+ if (currentPrice <= stopPrice) {
5085
+ const realizedLoss = (currentPrice - openFill.buyPrice) * openFill.quantity;
5086
+ openFill.closed = true;
5087
+ openFill.pnlUsd = realizedLoss;
5088
+ openFill.closedAt = now;
5089
+ pnlUsd += realizedLoss;
5090
+ grid.stats.totalPnlUsd += realizedLoss;
5091
+ grid.stats.todayPnlUsd += realizedLoss;
5092
+ grid.allocation += realizedLoss;
5093
+ console.error(chalk9.red(
5094
+ ` [grid] STOP-LOSS ${grid.token}: buy $${openFill.buyPrice.toFixed(2)} \u2192 close $${currentPrice.toFixed(2)} = $${realizedLoss.toFixed(2)}`
5095
+ ));
5096
+ }
5097
+ }
5098
+ }
4922
5099
  const pruneThreshold = now - 24 * 60 * 60 * 1e3;
4923
5100
  grid.openFills = grid.openFills.filter((f) => !f.closed || f.closedAt > pruneThreshold);
4924
5101
  return { fills, roundTrips, pnlUsd };
@@ -5028,7 +5205,15 @@ var DEFAULT_GRID_CONFIG = {
5028
5205
  // 12h
5029
5206
  tokenSplit: { bitcoin: 0.45, ethereum: 0.3, solana: 0.25 },
5030
5207
  minProfitPerFillUsd: 0.5,
5031
- pauseThresholdPct: 0.2
5208
+ pauseThresholdPct: 0.2,
5209
+ // post leverage-fix calibration: fires at ~4% adverse move on 5x (real dollar terms); old 0.40 required ~8% which essentially never fired before liquidation
5210
+ unpauseRecoveryPct: 0.05,
5211
+ // hysteresis: resume when pool recovers to within 5% of initial (half of pause threshold to avoid ping-pong)
5212
+ maintenanceMarginPct: 0.02,
5213
+ maxOpenNotionalMultiple: 2,
5214
+ downtrendBlockPct: 0.1,
5215
+ stopLossPct: 0.1
5216
+ // empirical Nov-May: 10% saves the strategy (-4%); 30% lets liquidations through (-37%)
5032
5217
  };
5033
5218
 
5034
5219
  // src/grid/hedge.ts
@@ -5042,7 +5227,7 @@ var ADJUSTMENT_THRESHOLD = 0.1;
5042
5227
  var HEDGE_STATE_PATH = join2(homedir2(), ".sherwood", "grid", "hedge.json");
5043
5228
  var GridHedgeManager = class {
5044
5229
  state = null;
5045
- async load() {
5230
+ async load(now = Date.now()) {
5046
5231
  if (this.state) return this.state;
5047
5232
  try {
5048
5233
  const raw = await readFile2(HEDGE_STATE_PATH, "utf-8");
@@ -5052,14 +5237,14 @@ var GridHedgeManager = class {
5052
5237
  positions: [],
5053
5238
  totalRealizedPnl: 0,
5054
5239
  todayRealizedPnl: 0,
5055
- lastDailyReset: Date.now()
5240
+ lastDailyReset: now
5056
5241
  };
5057
5242
  }
5058
- const todayMidnight = /* @__PURE__ */ new Date();
5243
+ const todayMidnight = new Date(now);
5059
5244
  todayMidnight.setUTCHours(0, 0, 0, 0);
5060
5245
  if (this.state.lastDailyReset < todayMidnight.getTime()) {
5061
5246
  this.state.todayRealizedPnl = 0;
5062
- this.state.lastDailyReset = Date.now();
5247
+ this.state.lastDailyReset = now;
5063
5248
  }
5064
5249
  return this.state;
5065
5250
  }
@@ -5077,8 +5262,8 @@ var GridHedgeManager = class {
5077
5262
  * @param openFills - Per-token open fill exposure from the grid portfolio
5078
5263
  * @param prices - Current mark prices per token
5079
5264
  */
5080
- async tick(openFills, prices) {
5081
- const state = await this.load();
5265
+ async tick(openFills, prices, now = Date.now()) {
5266
+ const state = await this.load(now);
5082
5267
  let adjustments = 0;
5083
5268
  let unrealizedPnl = 0;
5084
5269
  for (const fill of openFills) {
@@ -5242,13 +5427,13 @@ import chalk12 from "chalk";
5242
5427
  import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
5243
5428
  import { dirname as dirname3, join as join3 } from "path";
5244
5429
  import { homedir as homedir3 } from "os";
5245
- import { encodeAbiParameters as encodeAbiParameters10, parseUnits as parseUnits6 } from "viem";
5430
+ import { encodeAbiParameters as encodeAbiParameters11, parseUnits as parseUnits6 } from "viem";
5246
5431
 
5247
5432
  // src/grid/cloid.ts
5248
- import { keccak256, encodeAbiParameters as encodeAbiParameters9 } from "viem";
5433
+ import { keccak256, encodeAbiParameters as encodeAbiParameters10 } from "viem";
5249
5434
  function gridCloid(strategy2, assetIndex, isBuy, levelIndex, nonce) {
5250
5435
  const hash = keccak256(
5251
- encodeAbiParameters9(
5436
+ encodeAbiParameters10(
5252
5437
  [
5253
5438
  { type: "address" },
5254
5439
  { type: "uint32" },
@@ -5271,6 +5456,13 @@ var HYPERLIQUID_GRID_STRATEGY_ABI = [
5271
5456
  inputs: [{ name: "data", type: "bytes" }],
5272
5457
  outputs: [],
5273
5458
  stateMutability: "nonpayable"
5459
+ },
5460
+ {
5461
+ type: "function",
5462
+ name: "maxOrdersPerTick",
5463
+ inputs: [],
5464
+ outputs: [{ type: "uint32" }],
5465
+ stateMutability: "view"
5274
5466
  }
5275
5467
  ];
5276
5468
 
@@ -5280,6 +5472,13 @@ var UINT64_MAX = 18446744073709551615n;
5280
5472
  var ACTION_PLACE_GRID = 1;
5281
5473
  var ACTION_CANCEL_ALL = 2;
5282
5474
  var ACTION_CANCEL_AND_PLACE = 3;
5475
+ function chunkArray(arr, size2) {
5476
+ if (size2 <= 0) throw new Error(`chunk size must be > 0 (got ${size2})`);
5477
+ if (arr.length === 0) return [];
5478
+ const out = [];
5479
+ for (let i = 0; i < arr.length; i += size2) out.push(arr.slice(i, i + size2));
5480
+ return out;
5481
+ }
5283
5482
  var GRID_ORDER_COMPONENTS = [
5284
5483
  { name: "assetIndex", type: "uint32" },
5285
5484
  { name: "isBuy", type: "bool" },
@@ -5293,9 +5492,24 @@ var OnchainGridExecutor = class {
5293
5492
  placedCloids = /* @__PURE__ */ new Map();
5294
5493
  meta = null;
5295
5494
  busy = false;
5495
+ /** On-chain `maxOrdersPerTick`; cached at load(). Used to chunk batches so
5496
+ * the strategy never reverts with TooManyOrders(actual, max). */
5497
+ maxOrdersPerTick = null;
5296
5498
  constructor(cfg) {
5297
5499
  this.cfg = cfg;
5298
5500
  }
5501
+ /** Read the strategy's per-call order cap and cache it. */
5502
+ async fetchMaxOrdersPerTick() {
5503
+ if (this.maxOrdersPerTick !== null) return this.maxOrdersPerTick;
5504
+ const pub = getPublicClient();
5505
+ const value = await pub.readContract({
5506
+ address: this.cfg.strategyAddress,
5507
+ abi: HYPERLIQUID_GRID_STRATEGY_ABI,
5508
+ functionName: "maxOrdersPerTick"
5509
+ });
5510
+ this.maxOrdersPerTick = Number(value);
5511
+ return this.maxOrdersPerTick;
5512
+ }
5299
5513
  /** Load persisted state. Call once before the first execute(). */
5300
5514
  async load() {
5301
5515
  try {
@@ -5318,6 +5532,7 @@ var OnchainGridExecutor = class {
5318
5532
  }
5319
5533
  }
5320
5534
  if (!this.meta) this.meta = await hlGetMeta();
5535
+ await this.fetchMaxOrdersPerTick();
5321
5536
  }
5322
5537
  /**
5323
5538
  * Atomic save: write to .tmp, then POSIX-rename. A crash mid-write leaves
@@ -5383,8 +5598,8 @@ var OnchainGridExecutor = class {
5383
5598
  const wantPlace = orders.length > 0;
5384
5599
  try {
5385
5600
  if (wantCancel && wantPlace) {
5386
- const tx = await this.cancelAndPlace(token, assetIndex, orders, meta);
5387
- txs.push(tx);
5601
+ const txArr = await this.cancelAndPlace(token, assetIndex, orders, meta);
5602
+ txs.push(...txArr);
5388
5603
  cancelled += 1;
5389
5604
  placed += orders.length;
5390
5605
  } else if (wantCancel) {
@@ -5392,8 +5607,8 @@ var OnchainGridExecutor = class {
5392
5607
  txs.push(tx);
5393
5608
  cancelled += 1;
5394
5609
  } else if (wantPlace) {
5395
- const tx = await this.placeGrid(token, assetIndex, orders, meta);
5396
- txs.push(tx);
5610
+ const txArr = await this.placeGrid(token, assetIndex, orders, meta);
5611
+ txs.push(...txArr);
5397
5612
  placed += orders.length;
5398
5613
  }
5399
5614
  await this.save();
@@ -5437,16 +5652,31 @@ var OnchainGridExecutor = class {
5437
5652
  });
5438
5653
  return { encoded, cloids };
5439
5654
  }
5655
+ /**
5656
+ * Place orders, chunking into batches of ≤ maxOrdersPerTick per tx.
5657
+ * Each chunk uses its own nonce so CLOIDs stay unique across chunks.
5658
+ * placedCloids is committed once after the last chunk lands; if any
5659
+ * chunk reverts, the partial cloids stay tracked via the per-chunk save.
5660
+ */
5440
5661
  async placeGrid(token, assetIndex, orders, meta) {
5441
- const nonce = this.bumpNonce(token);
5442
- const { encoded, cloids } = this.encodeOrders(assetIndex, orders, meta, nonce);
5443
- const data = encodeAbiParameters10(
5444
- [{ type: "uint8" }, { type: "tuple[]", components: [...GRID_ORDER_COMPONENTS] }],
5445
- [ACTION_PLACE_GRID, encoded]
5446
- );
5447
- const tx = await this.send(data);
5448
- this.placedCloids.set(token, cloids);
5449
- return tx;
5662
+ const max = await this.fetchMaxOrdersPerTick();
5663
+ const chunks = chunkArray(orders, max);
5664
+ const txs = [];
5665
+ const allCloids = [...this.placedCloids.get(token) ?? []];
5666
+ for (const chunk of chunks) {
5667
+ const nonce = this.bumpNonce(token);
5668
+ const { encoded, cloids } = this.encodeOrders(assetIndex, chunk, meta, nonce);
5669
+ const data = encodeAbiParameters11(
5670
+ [{ type: "uint8" }, { type: "tuple[]", components: [...GRID_ORDER_COMPONENTS] }],
5671
+ [ACTION_PLACE_GRID, encoded]
5672
+ );
5673
+ const tx = await this.send(data);
5674
+ txs.push(tx);
5675
+ allCloids.push(...cloids);
5676
+ this.placedCloids.set(token, allCloids);
5677
+ await this.save();
5678
+ }
5679
+ return txs;
5450
5680
  }
5451
5681
  async cancelAll(token, assetIndex) {
5452
5682
  const cloids = this.placedCloids.get(token) ?? [];
@@ -5454,7 +5684,7 @@ var OnchainGridExecutor = class {
5454
5684
  this.bumpNonce(token);
5455
5685
  return "0x";
5456
5686
  }
5457
- const data = encodeAbiParameters10(
5687
+ const data = encodeAbiParameters11(
5458
5688
  [{ type: "uint8" }, { type: "uint32" }, { type: "uint128[]" }],
5459
5689
  [ACTION_CANCEL_ALL, assetIndex, cloids]
5460
5690
  );
@@ -5463,22 +5693,48 @@ var OnchainGridExecutor = class {
5463
5693
  this.bumpNonce(token);
5464
5694
  return tx;
5465
5695
  }
5696
+ /**
5697
+ * Atomic cancel-old + place-new on rebalance. When `orders` exceeds
5698
+ * maxOrdersPerTick, the first chunk uses ACTION_CANCEL_AND_PLACE (so the
5699
+ * old orders are cleared in the same tx as the first wave of new orders);
5700
+ * subsequent chunks use ACTION_PLACE_GRID. Atomicity holds for chunk #1;
5701
+ * after that there's a brief window where some new orders are live before
5702
+ * the rest are placed — acceptable for a 60s rebuild cycle.
5703
+ */
5466
5704
  async cancelAndPlace(token, assetIndex, orders, meta) {
5705
+ const max = await this.fetchMaxOrdersPerTick();
5706
+ const chunks = chunkArray(orders, max);
5467
5707
  const oldCloids = this.placedCloids.get(token) ?? [];
5468
- const newNonce = this.bumpNonce(token);
5469
- const { encoded, cloids: newCloids } = this.encodeOrders(assetIndex, orders, meta, newNonce);
5470
- const data = encodeAbiParameters10(
5471
- [
5472
- { type: "uint8" },
5473
- { type: "uint32" },
5474
- { type: "uint128[]" },
5475
- { type: "tuple[]", components: [...GRID_ORDER_COMPONENTS] }
5476
- ],
5477
- [ACTION_CANCEL_AND_PLACE, assetIndex, oldCloids, encoded]
5478
- );
5479
- const tx = await this.send(data);
5480
- this.placedCloids.set(token, newCloids);
5481
- return tx;
5708
+ const txs = [];
5709
+ const allNewCloids = [];
5710
+ for (let i = 0; i < chunks.length; i++) {
5711
+ const chunk = chunks[i];
5712
+ const newNonce = this.bumpNonce(token);
5713
+ const { encoded, cloids } = this.encodeOrders(assetIndex, chunk, meta, newNonce);
5714
+ let data;
5715
+ if (i === 0) {
5716
+ data = encodeAbiParameters11(
5717
+ [
5718
+ { type: "uint8" },
5719
+ { type: "uint32" },
5720
+ { type: "uint128[]" },
5721
+ { type: "tuple[]", components: [...GRID_ORDER_COMPONENTS] }
5722
+ ],
5723
+ [ACTION_CANCEL_AND_PLACE, assetIndex, oldCloids, encoded]
5724
+ );
5725
+ } else {
5726
+ data = encodeAbiParameters11(
5727
+ [{ type: "uint8" }, { type: "tuple[]", components: [...GRID_ORDER_COMPONENTS] }],
5728
+ [ACTION_PLACE_GRID, encoded]
5729
+ );
5730
+ }
5731
+ const tx = await this.send(data);
5732
+ txs.push(tx);
5733
+ allNewCloids.push(...cloids);
5734
+ this.placedCloids.set(token, [...allNewCloids]);
5735
+ await this.save();
5736
+ }
5737
+ return txs;
5482
5738
  }
5483
5739
  /**
5484
5740
  * Submit and wait for receipt. Throwing here surfaces to the per-token
@@ -5651,15 +5907,862 @@ var GridLoop = class {
5651
5907
  }
5652
5908
  };
5653
5909
 
5910
+ // src/grid/backtest.ts
5911
+ import { writeFile as writeFile5, mkdir as mkdir6 } from "fs/promises";
5912
+ import { join as join6, dirname as dirname4 } from "path";
5913
+ import { homedir as homedir6 } from "os";
5914
+ import { createHash } from "crypto";
5915
+
5916
+ // src/grid/backtest-portfolio.ts
5917
+ var BacktestPortfolio = class extends GridPortfolio {
5918
+ nowProvider;
5919
+ inMemoryState = null;
5920
+ constructor(nowProvider) {
5921
+ super();
5922
+ this.nowProvider = nowProvider;
5923
+ }
5924
+ /** Override: never read from disk. Returns the in-memory state. */
5925
+ async load() {
5926
+ return this.inMemoryState;
5927
+ }
5928
+ /** Override: never write to disk. Stores the state in memory. */
5929
+ async save(state) {
5930
+ this.inMemoryState = state;
5931
+ }
5932
+ /** Override: read in-memory state instead of parent's private field. */
5933
+ getState() {
5934
+ return this.inMemoryState;
5935
+ }
5936
+ /** Override: use the injected clock instead of Date.now(). */
5937
+ resetDailyStats(state) {
5938
+ return super.resetDailyStats(state, this.nowProvider());
5939
+ }
5940
+ };
5941
+
5942
+ // src/grid/historical-data-loader.ts
5943
+ import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
5944
+ import { join as join5 } from "path";
5945
+ import { homedir as homedir5 } from "os";
5946
+ var BINANCE_BASE = "https://api.binance.com/api/v3/klines";
5947
+ var DEFAULT_CACHE_DIR = join5(homedir5(), ".sherwood", "grid", "backtest-cache");
5948
+ var FOUR_HOURS_MS = 4 * 60 * 60 * 1e3;
5949
+ var ONE_MIN_MS = 60 * 1e3;
5950
+ var ATR_PERIOD = 14;
5951
+ var ATR_WARMUP_MS = ATR_PERIOD * FOUR_HOURS_MS;
5952
+ var MAX_BARS_PER_REQUEST = 1e3;
5953
+ var COIN_TO_BINANCE_SYMBOL = {
5954
+ BTC: "BTCUSDT",
5955
+ ETH: "ETHUSDT",
5956
+ SOL: "SOLUSDT",
5957
+ ARB: "ARBUSDT",
5958
+ LINK: "LINKUSDT",
5959
+ AAVE: "AAVEUSDT",
5960
+ UNI: "UNIUSDT",
5961
+ DOGE: "DOGEUSDT"
5962
+ };
5963
+ var HistoricalDataLoader = class {
5964
+ cacheDir;
5965
+ noCache;
5966
+ fetchImpl;
5967
+ constructor(opts = {}) {
5968
+ this.cacheDir = opts.cacheDir ?? DEFAULT_CACHE_DIR;
5969
+ this.noCache = opts.noCache ?? false;
5970
+ this.fetchImpl = opts.fetchImpl ?? fetch;
5971
+ }
5972
+ /**
5973
+ * Load 1-minute bars and an aligned ATR(14) series for `coinTokenId`
5974
+ * (CoinGecko ID; resolved internally to HL coin symbol).
5975
+ *
5976
+ * The 4-hour series is fetched with ATR_WARMUP_MS extra prefix so ATR
5977
+ * is well-defined at fromMs.
5978
+ */
5979
+ async load(coinTokenId, fromMs, toMs) {
5980
+ const coin = TOKEN_TO_COIN[coinTokenId];
5981
+ if (!coin) throw new Error(`Unknown token: ${coinTokenId}`);
5982
+ const minutes = await this.loadInterval(coin, "1m", fromMs, toMs);
5983
+ if (minutes.length === 0) {
5984
+ throw new Error(`no data for ${coinTokenId} in window \u2014 check symbol mapping or date range`);
5985
+ }
5986
+ const fourHour = await this.loadInterval(coin, "4h", fromMs - ATR_WARMUP_MS, toMs);
5987
+ const atrSeries = computeAtrSeries(fourHour);
5988
+ return { minutes, fourHour, atrSeries };
5989
+ }
5990
+ /** Fetch + cache one interval. Public for unit testing. */
5991
+ async loadInterval(coin, interval, fromMs, toMs) {
5992
+ if (!this.noCache) {
5993
+ const cached = await this.tryReadCache(coin, interval, fromMs, toMs);
5994
+ if (cached) return cached;
5995
+ }
5996
+ const fresh = await this.fetchPaginated(coin, interval, fromMs, toMs);
5997
+ if (!this.noCache && fresh.length > 0) {
5998
+ await this.writeCache(coin, interval, fromMs, toMs, fresh);
5999
+ }
6000
+ return fresh;
6001
+ }
6002
+ async tryReadCache(coin, interval, fromMs, toMs) {
6003
+ const path = this.cachePath(coin, interval, fromMs, toMs);
6004
+ try {
6005
+ const raw = await readFile4(path, "utf-8");
6006
+ const parsed = JSON.parse(raw);
6007
+ if (!Array.isArray(parsed.bars)) return null;
6008
+ return parsed.bars;
6009
+ } catch {
6010
+ return null;
6011
+ }
6012
+ }
6013
+ async writeCache(coin, interval, fromMs, toMs, bars) {
6014
+ await mkdir5(this.cacheDir, { recursive: true });
6015
+ const path = this.cachePath(coin, interval, fromMs, toMs);
6016
+ const body = JSON.stringify({
6017
+ coin,
6018
+ interval,
6019
+ fetchedAt: Date.now(),
6020
+ bars
6021
+ });
6022
+ await writeFile4(path, body, "utf-8");
6023
+ }
6024
+ cachePath(coin, interval, fromMs, toMs) {
6025
+ return join5(this.cacheDir, `${coin}-${interval}-${fromMs}-${toMs}.json`);
6026
+ }
6027
+ /** Paginate Binance klines until we cover [fromMs, toMs]. Dedup on merge. */
6028
+ async fetchPaginated(coin, interval, fromMs, toMs) {
6029
+ const intervalMs = interval === "1m" ? ONE_MIN_MS : FOUR_HOURS_MS;
6030
+ const pageSpanMs = MAX_BARS_PER_REQUEST * intervalMs;
6031
+ const seen = /* @__PURE__ */ new Set();
6032
+ const all = [];
6033
+ let cursor = fromMs;
6034
+ while (cursor < toMs) {
6035
+ const pageEnd = Math.min(cursor + pageSpanMs, toMs);
6036
+ const page = await this.fetchPageWithRetry(coin, interval, cursor, pageEnd);
6037
+ if (page.length === 0) break;
6038
+ for (const bar of page) {
6039
+ if (!seen.has(bar.t)) {
6040
+ seen.add(bar.t);
6041
+ all.push(bar);
6042
+ }
6043
+ }
6044
+ const last = page[page.length - 1];
6045
+ const next = last.t + intervalMs;
6046
+ if (next <= cursor) break;
6047
+ cursor = next;
6048
+ }
6049
+ all.sort((a, b) => a.t - b.t);
6050
+ return all;
6051
+ }
6052
+ async fetchPageWithRetry(coin, interval, startTime, endTime) {
6053
+ const delays = [1e3, 2e3, 4e3];
6054
+ let lastErr = null;
6055
+ for (let attempt = 0; attempt < delays.length + 1; attempt++) {
6056
+ try {
6057
+ return await this.fetchPage(coin, interval, startTime, endTime);
6058
+ } catch (err) {
6059
+ lastErr = err;
6060
+ if (attempt < delays.length) {
6061
+ await sleep(delays[attempt]);
6062
+ }
6063
+ }
6064
+ }
6065
+ throw lastErr ?? new Error("fetchPage failed");
6066
+ }
6067
+ async fetchPage(coin, interval, startTime, endTime) {
6068
+ const symbol = COIN_TO_BINANCE_SYMBOL[coin];
6069
+ if (!symbol) throw new Error(`No Binance symbol for coin: ${coin}`);
6070
+ const url = `${BINANCE_BASE}?symbol=${symbol}&interval=${interval}&startTime=${startTime}&endTime=${endTime}&limit=${MAX_BARS_PER_REQUEST}`;
6071
+ const res = await this.fetchImpl(url);
6072
+ if (!res.ok) throw new Error(`Binance ${res.status}: ${await res.text()}`);
6073
+ const raw = await res.json();
6074
+ if (!Array.isArray(raw)) return [];
6075
+ return raw.map((c) => ({
6076
+ t: c[0],
6077
+ o: Number(c[1]),
6078
+ h: Number(c[2]),
6079
+ l: Number(c[3]),
6080
+ c: Number(c[4]),
6081
+ v: Number(c[5])
6082
+ })).filter(
6083
+ (b) => Number.isFinite(b.o) && Number.isFinite(b.h) && Number.isFinite(b.l) && Number.isFinite(b.c)
6084
+ );
6085
+ }
6086
+ };
6087
+ function computeAtrSeries(fourHourBars) {
6088
+ if (fourHourBars.length < ATR_PERIOD) return [];
6089
+ const candles = fourHourBars.map((b) => ({
6090
+ timestamp: b.t,
6091
+ open: b.o,
6092
+ high: b.h,
6093
+ low: b.l,
6094
+ close: b.c,
6095
+ volume: b.v
6096
+ }));
6097
+ const atrArr = calculateATR(candles, ATR_PERIOD);
6098
+ const out = [];
6099
+ for (let i = 0; i < atrArr.length; i++) {
6100
+ if (Number.isFinite(atrArr[i])) {
6101
+ out.push({ ts: fourHourBars[i].t, atr: atrArr[i] });
6102
+ }
6103
+ }
6104
+ return out;
6105
+ }
6106
+ function sleep(ms) {
6107
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
6108
+ }
6109
+
6110
+ // src/grid/backtest-hedge.ts
6111
+ var BacktestHedgeManager = class extends GridHedgeManager {
6112
+ nowProvider;
6113
+ inMemoryState = null;
6114
+ constructor(nowProvider) {
6115
+ super();
6116
+ this.nowProvider = nowProvider;
6117
+ }
6118
+ async load(_now) {
6119
+ if (this.inMemoryState) {
6120
+ const now2 = this.nowProvider();
6121
+ const todayMidnight = new Date(now2);
6122
+ todayMidnight.setUTCHours(0, 0, 0, 0);
6123
+ if (this.inMemoryState.lastDailyReset < todayMidnight.getTime()) {
6124
+ this.inMemoryState.todayRealizedPnl = 0;
6125
+ this.inMemoryState.lastDailyReset = now2;
6126
+ }
6127
+ this.state = this.inMemoryState;
6128
+ return this.inMemoryState;
6129
+ }
6130
+ const now = this.nowProvider();
6131
+ this.inMemoryState = {
6132
+ positions: [],
6133
+ totalRealizedPnl: 0,
6134
+ todayRealizedPnl: 0,
6135
+ lastDailyReset: now
6136
+ };
6137
+ this.state = this.inMemoryState;
6138
+ return this.inMemoryState;
6139
+ }
6140
+ async save() {
6141
+ }
6142
+ };
6143
+
6144
+ // src/grid/backtest.ts
6145
+ var DEFAULT_OUT_DIR = join6(homedir6(), ".sherwood", "grid", "backtests");
6146
+ function shortHash(input2) {
6147
+ const h = createHash("sha256").update(JSON.stringify(input2)).digest("hex");
6148
+ return h.slice(0, 8);
6149
+ }
6150
+ function computeDrawdown(curve) {
6151
+ if (curve.length === 0) {
6152
+ return { maxUsd: 0, maxPct: 0, peakAt: 0, troughAt: 0 };
6153
+ }
6154
+ const equityAt = (p) => p.totalAllocation + p.totalPnl;
6155
+ let peak = equityAt(curve[0]);
6156
+ let peakAt = curve[0].t;
6157
+ let maxDrop = 0;
6158
+ let dropPeakAt = peakAt;
6159
+ let dropTroughAt = peakAt;
6160
+ for (const point of curve) {
6161
+ const eq = equityAt(point);
6162
+ if (eq > peak) {
6163
+ peak = eq;
6164
+ peakAt = point.t;
6165
+ }
6166
+ const drop = peak - eq;
6167
+ if (drop > maxDrop) {
6168
+ maxDrop = drop;
6169
+ dropPeakAt = peakAt;
6170
+ dropTroughAt = point.t;
6171
+ }
6172
+ }
6173
+ const peakPoint = curve.find((p) => p.t === dropPeakAt);
6174
+ const peakValue = peakPoint ? equityAt(peakPoint) : 0;
6175
+ return {
6176
+ maxUsd: maxDrop,
6177
+ maxPct: peakValue > 0 ? maxDrop / peakValue : 0,
6178
+ peakAt: dropPeakAt,
6179
+ troughAt: dropTroughAt
6180
+ };
6181
+ }
6182
+ function isoToYmdHms(ms) {
6183
+ const d = new Date(ms);
6184
+ const pad = (n) => n.toString().padStart(2, "0");
6185
+ return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
6186
+ }
6187
+ function makeRunId(fromMs, toMs, config) {
6188
+ return `bt-${isoToYmdHms(Date.now())}-${shortHash({ fromMs, toMs, config })}`;
6189
+ }
6190
+ async function runBacktest(opts) {
6191
+ const startedAt = Date.now();
6192
+ if (opts.fromMs >= opts.toMs) throw new Error("--from must be before --to");
6193
+ const FIFTY_SIX_HOURS = 56 * 60 * 60 * 1e3;
6194
+ if (opts.toMs - opts.fromMs < FIFTY_SIX_HOURS) {
6195
+ throw new Error("window too short \u2014 need \u2265 56h for ATR(14) warmup");
6196
+ }
6197
+ const splitSum = Object.values(opts.config.tokenSplit).reduce((a, b) => a + b, 0);
6198
+ if (Math.abs(splitSum - 1) > 1e-6) {
6199
+ throw new Error(`tokenSplit must sum to 1.0, got ${splitSum.toFixed(6)}`);
6200
+ }
6201
+ const loader = opts.loader ?? new HistoricalDataLoader({ noCache: opts.noCache });
6202
+ const snapshotEvery = opts.snapshotEveryMinutes ?? 60;
6203
+ const seriesByToken = {};
6204
+ await Promise.all(opts.config.tokens.map(async (token) => {
6205
+ seriesByToken[token] = await loader.load(token, opts.fromMs, opts.toMs);
6206
+ }));
6207
+ const minuteSets = opts.config.tokens.map((t) => new Set(seriesByToken[t].minutes.map((b) => b.t)));
6208
+ const masterMinutes = seriesByToken[opts.config.tokens[0]].minutes;
6209
+ const sharedTimestamps = [];
6210
+ let skippedSteps = 0;
6211
+ for (const bar of masterMinutes) {
6212
+ if (bar.t < opts.fromMs || bar.t >= opts.toMs) continue;
6213
+ if (minuteSets.every((s) => s.has(bar.t))) {
6214
+ sharedTimestamps.push(bar.t);
6215
+ } else {
6216
+ skippedSteps++;
6217
+ }
6218
+ }
6219
+ if (sharedTimestamps.length === 0) {
6220
+ throw new Error("no overlapping bars across requested tokens");
6221
+ }
6222
+ const barIndex = {};
6223
+ for (const token of opts.config.tokens) {
6224
+ const m = /* @__PURE__ */ new Map();
6225
+ for (const bar of seriesByToken[token].minutes) m.set(bar.t, bar);
6226
+ barIndex[token] = m;
6227
+ }
6228
+ let currentT = sharedTimestamps[0];
6229
+ const nowProvider = () => currentT;
6230
+ const candleFetcher = async (tokenId, interval, lookbackMs) => {
6231
+ if (interval !== "4h") return null;
6232
+ const series = seriesByToken[tokenId];
6233
+ if (!series) return null;
6234
+ const cutoff = currentT - lookbackMs;
6235
+ return series.fourHour.filter((b) => b.t <= currentT && b.t >= cutoff).map((b) => ({
6236
+ timestamp: b.t,
6237
+ open: b.o,
6238
+ high: b.h,
6239
+ low: b.l,
6240
+ close: b.c,
6241
+ volume: b.v
6242
+ }));
6243
+ };
6244
+ const portfolio = new BacktestPortfolio(nowProvider);
6245
+ const manager = new GridManager(opts.config, candleFetcher, void 0, void 0, portfolio, nowProvider);
6246
+ await manager.init(opts.capital);
6247
+ const initialAllocations = {};
6248
+ {
6249
+ const initState = portfolio.getState();
6250
+ if (initState) {
6251
+ for (const grid of initState.grids) {
6252
+ initialAllocations[grid.token] = grid.allocation;
6253
+ }
6254
+ }
6255
+ }
6256
+ const feeBps = opts.feeBps ?? 5;
6257
+ const notionalPerFillByToken = {};
6258
+ for (const token of opts.config.tokens) {
6259
+ const tokenInitial = initialAllocations[token] ?? 0;
6260
+ notionalPerFillByToken[token] = tokenInitial * opts.config.leverage / opts.config.levelsPerSide;
6261
+ }
6262
+ const feesByToken = {};
6263
+ for (const token of opts.config.tokens) feesByToken[token] = 0;
6264
+ const lastSeenFills = {};
6265
+ for (const token of opts.config.tokens) lastSeenFills[token] = 0;
6266
+ const hedgeEnabled = opts.hedge ?? true;
6267
+ const hedge = new BacktestHedgeManager(nowProvider);
6268
+ let hedgeAdjustments = 0;
6269
+ let hedgeRealized = 0;
6270
+ let hedgeUnrealized = 0;
6271
+ let totalRebuilds = 0;
6272
+ const lastRebalanceAt = {};
6273
+ for (const t of opts.config.tokens) lastRebalanceAt[t] = 0;
6274
+ const liquidatedTokens = /* @__PURE__ */ new Set();
6275
+ const liquidationEvents = [];
6276
+ let haltedAt = null;
6277
+ const equityCurve = [];
6278
+ let pausedSteps = 0;
6279
+ const originalConsoleError = console.error;
6280
+ if (!opts.verbose) {
6281
+ console.error = () => {
6282
+ };
6283
+ }
6284
+ try {
6285
+ let cycleCount = 0;
6286
+ const totalSteps = sharedTimestamps.length;
6287
+ const decileSize = Math.max(1, Math.floor(totalSteps / 10));
6288
+ for (const t of sharedTimestamps) {
6289
+ currentT = t;
6290
+ const passAPrices = {};
6291
+ const passBPrices = {};
6292
+ for (const token of opts.config.tokens) {
6293
+ const bar = barIndex[token].get(t);
6294
+ const isGreen = bar.c >= bar.o;
6295
+ passAPrices[token] = isGreen ? bar.l : bar.h;
6296
+ passBPrices[token] = isGreen ? bar.h : bar.l;
6297
+ }
6298
+ const r1 = await manager.tick(passAPrices);
6299
+ const r2 = await manager.tick(passBPrices);
6300
+ const stateAfter = portfolio.getState();
6301
+ if (stateAfter) {
6302
+ for (const grid of stateAfter.grids) {
6303
+ if (grid.stats.lastRebalanceAt > lastRebalanceAt[grid.token]) {
6304
+ totalRebuilds++;
6305
+ lastRebalanceAt[grid.token] = grid.stats.lastRebalanceAt;
6306
+ }
6307
+ }
6308
+ }
6309
+ if (r1.paused || r2.paused) pausedSteps++;
6310
+ if (hedgeEnabled) {
6311
+ const hedgePrices = {};
6312
+ for (const token of opts.config.tokens) {
6313
+ const bar = barIndex[token].get(t);
6314
+ hedgePrices[token] = bar.c;
6315
+ }
6316
+ const exposure = manager.getOpenFillExposure();
6317
+ const hedgeResult = await hedge.tick(exposure, hedgePrices, t);
6318
+ hedgeAdjustments += hedgeResult.adjustments;
6319
+ hedgeRealized = hedgeResult.totalRealizedPnl;
6320
+ hedgeUnrealized = hedgeResult.unrealizedPnl;
6321
+ }
6322
+ if (stateAfter) {
6323
+ for (const grid of stateAfter.grids) {
6324
+ const newFills = grid.stats.totalFills - lastSeenFills[grid.token];
6325
+ if (newFills > 0) {
6326
+ feesByToken[grid.token] += newFills * (notionalPerFillByToken[grid.token] ?? 0) * feeBps / 1e4;
6327
+ lastSeenFills[grid.token] = grid.stats.totalFills;
6328
+ }
6329
+ }
6330
+ }
6331
+ const stateLiq = portfolio.getState();
6332
+ if (stateLiq && haltedAt === null) {
6333
+ const lev = opts.config.leverage;
6334
+ const maint = opts.config.maintenanceMarginPct;
6335
+ const liquidationCoefficient = 1 - lev * maint;
6336
+ for (const grid of stateLiq.grids) {
6337
+ if (liquidatedTokens.has(grid.token)) continue;
6338
+ const close = barIndex[grid.token]?.get(t)?.c;
6339
+ if (close === void 0) continue;
6340
+ let unrealized = 0;
6341
+ for (const f of grid.openFills) {
6342
+ if (f.closed) continue;
6343
+ unrealized += (close - f.buyPrice) * f.quantity;
6344
+ }
6345
+ const tokenAlloc = initialAllocations[grid.token] ?? 0;
6346
+ const threshold = -tokenAlloc * liquidationCoefficient;
6347
+ if (tokenAlloc > 0 && unrealized <= threshold) {
6348
+ grid.stats.totalPnlUsd += unrealized;
6349
+ grid.stats.todayPnlUsd += unrealized;
6350
+ grid.allocation += unrealized;
6351
+ for (const f of grid.openFills) {
6352
+ if (!f.closed) {
6353
+ f.closed = true;
6354
+ f.closedAt = t;
6355
+ f.pnlUsd = (close - f.buyPrice) * f.quantity;
6356
+ }
6357
+ }
6358
+ grid.levels = [];
6359
+ liquidatedTokens.add(grid.token);
6360
+ liquidationEvents.push({
6361
+ token: grid.token,
6362
+ timestamp: t,
6363
+ tokenAllocation: tokenAlloc,
6364
+ unrealizedPnlAtLiquidation: unrealized,
6365
+ thresholdUsd: threshold
6366
+ });
6367
+ process.stderr.write(
6368
+ ` [backtest] LIQUIDATED ${grid.token} at t=${new Date(t).toISOString()}: unrealized=$${unrealized.toFixed(0)} (threshold=$${threshold.toFixed(0)})
6369
+ `
6370
+ );
6371
+ }
6372
+ }
6373
+ if (liquidatedTokens.size === opts.config.tokens.length) {
6374
+ haltedAt = t;
6375
+ process.stderr.write(` [backtest] All tokens liquidated; halting run at t=${new Date(t).toISOString()}
6376
+ `);
6377
+ }
6378
+ }
6379
+ if (haltedAt !== null) {
6380
+ const sFinal = portfolio.getState();
6381
+ if (sFinal) {
6382
+ const aggFinal = portfolio.aggregateStats(sFinal);
6383
+ const ofcFinal = sFinal.grids.reduce((sum, g) => sum + g.openFills.filter((f) => !f.closed).length, 0);
6384
+ const runningFeesFinal = Object.values(feesByToken).reduce((a, b) => a + b, 0);
6385
+ equityCurve.push({
6386
+ t,
6387
+ totalAllocation: sFinal.totalAllocation,
6388
+ totalPnl: aggFinal.totalPnlUsd - runningFeesFinal + hedgeRealized + hedgeUnrealized,
6389
+ totalRoundTrips: aggFinal.totalRoundTrips,
6390
+ openFillCount: ofcFinal,
6391
+ paused: sFinal.paused
6392
+ });
6393
+ }
6394
+ break;
6395
+ }
6396
+ cycleCount++;
6397
+ if (cycleCount % snapshotEvery === 0 || cycleCount === totalSteps) {
6398
+ const s = portfolio.getState();
6399
+ if (s) {
6400
+ const agg2 = portfolio.aggregateStats(s);
6401
+ const openFillCount = s.grids.reduce(
6402
+ (sum, g) => sum + g.openFills.filter((f) => !f.closed).length,
6403
+ 0
6404
+ );
6405
+ let unrealizedPnl = 0;
6406
+ for (const grid of s.grids) {
6407
+ const close = barIndex[grid.token]?.get(t)?.c;
6408
+ if (close === void 0) continue;
6409
+ for (const f of grid.openFills) {
6410
+ if (f.closed) continue;
6411
+ unrealizedPnl += (close - f.buyPrice) * f.quantity;
6412
+ }
6413
+ }
6414
+ const runningFees = Object.values(feesByToken).reduce((a, b) => a + b, 0);
6415
+ equityCurve.push({
6416
+ t,
6417
+ totalAllocation: s.totalAllocation,
6418
+ totalPnl: agg2.totalPnlUsd + unrealizedPnl - runningFees + hedgeRealized + hedgeUnrealized,
6419
+ totalRoundTrips: agg2.totalRoundTrips,
6420
+ openFillCount,
6421
+ paused: s.paused
6422
+ });
6423
+ }
6424
+ }
6425
+ if (cycleCount % decileSize === 0) {
6426
+ if (opts.verbose) {
6427
+ originalConsoleError(` [backtest] ${Math.round(cycleCount / totalSteps * 100)}% step ${cycleCount}/${totalSteps}`);
6428
+ } else {
6429
+ process.stderr.write(` [backtest] ${Math.round(cycleCount / totalSteps * 100)}% step ${cycleCount}/${totalSteps}
6430
+ `);
6431
+ }
6432
+ }
6433
+ }
6434
+ } finally {
6435
+ console.error = originalConsoleError;
6436
+ }
6437
+ const finalState = portfolio.getState();
6438
+ const agg = portfolio.aggregateStats(finalState);
6439
+ const finishedAt = Date.now();
6440
+ const runId = makeRunId(opts.fromMs, opts.toMs, opts.config);
6441
+ const initialPerToken = {};
6442
+ for (const token of opts.config.tokens) {
6443
+ initialPerToken[token] = opts.capital * (opts.config.tokenSplit[token] ?? 0);
6444
+ }
6445
+ const totalFeesUsd = Object.values(feesByToken).reduce((a, b) => a + b, 0);
6446
+ const totalFills = finalState.grids.reduce((s, g) => s + g.stats.totalFills, 0);
6447
+ const avgFeePerFill = totalFills > 0 ? totalFeesUsd / totalFills : 0;
6448
+ const grossPnl = agg.totalPnlUsd;
6449
+ const netPnl = grossPnl - totalFeesUsd;
6450
+ const hedgeStatus = hedgeEnabled ? hedge.getStatus() : null;
6451
+ const hedgePnl = hedgeRealized + hedgeUnrealized;
6452
+ const finalNetPnl = netPnl + hedgePnl;
6453
+ const perToken = finalState.grids.map((g) => ({
6454
+ token: g.token,
6455
+ allocation: { initial: initialPerToken[g.token] ?? 0, final: g.allocation },
6456
+ roundTrips: g.stats.totalRoundTrips,
6457
+ fills: g.stats.totalFills,
6458
+ pnlUsd: g.stats.totalPnlUsd,
6459
+ rebuilds: 0
6460
+ }));
6461
+ const result = {
6462
+ runId,
6463
+ startedAt,
6464
+ finishedAt,
6465
+ durationMs: finishedAt - startedAt,
6466
+ window: {
6467
+ fromMs: opts.fromMs,
6468
+ toMs: opts.toMs,
6469
+ fromIso: new Date(opts.fromMs).toISOString(),
6470
+ toIso: new Date(opts.toMs).toISOString(),
6471
+ days: (opts.toMs - opts.fromMs) / (24 * 60 * 60 * 1e3)
6472
+ },
6473
+ config: opts.config,
6474
+ capital: {
6475
+ initialUsd: opts.capital,
6476
+ finalUsd: opts.capital + finalNetPnl,
6477
+ pnlUsd: finalNetPnl,
6478
+ pnlPct: finalNetPnl / opts.capital,
6479
+ grossPnlUsd: grossPnl
6480
+ },
6481
+ fees: {
6482
+ bps: feeBps,
6483
+ totalUsd: totalFeesUsd,
6484
+ perFill: avgFeePerFill
6485
+ },
6486
+ hedge: {
6487
+ enabled: hedgeEnabled,
6488
+ realizedPnlUsd: hedgeRealized,
6489
+ unrealizedPnlUsd: hedgeUnrealized,
6490
+ adjustments: hedgeAdjustments,
6491
+ finalOpenPositions: hedgeStatus ? hedgeStatus.positions.length : 0
6492
+ },
6493
+ liquidations: {
6494
+ events: liquidationEvents,
6495
+ haltedAt
6496
+ },
6497
+ totals: {
6498
+ roundTrips: agg.totalRoundTrips,
6499
+ fills: finalState.grids.reduce((s, g) => s + g.stats.totalFills, 0),
6500
+ rebuilds: totalRebuilds,
6501
+ pausedSteps,
6502
+ skippedSteps,
6503
+ totalSteps: sharedTimestamps.length
6504
+ },
6505
+ perToken,
6506
+ drawdown: computeDrawdown(equityCurve),
6507
+ equityCurve
6508
+ };
6509
+ if (opts.outPath !== "") {
6510
+ const outPath = opts.outPath ?? join6(DEFAULT_OUT_DIR, `${runId}.json`);
6511
+ await mkdir6(dirname4(outPath), { recursive: true });
6512
+ await writeFile5(outPath, JSON.stringify(result, null, 2), "utf-8");
6513
+ }
6514
+ return result;
6515
+ }
6516
+
6517
+ // src/grid/sweep.ts
6518
+ import { writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
6519
+ import { join as join7 } from "path";
6520
+ import { homedir as homedir7 } from "os";
6521
+ import { createHash as createHash2 } from "crypto";
6522
+ var DEFAULT_SWEEP_DIR = join7(homedir7(), ".sherwood", "grid", "sweeps");
6523
+ function expandSweep(sweep, base2) {
6524
+ const fields = ["leverage", "levelsPerSide", "atrMultiplier", "rebalanceDriftPct"];
6525
+ const valueLists = {
6526
+ leverage: sweep.leverage && sweep.leverage.length > 0 ? sweep.leverage : [base2.leverage],
6527
+ levelsPerSide: sweep.levelsPerSide && sweep.levelsPerSide.length > 0 ? sweep.levelsPerSide : [base2.levelsPerSide],
6528
+ atrMultiplier: sweep.atrMultiplier && sweep.atrMultiplier.length > 0 ? sweep.atrMultiplier : [base2.atrMultiplier],
6529
+ rebalanceDriftPct: sweep.rebalanceDriftPct && sweep.rebalanceDriftPct.length > 0 ? sweep.rebalanceDriftPct : [base2.rebalanceDriftPct]
6530
+ };
6531
+ let combos = [{}];
6532
+ for (const field of fields) {
6533
+ const next = [];
6534
+ for (const combo of combos) {
6535
+ for (const value of valueLists[field]) {
6536
+ next.push({ ...combo, [field]: value });
6537
+ }
6538
+ }
6539
+ combos = next;
6540
+ }
6541
+ return combos;
6542
+ }
6543
+ function makeSweepId(opts) {
6544
+ const d = /* @__PURE__ */ new Date();
6545
+ const pad = (n) => n.toString().padStart(2, "0");
6546
+ const stamp = `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
6547
+ const hash = createHash2("sha256").update(JSON.stringify({
6548
+ fromMs: opts.fromMs,
6549
+ toMs: opts.toMs,
6550
+ sweep: opts.sweep,
6551
+ tokens: opts.tokens
6552
+ })).digest("hex").slice(0, 8);
6553
+ return `sw-${stamp}-${hash}`;
6554
+ }
6555
+ async function runSweep(opts) {
6556
+ const startedAt = Date.now();
6557
+ const sweepId = makeSweepId(opts);
6558
+ const outDir = opts.outDir ?? DEFAULT_SWEEP_DIR;
6559
+ const sweepDir = join7(outDir, sweepId);
6560
+ let tokenSplit;
6561
+ if (opts.tokenSplit) {
6562
+ tokenSplit = opts.tokenSplit;
6563
+ } else {
6564
+ const weight = 1 / opts.tokens.length;
6565
+ tokenSplit = {};
6566
+ for (const t of opts.tokens) tokenSplit[t] = weight;
6567
+ }
6568
+ const base2 = {
6569
+ ...DEFAULT_GRID_CONFIG,
6570
+ ...opts.baseConfig,
6571
+ tokens: opts.tokens,
6572
+ tokenSplit
6573
+ };
6574
+ const combos = expandSweep(opts.sweep, base2);
6575
+ if (combos.length === 0) throw new Error("sweep produced no combinations");
6576
+ const loader = new HistoricalDataLoader({ noCache: opts.noCache });
6577
+ if (opts.verbose !== false) {
6578
+ process.stderr.write(` [sweep] ${sweepId}: running ${combos.length} backtests\u2026
6579
+ `);
6580
+ }
6581
+ await mkdir7(sweepDir, { recursive: true });
6582
+ const runs = [];
6583
+ for (let i = 0; i < combos.length; i++) {
6584
+ const combo = combos[i];
6585
+ const config = { ...base2, ...combo };
6586
+ const runOutPath = join7(sweepDir, `run-${i.toString().padStart(3, "0")}.json`);
6587
+ if (opts.verbose !== false) {
6588
+ process.stderr.write(` [sweep] [${i + 1}/${combos.length}] ${JSON.stringify(combo)}
6589
+ `);
6590
+ }
6591
+ const result = await runBacktest({
6592
+ fromMs: opts.fromMs,
6593
+ toMs: opts.toMs,
6594
+ capital: opts.capital,
6595
+ config,
6596
+ loader,
6597
+ noCache: opts.noCache,
6598
+ feeBps: opts.feeBps,
6599
+ outPath: runOutPath
6600
+ });
6601
+ const ddPct = Math.abs(result.drawdown.maxPct * 100);
6602
+ const pnlPct = result.capital.pnlPct * 100;
6603
+ const riskAdjusted = pnlPct / Math.max(ddPct, 1);
6604
+ const survived = result.liquidations.haltedAt === null;
6605
+ runs.push({
6606
+ index: i,
6607
+ config: combo,
6608
+ runId: result.runId,
6609
+ resultPath: runOutPath,
6610
+ capital: result.capital,
6611
+ fees: { bps: result.fees.bps, totalUsd: result.fees.totalUsd },
6612
+ totals: {
6613
+ roundTrips: result.totals.roundTrips,
6614
+ fills: result.totals.fills,
6615
+ pausedSteps: result.totals.pausedSteps
6616
+ },
6617
+ drawdown: result.drawdown,
6618
+ riskAdjusted,
6619
+ survived,
6620
+ durationMs: result.durationMs
6621
+ });
6622
+ }
6623
+ runs.sort((a, b) => {
6624
+ if (a.survived !== b.survived) return a.survived ? -1 : 1;
6625
+ return b.riskAdjusted - a.riskAdjusted;
6626
+ });
6627
+ const finishedAt = Date.now();
6628
+ const sweep = {
6629
+ sweepId,
6630
+ startedAt,
6631
+ finishedAt,
6632
+ durationMs: finishedAt - startedAt,
6633
+ window: {
6634
+ fromMs: opts.fromMs,
6635
+ toMs: opts.toMs,
6636
+ days: (opts.toMs - opts.fromMs) / (24 * 60 * 60 * 1e3)
6637
+ },
6638
+ tokens: opts.tokens,
6639
+ capital: opts.capital,
6640
+ feeBps: opts.feeBps ?? 5,
6641
+ runs
6642
+ };
6643
+ const sweepJsonPath = join7(sweepDir, "sweep.json");
6644
+ await writeFile6(sweepJsonPath, JSON.stringify(sweep, null, 2), "utf-8");
6645
+ return sweep;
6646
+ }
6647
+
5654
6648
  // src/commands/grid.ts
5655
6649
  var DIM5 = chalk14.gray;
5656
6650
  var G5 = chalk14.green;
5657
6651
  var BOLD5 = chalk14.white.bold;
5658
6652
  var W5 = chalk14.white;
5659
6653
  var SEP5 = () => console.log(DIM5("\u2500".repeat(60)));
6654
+ function printSummary(r) {
6655
+ const dd = r.drawdown;
6656
+ const days = r.window.days;
6657
+ const rtPerDay = (r.totals.roundTrips / Math.max(days, 1)).toFixed(1);
6658
+ const fillPerDay = (r.totals.fills / Math.max(days, 1)).toFixed(1);
6659
+ const pnlSign = r.capital.pnlUsd >= 0 ? "+" : "-";
6660
+ const pnlAbs = Math.abs(r.capital.pnlUsd).toFixed(2);
6661
+ const pnlPctStr = (r.capital.pnlPct * 100).toFixed(2);
6662
+ console.log();
6663
+ console.log(G5.bold(` Grid Backtest \u2014 ${r.runId}`));
6664
+ console.log(DIM5("\u2500".repeat(64)));
6665
+ console.log(W5(` Window: ${r.window.fromIso.slice(0, 10)} \u2192 ${r.window.toIso.slice(0, 10)} (${days.toFixed(0)} days)`));
6666
+ console.log(W5(` Capital: $${r.capital.initialUsd.toLocaleString()} \u2192 $${r.capital.finalUsd.toFixed(2)} (${pnlSign}$${pnlAbs}, ${pnlSign}${pnlPctStr}%)`));
6667
+ console.log(W5(` Gross PnL: $${r.capital.grossPnlUsd.toFixed(2)} (before fees)`));
6668
+ console.log(W5(` Fees (${r.fees.bps}bps): -$${r.fees.totalUsd.toFixed(2)} ($${r.fees.perFill.toFixed(4)}/fill)`));
6669
+ if (r.hedge.enabled) {
6670
+ const hedgeNet = r.hedge.realizedPnlUsd + r.hedge.unrealizedPnlUsd;
6671
+ const hedgeSign = hedgeNet >= 0 ? "+" : "-";
6672
+ const hedgeAbs = Math.abs(hedgeNet).toFixed(2);
6673
+ console.log(W5(` Hedge: ${hedgeSign}$${hedgeAbs} (realized $${r.hedge.realizedPnlUsd.toFixed(2)} + unrealized $${r.hedge.unrealizedPnlUsd.toFixed(2)}, ${r.hedge.adjustments} adj)`));
6674
+ } else {
6675
+ console.log(W5(` Hedge: disabled`));
6676
+ }
6677
+ console.log(W5(` Round trips: ${r.totals.roundTrips} (${rtPerDay}/day)`));
6678
+ console.log(W5(` Fills: ${r.totals.fills} (${fillPerDay}/day)`));
6679
+ console.log(W5(` Rebuilds: ${r.totals.rebuilds}`));
6680
+ console.log(W5(` Max drawdown: -$${dd.maxUsd.toFixed(2)} (-${(dd.maxPct * 100).toFixed(2)}%)`));
6681
+ console.log(W5(` Paused: ${r.totals.pausedSteps} steps (${(r.totals.pausedSteps / Math.max(r.totals.totalSteps, 1) * 100).toFixed(1)}%)`));
6682
+ console.log(W5(` Skipped: ${r.totals.skippedSteps} steps`));
6683
+ console.log();
6684
+ console.log(BOLD5(" Per token:"));
6685
+ for (const t of r.perToken) {
6686
+ const tokPnl = t.pnlUsd >= 0 ? G5(`+$${t.pnlUsd.toFixed(2)}`) : chalk14.red(`-$${Math.abs(t.pnlUsd).toFixed(2)}`);
6687
+ console.log(W5(` ${t.token.padEnd(10)} $${t.allocation.initial.toFixed(0).padStart(6)} \u2192 $${t.allocation.final.toFixed(0).padStart(6)} ${tokPnl} RTs=${t.roundTrips} fills=${t.fills}`));
6688
+ }
6689
+ if (r.liquidations.events.length > 0) {
6690
+ console.log();
6691
+ console.log(BOLD5(" Liquidations:"));
6692
+ for (const ev of r.liquidations.events) {
6693
+ const date = new Date(ev.timestamp).toISOString().slice(0, 10);
6694
+ console.log(W5(` ${ev.token.padEnd(10)} ${date} unrealized=$${ev.unrealizedPnlAtLiquidation.toFixed(0)} threshold=$${ev.thresholdUsd.toFixed(0)}`));
6695
+ }
6696
+ if (r.liquidations.haltedAt !== null) {
6697
+ const haltDate = new Date(r.liquidations.haltedAt).toISOString().slice(0, 10);
6698
+ console.log(chalk14.red.bold(` RUN HALTED on ${haltDate} \u2014 all tokens liquidated.`));
6699
+ }
6700
+ }
6701
+ console.log();
6702
+ console.log(W5(` Wall time: ${(r.durationMs / 1e3).toFixed(1)}s`));
6703
+ console.log(DIM5("\u2500".repeat(64)));
6704
+ }
6705
+ function printSweepSummary(s) {
6706
+ const days = s.window.days;
6707
+ console.log();
6708
+ console.log(G5.bold(` Grid Sweep \u2014 ${s.sweepId}`));
6709
+ console.log(DIM5("\u2500".repeat(96)));
6710
+ console.log(W5(` Window: ${new Date(s.window.fromMs).toISOString().slice(0, 10)} \u2192 ${new Date(s.window.toMs).toISOString().slice(0, 10)} (${days.toFixed(0)} days)`));
6711
+ console.log(W5(` Tokens: ${s.tokens.join(", ")}`));
6712
+ console.log(W5(` Capital: $${s.capital.toLocaleString()}`));
6713
+ console.log(W5(` Fees: ${s.feeBps} bps/fill`));
6714
+ console.log(W5(` Runs: ${s.runs.length}`));
6715
+ console.log(W5(` Wall: ${(s.durationMs / 1e3).toFixed(1)}s`));
6716
+ console.log(DIM5("\u2500".repeat(96)));
6717
+ console.log();
6718
+ console.log(BOLD5(" Rank Surv Lev Levels ATR\xD7 Drift RTs NetPnL$ NetPnL% DD% Risk-Adj"));
6719
+ console.log(DIM5(" " + "\u2500".repeat(102)));
6720
+ for (let i = 0; i < s.runs.length; i++) {
6721
+ const r = s.runs[i];
6722
+ const rank = String(i + 1).padStart(4);
6723
+ const surv = r.survived ? G5(" \u2713 ") : chalk14.red(" \u2717 ");
6724
+ const lev = String(r.config.leverage ?? "").padStart(3);
6725
+ const lvls = String(r.config.levelsPerSide ?? "").padStart(6);
6726
+ const atr = String(r.config.atrMultiplier ?? "").padStart(4);
6727
+ const drift = String(r.config.rebalanceDriftPct ?? "").padStart(5);
6728
+ const rts = String(r.totals.roundTrips).padStart(5);
6729
+ const netPnl = `$${r.capital.pnlUsd.toFixed(0)}`.padStart(11);
6730
+ const netPct = `${(r.capital.pnlPct * 100).toFixed(1)}%`.padStart(8);
6731
+ const ddPct = `${(r.drawdown.maxPct * 100).toFixed(1)}%`.padStart(8);
6732
+ const ra = r.riskAdjusted.toFixed(2).padStart(8);
6733
+ const pnlColor = r.capital.pnlUsd >= 0 ? G5 : chalk14.red;
6734
+ console.log(` ${rank} ${surv} ${lev} ${lvls} ${atr} ${drift} ${rts} ${pnlColor(netPnl)} ${pnlColor(netPct)} ${ddPct} ${ra}`);
6735
+ }
6736
+ console.log(DIM5(" " + "\u2500".repeat(102)));
6737
+ console.log();
6738
+ console.log(W5(` Saved: ${s.sweepId}/sweep.json + per-run JSONs in ~/.sherwood/grid/sweeps/`));
6739
+ console.log();
6740
+ }
6741
+ function parseTokenSplit(raw, tokens) {
6742
+ const split = {};
6743
+ for (const pair of raw.split(",")) {
6744
+ const [tok, w] = pair.split("=");
6745
+ if (!tok || !w) throw new Error(`Bad token-split pair: '${pair}' (expected token=weight)`);
6746
+ const weight = Number(w.trim());
6747
+ if (!Number.isFinite(weight) || weight < 0) {
6748
+ throw new Error(`Bad token-split weight for '${tok}': '${w}'`);
6749
+ }
6750
+ split[tok.trim()] = weight;
6751
+ }
6752
+ for (const t of tokens) {
6753
+ if (!(t in split)) {
6754
+ throw new Error(`--token-split missing weight for '${t}'`);
6755
+ }
6756
+ }
6757
+ const sum = Object.values(split).reduce((a, b) => a + b, 0);
6758
+ if (Math.abs(sum - 1) > 1e-6) {
6759
+ throw new Error(`--token-split weights must sum to 1.0, got ${sum.toFixed(6)}`);
6760
+ }
6761
+ return split;
6762
+ }
5660
6763
  function registerGridCommand(program2) {
5661
6764
  const grid = program2.command("grid").description("Grid trading strategy \u2014 ATR-based grid on BTC/ETH/SOL");
5662
- grid.command("start").description("Start the grid trading loop").option("--capital <usd>", "Starting capital in USD", "5000").option("--cycle <seconds>", "Cycle interval in seconds", "60").option("--tokens <list>", "Comma-separated token list", "bitcoin,ethereum,solana").option("--leverage <n>", "Leverage multiplier", "5").option("--levels <n>", "Grid levels per side", "15").option("--live", "enable live execution (places real orders on Hyperliquid)").option("--asset-indices <pairs>", "comma-separated token=index pairs (e.g. bitcoin=3,ethereum=4,solana=5)").option("--strategy <address>", "on-chain strategy contract address (enables on-chain executor)").action(async (opts) => {
6765
+ grid.command("start").description("Start the grid trading loop").option("--capital <usd>", "Starting capital in USD", "5000").option("--cycle <seconds>", "Cycle interval in seconds", "60").option("--tokens <list>", "Comma-separated token list", "bitcoin,ethereum,solana").option("--leverage <n>", "Leverage multiplier", "5").option("--levels <n>", "Grid levels per side", "15").option("--live", "enable live execution (places real orders on Hyperliquid)").option("--asset-indices <pairs>", "comma-separated token=index pairs (e.g. bitcoin=3,ethereum=4,solana=5)").option("--strategy <address>", "on-chain strategy contract address (enables on-chain executor)").option("--token-split <pairs>", "Comma-separated token=weight pairs (must sum to 1.0). Default: equal-weight.").action(async (opts) => {
5663
6766
  const capital = parseFloat(opts.capital);
5664
6767
  const cycleMs = parseInt(opts.cycle, 10) * 1e3;
5665
6768
  const tokens = opts.tokens.split(",").map((t) => t.trim());
@@ -5682,10 +6785,15 @@ function registerGridCommand(program2) {
5682
6785
  if (strategyAddress && !live) {
5683
6786
  throw new Error("--strategy requires --live");
5684
6787
  }
5685
- const weight = 1 / tokens.length;
5686
- const tokenSplit = {};
5687
- for (const t of tokens) {
5688
- tokenSplit[t] = weight;
6788
+ let tokenSplit;
6789
+ if (opts.tokenSplit) {
6790
+ tokenSplit = parseTokenSplit(opts.tokenSplit, tokens);
6791
+ } else {
6792
+ const weight = 1 / tokens.length;
6793
+ tokenSplit = {};
6794
+ for (const t of tokens) {
6795
+ tokenSplit[t] = weight;
6796
+ }
5689
6797
  }
5690
6798
  console.log();
5691
6799
  console.log(G5.bold(" Grid Strategy"));
@@ -5748,6 +6856,131 @@ function registerGridCommand(program2) {
5748
6856
  console.log(` ${BOLD5("TOTAL".padEnd(13))} ${`$${state.totalAllocation.toFixed(0)}`.padStart(8)} ${String(agg.totalRoundTrips).padStart(6)} ${totalPnlColor(`$${agg.totalPnlUsd.toFixed(2)}`.padStart(10))} ${todayTotalColor(`$${agg.todayPnlUsd.toFixed(2)}`.padStart(10))} ${String(agg.todayFills).padStart(6)}`);
5749
6857
  console.log();
5750
6858
  });
6859
+ grid.command("backtest").description("Replay historical Hyperliquid prices through the grid strategy").option("--from <iso>", "Window start (ISO date). Default: 30d ago.").option("--to <iso>", "Window end (ISO date). Default: now.").option("--capital <usd>", "Starting capital in USD", "5000").option("--tokens <list>", "Comma-separated token list", "bitcoin,ethereum,solana").option("--leverage <n>", "Override leverage").option("--levels <n>", "Override levels per side").option("--atr-multiplier <n>", "Override ATR multiplier").option("--rebalance-drift <n>", "Override rebalanceDriftPct").option("--snapshot-every <min>", "Equity-curve snapshot cadence (minutes)", "60").option("--verbose", "Print manager fill logs during replay").option("--no-cache", "Skip cache; always fetch fresh data").option("--out <path>", "Override output path").option("--fee-bps <n>", "Trading fee in basis points per fill (default 5 = 0.05%)", "5").option("--no-hedge", "Disable hedge simulation (hedge is ON by default to match live grid)").option("--maintenance-pct <n>", "Per-token maintenance margin fraction (default 0.02 = 2%, typical Hyperliquid)", "0.02").option("--pause-threshold <n>", "Pool drop fraction that triggers pause (default 0.40, may be too lenient post leverage-fix)").option("--unpause-recovery <n>", "Pool drop fraction below which paused grid resumes (default 0.10)").option("--token-split <pairs>", "Comma-separated token=weight pairs (must sum to 1.0). Default: equal-weight or 0.45/0.30/0.25 for BTC/ETH/SOL.").option("--stop-loss <n>", "Per-fill stop-loss fraction (default 0.10 = 10%, 0 disables)").action(async (opts) => {
6860
+ const now = Date.now();
6861
+ const toMs = opts.to ? Date.parse(opts.to) : now;
6862
+ const fromMs = opts.from ? Date.parse(opts.from) : now - 30 * 24 * 36e5;
6863
+ if (Number.isNaN(toMs) || Number.isNaN(fromMs)) {
6864
+ throw new Error("--from / --to must be ISO dates (e.g. 2026-04-01)");
6865
+ }
6866
+ const tokens = opts.tokens.split(",").map((t) => t.trim());
6867
+ let tokenSplit;
6868
+ if (opts.tokenSplit) {
6869
+ tokenSplit = parseTokenSplit(opts.tokenSplit, tokens);
6870
+ } else {
6871
+ const weight = 1 / tokens.length;
6872
+ tokenSplit = {};
6873
+ for (const t of tokens) tokenSplit[t] = weight;
6874
+ }
6875
+ const config = {
6876
+ ...DEFAULT_GRID_CONFIG,
6877
+ tokens,
6878
+ tokenSplit,
6879
+ leverage: opts.leverage ? Number(opts.leverage) : DEFAULT_GRID_CONFIG.leverage,
6880
+ levelsPerSide: opts.levels ? Number(opts.levels) : DEFAULT_GRID_CONFIG.levelsPerSide,
6881
+ atrMultiplier: opts.atrMultiplier ? Number(opts.atrMultiplier) : DEFAULT_GRID_CONFIG.atrMultiplier,
6882
+ rebalanceDriftPct: opts.rebalanceDrift ? Number(opts.rebalanceDrift) : DEFAULT_GRID_CONFIG.rebalanceDriftPct,
6883
+ maintenanceMarginPct: opts.maintenancePct ? Number(opts.maintenancePct) : DEFAULT_GRID_CONFIG.maintenanceMarginPct,
6884
+ pauseThresholdPct: opts.pauseThreshold ? Number(opts.pauseThreshold) : DEFAULT_GRID_CONFIG.pauseThresholdPct,
6885
+ unpauseRecoveryPct: opts.unpauseRecovery ? Number(opts.unpauseRecovery) : DEFAULT_GRID_CONFIG.unpauseRecoveryPct,
6886
+ stopLossPct: opts.stopLoss !== void 0 ? Number(opts.stopLoss) : DEFAULT_GRID_CONFIG.stopLossPct
6887
+ };
6888
+ const capital = Number(opts.capital);
6889
+ const snapshotEveryMinutes = Number(opts.snapshotEvery);
6890
+ console.log();
6891
+ console.log(G5.bold(" Grid Backtest"));
6892
+ SEP5();
6893
+ console.log(W5(` Window: ${new Date(fromMs).toISOString().slice(0, 10)} \u2192 ${new Date(toMs).toISOString().slice(0, 10)}`));
6894
+ console.log(W5(` Capital: $${capital.toLocaleString()}`));
6895
+ console.log(W5(` Tokens: ${tokens.join(", ")}`));
6896
+ console.log(W5(` Leverage: ${config.leverage}x`));
6897
+ console.log(W5(` Levels: ${config.levelsPerSide}/side`));
6898
+ SEP5();
6899
+ const result = await runBacktest({
6900
+ fromMs,
6901
+ toMs,
6902
+ capital,
6903
+ config,
6904
+ snapshotEveryMinutes,
6905
+ verbose: !!opts.verbose,
6906
+ noCache: opts.cache === false,
6907
+ outPath: opts.out,
6908
+ feeBps: Number(opts.feeBps),
6909
+ hedge: opts.hedge !== false
6910
+ });
6911
+ printSummary(result);
6912
+ });
6913
+ grid.command("sweep").description("Run a parameter sweep over multiple grid configurations").option("--from <iso>", "Window start (ISO date). Default: 30d ago.").option("--to <iso>", "Window end (ISO date). Default: now.").option("--capital <usd>", "Starting capital in USD", "5000").option("--tokens <list>", "Comma-separated token list", "bitcoin,ethereum,solana").option("--leverage <list>", "Comma-separated leverage values to sweep").option("--levels <list>", "Comma-separated levels-per-side values to sweep").option("--atr-multiplier <list>", "Comma-separated ATR multiplier values to sweep").option("--rebalance-drift <list>", "Comma-separated rebalance drift values to sweep").option("--fee-bps <n>", "Trading fee in basis points per fill (default 5)", "5").option("--no-cache", "Skip cache; always fetch fresh data").option("--token-split <pairs>", "Comma-separated token=weight pairs (must sum to 1.0). Default: equal-weight or 0.45/0.30/0.25 for BTC/ETH/SOL.").action(async (opts) => {
6914
+ const now = Date.now();
6915
+ const toMs = opts.to ? Date.parse(opts.to) : now;
6916
+ const fromMs = opts.from ? Date.parse(opts.from) : now - 30 * 24 * 36e5;
6917
+ if (Number.isNaN(toMs) || Number.isNaN(fromMs)) {
6918
+ throw new Error("--from / --to must be ISO dates (e.g. 2026-04-01)");
6919
+ }
6920
+ const tokens = opts.tokens.split(",").map((t) => t.trim());
6921
+ const parseList = (s) => {
6922
+ if (!s) return void 0;
6923
+ return s.split(",").map((v) => Number(v.trim())).filter((n) => Number.isFinite(n));
6924
+ };
6925
+ const sweep = {
6926
+ leverage: parseList(opts.leverage),
6927
+ levelsPerSide: parseList(opts.levels),
6928
+ atrMultiplier: parseList(opts.atrMultiplier),
6929
+ rebalanceDriftPct: parseList(opts.rebalanceDrift)
6930
+ };
6931
+ const tokenSplit = opts.tokenSplit ? parseTokenSplit(opts.tokenSplit, tokens) : void 0;
6932
+ const result = await runSweep({
6933
+ fromMs,
6934
+ toMs,
6935
+ capital: Number(opts.capital),
6936
+ tokens,
6937
+ tokenSplit,
6938
+ sweep,
6939
+ feeBps: Number(opts.feeBps),
6940
+ noCache: opts.cache === false
6941
+ });
6942
+ printSweepSummary(result);
6943
+ });
6944
+ grid.command("pause").description("Manually pause the live grid (sets state.paused=true)").option("--reason <text>", "Reason for the pause (shown in status output)", "Manually paused by operator").action(async (opts) => {
6945
+ const portfolio = new GridPortfolio();
6946
+ const state = await portfolio.load();
6947
+ if (!state) {
6948
+ console.log(DIM5("\n No grid portfolio found. Nothing to pause.\n"));
6949
+ return;
6950
+ }
6951
+ if (state.paused) {
6952
+ console.log(DIM5(`
6953
+ Grid already paused: ${state.pauseReason}
6954
+ `));
6955
+ return;
6956
+ }
6957
+ state.paused = true;
6958
+ state.pauseReason = opts.reason;
6959
+ await portfolio.save(state);
6960
+ console.log();
6961
+ console.log(chalk14.yellow(` Grid paused. Reason: ${state.pauseReason}`));
6962
+ console.log(DIM5(" Next manager.tick will be a no-op until you run `sherwood grid resume`."));
6963
+ console.log();
6964
+ });
6965
+ grid.command("resume").description("Resume a paused grid (clears state.paused)").action(async () => {
6966
+ const portfolio = new GridPortfolio();
6967
+ const state = await portfolio.load();
6968
+ if (!state) {
6969
+ console.log(DIM5("\n No grid portfolio found. Nothing to resume.\n"));
6970
+ return;
6971
+ }
6972
+ if (!state.paused) {
6973
+ console.log(DIM5("\n Grid is not paused. Nothing to do.\n"));
6974
+ return;
6975
+ }
6976
+ const wasReason = state.pauseReason;
6977
+ state.paused = false;
6978
+ state.pauseReason = "";
6979
+ await portfolio.save(state);
6980
+ console.log();
6981
+ console.log(G5(` Grid resumed. (was paused: ${wasReason || "<unknown>"})`));
6982
+ console.log();
6983
+ });
5751
6984
  }
5752
6985
 
5753
6986
  // src/index.ts
@@ -5758,7 +6991,7 @@ try {
5758
6991
  var require2 = createRequire(import.meta.url);
5759
6992
  var { version: CLI_VERSION } = require2("../package.json");
5760
6993
  async function loadXmtp() {
5761
- return import("./xmtp-ZACWHUI5.js");
6994
+ return import("./xmtp-KEFA7F24.js");
5762
6995
  }
5763
6996
  async function loadCron() {
5764
6997
  return import("./cron-EOULYO3R.js");
@@ -6283,7 +7516,7 @@ syndicate.command("add").description("Register an agent on a syndicate vault (cr
6283
7516
  if (opts.agentId) {
6284
7517
  agentId = BigInt(opts.agentId);
6285
7518
  } else {
6286
- const { AGENT_REGISTRY: AGENT_REGISTRY2 } = await import("./addresses-DOWLGXGE.js");
7519
+ const { AGENT_REGISTRY: AGENT_REGISTRY2 } = await import("./addresses-CKPOWDDV.js");
6287
7520
  const registry2 = AGENT_REGISTRY2().IDENTITY_REGISTRY;
6288
7521
  if (registry2 === "0x0000000000000000000000000000000000000000") {
6289
7522
  agentId = 0n;
@@ -6908,7 +8141,7 @@ ${info.name} (${info.type})`);
6908
8141
  }
6909
8142
  });
6910
8143
  try {
6911
- const { registerChatCommands } = await import("./chat-TD6V5LFD.js");
8144
+ const { registerChatCommands } = await import("./chat-3IW4XDZJ.js");
6912
8145
  registerChatCommands(program);
6913
8146
  } catch {
6914
8147
  program.command("chat <name> [action] [actionArgs...]").description("Syndicate chat (XMTP) \u2014 requires @xmtp/cli").action(() => {
@@ -6918,7 +8151,7 @@ try {
6918
8151
  process.exit(1);
6919
8152
  });
6920
8153
  }
6921
- var { registerSessionCommands } = await import("./session-IYT3N7MG.js");
8154
+ var { registerSessionCommands } = await import("./session-3QAL5VEV.js");
6922
8155
  registerSessionCommands(program);
6923
8156
  registerVeniceCommands(program);
6924
8157
  registerAllowanceCommands(program);
@@ -6926,11 +8159,11 @@ registerIdentityCommands(program);
6926
8159
  registerProposalCommands(program);
6927
8160
  registerGovernorCommands(program);
6928
8161
  registerGuardianCommands(program);
6929
- var { registerResearchCommands } = await import("./research-WBZU4QNU.js");
8162
+ var { registerResearchCommands } = await import("./research-2GOILSS7.js");
6930
8163
  registerResearchCommands(program);
6931
- var { registerAgentCommands } = await import("./agent-E5GHURKJ.js");
8164
+ var { registerAgentCommands } = await import("./agent-ER5GYBVQ.js");
6932
8165
  registerAgentCommands(program);
6933
- var { registerTradeCommands } = await import("./trade-PTYLNJBU.js");
8166
+ var { registerTradeCommands } = await import("./trade-UXQOCNLJ.js");
6934
8167
  registerTradeCommands(program);
6935
8168
  registerGridCommand(program);
6936
8169
  var configCmd = program.command("config");