@sherwoodagent/cli 0.54.3 → 0.56.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2933,10 +2933,49 @@ function registerIdentityCommands(program2) {
2933
2933
  } catch {
2934
2934
  console.log(` Saved ID: #${savedId} ${chalk4.red("(token not found)")}`);
2935
2935
  }
2936
+ console.log();
2937
+ } else if (balance > 0n) {
2938
+ console.log(` Saved ID: ${chalk4.yellow("none (but this wallet owns NFTs \u2014 listing below)")}`);
2939
+ console.log();
2940
+ const lookupSpinner = ora4("Resolving owned token IDs via Agent0 SDK...").start();
2941
+ try {
2942
+ const sdk = getAgent0SDK();
2943
+ const results = await sdk.searchAgents({
2944
+ walletAddress: account.address,
2945
+ chains: [getChain().id]
2946
+ });
2947
+ lookupSpinner.stop();
2948
+ if (results.length === 0) {
2949
+ console.log(chalk4.dim(" No agents resolved for this wallet on the active chain."));
2950
+ console.log(chalk4.dim(" If you minted on a different chain, try:"));
2951
+ console.log(chalk4.dim(" sherwood identity find --wallet " + account.address + " --all-chains"));
2952
+ } else {
2953
+ console.log(chalk4.bold(` Owned identities on this chain (${results.length}):`));
2954
+ for (const a of results) {
2955
+ const idStr = String(a.agentId);
2956
+ const tokenId = idStr.includes(":") ? idStr.split(":")[1] : idStr;
2957
+ console.log(` ${chalk4.bold(`#${tokenId}`)} ${a.name || chalk4.dim("(no name)")}`);
2958
+ }
2959
+ console.log();
2960
+ console.log(chalk4.green(" Bind one to this machine:"));
2961
+ if (results.length === 1) {
2962
+ const idStr = String(results[0].agentId);
2963
+ const tokenId = idStr.includes(":") ? idStr.split(":")[1] : idStr;
2964
+ console.log(chalk4.dim(` sherwood identity load --id ${tokenId}`));
2965
+ } else {
2966
+ console.log(chalk4.dim(" sherwood identity load --id <tokenId>"));
2967
+ }
2968
+ }
2969
+ } catch (err) {
2970
+ lookupSpinner.warn("Could not resolve owned token IDs via Agent0 SDK");
2971
+ console.log(chalk4.dim(` ${err.message}`));
2972
+ console.log(chalk4.dim(" Try: sherwood identity find --wallet " + account.address));
2973
+ }
2974
+ console.log();
2936
2975
  } else {
2937
2976
  console.log(` Saved ID: ${chalk4.dim("none \u2014 run 'sherwood identity mint --name <name>'")}`);
2977
+ console.log();
2938
2978
  }
2939
- console.log();
2940
2979
  } catch (err) {
2941
2980
  spinner.fail("Failed to check identity");
2942
2981
  console.error(chalk4.red(formatContractError(err)));
@@ -4187,10 +4226,10 @@ function registerProposalCommands(program2) {
4187
4226
  }
4188
4227
  if (!opts.yes) {
4189
4228
  process.stdout.write(chalk6.yellow(" Proceed? [y/N] "));
4190
- const answer = await new Promise((resolve2) => {
4229
+ const answer = await new Promise((resolve3) => {
4191
4230
  process.stdin.resume();
4192
4231
  process.stdin.setEncoding("utf-8");
4193
- process.stdin.once("data", (d) => resolve2(String(d).trim().toLowerCase()));
4232
+ process.stdin.once("data", (d) => resolve3(String(d).trim().toLowerCase()));
4194
4233
  });
4195
4234
  process.stdin.pause();
4196
4235
  if (answer !== "y" && answer !== "yes") {
@@ -4741,17 +4780,28 @@ import chalk14 from "chalk";
4741
4780
  // src/grid/loop.ts
4742
4781
  import chalk13 from "chalk";
4743
4782
  import { appendFile, mkdir as mkdir4 } from "fs/promises";
4744
- import { join as join4 } from "path";
4745
- import { homedir as homedir4 } from "os";
4783
+ import { dirname as dirname4 } from "path";
4746
4784
 
4747
4785
  // src/grid/manager.ts
4748
4786
  import chalk9 from "chalk";
4749
4787
 
4750
4788
  // src/grid/portfolio.ts
4751
4789
  import { readFile, writeFile, rename, mkdir } from "fs/promises";
4752
- import { join, dirname } from "path";
4790
+ import { dirname } from "path";
4791
+
4792
+ // src/grid/paths.ts
4793
+ import { join, resolve as resolve2 } from "path";
4753
4794
  import { homedir } from "os";
4754
- var GRID_STATE_PATH = join(homedir(), ".sherwood", "grid", "portfolio.json");
4795
+ var DEFAULT_GRID_STATE_DIR = join(homedir(), ".sherwood", "grid");
4796
+ function gridStateDir(override) {
4797
+ if (!override || override.trim().length === 0) return DEFAULT_GRID_STATE_DIR;
4798
+ return resolve2(override);
4799
+ }
4800
+ function gridStatePath(file, override) {
4801
+ return join(gridStateDir(override), file);
4802
+ }
4803
+
4804
+ // src/grid/portfolio.ts
4755
4805
  function emptyStats() {
4756
4806
  return {
4757
4807
  totalRoundTrips: 0,
@@ -4765,10 +4815,14 @@ function emptyStats() {
4765
4815
  }
4766
4816
  var GridPortfolio = class {
4767
4817
  state = null;
4818
+ statePath;
4819
+ constructor(stateDir) {
4820
+ this.statePath = gridStatePath("portfolio.json", stateDir);
4821
+ }
4768
4822
  /** Load grid state from disk. Returns null if no grid initialized yet. */
4769
4823
  async load() {
4770
4824
  try {
4771
- const raw = await readFile(GRID_STATE_PATH, "utf-8");
4825
+ const raw = await readFile(this.statePath, "utf-8");
4772
4826
  this.state = JSON.parse(raw);
4773
4827
  return this.state;
4774
4828
  } catch {
@@ -4778,10 +4832,10 @@ var GridPortfolio = class {
4778
4832
  /** Save grid state to disk (atomic write). */
4779
4833
  async save(state) {
4780
4834
  this.state = state;
4781
- await mkdir(dirname(GRID_STATE_PATH), { recursive: true });
4782
- const tmp = `${GRID_STATE_PATH}.tmp.${process.pid}`;
4835
+ await mkdir(dirname(this.statePath), { recursive: true });
4836
+ const tmp = `${this.statePath}.tmp.${process.pid}`;
4783
4837
  await writeFile(tmp, JSON.stringify(state, null, 2), "utf-8");
4784
- await rename(tmp, GRID_STATE_PATH);
4838
+ await rename(tmp, this.statePath);
4785
4839
  }
4786
4840
  /**
4787
4841
  * Initialize the grid by carving capital from the directional portfolio.
@@ -4844,12 +4898,19 @@ var GridPortfolio = class {
4844
4898
  return sum + g.allocation + unrealized;
4845
4899
  }, 0);
4846
4900
  const dropPct = 1 - currentValue / state.totalAllocation;
4847
- if (!state.paused && dropPct >= config.pauseThresholdPct) {
4901
+ if (!state.circuitBroken && dropPct >= config.circuitBreakerThresholdPct) {
4902
+ state.circuitBroken = true;
4903
+ state.circuitBrokenAt = Date.now();
4904
+ state.paused = true;
4905
+ state.pauseReason = `Hard circuit breaker tripped \u2014 pool dropped ${(dropPct * 100).toFixed(1)}% (>= ${(config.circuitBreakerThresholdPct * 100).toFixed(0)}%). Manual resume required: \`sherwood grid resume\`.`;
4906
+ return true;
4907
+ }
4908
+ if (!state.circuitBroken && !state.paused && dropPct >= config.pauseThresholdPct) {
4848
4909
  state.paused = true;
4849
4910
  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
4911
  return true;
4851
4912
  }
4852
- if (state.paused && dropPct <= config.unpauseRecoveryPct) {
4913
+ if (!state.circuitBroken && state.paused && dropPct <= config.unpauseRecoveryPct) {
4853
4914
  state.paused = false;
4854
4915
  state.pauseReason = "";
4855
4916
  return true;
@@ -4878,9 +4939,9 @@ var GridManager = class {
4878
4939
  fillDetector;
4879
4940
  closeFillDetector;
4880
4941
  nowProvider;
4881
- constructor(config, candleFetcher, fillDetector, closeFillDetector, portfolio, nowProvider) {
4942
+ constructor(config, candleFetcher, fillDetector, closeFillDetector, portfolio, nowProvider, stateDir) {
4882
4943
  this.config = config;
4883
- this.portfolio = portfolio ?? new GridPortfolio();
4944
+ this.portfolio = portfolio ?? new GridPortfolio(stateDir);
4884
4945
  this.hl = new HyperliquidProvider();
4885
4946
  this.candleFetcher = candleFetcher ?? ((tokenId, interval, lookbackMs) => this.hl.getCandles(tokenId, interval, lookbackMs));
4886
4947
  this.fillDetector = fillDetector ?? defaultFillDetector;
@@ -5154,6 +5215,21 @@ var GridManager = class {
5154
5215
  ));
5155
5216
  await this.buildGrid(grid, currentPrice);
5156
5217
  }
5218
+ /** Recent market trend for hedge sizing — BTC's grid.trend (already
5219
+ * maintained at 56h cadence by the asymmetric-grid filter), falling back
5220
+ * to the median across tracked tokens if BTC is not in the universe.
5221
+ * Used by the trend-adaptive hedge ratio. Returns undefined if no signal
5222
+ * is yet available (cold start). */
5223
+ getMarketTrend() {
5224
+ const state = this.portfolio.getState();
5225
+ if (!state || state.grids.length === 0) return void 0;
5226
+ const btc = state.grids.find((g) => g.token === "bitcoin");
5227
+ if (btc && Number.isFinite(btc.trend) && btc.lastTrendRefreshAt > 0) return btc.trend;
5228
+ const trends = state.grids.filter((g) => Number.isFinite(g.trend) && g.lastTrendRefreshAt > 0).map((g) => g.trend);
5229
+ if (trends.length === 0) return void 0;
5230
+ trends.sort((a, b) => a - b);
5231
+ return trends[Math.floor(trends.length / 2)];
5232
+ }
5157
5233
  /** Get open fill exposure per token — used by the hedge manager. */
5158
5234
  getOpenFillExposure() {
5159
5235
  const state = this.portfolio.getState();
@@ -5245,6 +5321,8 @@ var DEFAULT_GRID_CONFIG = {
5245
5321
  // 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
5322
  unpauseRecoveryPct: 0.05,
5247
5323
  // hysteresis: resume when pool recovers to within 5% of initial (half of pause threshold to avoid ping-pong)
5324
+ circuitBreakerThresholdPct: 0.3,
5325
+ // 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
5326
  maintenanceMarginPct: 0.02,
5249
5327
  maxOpenNotionalMultiple: 2,
5250
5328
  downtrendBlockPct: 0.1,
@@ -5254,19 +5332,33 @@ var DEFAULT_GRID_CONFIG = {
5254
5332
 
5255
5333
  // src/grid/hedge.ts
5256
5334
  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";
5335
+ import { dirname as dirname2 } from "path";
5259
5336
  import chalk10 from "chalk";
5260
- var HEDGE_RATIO = 0.3;
5337
+ var HEDGE_RATIO_DEFAULT = Number.isFinite(Number(process.env.HEDGE_RATIO)) ? Number(process.env.HEDGE_RATIO) : 0.3;
5338
+ function hedgeRatioFor(marketTrend) {
5339
+ if (process.env.HEDGE_RATIO !== void 0 && Number.isFinite(Number(process.env.HEDGE_RATIO))) {
5340
+ return Number(process.env.HEDGE_RATIO);
5341
+ }
5342
+ if (marketTrend === void 0 || !Number.isFinite(marketTrend)) {
5343
+ return HEDGE_RATIO_DEFAULT;
5344
+ }
5345
+ if (marketTrend <= -0.05) return 0.7;
5346
+ if (marketTrend <= -0.025) return 0.5;
5347
+ if (marketTrend >= 0.025) return 0.1;
5348
+ return HEDGE_RATIO_DEFAULT;
5349
+ }
5261
5350
  var MIN_FILLS_TO_HEDGE = 3;
5262
5351
  var ADJUSTMENT_THRESHOLD = 0.1;
5263
- var HEDGE_STATE_PATH = join2(homedir2(), ".sherwood", "grid", "hedge.json");
5264
5352
  var GridHedgeManager = class {
5265
5353
  state = null;
5354
+ statePath;
5355
+ constructor(stateDir) {
5356
+ this.statePath = gridStatePath("hedge.json", stateDir);
5357
+ }
5266
5358
  async load(now = Date.now()) {
5267
5359
  if (this.state) return this.state;
5268
5360
  try {
5269
- const raw = await readFile2(HEDGE_STATE_PATH, "utf-8");
5361
+ const raw = await readFile2(this.statePath, "utf-8");
5270
5362
  this.state = JSON.parse(raw);
5271
5363
  } catch {
5272
5364
  this.state = {
@@ -5286,26 +5378,31 @@ var GridHedgeManager = class {
5286
5378
  }
5287
5379
  async save() {
5288
5380
  if (!this.state) return;
5289
- await mkdir2(dirname2(HEDGE_STATE_PATH), { recursive: true });
5290
- const tmp = `${HEDGE_STATE_PATH}.tmp.${process.pid}`;
5381
+ await mkdir2(dirname2(this.statePath), { recursive: true });
5382
+ const tmp = `${this.statePath}.tmp.${process.pid}`;
5291
5383
  await writeFile2(tmp, JSON.stringify(this.state, null, 2), "utf-8");
5292
5384
  const { rename: rename3 } = await import("fs/promises");
5293
- await rename3(tmp, HEDGE_STATE_PATH);
5385
+ await rename3(tmp, this.statePath);
5294
5386
  }
5295
5387
  /**
5296
5388
  * Run one hedge tick. Call after grid tick with current prices and open fill data.
5297
5389
  *
5298
5390
  * @param openFills - Per-token open fill exposure from the grid portfolio
5299
5391
  * @param prices - Current mark prices per token
5392
+ * @param now - injected clock (backtester drives this)
5393
+ * @param marketTrend - recent market trend (e.g. BTC's 56h trend) used by
5394
+ * `hedgeRatioFor()` to scale hedge size by regime. Pass
5395
+ * undefined to use the static default.
5300
5396
  */
5301
- async tick(openFills, prices, now = Date.now()) {
5397
+ async tick(openFills, prices, now = Date.now(), marketTrend) {
5302
5398
  const state = await this.load(now);
5303
5399
  let adjustments = 0;
5304
5400
  let unrealizedPnl = 0;
5401
+ const activeRatio = hedgeRatioFor(marketTrend);
5305
5402
  for (const fill of openFills) {
5306
5403
  const price = prices[fill.token];
5307
5404
  if (!price || price <= 0) continue;
5308
- const desiredShortQty = fill.fillCount >= MIN_FILLS_TO_HEDGE ? fill.totalQuantity * HEDGE_RATIO : 0;
5405
+ const desiredShortQty = fill.fillCount >= MIN_FILLS_TO_HEDGE ? fill.totalQuantity * activeRatio : 0;
5309
5406
  const existing = state.positions.find((p) => p.token === fill.token);
5310
5407
  if (desiredShortQty <= 0) {
5311
5408
  if (existing && existing.quantity > 0) {
@@ -5331,7 +5428,7 @@ var GridHedgeManager = class {
5331
5428
  realizedPnl: 0
5332
5429
  });
5333
5430
  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)`
5431
+ ` [hedge] OPEN ${fill.token} short ${desiredShortQty.toFixed(6)} @ $${price.toFixed(2)} (${(activeRatio * 100).toFixed(0)}% of ${fill.fillCount} open fills)`
5335
5432
  ));
5336
5433
  adjustments++;
5337
5434
  } else if (existing.quantity > 0) {
@@ -5461,8 +5558,7 @@ var GridExecutor = class {
5461
5558
  // src/grid/onchain-executor.ts
5462
5559
  import chalk12 from "chalk";
5463
5560
  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";
5561
+ import { dirname as dirname3 } from "path";
5466
5562
  import { encodeAbiParameters as encodeAbiParameters11, parseUnits as parseUnits6 } from "viem";
5467
5563
 
5468
5564
  // src/grid/cloid.ts
@@ -5503,7 +5599,6 @@ var HYPERLIQUID_GRID_STRATEGY_ABI = [
5503
5599
  ];
5504
5600
 
5505
5601
  // src/grid/onchain-executor.ts
5506
- var ONCHAIN_STATE_PATH = join3(homedir3(), ".sherwood", "grid", "onchain-state.json");
5507
5602
  var UINT64_MAX = 18446744073709551615n;
5508
5603
  var ACTION_PLACE_GRID = 1;
5509
5604
  var ACTION_CANCEL_ALL = 2;
@@ -5531,8 +5626,10 @@ var OnchainGridExecutor = class {
5531
5626
  /** On-chain `maxOrdersPerTick`; cached at load(). Used to chunk batches so
5532
5627
  * the strategy never reverts with TooManyOrders(actual, max). */
5533
5628
  maxOrdersPerTick = null;
5629
+ statePath;
5534
5630
  constructor(cfg) {
5535
5631
  this.cfg = cfg;
5632
+ this.statePath = gridStatePath("onchain-state.json", cfg.stateDir);
5536
5633
  }
5537
5634
  /** Read the strategy's per-call order cap and cache it. */
5538
5635
  async fetchMaxOrdersPerTick() {
@@ -5549,7 +5646,7 @@ var OnchainGridExecutor = class {
5549
5646
  /** Load persisted state. Call once before the first execute(). */
5550
5647
  async load() {
5551
5648
  try {
5552
- const raw = await readFile3(ONCHAIN_STATE_PATH, "utf-8");
5649
+ const raw = await readFile3(this.statePath, "utf-8");
5553
5650
  const state = JSON.parse(raw);
5554
5651
  for (const [tok, n] of Object.entries(state.nonces)) this.nonces.set(tok, n);
5555
5652
  for (const [tok, cloids] of Object.entries(state.placedCloids)) {
@@ -5582,10 +5679,10 @@ var OnchainGridExecutor = class {
5582
5679
  [...this.placedCloids].map(([tok, cloids]) => [tok, cloids.map((c) => c.toString())])
5583
5680
  )
5584
5681
  };
5585
- await mkdir3(dirname3(ONCHAIN_STATE_PATH), { recursive: true });
5586
- const tmp = ONCHAIN_STATE_PATH + ".tmp";
5682
+ await mkdir3(dirname3(this.statePath), { recursive: true });
5683
+ const tmp = this.statePath + ".tmp";
5587
5684
  await writeFile3(tmp, JSON.stringify(state, null, 2));
5588
- await rename2(tmp, ONCHAIN_STATE_PATH);
5685
+ await rename2(tmp, this.statePath);
5589
5686
  }
5590
5687
  /**
5591
5688
  * Execute the order plan: cancel stale orders for rebalanced tokens, then
@@ -5795,7 +5892,6 @@ var OnchainGridExecutor = class {
5795
5892
  };
5796
5893
 
5797
5894
  // src/grid/loop.ts
5798
- var GRID_CYCLES_PATH = join4(homedir4(), ".sherwood", "grid", "cycles.jsonl");
5799
5895
  var GridLoop = class {
5800
5896
  cfg;
5801
5897
  gridConfig;
@@ -5806,11 +5902,21 @@ var GridLoop = class {
5806
5902
  running = false;
5807
5903
  cycleCount = 0;
5808
5904
  timer = null;
5905
+ cyclesPath;
5809
5906
  constructor(cfg) {
5810
5907
  this.cfg = cfg;
5811
5908
  this.gridConfig = { ...DEFAULT_GRID_CONFIG, ...cfg.config };
5812
- this.manager = new GridManager(this.gridConfig);
5813
- this.hedge = new GridHedgeManager();
5909
+ this.cyclesPath = gridStatePath("cycles.jsonl", cfg.stateDir);
5910
+ this.manager = new GridManager(
5911
+ this.gridConfig,
5912
+ void 0,
5913
+ void 0,
5914
+ void 0,
5915
+ new GridPortfolio(cfg.stateDir),
5916
+ void 0,
5917
+ cfg.stateDir
5918
+ );
5919
+ this.hedge = new GridHedgeManager(cfg.stateDir);
5814
5920
  this.hl = new HyperliquidProvider();
5815
5921
  if (cfg.live) {
5816
5922
  if (!cfg.assetIndices) {
@@ -5819,7 +5925,8 @@ var GridLoop = class {
5819
5925
  if (cfg.strategyAddress) {
5820
5926
  this.executor = new OnchainGridExecutor({
5821
5927
  strategyAddress: cfg.strategyAddress,
5822
- assetIndices: cfg.assetIndices
5928
+ assetIndices: cfg.assetIndices,
5929
+ stateDir: cfg.stateDir
5823
5930
  });
5824
5931
  } else {
5825
5932
  this.executor = new GridExecutor({ assetIndices: cfg.assetIndices });
@@ -5845,7 +5952,7 @@ var GridLoop = class {
5845
5952
  }
5846
5953
  console.error(chalk13.cyan(
5847
5954
  `
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
5955
+ [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
5956
  `
5850
5957
  ));
5851
5958
  while (this.running) {
@@ -5855,8 +5962,8 @@ var GridLoop = class {
5855
5962
  console.error(chalk13.red(` [grid-loop] Tick error: ${err.message}`));
5856
5963
  }
5857
5964
  if (this.running) {
5858
- await new Promise((resolve2) => {
5859
- this.timer = setTimeout(resolve2, this.cfg.cycle);
5965
+ await new Promise((resolve3) => {
5966
+ this.timer = setTimeout(resolve3, this.cfg.cycle);
5860
5967
  });
5861
5968
  }
5862
5969
  }
@@ -5904,7 +6011,8 @@ var GridLoop = class {
5904
6011
  ));
5905
6012
  }
5906
6013
  const openExposure = this.manager.getOpenFillExposure();
5907
- const hedgeResult = await this.hedge.tick(openExposure, prices);
6014
+ const marketTrend = this.manager.getMarketTrend();
6015
+ const hedgeResult = await this.hedge.tick(openExposure, prices, Date.now(), marketTrend);
5908
6016
  if (hedgeResult.adjustments > 0) {
5909
6017
  console.error(chalk13.magenta(
5910
6018
  ` [hedge] ${hedgeResult.adjustments} adjustment(s), unrealized: $${hedgeResult.unrealizedPnl.toFixed(2)}, total realized: $${hedgeResult.totalRealizedPnl.toFixed(2)}`
@@ -5926,8 +6034,8 @@ var GridLoop = class {
5926
6034
  hedgeTotalRealizedPnl: hedgeResult.totalRealizedPnl
5927
6035
  };
5928
6036
  try {
5929
- await mkdir4(join4(homedir4(), ".sherwood", "grid"), { recursive: true });
5930
- await appendFile(GRID_CYCLES_PATH, JSON.stringify(cycleEntry) + "\n");
6037
+ await mkdir4(dirname4(this.cyclesPath), { recursive: true });
6038
+ await appendFile(this.cyclesPath, JSON.stringify(cycleEntry) + "\n");
5931
6039
  } catch {
5932
6040
  }
5933
6041
  if (this.cycleCount % 60 === 0) {
@@ -5945,8 +6053,8 @@ var GridLoop = class {
5945
6053
 
5946
6054
  // src/grid/backtest.ts
5947
6055
  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";
6056
+ import { join as join3, dirname as dirname5 } from "path";
6057
+ import { homedir as homedir3 } from "os";
5950
6058
  import { createHash } from "crypto";
5951
6059
 
5952
6060
  // src/grid/backtest-portfolio.ts
@@ -5977,10 +6085,10 @@ var BacktestPortfolio = class extends GridPortfolio {
5977
6085
 
5978
6086
  // src/grid/historical-data-loader.ts
5979
6087
  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";
6088
+ import { join as join2 } from "path";
6089
+ import { homedir as homedir2 } from "os";
5982
6090
  var BINANCE_BASE = "https://api.binance.com/api/v3/klines";
5983
- var DEFAULT_CACHE_DIR = join5(homedir5(), ".sherwood", "grid", "backtest-cache");
6091
+ var DEFAULT_CACHE_DIR = join2(homedir2(), ".sherwood", "grid", "backtest-cache");
5984
6092
  var FOUR_HOURS_MS = 4 * 60 * 60 * 1e3;
5985
6093
  var ONE_MIN_MS = 60 * 1e3;
5986
6094
  var ATR_PERIOD = 14;
@@ -6058,7 +6166,7 @@ var HistoricalDataLoader = class {
6058
6166
  await writeFile4(path, body, "utf-8");
6059
6167
  }
6060
6168
  cachePath(coin, interval, fromMs, toMs) {
6061
- return join5(this.cacheDir, `${coin}-${interval}-${fromMs}-${toMs}.json`);
6169
+ return join2(this.cacheDir, `${coin}-${interval}-${fromMs}-${toMs}.json`);
6062
6170
  }
6063
6171
  /** Paginate Binance klines until we cover [fromMs, toMs]. Dedup on merge. */
6064
6172
  async fetchPaginated(coin, interval, fromMs, toMs) {
@@ -6140,7 +6248,7 @@ function computeAtrSeries(fourHourBars) {
6140
6248
  return out;
6141
6249
  }
6142
6250
  function sleep(ms) {
6143
- return new Promise((resolve2) => setTimeout(resolve2, ms));
6251
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
6144
6252
  }
6145
6253
 
6146
6254
  // src/grid/backtest-hedge.ts
@@ -6178,7 +6286,7 @@ var BacktestHedgeManager = class extends GridHedgeManager {
6178
6286
  };
6179
6287
 
6180
6288
  // src/grid/backtest.ts
6181
- var DEFAULT_OUT_DIR = join6(homedir6(), ".sherwood", "grid", "backtests");
6289
+ var DEFAULT_OUT_DIR = join3(homedir3(), ".sherwood", "grid", "backtests");
6182
6290
  function shortHash(input2) {
6183
6291
  const h = createHash("sha256").update(JSON.stringify(input2)).digest("hex");
6184
6292
  return h.slice(0, 8);
@@ -6350,7 +6458,8 @@ async function runBacktest(opts) {
6350
6458
  hedgePrices[token] = bar.c;
6351
6459
  }
6352
6460
  const exposure = manager.getOpenFillExposure();
6353
- const hedgeResult = await hedge.tick(exposure, hedgePrices, t);
6461
+ const marketTrend = manager.getMarketTrend();
6462
+ const hedgeResult = await hedge.tick(exposure, hedgePrices, t, marketTrend);
6354
6463
  hedgeAdjustments += hedgeResult.adjustments;
6355
6464
  hedgeRealized = hedgeResult.totalRealizedPnl;
6356
6465
  hedgeUnrealized = hedgeResult.unrealizedPnl;
@@ -6543,8 +6652,8 @@ async function runBacktest(opts) {
6543
6652
  equityCurve
6544
6653
  };
6545
6654
  if (opts.outPath !== "") {
6546
- const outPath = opts.outPath ?? join6(DEFAULT_OUT_DIR, `${runId}.json`);
6547
- await mkdir6(dirname4(outPath), { recursive: true });
6655
+ const outPath = opts.outPath ?? join3(DEFAULT_OUT_DIR, `${runId}.json`);
6656
+ await mkdir6(dirname5(outPath), { recursive: true });
6548
6657
  await writeFile5(outPath, JSON.stringify(result, null, 2), "utf-8");
6549
6658
  }
6550
6659
  return result;
@@ -6552,10 +6661,10 @@ async function runBacktest(opts) {
6552
6661
 
6553
6662
  // src/grid/sweep.ts
6554
6663
  import { writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
6555
- import { join as join7 } from "path";
6556
- import { homedir as homedir7 } from "os";
6664
+ import { join as join4 } from "path";
6665
+ import { homedir as homedir4 } from "os";
6557
6666
  import { createHash as createHash2 } from "crypto";
6558
- var DEFAULT_SWEEP_DIR = join7(homedir7(), ".sherwood", "grid", "sweeps");
6667
+ var DEFAULT_SWEEP_DIR = join4(homedir4(), ".sherwood", "grid", "sweeps");
6559
6668
  function expandSweep(sweep, base2) {
6560
6669
  const fields = ["leverage", "levelsPerSide", "atrMultiplier", "rebalanceDriftPct"];
6561
6670
  const valueLists = {
@@ -6592,7 +6701,7 @@ async function runSweep(opts) {
6592
6701
  const startedAt = Date.now();
6593
6702
  const sweepId = makeSweepId(opts);
6594
6703
  const outDir = opts.outDir ?? DEFAULT_SWEEP_DIR;
6595
- const sweepDir = join7(outDir, sweepId);
6704
+ const sweepDir = join4(outDir, sweepId);
6596
6705
  let tokenSplit;
6597
6706
  if (opts.tokenSplit) {
6598
6707
  tokenSplit = opts.tokenSplit;
@@ -6619,7 +6728,7 @@ async function runSweep(opts) {
6619
6728
  for (let i = 0; i < combos.length; i++) {
6620
6729
  const combo = combos[i];
6621
6730
  const config = { ...base2, ...combo };
6622
- const runOutPath = join7(sweepDir, `run-${i.toString().padStart(3, "0")}.json`);
6731
+ const runOutPath = join4(sweepDir, `run-${i.toString().padStart(3, "0")}.json`);
6623
6732
  if (opts.verbose !== false) {
6624
6733
  process.stderr.write(` [sweep] [${i + 1}/${combos.length}] ${JSON.stringify(combo)}
6625
6734
  `);
@@ -6676,7 +6785,7 @@ async function runSweep(opts) {
6676
6785
  feeBps: opts.feeBps ?? 5,
6677
6786
  runs
6678
6787
  };
6679
- const sweepJsonPath = join7(sweepDir, "sweep.json");
6788
+ const sweepJsonPath = join4(sweepDir, "sweep.json");
6680
6789
  await writeFile6(sweepJsonPath, JSON.stringify(sweep, null, 2), "utf-8");
6681
6790
  return sweep;
6682
6791
  }
@@ -6798,12 +6907,13 @@ function parseTokenSplit(raw, tokens) {
6798
6907
  }
6799
6908
  function registerGridCommand(program2) {
6800
6909
  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) => {
6910
+ 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
6911
  const capital = parseFloat(opts.capital);
6803
6912
  const cycleMs = parseInt(opts.cycle, 10) * 1e3;
6804
6913
  const tokens = opts.tokens.split(",").map((t) => t.trim());
6805
6914
  const leverage = parseFloat(opts.leverage);
6806
6915
  const levels = parseInt(opts.levels, 10);
6916
+ const stateDir = opts.stateDir;
6807
6917
  const live = !!opts.live;
6808
6918
  let assetIndices;
6809
6919
  if (live) {
@@ -6846,6 +6956,7 @@ function registerGridCommand(program2) {
6846
6956
  live,
6847
6957
  assetIndices,
6848
6958
  strategyAddress,
6959
+ stateDir,
6849
6960
  config: {
6850
6961
  ...DEFAULT_GRID_CONFIG,
6851
6962
  tokens,
@@ -6856,8 +6967,8 @@ function registerGridCommand(program2) {
6856
6967
  });
6857
6968
  await loop.start();
6858
6969
  });
6859
- grid.command("status").description("Show current grid portfolio status").action(async () => {
6860
- const portfolio = new GridPortfolio();
6970
+ 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) => {
6971
+ const portfolio = new GridPortfolio(opts.stateDir);
6861
6972
  const state = await portfolio.load();
6862
6973
  if (!state) {
6863
6974
  console.log(DIM5("\n No grid portfolio found. Run `sherwood grid start` first.\n"));
@@ -6977,8 +7088,8 @@ function registerGridCommand(program2) {
6977
7088
  });
6978
7089
  printSweepSummary(result);
6979
7090
  });
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();
7091
+ 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) => {
7092
+ const portfolio = new GridPortfolio(opts.stateDir);
6982
7093
  const state = await portfolio.load();
6983
7094
  if (!state) {
6984
7095
  console.log(DIM5("\n No grid portfolio found. Nothing to pause.\n"));
@@ -6998,8 +7109,8 @@ function registerGridCommand(program2) {
6998
7109
  console.log(DIM5(" Next manager.tick will be a no-op until you run `sherwood grid resume`."));
6999
7110
  console.log();
7000
7111
  });
7001
- grid.command("resume").description("Resume a paused grid (clears state.paused)").action(async () => {
7002
- const portfolio = new GridPortfolio();
7112
+ 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) => {
7113
+ const portfolio = new GridPortfolio(opts.stateDir);
7003
7114
  const state = await portfolio.load();
7004
7115
  if (!state) {
7005
7116
  console.log(DIM5("\n No grid portfolio found. Nothing to resume.\n"));
@@ -7010,10 +7121,16 @@ function registerGridCommand(program2) {
7010
7121
  return;
7011
7122
  }
7012
7123
  const wasReason = state.pauseReason;
7124
+ const wasCircuitBroken = state.circuitBroken === true;
7013
7125
  state.paused = false;
7014
7126
  state.pauseReason = "";
7127
+ state.circuitBroken = false;
7128
+ state.circuitBrokenAt = 0;
7015
7129
  await portfolio.save(state);
7016
7130
  console.log();
7131
+ if (wasCircuitBroken) {
7132
+ console.log(chalk14.yellow(` Hard circuit breaker cleared.`));
7133
+ }
7017
7134
  console.log(G5(` Grid resumed. (was paused: ${wasReason || "<unknown>"})`));
7018
7135
  console.log();
7019
7136
  });