@sherwoodagent/cli 0.54.3 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4187,10 +4187,10 @@ function registerProposalCommands(program2) {
4187
4187
  }
4188
4188
  if (!opts.yes) {
4189
4189
  process.stdout.write(chalk6.yellow(" Proceed? [y/N] "));
4190
- const answer = await new Promise((resolve2) => {
4190
+ const answer = await new Promise((resolve3) => {
4191
4191
  process.stdin.resume();
4192
4192
  process.stdin.setEncoding("utf-8");
4193
- process.stdin.once("data", (d) => resolve2(String(d).trim().toLowerCase()));
4193
+ process.stdin.once("data", (d) => resolve3(String(d).trim().toLowerCase()));
4194
4194
  });
4195
4195
  process.stdin.pause();
4196
4196
  if (answer !== "y" && answer !== "yes") {
@@ -4741,17 +4741,28 @@ import chalk14 from "chalk";
4741
4741
  // src/grid/loop.ts
4742
4742
  import chalk13 from "chalk";
4743
4743
  import { appendFile, mkdir as mkdir4 } from "fs/promises";
4744
- import { join as join4 } from "path";
4745
- import { homedir as homedir4 } from "os";
4744
+ import { dirname as dirname4 } from "path";
4746
4745
 
4747
4746
  // src/grid/manager.ts
4748
4747
  import chalk9 from "chalk";
4749
4748
 
4750
4749
  // src/grid/portfolio.ts
4751
4750
  import { readFile, writeFile, rename, mkdir } from "fs/promises";
4752
- import { join, dirname } from "path";
4751
+ import { dirname } from "path";
4752
+
4753
+ // src/grid/paths.ts
4754
+ import { join, resolve as resolve2 } from "path";
4753
4755
  import { homedir } from "os";
4754
- var GRID_STATE_PATH = join(homedir(), ".sherwood", "grid", "portfolio.json");
4756
+ var DEFAULT_GRID_STATE_DIR = join(homedir(), ".sherwood", "grid");
4757
+ function gridStateDir(override) {
4758
+ if (!override || override.trim().length === 0) return DEFAULT_GRID_STATE_DIR;
4759
+ return resolve2(override);
4760
+ }
4761
+ function gridStatePath(file, override) {
4762
+ return join(gridStateDir(override), file);
4763
+ }
4764
+
4765
+ // src/grid/portfolio.ts
4755
4766
  function emptyStats() {
4756
4767
  return {
4757
4768
  totalRoundTrips: 0,
@@ -4765,10 +4776,14 @@ function emptyStats() {
4765
4776
  }
4766
4777
  var GridPortfolio = class {
4767
4778
  state = null;
4779
+ statePath;
4780
+ constructor(stateDir) {
4781
+ this.statePath = gridStatePath("portfolio.json", stateDir);
4782
+ }
4768
4783
  /** Load grid state from disk. Returns null if no grid initialized yet. */
4769
4784
  async load() {
4770
4785
  try {
4771
- const raw = await readFile(GRID_STATE_PATH, "utf-8");
4786
+ const raw = await readFile(this.statePath, "utf-8");
4772
4787
  this.state = JSON.parse(raw);
4773
4788
  return this.state;
4774
4789
  } catch {
@@ -4778,10 +4793,10 @@ var GridPortfolio = class {
4778
4793
  /** Save grid state to disk (atomic write). */
4779
4794
  async save(state) {
4780
4795
  this.state = state;
4781
- await mkdir(dirname(GRID_STATE_PATH), { recursive: true });
4782
- const tmp = `${GRID_STATE_PATH}.tmp.${process.pid}`;
4796
+ await mkdir(dirname(this.statePath), { recursive: true });
4797
+ const tmp = `${this.statePath}.tmp.${process.pid}`;
4783
4798
  await writeFile(tmp, JSON.stringify(state, null, 2), "utf-8");
4784
- await rename(tmp, GRID_STATE_PATH);
4799
+ await rename(tmp, this.statePath);
4785
4800
  }
4786
4801
  /**
4787
4802
  * Initialize the grid by carving capital from the directional portfolio.
@@ -4844,12 +4859,19 @@ var GridPortfolio = class {
4844
4859
  return sum + g.allocation + unrealized;
4845
4860
  }, 0);
4846
4861
  const dropPct = 1 - currentValue / state.totalAllocation;
4847
- if (!state.paused && dropPct >= config.pauseThresholdPct) {
4862
+ if (!state.circuitBroken && dropPct >= config.circuitBreakerThresholdPct) {
4863
+ state.circuitBroken = true;
4864
+ state.circuitBrokenAt = Date.now();
4865
+ state.paused = true;
4866
+ state.pauseReason = `Hard circuit breaker tripped \u2014 pool dropped ${(dropPct * 100).toFixed(1)}% (>= ${(config.circuitBreakerThresholdPct * 100).toFixed(0)}%). Manual resume required: \`sherwood grid resume\`.`;
4867
+ return true;
4868
+ }
4869
+ if (!state.circuitBroken && !state.paused && dropPct >= config.pauseThresholdPct) {
4848
4870
  state.paused = true;
4849
4871
  state.pauseReason = `Grid pool dropped ${(dropPct * 100).toFixed(1)}% (pause threshold: ${(config.pauseThresholdPct * 100).toFixed(0)}%, resume when \u2264 ${(config.unpauseRecoveryPct * 100).toFixed(0)}%)`;
4850
4872
  return true;
4851
4873
  }
4852
- if (state.paused && dropPct <= config.unpauseRecoveryPct) {
4874
+ if (!state.circuitBroken && state.paused && dropPct <= config.unpauseRecoveryPct) {
4853
4875
  state.paused = false;
4854
4876
  state.pauseReason = "";
4855
4877
  return true;
@@ -4878,9 +4900,9 @@ var GridManager = class {
4878
4900
  fillDetector;
4879
4901
  closeFillDetector;
4880
4902
  nowProvider;
4881
- constructor(config, candleFetcher, fillDetector, closeFillDetector, portfolio, nowProvider) {
4903
+ constructor(config, candleFetcher, fillDetector, closeFillDetector, portfolio, nowProvider, stateDir) {
4882
4904
  this.config = config;
4883
- this.portfolio = portfolio ?? new GridPortfolio();
4905
+ this.portfolio = portfolio ?? new GridPortfolio(stateDir);
4884
4906
  this.hl = new HyperliquidProvider();
4885
4907
  this.candleFetcher = candleFetcher ?? ((tokenId, interval, lookbackMs) => this.hl.getCandles(tokenId, interval, lookbackMs));
4886
4908
  this.fillDetector = fillDetector ?? defaultFillDetector;
@@ -5154,6 +5176,21 @@ var GridManager = class {
5154
5176
  ));
5155
5177
  await this.buildGrid(grid, currentPrice);
5156
5178
  }
5179
+ /** Recent market trend for hedge sizing — BTC's grid.trend (already
5180
+ * maintained at 56h cadence by the asymmetric-grid filter), falling back
5181
+ * to the median across tracked tokens if BTC is not in the universe.
5182
+ * Used by the trend-adaptive hedge ratio. Returns undefined if no signal
5183
+ * is yet available (cold start). */
5184
+ getMarketTrend() {
5185
+ const state = this.portfolio.getState();
5186
+ if (!state || state.grids.length === 0) return void 0;
5187
+ const btc = state.grids.find((g) => g.token === "bitcoin");
5188
+ if (btc && Number.isFinite(btc.trend) && btc.lastTrendRefreshAt > 0) return btc.trend;
5189
+ const trends = state.grids.filter((g) => Number.isFinite(g.trend) && g.lastTrendRefreshAt > 0).map((g) => g.trend);
5190
+ if (trends.length === 0) return void 0;
5191
+ trends.sort((a, b) => a - b);
5192
+ return trends[Math.floor(trends.length / 2)];
5193
+ }
5157
5194
  /** Get open fill exposure per token — used by the hedge manager. */
5158
5195
  getOpenFillExposure() {
5159
5196
  const state = this.portfolio.getState();
@@ -5245,6 +5282,8 @@ var DEFAULT_GRID_CONFIG = {
5245
5282
  // 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
5246
5283
  unpauseRecoveryPct: 0.05,
5247
5284
  // hysteresis: resume when pool recovers to within 5% of initial (half of pause threshold to avoid ping-pong)
5285
+ circuitBreakerThresholdPct: 0.3,
5286
+ // hard halt at 30% drawdown — manual resume required. Bear-sweep: BEAR-60d went from -20% to -53% in second month; halting at -30% saves the rest.
5248
5287
  maintenanceMarginPct: 0.02,
5249
5288
  maxOpenNotionalMultiple: 2,
5250
5289
  downtrendBlockPct: 0.1,
@@ -5254,19 +5293,33 @@ var DEFAULT_GRID_CONFIG = {
5254
5293
 
5255
5294
  // src/grid/hedge.ts
5256
5295
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
5257
- import { join as join2, dirname as dirname2 } from "path";
5258
- import { homedir as homedir2 } from "os";
5296
+ import { dirname as dirname2 } from "path";
5259
5297
  import chalk10 from "chalk";
5260
- var HEDGE_RATIO = 0.3;
5298
+ var HEDGE_RATIO_DEFAULT = Number.isFinite(Number(process.env.HEDGE_RATIO)) ? Number(process.env.HEDGE_RATIO) : 0.3;
5299
+ function hedgeRatioFor(marketTrend) {
5300
+ if (process.env.HEDGE_RATIO !== void 0 && Number.isFinite(Number(process.env.HEDGE_RATIO))) {
5301
+ return Number(process.env.HEDGE_RATIO);
5302
+ }
5303
+ if (marketTrend === void 0 || !Number.isFinite(marketTrend)) {
5304
+ return HEDGE_RATIO_DEFAULT;
5305
+ }
5306
+ if (marketTrend <= -0.05) return 0.7;
5307
+ if (marketTrend <= -0.025) return 0.5;
5308
+ if (marketTrend >= 0.025) return 0.1;
5309
+ return HEDGE_RATIO_DEFAULT;
5310
+ }
5261
5311
  var MIN_FILLS_TO_HEDGE = 3;
5262
5312
  var ADJUSTMENT_THRESHOLD = 0.1;
5263
- var HEDGE_STATE_PATH = join2(homedir2(), ".sherwood", "grid", "hedge.json");
5264
5313
  var GridHedgeManager = class {
5265
5314
  state = null;
5315
+ statePath;
5316
+ constructor(stateDir) {
5317
+ this.statePath = gridStatePath("hedge.json", stateDir);
5318
+ }
5266
5319
  async load(now = Date.now()) {
5267
5320
  if (this.state) return this.state;
5268
5321
  try {
5269
- const raw = await readFile2(HEDGE_STATE_PATH, "utf-8");
5322
+ const raw = await readFile2(this.statePath, "utf-8");
5270
5323
  this.state = JSON.parse(raw);
5271
5324
  } catch {
5272
5325
  this.state = {
@@ -5286,26 +5339,31 @@ var GridHedgeManager = class {
5286
5339
  }
5287
5340
  async save() {
5288
5341
  if (!this.state) return;
5289
- await mkdir2(dirname2(HEDGE_STATE_PATH), { recursive: true });
5290
- const tmp = `${HEDGE_STATE_PATH}.tmp.${process.pid}`;
5342
+ await mkdir2(dirname2(this.statePath), { recursive: true });
5343
+ const tmp = `${this.statePath}.tmp.${process.pid}`;
5291
5344
  await writeFile2(tmp, JSON.stringify(this.state, null, 2), "utf-8");
5292
5345
  const { rename: rename3 } = await import("fs/promises");
5293
- await rename3(tmp, HEDGE_STATE_PATH);
5346
+ await rename3(tmp, this.statePath);
5294
5347
  }
5295
5348
  /**
5296
5349
  * Run one hedge tick. Call after grid tick with current prices and open fill data.
5297
5350
  *
5298
5351
  * @param openFills - Per-token open fill exposure from the grid portfolio
5299
5352
  * @param prices - Current mark prices per token
5353
+ * @param now - injected clock (backtester drives this)
5354
+ * @param marketTrend - recent market trend (e.g. BTC's 56h trend) used by
5355
+ * `hedgeRatioFor()` to scale hedge size by regime. Pass
5356
+ * undefined to use the static default.
5300
5357
  */
5301
- async tick(openFills, prices, now = Date.now()) {
5358
+ async tick(openFills, prices, now = Date.now(), marketTrend) {
5302
5359
  const state = await this.load(now);
5303
5360
  let adjustments = 0;
5304
5361
  let unrealizedPnl = 0;
5362
+ const activeRatio = hedgeRatioFor(marketTrend);
5305
5363
  for (const fill of openFills) {
5306
5364
  const price = prices[fill.token];
5307
5365
  if (!price || price <= 0) continue;
5308
- const desiredShortQty = fill.fillCount >= MIN_FILLS_TO_HEDGE ? fill.totalQuantity * HEDGE_RATIO : 0;
5366
+ const desiredShortQty = fill.fillCount >= MIN_FILLS_TO_HEDGE ? fill.totalQuantity * activeRatio : 0;
5309
5367
  const existing = state.positions.find((p) => p.token === fill.token);
5310
5368
  if (desiredShortQty <= 0) {
5311
5369
  if (existing && existing.quantity > 0) {
@@ -5331,7 +5389,7 @@ var GridHedgeManager = class {
5331
5389
  realizedPnl: 0
5332
5390
  });
5333
5391
  console.error(chalk10.magenta(
5334
- ` [hedge] OPEN ${fill.token} short ${desiredShortQty.toFixed(6)} @ $${price.toFixed(2)} (${(HEDGE_RATIO * 100).toFixed(0)}% of ${fill.fillCount} open fills)`
5392
+ ` [hedge] OPEN ${fill.token} short ${desiredShortQty.toFixed(6)} @ $${price.toFixed(2)} (${(activeRatio * 100).toFixed(0)}% of ${fill.fillCount} open fills)`
5335
5393
  ));
5336
5394
  adjustments++;
5337
5395
  } else if (existing.quantity > 0) {
@@ -5461,8 +5519,7 @@ var GridExecutor = class {
5461
5519
  // src/grid/onchain-executor.ts
5462
5520
  import chalk12 from "chalk";
5463
5521
  import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
5464
- import { dirname as dirname3, join as join3 } from "path";
5465
- import { homedir as homedir3 } from "os";
5522
+ import { dirname as dirname3 } from "path";
5466
5523
  import { encodeAbiParameters as encodeAbiParameters11, parseUnits as parseUnits6 } from "viem";
5467
5524
 
5468
5525
  // src/grid/cloid.ts
@@ -5503,7 +5560,6 @@ var HYPERLIQUID_GRID_STRATEGY_ABI = [
5503
5560
  ];
5504
5561
 
5505
5562
  // src/grid/onchain-executor.ts
5506
- var ONCHAIN_STATE_PATH = join3(homedir3(), ".sherwood", "grid", "onchain-state.json");
5507
5563
  var UINT64_MAX = 18446744073709551615n;
5508
5564
  var ACTION_PLACE_GRID = 1;
5509
5565
  var ACTION_CANCEL_ALL = 2;
@@ -5531,8 +5587,10 @@ var OnchainGridExecutor = class {
5531
5587
  /** On-chain `maxOrdersPerTick`; cached at load(). Used to chunk batches so
5532
5588
  * the strategy never reverts with TooManyOrders(actual, max). */
5533
5589
  maxOrdersPerTick = null;
5590
+ statePath;
5534
5591
  constructor(cfg) {
5535
5592
  this.cfg = cfg;
5593
+ this.statePath = gridStatePath("onchain-state.json", cfg.stateDir);
5536
5594
  }
5537
5595
  /** Read the strategy's per-call order cap and cache it. */
5538
5596
  async fetchMaxOrdersPerTick() {
@@ -5549,7 +5607,7 @@ var OnchainGridExecutor = class {
5549
5607
  /** Load persisted state. Call once before the first execute(). */
5550
5608
  async load() {
5551
5609
  try {
5552
- const raw = await readFile3(ONCHAIN_STATE_PATH, "utf-8");
5610
+ const raw = await readFile3(this.statePath, "utf-8");
5553
5611
  const state = JSON.parse(raw);
5554
5612
  for (const [tok, n] of Object.entries(state.nonces)) this.nonces.set(tok, n);
5555
5613
  for (const [tok, cloids] of Object.entries(state.placedCloids)) {
@@ -5582,10 +5640,10 @@ var OnchainGridExecutor = class {
5582
5640
  [...this.placedCloids].map(([tok, cloids]) => [tok, cloids.map((c) => c.toString())])
5583
5641
  )
5584
5642
  };
5585
- await mkdir3(dirname3(ONCHAIN_STATE_PATH), { recursive: true });
5586
- const tmp = ONCHAIN_STATE_PATH + ".tmp";
5643
+ await mkdir3(dirname3(this.statePath), { recursive: true });
5644
+ const tmp = this.statePath + ".tmp";
5587
5645
  await writeFile3(tmp, JSON.stringify(state, null, 2));
5588
- await rename2(tmp, ONCHAIN_STATE_PATH);
5646
+ await rename2(tmp, this.statePath);
5589
5647
  }
5590
5648
  /**
5591
5649
  * Execute the order plan: cancel stale orders for rebalanced tokens, then
@@ -5795,7 +5853,6 @@ var OnchainGridExecutor = class {
5795
5853
  };
5796
5854
 
5797
5855
  // src/grid/loop.ts
5798
- var GRID_CYCLES_PATH = join4(homedir4(), ".sherwood", "grid", "cycles.jsonl");
5799
5856
  var GridLoop = class {
5800
5857
  cfg;
5801
5858
  gridConfig;
@@ -5806,11 +5863,21 @@ var GridLoop = class {
5806
5863
  running = false;
5807
5864
  cycleCount = 0;
5808
5865
  timer = null;
5866
+ cyclesPath;
5809
5867
  constructor(cfg) {
5810
5868
  this.cfg = cfg;
5811
5869
  this.gridConfig = { ...DEFAULT_GRID_CONFIG, ...cfg.config };
5812
- this.manager = new GridManager(this.gridConfig);
5813
- this.hedge = new GridHedgeManager();
5870
+ this.cyclesPath = gridStatePath("cycles.jsonl", cfg.stateDir);
5871
+ this.manager = new GridManager(
5872
+ this.gridConfig,
5873
+ void 0,
5874
+ void 0,
5875
+ void 0,
5876
+ new GridPortfolio(cfg.stateDir),
5877
+ void 0,
5878
+ cfg.stateDir
5879
+ );
5880
+ this.hedge = new GridHedgeManager(cfg.stateDir);
5814
5881
  this.hl = new HyperliquidProvider();
5815
5882
  if (cfg.live) {
5816
5883
  if (!cfg.assetIndices) {
@@ -5819,7 +5886,8 @@ var GridLoop = class {
5819
5886
  if (cfg.strategyAddress) {
5820
5887
  this.executor = new OnchainGridExecutor({
5821
5888
  strategyAddress: cfg.strategyAddress,
5822
- assetIndices: cfg.assetIndices
5889
+ assetIndices: cfg.assetIndices,
5890
+ stateDir: cfg.stateDir
5823
5891
  });
5824
5892
  } else {
5825
5893
  this.executor = new GridExecutor({ assetIndices: cfg.assetIndices });
@@ -5845,7 +5913,7 @@ var GridLoop = class {
5845
5913
  }
5846
5914
  console.error(chalk13.cyan(
5847
5915
  `
5848
- [grid-loop] Started \u2014 capital=$${this.cfg.capital.toFixed(0)} cycle=${(this.cfg.cycle / 1e3).toFixed(0)}s tokens=[${this.gridConfig.tokens.join(", ")}] leverage=${this.gridConfig.leverage}x levels=${this.gridConfig.levelsPerSide}/side
5916
+ [grid-loop] Started \u2014 capital=$${this.cfg.capital.toFixed(0)} cycle=${(this.cfg.cycle / 1e3).toFixed(0)}s tokens=[${this.gridConfig.tokens.join(", ")}] leverage=${this.gridConfig.leverage}x levels=${this.gridConfig.levelsPerSide}/side state-dir=${gridStateDir(this.cfg.stateDir)}
5849
5917
  `
5850
5918
  ));
5851
5919
  while (this.running) {
@@ -5855,8 +5923,8 @@ var GridLoop = class {
5855
5923
  console.error(chalk13.red(` [grid-loop] Tick error: ${err.message}`));
5856
5924
  }
5857
5925
  if (this.running) {
5858
- await new Promise((resolve2) => {
5859
- this.timer = setTimeout(resolve2, this.cfg.cycle);
5926
+ await new Promise((resolve3) => {
5927
+ this.timer = setTimeout(resolve3, this.cfg.cycle);
5860
5928
  });
5861
5929
  }
5862
5930
  }
@@ -5904,7 +5972,8 @@ var GridLoop = class {
5904
5972
  ));
5905
5973
  }
5906
5974
  const openExposure = this.manager.getOpenFillExposure();
5907
- const hedgeResult = await this.hedge.tick(openExposure, prices);
5975
+ const marketTrend = this.manager.getMarketTrend();
5976
+ const hedgeResult = await this.hedge.tick(openExposure, prices, Date.now(), marketTrend);
5908
5977
  if (hedgeResult.adjustments > 0) {
5909
5978
  console.error(chalk13.magenta(
5910
5979
  ` [hedge] ${hedgeResult.adjustments} adjustment(s), unrealized: $${hedgeResult.unrealizedPnl.toFixed(2)}, total realized: $${hedgeResult.totalRealizedPnl.toFixed(2)}`
@@ -5926,8 +5995,8 @@ var GridLoop = class {
5926
5995
  hedgeTotalRealizedPnl: hedgeResult.totalRealizedPnl
5927
5996
  };
5928
5997
  try {
5929
- await mkdir4(join4(homedir4(), ".sherwood", "grid"), { recursive: true });
5930
- await appendFile(GRID_CYCLES_PATH, JSON.stringify(cycleEntry) + "\n");
5998
+ await mkdir4(dirname4(this.cyclesPath), { recursive: true });
5999
+ await appendFile(this.cyclesPath, JSON.stringify(cycleEntry) + "\n");
5931
6000
  } catch {
5932
6001
  }
5933
6002
  if (this.cycleCount % 60 === 0) {
@@ -5945,8 +6014,8 @@ var GridLoop = class {
5945
6014
 
5946
6015
  // src/grid/backtest.ts
5947
6016
  import { writeFile as writeFile5, mkdir as mkdir6 } from "fs/promises";
5948
- import { join as join6, dirname as dirname4 } from "path";
5949
- import { homedir as homedir6 } from "os";
6017
+ import { join as join3, dirname as dirname5 } from "path";
6018
+ import { homedir as homedir3 } from "os";
5950
6019
  import { createHash } from "crypto";
5951
6020
 
5952
6021
  // src/grid/backtest-portfolio.ts
@@ -5977,10 +6046,10 @@ var BacktestPortfolio = class extends GridPortfolio {
5977
6046
 
5978
6047
  // src/grid/historical-data-loader.ts
5979
6048
  import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
5980
- import { join as join5 } from "path";
5981
- import { homedir as homedir5 } from "os";
6049
+ import { join as join2 } from "path";
6050
+ import { homedir as homedir2 } from "os";
5982
6051
  var BINANCE_BASE = "https://api.binance.com/api/v3/klines";
5983
- var DEFAULT_CACHE_DIR = join5(homedir5(), ".sherwood", "grid", "backtest-cache");
6052
+ var DEFAULT_CACHE_DIR = join2(homedir2(), ".sherwood", "grid", "backtest-cache");
5984
6053
  var FOUR_HOURS_MS = 4 * 60 * 60 * 1e3;
5985
6054
  var ONE_MIN_MS = 60 * 1e3;
5986
6055
  var ATR_PERIOD = 14;
@@ -6058,7 +6127,7 @@ var HistoricalDataLoader = class {
6058
6127
  await writeFile4(path, body, "utf-8");
6059
6128
  }
6060
6129
  cachePath(coin, interval, fromMs, toMs) {
6061
- return join5(this.cacheDir, `${coin}-${interval}-${fromMs}-${toMs}.json`);
6130
+ return join2(this.cacheDir, `${coin}-${interval}-${fromMs}-${toMs}.json`);
6062
6131
  }
6063
6132
  /** Paginate Binance klines until we cover [fromMs, toMs]. Dedup on merge. */
6064
6133
  async fetchPaginated(coin, interval, fromMs, toMs) {
@@ -6140,7 +6209,7 @@ function computeAtrSeries(fourHourBars) {
6140
6209
  return out;
6141
6210
  }
6142
6211
  function sleep(ms) {
6143
- return new Promise((resolve2) => setTimeout(resolve2, ms));
6212
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
6144
6213
  }
6145
6214
 
6146
6215
  // src/grid/backtest-hedge.ts
@@ -6178,7 +6247,7 @@ var BacktestHedgeManager = class extends GridHedgeManager {
6178
6247
  };
6179
6248
 
6180
6249
  // src/grid/backtest.ts
6181
- var DEFAULT_OUT_DIR = join6(homedir6(), ".sherwood", "grid", "backtests");
6250
+ var DEFAULT_OUT_DIR = join3(homedir3(), ".sherwood", "grid", "backtests");
6182
6251
  function shortHash(input2) {
6183
6252
  const h = createHash("sha256").update(JSON.stringify(input2)).digest("hex");
6184
6253
  return h.slice(0, 8);
@@ -6350,7 +6419,8 @@ async function runBacktest(opts) {
6350
6419
  hedgePrices[token] = bar.c;
6351
6420
  }
6352
6421
  const exposure = manager.getOpenFillExposure();
6353
- const hedgeResult = await hedge.tick(exposure, hedgePrices, t);
6422
+ const marketTrend = manager.getMarketTrend();
6423
+ const hedgeResult = await hedge.tick(exposure, hedgePrices, t, marketTrend);
6354
6424
  hedgeAdjustments += hedgeResult.adjustments;
6355
6425
  hedgeRealized = hedgeResult.totalRealizedPnl;
6356
6426
  hedgeUnrealized = hedgeResult.unrealizedPnl;
@@ -6543,8 +6613,8 @@ async function runBacktest(opts) {
6543
6613
  equityCurve
6544
6614
  };
6545
6615
  if (opts.outPath !== "") {
6546
- const outPath = opts.outPath ?? join6(DEFAULT_OUT_DIR, `${runId}.json`);
6547
- await mkdir6(dirname4(outPath), { recursive: true });
6616
+ const outPath = opts.outPath ?? join3(DEFAULT_OUT_DIR, `${runId}.json`);
6617
+ await mkdir6(dirname5(outPath), { recursive: true });
6548
6618
  await writeFile5(outPath, JSON.stringify(result, null, 2), "utf-8");
6549
6619
  }
6550
6620
  return result;
@@ -6552,10 +6622,10 @@ async function runBacktest(opts) {
6552
6622
 
6553
6623
  // src/grid/sweep.ts
6554
6624
  import { writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
6555
- import { join as join7 } from "path";
6556
- import { homedir as homedir7 } from "os";
6625
+ import { join as join4 } from "path";
6626
+ import { homedir as homedir4 } from "os";
6557
6627
  import { createHash as createHash2 } from "crypto";
6558
- var DEFAULT_SWEEP_DIR = join7(homedir7(), ".sherwood", "grid", "sweeps");
6628
+ var DEFAULT_SWEEP_DIR = join4(homedir4(), ".sherwood", "grid", "sweeps");
6559
6629
  function expandSweep(sweep, base2) {
6560
6630
  const fields = ["leverage", "levelsPerSide", "atrMultiplier", "rebalanceDriftPct"];
6561
6631
  const valueLists = {
@@ -6592,7 +6662,7 @@ async function runSweep(opts) {
6592
6662
  const startedAt = Date.now();
6593
6663
  const sweepId = makeSweepId(opts);
6594
6664
  const outDir = opts.outDir ?? DEFAULT_SWEEP_DIR;
6595
- const sweepDir = join7(outDir, sweepId);
6665
+ const sweepDir = join4(outDir, sweepId);
6596
6666
  let tokenSplit;
6597
6667
  if (opts.tokenSplit) {
6598
6668
  tokenSplit = opts.tokenSplit;
@@ -6619,7 +6689,7 @@ async function runSweep(opts) {
6619
6689
  for (let i = 0; i < combos.length; i++) {
6620
6690
  const combo = combos[i];
6621
6691
  const config = { ...base2, ...combo };
6622
- const runOutPath = join7(sweepDir, `run-${i.toString().padStart(3, "0")}.json`);
6692
+ const runOutPath = join4(sweepDir, `run-${i.toString().padStart(3, "0")}.json`);
6623
6693
  if (opts.verbose !== false) {
6624
6694
  process.stderr.write(` [sweep] [${i + 1}/${combos.length}] ${JSON.stringify(combo)}
6625
6695
  `);
@@ -6676,7 +6746,7 @@ async function runSweep(opts) {
6676
6746
  feeBps: opts.feeBps ?? 5,
6677
6747
  runs
6678
6748
  };
6679
- const sweepJsonPath = join7(sweepDir, "sweep.json");
6749
+ const sweepJsonPath = join4(sweepDir, "sweep.json");
6680
6750
  await writeFile6(sweepJsonPath, JSON.stringify(sweep, null, 2), "utf-8");
6681
6751
  return sweep;
6682
6752
  }
@@ -6798,12 +6868,13 @@ function parseTokenSplit(raw, tokens) {
6798
6868
  }
6799
6869
  function registerGridCommand(program2) {
6800
6870
  const grid = program2.command("grid").description("Grid trading strategy \u2014 ATR-based grid on BTC/ETH/SOL");
6801
- 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", String(DEFAULT_GRID_CONFIG.leverage)).option("--levels <n>", "Grid levels per side", String(DEFAULT_GRID_CONFIG.levelsPerSide)).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) => {
6871
+ 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", String(DEFAULT_GRID_CONFIG.leverage)).option("--levels <n>", "Grid levels per side", String(DEFAULT_GRID_CONFIG.levelsPerSide)).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.").option("--state-dir <path>", "Override directory for portfolio.json / hedge.json / cycles.jsonl / onchain-state.json. Defaults to ~/.sherwood/grid. Use a separate dir to run paper and live grids in parallel.").action(async (opts) => {
6802
6872
  const capital = parseFloat(opts.capital);
6803
6873
  const cycleMs = parseInt(opts.cycle, 10) * 1e3;
6804
6874
  const tokens = opts.tokens.split(",").map((t) => t.trim());
6805
6875
  const leverage = parseFloat(opts.leverage);
6806
6876
  const levels = parseInt(opts.levels, 10);
6877
+ const stateDir = opts.stateDir;
6807
6878
  const live = !!opts.live;
6808
6879
  let assetIndices;
6809
6880
  if (live) {
@@ -6846,6 +6917,7 @@ function registerGridCommand(program2) {
6846
6917
  live,
6847
6918
  assetIndices,
6848
6919
  strategyAddress,
6920
+ stateDir,
6849
6921
  config: {
6850
6922
  ...DEFAULT_GRID_CONFIG,
6851
6923
  tokens,
@@ -6856,8 +6928,8 @@ function registerGridCommand(program2) {
6856
6928
  });
6857
6929
  await loop.start();
6858
6930
  });
6859
- grid.command("status").description("Show current grid portfolio status").action(async () => {
6860
- const portfolio = new GridPortfolio();
6931
+ grid.command("status").description("Show current grid portfolio status").option("--state-dir <path>", "Read state from a custom directory (defaults to ~/.sherwood/grid)").action(async (opts) => {
6932
+ const portfolio = new GridPortfolio(opts.stateDir);
6861
6933
  const state = await portfolio.load();
6862
6934
  if (!state) {
6863
6935
  console.log(DIM5("\n No grid portfolio found. Run `sherwood grid start` first.\n"));
@@ -6977,8 +7049,8 @@ function registerGridCommand(program2) {
6977
7049
  });
6978
7050
  printSweepSummary(result);
6979
7051
  });
6980
- 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) => {
6981
- const portfolio = new GridPortfolio();
7052
+ 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").option("--state-dir <path>", "Read/write state from a custom directory (defaults to ~/.sherwood/grid)").action(async (opts) => {
7053
+ const portfolio = new GridPortfolio(opts.stateDir);
6982
7054
  const state = await portfolio.load();
6983
7055
  if (!state) {
6984
7056
  console.log(DIM5("\n No grid portfolio found. Nothing to pause.\n"));
@@ -6998,8 +7070,8 @@ function registerGridCommand(program2) {
6998
7070
  console.log(DIM5(" Next manager.tick will be a no-op until you run `sherwood grid resume`."));
6999
7071
  console.log();
7000
7072
  });
7001
- grid.command("resume").description("Resume a paused grid (clears state.paused)").action(async () => {
7002
- const portfolio = new GridPortfolio();
7073
+ grid.command("resume").description("Resume a paused grid (clears state.paused)").option("--state-dir <path>", "Read/write state from a custom directory (defaults to ~/.sherwood/grid)").action(async (opts) => {
7074
+ const portfolio = new GridPortfolio(opts.stateDir);
7003
7075
  const state = await portfolio.load();
7004
7076
  if (!state) {
7005
7077
  console.log(DIM5("\n No grid portfolio found. Nothing to resume.\n"));
@@ -7010,10 +7082,16 @@ function registerGridCommand(program2) {
7010
7082
  return;
7011
7083
  }
7012
7084
  const wasReason = state.pauseReason;
7085
+ const wasCircuitBroken = state.circuitBroken === true;
7013
7086
  state.paused = false;
7014
7087
  state.pauseReason = "";
7088
+ state.circuitBroken = false;
7089
+ state.circuitBrokenAt = 0;
7015
7090
  await portfolio.save(state);
7016
7091
  console.log();
7092
+ if (wasCircuitBroken) {
7093
+ console.log(chalk14.yellow(` Hard circuit breaker cleared.`));
7094
+ }
7017
7095
  console.log(G5(` Grid resumed. (was paused: ${wasReason || "<unknown>"})`));
7018
7096
  console.log();
7019
7097
  });