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