@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 +144 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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((
|
|
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) =>
|
|
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 {
|
|
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 {
|
|
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
|
|
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(
|
|
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(
|
|
4782
|
-
const tmp = `${
|
|
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,
|
|
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.
|
|
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 {
|
|
5258
|
-
import { homedir as homedir2 } from "os";
|
|
5296
|
+
import { dirname as dirname2 } from "path";
|
|
5259
5297
|
import chalk10 from "chalk";
|
|
5260
|
-
var
|
|
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(
|
|
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(
|
|
5290
|
-
const tmp = `${
|
|
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,
|
|
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 *
|
|
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)} (${(
|
|
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
|
|
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(
|
|
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(
|
|
5586
|
-
const 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,
|
|
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.
|
|
5813
|
-
this.
|
|
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((
|
|
5859
|
-
this.timer = setTimeout(
|
|
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
|
|
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(
|
|
5930
|
-
await appendFile(
|
|
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
|
|
5949
|
-
import { homedir as
|
|
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
|
|
5981
|
-
import { homedir as
|
|
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 =
|
|
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
|
|
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((
|
|
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 =
|
|
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
|
|
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 ??
|
|
6547
|
-
await mkdir6(
|
|
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
|
|
6556
|
-
import { homedir as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
});
|