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