@hedge-layer/cli 1.8.0 → 2.1.0
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/README.md +24 -5
- package/dist/index.mjs +312 -316
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command4 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/auth.ts
|
|
7
7
|
import readline from "readline/promises";
|
|
@@ -980,6 +980,7 @@ function displayMarketBrief(brief, globalOpts) {
|
|
|
980
980
|
}
|
|
981
981
|
|
|
982
982
|
// src/commands/feed.ts
|
|
983
|
+
import { writeFile } from "fs/promises";
|
|
983
984
|
function requireAuth4(client) {
|
|
984
985
|
if (!client.isAuthenticated) {
|
|
985
986
|
error("Not authenticated. Run `hl auth login` first.");
|
|
@@ -1024,8 +1025,82 @@ function feedQueryParams(opts) {
|
|
|
1024
1025
|
add("limit", opts.limit);
|
|
1025
1026
|
return Object.fromEntries(entries);
|
|
1026
1027
|
}
|
|
1028
|
+
var ENSEMBLE_SOURCES = [
|
|
1029
|
+
{ name: "liquid-new-or-long", params: { profile: "liquid-new-or-long", sortBy: "liquidity" } },
|
|
1030
|
+
{ name: "liquidity-provider", params: { profile: "liquidity-provider", sortBy: "lpExpectedReturn" } },
|
|
1031
|
+
{ name: "lp-opportunity", params: { profile: "lp-opportunity", sortBy: "rewardYield" } },
|
|
1032
|
+
{ name: "movement", params: { sortBy: "movement", preset: "price-movers" } },
|
|
1033
|
+
{ name: "spread", params: { sortBy: "spread", preset: "liquidity-provider" } },
|
|
1034
|
+
{ name: "reward-yield", params: { sortBy: "rewardYield", preset: "rewards-optimizer", minRewardsDailyRate: "0.01" } }
|
|
1035
|
+
];
|
|
1036
|
+
function num(value) {
|
|
1037
|
+
return Number.isFinite(value) ? Number(value) : 0;
|
|
1038
|
+
}
|
|
1039
|
+
function scoreCandidate(candidate, sourceCount) {
|
|
1040
|
+
const liquidityScore = Math.min(25, Math.log1p(Math.max(0, candidate.liquidity)) / Math.log1p(1e6) * 25);
|
|
1041
|
+
const volumeScore = Math.min(25, Math.log1p(Math.max(0, candidate.volume24h)) / Math.log1p(1e6) * 25);
|
|
1042
|
+
const spreadScore = Math.max(0, Math.min(15, (0.12 - Math.max(0, candidate.spread)) / 0.12 * 15));
|
|
1043
|
+
const movementPenalty = Math.min(20, Math.abs(candidate.oneDayPriceChange) * 100);
|
|
1044
|
+
const days = candidate.daysToEnd ?? null;
|
|
1045
|
+
const horizonScore = days === null ? 2 : days < 3 ? 0 : Math.min(10, Math.log1p(days) / Math.log1p(365) * 10);
|
|
1046
|
+
const rewardScore = Math.max(
|
|
1047
|
+
0,
|
|
1048
|
+
Math.min(20, num(candidate.components?.rewardYield) * 0.1 + Math.max(0, num(candidate.lpExpectedReturnDailyPct)) * 50)
|
|
1049
|
+
);
|
|
1050
|
+
const sourceScore = Math.min(16, sourceCount * 4);
|
|
1051
|
+
return Math.round((liquidityScore + volumeScore + spreadScore + horizonScore + rewardScore + sourceScore - movementPenalty) * 10) / 10;
|
|
1052
|
+
}
|
|
1053
|
+
function buildFeedEnsemble(sourceResults, limit, outputPath = "candidates.json", generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1054
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
1055
|
+
let totalRawMarkets = 0;
|
|
1056
|
+
for (const { source, result } of sourceResults) {
|
|
1057
|
+
for (const market of result.markets ?? []) {
|
|
1058
|
+
totalRawMarkets++;
|
|
1059
|
+
const existing = bySlug.get(market.slug);
|
|
1060
|
+
if (!existing) {
|
|
1061
|
+
bySlug.set(market.slug, {
|
|
1062
|
+
...market,
|
|
1063
|
+
ensembleScore: 0,
|
|
1064
|
+
sourceProfiles: [source],
|
|
1065
|
+
sourceRanks: { [source]: market.rank }
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
existing.sourceProfiles.push(source);
|
|
1070
|
+
existing.sourceRanks[source] = market.rank;
|
|
1071
|
+
if (market.score > existing.score) existing.score = market.score;
|
|
1072
|
+
existing.volume24h = Math.max(existing.volume24h, market.volume24h);
|
|
1073
|
+
existing.liquidity = Math.max(existing.liquidity, market.liquidity);
|
|
1074
|
+
existing.rewardsDailyRate = Math.max(existing.rewardsDailyRate, market.rewardsDailyRate);
|
|
1075
|
+
existing.lpExpectedReturnDailyPct = Math.max(
|
|
1076
|
+
num(existing.lpExpectedReturnDailyPct),
|
|
1077
|
+
num(market.lpExpectedReturnDailyPct)
|
|
1078
|
+
);
|
|
1079
|
+
existing.lpRiskFlags = [.../* @__PURE__ */ new Set([...existing.lpRiskFlags ?? [], ...market.lpRiskFlags ?? []])];
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const candidates = [...bySlug.values()].map((candidate) => ({
|
|
1083
|
+
...candidate,
|
|
1084
|
+
sourceProfiles: [...new Set(candidate.sourceProfiles)],
|
|
1085
|
+
ensembleScore: scoreCandidate(candidate, new Set(candidate.sourceProfiles).size)
|
|
1086
|
+
})).sort((a, b) => b.ensembleScore - a.ensembleScore || b.score - a.score).slice(0, limit);
|
|
1087
|
+
return {
|
|
1088
|
+
generatedAt,
|
|
1089
|
+
outputPath,
|
|
1090
|
+
totalSources: sourceResults.length,
|
|
1091
|
+
totalRawMarkets,
|
|
1092
|
+
totalCandidates: bySlug.size,
|
|
1093
|
+
marketsReturned: candidates.length,
|
|
1094
|
+
candidates
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
function parseLimit(value, fallback) {
|
|
1098
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
1099
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
1100
|
+
return Math.max(1, Math.min(100, parsed));
|
|
1101
|
+
}
|
|
1027
1102
|
function registerFeedCommand(program2) {
|
|
1028
|
-
program2.command("feed").description(
|
|
1103
|
+
const feed = program2.command("feed").description(
|
|
1029
1104
|
"Rank active Polymarket markets (same engine as chat getFeed / GET /api/feed). Use --profile for curated screens."
|
|
1030
1105
|
).argument(
|
|
1031
1106
|
"[screening]",
|
|
@@ -1033,7 +1108,50 @@ function registerFeedCommand(program2) {
|
|
|
1033
1108
|
).option(
|
|
1034
1109
|
"--profile <name>",
|
|
1035
1110
|
`Screening defaults: ${PROFILE_CHOICES.join(", ")} \u2014 explicit flags override`
|
|
1036
|
-
).option("--sort-by <key>", "score | volume | liquidity | movement | spread | recency | extremity | rewards | rewardYield | lpExpectedReturn | horizon").option("--preset <name>", "Attention weight preset (default, volume-hunter, lp-opportunity, \u2026)").option("--tag <slug>", "Polymarket category tag, e.g. crypto, politics").option("--min-volume <usd>", "Minimum 24h volume (USD)").option("--min-liquidity <usd>", "Minimum displayed liquidity (USD)").option("--max-liquidity <usd>", "Maximum displayed liquidity (USD)").option("--min-rewards-daily-rate <usd>", "Minimum LP rewards USD/day").option("--min-days-to-end <n>", "Min calendar days until resolution").option("--max-days-to-end <n>", "Max calendar days until resolution").option("--max-market-age-hours <n>", 'With liquid-new-or-long: max age (hours) for the "new" branch').option("--liquid-profile <mode>", "new-or-long (used by liquid-new-or-long screen)").option("--limit <n>", "Max markets to return (1\u2013100, default 15)", "15")
|
|
1111
|
+
).option("--sort-by <key>", "score | volume | liquidity | movement | spread | recency | extremity | rewards | rewardYield | lpExpectedReturn | horizon").option("--preset <name>", "Attention weight preset (default, volume-hunter, lp-opportunity, \u2026)").option("--tag <slug>", "Polymarket category tag, e.g. crypto, politics").option("--min-volume <usd>", "Minimum 24h volume (USD)").option("--min-liquidity <usd>", "Minimum displayed liquidity (USD)").option("--max-liquidity <usd>", "Maximum displayed liquidity (USD)").option("--min-rewards-daily-rate <usd>", "Minimum LP rewards USD/day").option("--min-days-to-end <n>", "Min calendar days until resolution").option("--max-days-to-end <n>", "Max calendar days until resolution").option("--max-market-age-hours <n>", 'With liquid-new-or-long: max age (hours) for the "new" branch').option("--liquid-profile <mode>", "new-or-long (used by liquid-new-or-long screen)").option("--limit <n>", "Max markets to return (1\u2013100, default 15)", "15");
|
|
1112
|
+
feed.command("ensemble").description("Run multiple feed screens, merge by slug, and write ranked candidates JSON").option("--limit <n>", "Max candidates to return/write (1-100, default 25)", "25").option("--output <file>", "Output JSON path", "candidates.json").action(async (o) => {
|
|
1113
|
+
const globalOpts = program2.opts();
|
|
1114
|
+
const client = new ApiClient(globalOpts);
|
|
1115
|
+
requireAuth4(client);
|
|
1116
|
+
const perSourceLimit = "100";
|
|
1117
|
+
const sourceResults = [];
|
|
1118
|
+
try {
|
|
1119
|
+
for (const source of ENSEMBLE_SOURCES) {
|
|
1120
|
+
const result = await client.get("/api/feed", {
|
|
1121
|
+
...source.params,
|
|
1122
|
+
limit: perSourceLimit
|
|
1123
|
+
});
|
|
1124
|
+
if (result.error) {
|
|
1125
|
+
throw new Error(result.error);
|
|
1126
|
+
}
|
|
1127
|
+
sourceResults.push({ source: source.name, result });
|
|
1128
|
+
}
|
|
1129
|
+
const outputPath = o.output ?? "candidates.json";
|
|
1130
|
+
const ensemble = buildFeedEnsemble(sourceResults, parseLimit(o.limit, 25), outputPath);
|
|
1131
|
+
await writeFile(outputPath, JSON.stringify(ensemble, null, 2) + "\n", "utf8");
|
|
1132
|
+
if (globalOpts.json) {
|
|
1133
|
+
json(ensemble);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
heading(`Feed Ensemble \u2014 ${ensemble.marketsReturned} candidates`);
|
|
1137
|
+
table(
|
|
1138
|
+
ensemble.candidates.slice(0, 15).map((m) => [
|
|
1139
|
+
String(Math.round(m.ensembleScore)),
|
|
1140
|
+
truncate(m.question, 48),
|
|
1141
|
+
`${Math.round(m.yesPrice * 100)}%`,
|
|
1142
|
+
compactCurrency(m.volume24h),
|
|
1143
|
+
compactCurrency(m.liquidity),
|
|
1144
|
+
m.sourceProfiles.join(",")
|
|
1145
|
+
]),
|
|
1146
|
+
["Score", "Market", "YES", "24h Vol", "Liq", "Sources"]
|
|
1147
|
+
);
|
|
1148
|
+
success(`Wrote ${outputPath}`);
|
|
1149
|
+
} catch (e) {
|
|
1150
|
+
error(e instanceof Error ? e.message : String(e));
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
feed.action(async (screening, o) => {
|
|
1037
1155
|
const globalOpts = program2.opts();
|
|
1038
1156
|
let profile;
|
|
1039
1157
|
try {
|
|
@@ -1073,10 +1191,9 @@ function registerFeedCommand(program2) {
|
|
|
1073
1191
|
});
|
|
1074
1192
|
}
|
|
1075
1193
|
|
|
1076
|
-
// src/commands/
|
|
1077
|
-
import { InvalidArgumentError } from "commander";
|
|
1078
|
-
import {
|
|
1079
|
-
import chalk6 from "chalk";
|
|
1194
|
+
// src/commands/lp.ts
|
|
1195
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1196
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
1080
1197
|
|
|
1081
1198
|
// src/allocator-display.ts
|
|
1082
1199
|
import chalk5 from "chalk";
|
|
@@ -1096,42 +1213,10 @@ function actionColor(action) {
|
|
|
1096
1213
|
return action;
|
|
1097
1214
|
}
|
|
1098
1215
|
}
|
|
1099
|
-
function
|
|
1216
|
+
function num2(value, fallback = 0) {
|
|
1100
1217
|
const n = Number(value);
|
|
1101
1218
|
return Number.isFinite(n) ? n : fallback;
|
|
1102
1219
|
}
|
|
1103
|
-
function feedMarketsToAllocatorMarkets(markets) {
|
|
1104
|
-
return markets.slice(0, 25).map((m) => {
|
|
1105
|
-
const probability = num(m.probability ?? m.yesPrice, 0.5);
|
|
1106
|
-
const daysToEnd = m.daysToEnd == null || m.daysToEnd === void 0 ? void 0 : num(m.daysToEnd);
|
|
1107
|
-
return {
|
|
1108
|
-
slug: String(m.slug ?? ""),
|
|
1109
|
-
question: String(m.question ?? ""),
|
|
1110
|
-
yesTokenId: m.yesTokenId ? String(m.yesTokenId) : void 0,
|
|
1111
|
-
noTokenId: m.noTokenId ? String(m.noTokenId) : void 0,
|
|
1112
|
-
yesPrice: num(m.yesPrice, probability),
|
|
1113
|
-
noPrice: num(m.noPrice, 1 - probability),
|
|
1114
|
-
liquidity: num(m.liquidity),
|
|
1115
|
-
volume24h: num(m.volume24h),
|
|
1116
|
-
spread: num(m.spread),
|
|
1117
|
-
rewardsDailyRate: num(m.rewardsDailyRate),
|
|
1118
|
-
oneDayPriceChange: num(m.oneDayPriceChange),
|
|
1119
|
-
...daysToEnd !== void 0 && { daysToEnd },
|
|
1120
|
-
active: m.active == null ? true : Boolean(m.active)
|
|
1121
|
-
};
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
function allocationsFromDecisions(decisions) {
|
|
1125
|
-
return decisions.filter((d) => String(d.market_slug ?? "")).map((d) => ({
|
|
1126
|
-
market_slug: String(d.market_slug),
|
|
1127
|
-
status: String(d.action ?? "planned").toLowerCase(),
|
|
1128
|
-
allocated_capital: num(d.target_capital),
|
|
1129
|
-
locked_capital: 0,
|
|
1130
|
-
inventory_yes: 0,
|
|
1131
|
-
inventory_no: 0,
|
|
1132
|
-
open_order_notional: Array.isArray(d.order_plan) ? d.order_plan.reduce((sum, order) => sum + num(order.notional), 0) : 0
|
|
1133
|
-
}));
|
|
1134
|
-
}
|
|
1135
1220
|
function displayAllocatorCycleResult(result, globalOpts) {
|
|
1136
1221
|
if (globalOpts.json) {
|
|
1137
1222
|
json(result);
|
|
@@ -1143,9 +1228,9 @@ function displayAllocatorCycleResult(result, globalOpts) {
|
|
|
1143
1228
|
heading(`Allocator Cycle \u2014 ${dryRun}`);
|
|
1144
1229
|
process.stdout.write(
|
|
1145
1230
|
chalk5.dim(
|
|
1146
|
-
` ${
|
|
1147
|
-
|
|
1148
|
-
)} target capital
|
|
1231
|
+
` ${num2(result.total_markets, decisions.length)} markets \xB7 ${currency(
|
|
1232
|
+
num2(summary.target_capital)
|
|
1233
|
+
)} target capital
|
|
1149
1234
|
|
|
1150
1235
|
`
|
|
1151
1236
|
)
|
|
@@ -1156,22 +1241,30 @@ function displayAllocatorCycleResult(result, globalOpts) {
|
|
|
1156
1241
|
}
|
|
1157
1242
|
const rows = decisions.map((d) => {
|
|
1158
1243
|
const action = String(d.action ?? "UNKNOWN");
|
|
1159
|
-
const score =
|
|
1160
|
-
const expected =
|
|
1161
|
-
const orders = Array.isArray(d.order_plan) ? d.order_plan.length : 0;
|
|
1244
|
+
const score = num2(d.score?.score);
|
|
1245
|
+
const expected = num2(d.score?.expected_return_daily_pct);
|
|
1162
1246
|
const failedChecks = Array.isArray(d.safety_checks) ? d.safety_checks.filter((check) => check.passed === false).length : 0;
|
|
1163
1247
|
return [
|
|
1164
1248
|
truncate(String(d.question ?? d.market_slug ?? "\u2014"), 42),
|
|
1165
1249
|
actionColor(action),
|
|
1166
|
-
currency(
|
|
1167
|
-
signedCurrency(
|
|
1250
|
+
currency(num2(d.target_capital)),
|
|
1251
|
+
signedCurrency(num2(d.capital_delta)),
|
|
1168
1252
|
`${expected.toFixed(3)}%`,
|
|
1253
|
+
regimeLabel(String(d.quote_regime ?? "\u2014")),
|
|
1169
1254
|
String(Math.round(score)),
|
|
1170
|
-
String(orders),
|
|
1171
1255
|
failedChecks === 0 ? chalk5.green("0") : chalk5.yellow(String(failedChecks))
|
|
1172
1256
|
];
|
|
1173
1257
|
});
|
|
1174
|
-
table(rows, [
|
|
1258
|
+
table(rows, [
|
|
1259
|
+
"Market",
|
|
1260
|
+
"Action",
|
|
1261
|
+
"Target",
|
|
1262
|
+
"Delta",
|
|
1263
|
+
"Exp/day",
|
|
1264
|
+
"Regime",
|
|
1265
|
+
"Score",
|
|
1266
|
+
"Fails"
|
|
1267
|
+
]);
|
|
1175
1268
|
process.stdout.write("\n");
|
|
1176
1269
|
for (const decision of decisions.slice(0, 5)) {
|
|
1177
1270
|
const action = String(decision.action ?? "UNKNOWN");
|
|
@@ -1181,17 +1274,13 @@ function displayAllocatorCycleResult(result, globalOpts) {
|
|
|
1181
1274
|
process.stdout.write(" " + chalk5.dim("Rationale: ") + truncate(decision.rationale, 120));
|
|
1182
1275
|
}
|
|
1183
1276
|
process.stdout.write("\n");
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1277
|
+
const economics = decision.economics ?? {};
|
|
1278
|
+
if (economics.realized_spread_pnl !== void 0 || economics.reward_income !== void 0 || economics.net_realized_pnl !== void 0) {
|
|
1186
1279
|
process.stdout.write(
|
|
1187
|
-
" " + chalk5.dim("
|
|
1280
|
+
" " + chalk5.dim("Economics: ") + `spread ${signedCurrency(num2(economics.realized_spread_pnl))}, rewards ${signedCurrency(num2(economics.reward_income))}, net ${signedCurrency(num2(economics.net_realized_pnl))}
|
|
1188
1281
|
`
|
|
1189
1282
|
);
|
|
1190
1283
|
}
|
|
1191
|
-
if (orders.length > 2) {
|
|
1192
|
-
process.stdout.write(chalk5.dim(` \u2026 ${orders.length - 2} more orders
|
|
1193
|
-
`));
|
|
1194
|
-
}
|
|
1195
1284
|
}
|
|
1196
1285
|
if (decisions.length > 5) {
|
|
1197
1286
|
process.stdout.write(chalk5.dim(`
|
|
@@ -1199,6 +1288,18 @@ function displayAllocatorCycleResult(result, globalOpts) {
|
|
|
1199
1288
|
`));
|
|
1200
1289
|
}
|
|
1201
1290
|
}
|
|
1291
|
+
function regimeLabel(value) {
|
|
1292
|
+
switch (value) {
|
|
1293
|
+
case "reward_optimized":
|
|
1294
|
+
return chalk5.green("reward");
|
|
1295
|
+
case "defensive":
|
|
1296
|
+
return chalk5.yellow("defense");
|
|
1297
|
+
case "no_quote":
|
|
1298
|
+
return chalk5.red("no quote");
|
|
1299
|
+
default:
|
|
1300
|
+
return chalk5.dim(value);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1202
1303
|
function signedCurrency(value) {
|
|
1203
1304
|
const formatted = currency(Math.abs(value));
|
|
1204
1305
|
if (value > 0) return chalk5.green(`+${formatted}`);
|
|
@@ -1207,23 +1308,8 @@ function signedCurrency(value) {
|
|
|
1207
1308
|
}
|
|
1208
1309
|
|
|
1209
1310
|
// src/commands/allocator.ts
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
if (!client.isAuthenticated) {
|
|
1213
|
-
error("Not authenticated. Run `hl auth login` first.");
|
|
1214
|
-
process.exit(1);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
function isProfile2(s) {
|
|
1218
|
-
return s !== void 0 && PROFILE_CHOICES2.includes(s);
|
|
1219
|
-
}
|
|
1220
|
-
function parseNonNegative(value) {
|
|
1221
|
-
const n = Number(value);
|
|
1222
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
1223
|
-
throw new InvalidArgumentError("Expected a non-negative number");
|
|
1224
|
-
}
|
|
1225
|
-
return n;
|
|
1226
|
-
}
|
|
1311
|
+
import { InvalidArgumentError } from "commander";
|
|
1312
|
+
import { readFile } from "fs/promises";
|
|
1227
1313
|
function parsePositiveNumber(value) {
|
|
1228
1314
|
const n = Number(value);
|
|
1229
1315
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -1231,69 +1317,49 @@ function parsePositiveNumber(value) {
|
|
|
1231
1317
|
}
|
|
1232
1318
|
return n;
|
|
1233
1319
|
}
|
|
1234
|
-
function
|
|
1235
|
-
const
|
|
1236
|
-
if (!
|
|
1237
|
-
throw new
|
|
1320
|
+
function parseAllocationsInput(parsed) {
|
|
1321
|
+
const rows = extractArray(parsed, "allocations");
|
|
1322
|
+
if (!rows) {
|
|
1323
|
+
throw new Error("Allocations JSON must be an array or { allocations: [...] }");
|
|
1238
1324
|
}
|
|
1239
|
-
return
|
|
1325
|
+
return rows;
|
|
1240
1326
|
}
|
|
1241
|
-
function
|
|
1242
|
-
|
|
1327
|
+
function parsePnlContextInput(parsed) {
|
|
1328
|
+
const rows = extractArray(parsed, "pnl_context");
|
|
1329
|
+
if (!rows) {
|
|
1330
|
+
throw new Error("PnL JSON must be an array or { pnl_context: [...] }");
|
|
1331
|
+
}
|
|
1332
|
+
return rows;
|
|
1243
1333
|
}
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
maybeAdd(entries, "sortBy", opts.sortBy);
|
|
1248
|
-
maybeAdd(entries, "tag", opts.tag);
|
|
1249
|
-
maybeAdd(entries, "minVolume", opts.minVolume);
|
|
1250
|
-
maybeAdd(entries, "minLiquidity", opts.minLiquidity);
|
|
1251
|
-
maybeAdd(entries, "maxLiquidity", opts.maxLiquidity);
|
|
1252
|
-
maybeAdd(entries, "minRewardsDailyRate", opts.minRewardsDailyRate);
|
|
1253
|
-
maybeAdd(entries, "minDaysToEnd", opts.minDaysToEnd);
|
|
1254
|
-
maybeAdd(entries, "maxDaysToEnd", opts.maxDaysToEnd);
|
|
1255
|
-
maybeAdd(entries, "maxMarketAgeHours", opts.maxMarketAgeHours);
|
|
1256
|
-
maybeAdd(entries, "liquidProfile", opts.liquidProfile);
|
|
1257
|
-
maybeAdd(entries, "limit", opts.limit);
|
|
1258
|
-
return Object.fromEntries(entries);
|
|
1334
|
+
async function readAllocations(path) {
|
|
1335
|
+
if (!path) return [];
|
|
1336
|
+
return parseAllocationsInput(await readJsonInput(path));
|
|
1259
1337
|
}
|
|
1260
|
-
function
|
|
1261
|
-
return
|
|
1262
|
-
|
|
1263
|
-
name: "CLI dry-run LP strategy",
|
|
1264
|
-
status: opts.paused ? "paused" : "dry_run",
|
|
1265
|
-
...opts.totalHoldings !== void 0 && { total_holdings: opts.totalHoldings },
|
|
1266
|
-
...opts.capitalLimitPct !== void 0 && { capital_limit_pct: opts.capitalLimitPct },
|
|
1267
|
-
...opts.perMarketLimitPct !== void 0 && { per_market_limit_pct: opts.perMarketLimitPct },
|
|
1268
|
-
capital_limit: opts.capitalLimit,
|
|
1269
|
-
per_market_limit: opts.perMarketLimit,
|
|
1270
|
-
min_expected_return_daily_pct: opts.minExpectedReturnDailyPct,
|
|
1271
|
-
max_inventory_imbalance: opts.maxInventoryImbalance,
|
|
1272
|
-
max_order_notional: opts.maxOrderNotional,
|
|
1273
|
-
quote_edge_bps: opts.quoteEdgeBps,
|
|
1274
|
-
min_liquidity: opts.allocatorMinLiquidity,
|
|
1275
|
-
max_spread: opts.maxSpread,
|
|
1276
|
-
min_days_to_end: opts.allocatorMinDaysToEnd,
|
|
1277
|
-
max_markets: opts.maxMarkets
|
|
1278
|
-
};
|
|
1338
|
+
async function readPnlContext(path) {
|
|
1339
|
+
if (!path) return [];
|
|
1340
|
+
return parsePnlContextInput(await readJsonInput(path));
|
|
1279
1341
|
}
|
|
1280
|
-
function
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
);
|
|
1286
|
-
process.exit(1);
|
|
1342
|
+
async function readMarketPayload(path) {
|
|
1343
|
+
if (!path) return void 0;
|
|
1344
|
+
const parsed = await readJsonInput(path);
|
|
1345
|
+
const rows = extractArray(parsed, "markets");
|
|
1346
|
+
if (!rows) {
|
|
1347
|
+
throw new Error("Markets JSON must be an array or { markets: [...] }");
|
|
1287
1348
|
}
|
|
1349
|
+
return rows;
|
|
1288
1350
|
}
|
|
1289
|
-
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
1351
|
+
function extractArray(parsed, key) {
|
|
1352
|
+
if (Array.isArray(parsed)) {
|
|
1353
|
+
return parsed;
|
|
1354
|
+
}
|
|
1355
|
+
if (parsed && typeof parsed === "object" && Array.isArray(parsed[key])) {
|
|
1356
|
+
return parsed[key];
|
|
1295
1357
|
}
|
|
1296
|
-
return
|
|
1358
|
+
return void 0;
|
|
1359
|
+
}
|
|
1360
|
+
async function readJsonInput(path) {
|
|
1361
|
+
const raw = path === "-" ? await readStdin() : await readFile(path, "utf8");
|
|
1362
|
+
return JSON.parse(raw);
|
|
1297
1363
|
}
|
|
1298
1364
|
async function readStdin() {
|
|
1299
1365
|
const chunks = [];
|
|
@@ -1302,96 +1368,22 @@ async function readStdin() {
|
|
|
1302
1368
|
}
|
|
1303
1369
|
return Buffer.concat(chunks).toString("utf8");
|
|
1304
1370
|
}
|
|
1305
|
-
async function runAllocatorCycle(client, payload) {
|
|
1306
|
-
return client.post("/api/allocator/cycle", payload);
|
|
1307
|
-
}
|
|
1308
|
-
function registerAllocatorCommands(program2) {
|
|
1309
|
-
const allocator = program2.command("allocator").description("Run dry-run liquidity allocation cycles");
|
|
1310
|
-
allocator.command("cycle").description("Find LP candidates from /api/feed and run a dry-run allocator cycle").argument(
|
|
1311
|
-
"[screening]",
|
|
1312
|
-
`Optional screening preset: ${PROFILE_CHOICES2.join(" | ")} (same as --profile)`,
|
|
1313
|
-
"lp-opportunity"
|
|
1314
|
-
).option("--profile <name>", `Screening defaults: ${PROFILE_CHOICES2.join(", ")}`).option("--sort-by <key>", "score | volume | liquidity | movement | spread | rewards | rewardYield | lpExpectedReturn | horizon").option("--tag <slug>", "Polymarket category tag, e.g. crypto, politics").option("--min-volume <usd>", "Minimum 24h volume (USD)").option("--min-liquidity <usd>", "Minimum displayed liquidity (USD)").option("--max-liquidity <usd>", "Maximum displayed liquidity (USD)").option("--min-rewards-daily-rate <usd>", "Minimum LP rewards USD/day").option("--min-days-to-end <n>", "Feed filter: minimum days until resolution").option("--max-days-to-end <n>", "Feed filter: maximum days until resolution").option("--max-market-age-hours <n>", "With liquid-new-or-long: max age for the new branch").option("--liquid-profile <mode>", "new-or-long (used by liquid-new-or-long screen)").option("--limit <n>", "Max feed markets to fetch (1-100, default 15)", "15").option("--total-holdings <usd>", "Total user holdings / portfolio value used for percentage sizing", parsePositiveNumber).option("--capital-limit-pct <pct>", "Portfolio-level allocation cap as a percent of total holdings", parsePositiveNumber).option("--per-market-limit-pct <pct>", "Per-market target cap as a percent of total holdings", parsePositiveNumber).option("--capital-limit <usd>", "Portfolio capital limit for this cycle", parseNonNegative, 500).option("--per-market-limit <usd>", "Per-market target cap", parseNonNegative, 100).option("--min-expected-return-daily-pct <pct>", "Minimum expected daily return percent", parseNonNegative, 0.02).option("--max-inventory-imbalance <ratio>", "Maximum inventory imbalance", parseNonNegative, 0.25).option("--max-order-notional <usd>", "Maximum notional per planned passive order", parseNonNegative, 25).option("--quote-edge-bps <bps>", "Passive quote edge in basis points", parseNonNegative, 100).option("--allocator-min-liquidity <usd>", "Allocator safety gate: minimum market liquidity", parseNonNegative, 500).option("--max-spread <ratio>", "Allocator safety gate: maximum spread", parseNonNegative, 0.12).option("--allocator-min-days-to-end <n>", "Allocator safety gate: minimum days to resolution", parseNonNegative, 3).option("--max-markets <n>", "Maximum markets allocator may target", parsePositiveInt, 5).option("--allocations <file>", "Existing allocations JSON array; use '-' to read stdin").option("--repeat", "Run a second cycle using targets returned by the first cycle").option("--paused", "Send strategy status paused instead of dry_run").action(async (screening, o) => {
|
|
1315
|
-
const globalOpts = program2.opts();
|
|
1316
|
-
const client = new ApiClient(globalOpts);
|
|
1317
|
-
requireAuth5(client);
|
|
1318
|
-
validatePercentageSizing(o);
|
|
1319
|
-
let profile = o.profile ?? screening ?? "lp-opportunity";
|
|
1320
|
-
if (!isProfile2(profile)) {
|
|
1321
|
-
error(`Unknown screening "${profile}". Use: ${PROFILE_CHOICES2.join(" or ")}`);
|
|
1322
|
-
process.exit(1);
|
|
1323
|
-
}
|
|
1324
|
-
if (o.profile && screening && screening !== "lp-opportunity" && o.profile !== screening) {
|
|
1325
|
-
warn(`Both positional and --profile set; using --profile (${o.profile}).`);
|
|
1326
|
-
profile = o.profile;
|
|
1327
|
-
}
|
|
1328
|
-
const feed = await client.get("/api/feed", feedQueryParams2(profile, o));
|
|
1329
|
-
if (feed.error) {
|
|
1330
|
-
error(feed.error);
|
|
1331
|
-
process.exit(1);
|
|
1332
|
-
}
|
|
1333
|
-
if (feed.markets.length === 0) {
|
|
1334
|
-
warn("No feed markets matched the allocator criteria.");
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
const allocations = await readAllocations(o.allocations);
|
|
1338
|
-
const payload = {
|
|
1339
|
-
strategy: strategyFromOptions(o),
|
|
1340
|
-
markets: feedMarketsToAllocatorMarkets(feed.markets),
|
|
1341
|
-
allocations
|
|
1342
|
-
};
|
|
1343
|
-
if (!globalOpts.json) {
|
|
1344
|
-
process.stderr.write(
|
|
1345
|
-
chalk6.dim(
|
|
1346
|
-
` Running allocator dry-run on ${feed.markets.length} ${profile} candidates...
|
|
1347
|
-
`
|
|
1348
|
-
)
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
const first = await runAllocatorCycle(client, payload);
|
|
1352
|
-
const firstResult = first.result ?? {};
|
|
1353
|
-
if (o.repeat) {
|
|
1354
|
-
const repeatPayload = {
|
|
1355
|
-
...payload,
|
|
1356
|
-
allocations: allocationsFromDecisions(firstResult.decisions ?? [])
|
|
1357
|
-
};
|
|
1358
|
-
const second = await runAllocatorCycle(client, repeatPayload);
|
|
1359
|
-
if (globalOpts.json) {
|
|
1360
|
-
json({ initial: first, repeat: second });
|
|
1361
|
-
} else {
|
|
1362
|
-
displayAllocatorCycleResult(firstResult, globalOpts);
|
|
1363
|
-
process.stdout.write("\n" + chalk6.bold("Repeated with target allocations") + "\n");
|
|
1364
|
-
displayAllocatorCycleResult(second.result ?? {}, globalOpts);
|
|
1365
|
-
}
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
if (globalOpts.json) {
|
|
1369
|
-
json(first);
|
|
1370
|
-
} else {
|
|
1371
|
-
displayAllocatorCycleResult(firstResult, globalOpts);
|
|
1372
|
-
}
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// src/commands/lp.ts
|
|
1377
|
-
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
1378
|
-
import { writeFile } from "fs/promises";
|
|
1379
1371
|
|
|
1380
1372
|
// src/lp-display.ts
|
|
1381
|
-
import
|
|
1382
|
-
function
|
|
1373
|
+
import chalk6 from "chalk";
|
|
1374
|
+
function num3(value, fallback = 0) {
|
|
1383
1375
|
const n = Number(value);
|
|
1384
1376
|
return Number.isFinite(n) ? n : fallback;
|
|
1385
1377
|
}
|
|
1386
1378
|
function signedCurrency2(value) {
|
|
1387
1379
|
const formatted = currency(Math.abs(value));
|
|
1388
|
-
if (value > 0) return
|
|
1389
|
-
if (value < 0) return
|
|
1390
|
-
return
|
|
1380
|
+
if (value > 0) return chalk6.green(`+${formatted}`);
|
|
1381
|
+
if (value < 0) return chalk6.yellow(`-${formatted}`);
|
|
1382
|
+
return chalk6.dim("$0.00");
|
|
1391
1383
|
}
|
|
1392
1384
|
function actionSummary(actions) {
|
|
1393
1385
|
if (!actions) return "none";
|
|
1394
|
-
return Object.entries(actions).filter(([, count]) =>
|
|
1386
|
+
return Object.entries(actions).filter(([, count]) => num3(count) > 0).map(([action, count]) => `${action}:${num3(count)}`).join(" ");
|
|
1395
1387
|
}
|
|
1396
1388
|
function displayLpScanResult(result, globalOpts) {
|
|
1397
1389
|
if (globalOpts.json) {
|
|
@@ -1400,13 +1392,13 @@ function displayLpScanResult(result, globalOpts) {
|
|
|
1400
1392
|
}
|
|
1401
1393
|
heading("LP Scan");
|
|
1402
1394
|
process.stdout.write(
|
|
1403
|
-
|
|
1395
|
+
chalk6.dim(
|
|
1404
1396
|
` scan ${result.scanId} \xB7 strategy ${result.strategyId} \xB7 ${result.evidenceSaved} evidence rows saved
|
|
1405
1397
|
`
|
|
1406
1398
|
)
|
|
1407
1399
|
);
|
|
1408
1400
|
process.stdout.write(
|
|
1409
|
-
|
|
1401
|
+
chalk6.dim(
|
|
1410
1402
|
` ${result.totalScanned.toLocaleString()} scanned \xB7 ${result.totalAfterFilter.toLocaleString()} after filters \xB7 profile ${result.profile}
|
|
1411
1403
|
|
|
1412
1404
|
`
|
|
@@ -1420,10 +1412,10 @@ function displayLpScanResult(result, globalOpts) {
|
|
|
1420
1412
|
result.markets.slice(0, 10).map((market) => [
|
|
1421
1413
|
String(market.rank),
|
|
1422
1414
|
truncate(market.question, 46),
|
|
1423
|
-
String(Math.round(
|
|
1424
|
-
compactCurrency(
|
|
1425
|
-
compactCurrency(
|
|
1426
|
-
`${
|
|
1415
|
+
String(Math.round(num3(market.score))),
|
|
1416
|
+
compactCurrency(num3(market.liquidity)),
|
|
1417
|
+
compactCurrency(num3(market.rewardsDailyRate)) + "/day",
|
|
1418
|
+
`${num3(market.lpExpectedReturnDailyPct).toFixed(3)}%`
|
|
1427
1419
|
]),
|
|
1428
1420
|
["#", "Market", "Score", "Liq", "Rewards", "Exp/day"]
|
|
1429
1421
|
);
|
|
@@ -1435,13 +1427,13 @@ function displayLpRecommendResult(result, globalOpts) {
|
|
|
1435
1427
|
}
|
|
1436
1428
|
heading("LP Recommendations");
|
|
1437
1429
|
process.stdout.write(
|
|
1438
|
-
|
|
1430
|
+
chalk6.dim(
|
|
1439
1431
|
` cycle ${result.cycleId} \xB7 strategy ${result.strategyId} \xB7 ${result.candidatesSubmitted} markets \xB7 ${result.allocationsSubmitted} current allocations
|
|
1440
1432
|
`
|
|
1441
1433
|
)
|
|
1442
1434
|
);
|
|
1443
1435
|
process.stdout.write(
|
|
1444
|
-
|
|
1436
|
+
chalk6.dim(
|
|
1445
1437
|
` PnL context ${result.pnlContextCount} rows${result.pnlSynced ? " \xB7 synced" : ""} \xB7 approvals required
|
|
1446
1438
|
|
|
1447
1439
|
`
|
|
@@ -1456,11 +1448,11 @@ function displayLpEvaluateResult(result, globalOpts) {
|
|
|
1456
1448
|
}
|
|
1457
1449
|
heading("LP Evaluation");
|
|
1458
1450
|
process.stdout.write(
|
|
1459
|
-
|
|
1451
|
+
chalk6.dim(
|
|
1460
1452
|
` strategy ${result.strategyId} \xB7 ${result.summary.snapshots} snapshots \xB7 ${result.summary.markets} markets`
|
|
1461
1453
|
)
|
|
1462
1454
|
);
|
|
1463
|
-
if (result.pnlSynced) process.stdout.write(
|
|
1455
|
+
if (result.pnlSynced) process.stdout.write(chalk6.dim(" \xB7 synced"));
|
|
1464
1456
|
process.stdout.write("\n\n");
|
|
1465
1457
|
if (result.syncError) warn(result.syncError);
|
|
1466
1458
|
table(
|
|
@@ -1482,64 +1474,64 @@ function displayLpEvaluateResult(result, globalOpts) {
|
|
|
1482
1474
|
result.lessons.slice(0, 8).map((lesson) => [
|
|
1483
1475
|
truncate(String(lesson.market_slug ?? "portfolio"), 26),
|
|
1484
1476
|
String(lesson.outcome ?? "flat"),
|
|
1485
|
-
signedCurrency2(
|
|
1477
|
+
signedCurrency2(num3(lesson.net_pnl)),
|
|
1486
1478
|
truncate(String(lesson.lesson ?? "no lesson"), 64)
|
|
1487
1479
|
]),
|
|
1488
1480
|
["Market", "Outcome", "Net", "Lesson"]
|
|
1489
1481
|
);
|
|
1490
1482
|
}
|
|
1491
|
-
function displayLpRunResult(result, globalOpts) {
|
|
1492
|
-
if (globalOpts.json) {
|
|
1493
|
-
json(result);
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
heading("LP Run");
|
|
1497
|
-
process.stdout.write(
|
|
1498
|
-
chalk7.dim(
|
|
1499
|
-
` cycle ${result.run.cycleId} \xB7 scan ${result.run.scanId ?? "n/a"} \xB7 strategy ${result.run.strategyId}
|
|
1500
|
-
`
|
|
1501
|
-
)
|
|
1502
|
-
);
|
|
1503
|
-
process.stdout.write(
|
|
1504
|
-
chalk7.dim(
|
|
1505
|
-
` ${result.run.opportunitiesFound} opportunities \xB7 PnL ${result.run.pnlSynced ? "synced" : "not synced"} \xB7 approvals required
|
|
1506
|
-
|
|
1507
|
-
`
|
|
1508
|
-
)
|
|
1509
|
-
);
|
|
1510
|
-
displayAllocatorCycleResult(result.run.result, globalOpts);
|
|
1511
|
-
if (result.evaluation) {
|
|
1512
|
-
process.stdout.write("\n");
|
|
1513
|
-
process.stdout.write(
|
|
1514
|
-
chalk7.dim(
|
|
1515
|
-
` Evaluation: ${signedCurrency2(result.evaluation.summary.netPnl)} net PnL across ${result.evaluation.summary.markets} markets
|
|
1516
|
-
`
|
|
1517
|
-
)
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
1483
|
|
|
1522
1484
|
// src/commands/lp.ts
|
|
1523
|
-
function
|
|
1485
|
+
function requireAuth5(client) {
|
|
1524
1486
|
if (!client.isAuthenticated) {
|
|
1525
1487
|
error("Not authenticated. Run `hl auth login` first.");
|
|
1526
1488
|
process.exit(1);
|
|
1527
1489
|
}
|
|
1528
1490
|
}
|
|
1529
|
-
function
|
|
1491
|
+
function parseNonNegative(value) {
|
|
1530
1492
|
const n = Number(value);
|
|
1531
1493
|
if (!Number.isFinite(n) || n < 0) {
|
|
1532
1494
|
throw new InvalidArgumentError2("Expected a non-negative number");
|
|
1533
1495
|
}
|
|
1534
1496
|
return n;
|
|
1535
1497
|
}
|
|
1536
|
-
function
|
|
1498
|
+
function parsePositiveInt(value) {
|
|
1537
1499
|
const n = Number(value);
|
|
1538
1500
|
if (!Number.isInteger(n) || n < 1) {
|
|
1539
1501
|
throw new InvalidArgumentError2("Expected a positive integer");
|
|
1540
1502
|
}
|
|
1541
1503
|
return n;
|
|
1542
1504
|
}
|
|
1505
|
+
function strategyFromAllocatorOptions(opts) {
|
|
1506
|
+
return {
|
|
1507
|
+
id: "cli-lp-allocator",
|
|
1508
|
+
name: "CLI LP allocator dry run",
|
|
1509
|
+
status: opts.paused ? "paused" : "dry_run",
|
|
1510
|
+
...opts.totalHoldings !== void 0 && { total_holdings: opts.totalHoldings },
|
|
1511
|
+
...opts.capitalLimitPct !== void 0 && { capital_limit_pct: opts.capitalLimitPct },
|
|
1512
|
+
...opts.perMarketLimitPct !== void 0 && { per_market_limit_pct: opts.perMarketLimitPct },
|
|
1513
|
+
capital_limit: opts.capitalLimit,
|
|
1514
|
+
per_market_limit: opts.perMarketLimit,
|
|
1515
|
+
min_expected_return_daily_pct: opts.minExpectedReturnDailyPct,
|
|
1516
|
+
max_inventory_imbalance: opts.maxInventoryImbalance,
|
|
1517
|
+
volatility_fill_spike_threshold: opts.volatilityFillSpikeThreshold,
|
|
1518
|
+
event_no_quote_minutes_before: opts.eventNoQuoteMinutesBefore,
|
|
1519
|
+
event_no_quote_minutes_after: opts.eventNoQuoteMinutesAfter,
|
|
1520
|
+
min_liquidity: opts.allocatorMinLiquidity,
|
|
1521
|
+
max_spread: opts.maxSpread,
|
|
1522
|
+
min_days_to_end: opts.allocatorMinDaysToEnd,
|
|
1523
|
+
max_markets: opts.maxMarkets
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function validateAllocatorPercentageSizing(opts) {
|
|
1527
|
+
const usesPercentageSizing = opts.capitalLimitPct !== void 0 || opts.perMarketLimitPct !== void 0;
|
|
1528
|
+
if (usesPercentageSizing && opts.totalHoldings === void 0) {
|
|
1529
|
+
error(
|
|
1530
|
+
"Percentage sizing requires --total-holdings so the allocator can convert percentages into dollar caps."
|
|
1531
|
+
);
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1543
1535
|
function compact(payload) {
|
|
1544
1536
|
return Object.fromEntries(
|
|
1545
1537
|
Object.entries(payload).filter(([, value]) => value !== void 0 && value !== "")
|
|
@@ -1547,7 +1539,7 @@ function compact(payload) {
|
|
|
1547
1539
|
}
|
|
1548
1540
|
async function writeArtifact(path, data, jsonMode) {
|
|
1549
1541
|
if (!path) return;
|
|
1550
|
-
await
|
|
1542
|
+
await writeFile2(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
1551
1543
|
if (!jsonMode) {
|
|
1552
1544
|
process.stderr.write(dim(` Saved artifact to ${path}
|
|
1553
1545
|
`));
|
|
@@ -1587,19 +1579,49 @@ function buildLpEvaluatePayload(opts) {
|
|
|
1587
1579
|
limit: opts.limit
|
|
1588
1580
|
});
|
|
1589
1581
|
}
|
|
1590
|
-
function
|
|
1591
|
-
return
|
|
1592
|
-
strategyId: opts.strategyId,
|
|
1593
|
-
limit: opts.limit,
|
|
1594
|
-
syncPnl: opts.syncPnl
|
|
1595
|
-
});
|
|
1582
|
+
async function runAllocatorCycle(client, payload) {
|
|
1583
|
+
return client.post("/api/lp/allocator", payload);
|
|
1596
1584
|
}
|
|
1597
1585
|
function registerLpCommands(program2) {
|
|
1598
1586
|
const lp = program2.command("lp").description("Run persisted liquidity-provider scan, recommendation, and evaluation workflows");
|
|
1599
|
-
lp.command("
|
|
1587
|
+
lp.command("allocator").description("Run the allocator agent on an explicit market list").requiredOption("--markets <file>", "Candidate market JSON array or { markets }; use '-' to read stdin").option("--allocations <file>", "Existing allocations JSON array or { allocations }; use '-' to read stdin").option("--pnl <file>", "Per-market PnL context JSON array or { pnl_context } (hl-trader pnl --json output); use '-' to read stdin").option("--total-holdings <usd>", "Total holdings / portfolio value used for percentage sizing", parsePositiveNumber).option("--capital-limit-pct <pct>", "Portfolio-level allocation cap as a percent of total holdings", parsePositiveNumber).option("--per-market-limit-pct <pct>", "Per-market target cap as a percent of total holdings", parsePositiveNumber).option("--capital-limit <usd>", "Portfolio capital limit for this allocator request", parseNonNegative, 500).option("--per-market-limit <usd>", "Per-market target cap", parseNonNegative, 100).option("--min-expected-return-daily-pct <pct>", "Minimum expected daily return percent", parseNonNegative, 0.02).option("--max-inventory-imbalance <ratio>", "Maximum inventory imbalance", parseNonNegative, 0.25).option("--volatility-fill-spike-threshold <ratio>", "Fill-rate imbalance that switches quotes to defensive mode", parseNonNegative, 0.35).option("--event-no-quote-minutes-before <n>", "No-quote window before scheduled events", parseNonNegative, 60).option("--event-no-quote-minutes-after <n>", "No-quote window after scheduled events", parseNonNegative, 30).option("--allocator-min-liquidity <usd>", "Allocator safety gate: minimum market liquidity", parseNonNegative, 500).option("--max-spread <ratio>", "Allocator safety gate: maximum spread", parseNonNegative, 0.12).option("--allocator-min-days-to-end <n>", "Allocator safety gate: minimum days to resolution", parseNonNegative, 3).option("--max-markets <n>", "Maximum markets allocator may target", parsePositiveInt, 5).option("--paused", "Send strategy status paused instead of dry_run").action(async (opts) => {
|
|
1600
1588
|
const globalOpts = program2.opts();
|
|
1601
1589
|
const client = new ApiClient(globalOpts);
|
|
1602
|
-
|
|
1590
|
+
requireAuth5(client);
|
|
1591
|
+
validateAllocatorPercentageSizing(opts);
|
|
1592
|
+
const markets = await readMarketPayload(opts.markets);
|
|
1593
|
+
if (!markets || markets.length === 0) {
|
|
1594
|
+
error("Markets JSON must include at least one market.");
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
if (opts.allocations === "-" && opts.pnl === "-") {
|
|
1598
|
+
error("Only one of --allocations and --pnl can read from stdin.");
|
|
1599
|
+
process.exit(1);
|
|
1600
|
+
}
|
|
1601
|
+
const pnlContext = await readPnlContext(opts.pnl);
|
|
1602
|
+
const payload = {
|
|
1603
|
+
strategy: strategyFromAllocatorOptions(opts),
|
|
1604
|
+
markets,
|
|
1605
|
+
allocations: await readAllocations(opts.allocations),
|
|
1606
|
+
...pnlContext.length > 0 && { pnl_context: pnlContext }
|
|
1607
|
+
};
|
|
1608
|
+
if (!globalOpts.json) {
|
|
1609
|
+
process.stderr.write(
|
|
1610
|
+
dim(` Running allocator dry-run on ${markets.length} provided candidates...
|
|
1611
|
+
`)
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
const response = await runAllocatorCycle(client, payload);
|
|
1615
|
+
if (globalOpts.json) {
|
|
1616
|
+
json(response);
|
|
1617
|
+
} else {
|
|
1618
|
+
displayAllocatorCycleResult(response.result ?? {}, globalOpts);
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
lp.command("scan").description("Scan LP candidates and persist them as evidence").argument("[topic]", "Human label for this scan, e.g. liquidity opportunities").option("--profile <name>", "lp-opportunity | liquidity-provider | liquid-new-or-long", "liquidity-provider").option("--sort-by <key>", "score | volume | liquidity | movement | spread | rewards | rewardYield | lpExpectedReturn | horizon").option("--tag <slug>", "Polymarket category tag, e.g. crypto, politics").option("--min-volume <usd>", "Minimum 24h volume (USD)", parseNonNegative).option("--min-liquidity <usd>", "Minimum displayed liquidity (USD)", parseNonNegative).option("--max-liquidity <usd>", "Maximum displayed liquidity (USD)", parseNonNegative).option("--min-rewards-daily-rate <usd>", "Minimum LP rewards USD/day", parseNonNegative).option("--min-days-to-end <n>", "Minimum days until resolution", parseNonNegative).option("--max-days-to-end <n>", "Maximum days until resolution", parseNonNegative).option("--max-market-age-hours <n>", "With liquid-new-or-long: max age for the new branch", parseNonNegative).option("--liquid-profile <mode>", "new-or-long").option("--limit <n>", "Max markets to scan/save (1-100, default 15)", parsePositiveInt, 15).option("--strategy-id <uuid>", "LP strategy id").option("--output <file>", "Write the evidence JSON response to a local file").action(async (topic, opts) => {
|
|
1622
|
+
const globalOpts = program2.opts();
|
|
1623
|
+
const client = new ApiClient(globalOpts);
|
|
1624
|
+
requireAuth5(client);
|
|
1603
1625
|
const result = await client.post(
|
|
1604
1626
|
"/api/lp/scan",
|
|
1605
1627
|
buildLpScanPayload(topic, opts)
|
|
@@ -1607,10 +1629,10 @@ function registerLpCommands(program2) {
|
|
|
1607
1629
|
await writeArtifact(opts.output, result, Boolean(globalOpts.json));
|
|
1608
1630
|
displayLpScanResult(result, globalOpts);
|
|
1609
1631
|
});
|
|
1610
|
-
lp.command("recommend").description("Run allocator recommendations from saved LP evidence and current allocations").option("--strategy-id <uuid>", "LP strategy id").option("--scan-id <uuid>", "Use candidates from a specific saved scan").option("--limit <n>", "Max saved candidates to submit (1-25, default 15)",
|
|
1632
|
+
lp.command("recommend").description("Run allocator recommendations from saved LP evidence and current allocations").option("--strategy-id <uuid>", "LP strategy id").option("--scan-id <uuid>", "Use candidates from a specific saved scan").option("--limit <n>", "Max saved candidates to submit (1-25, default 15)", parsePositiveInt, 15).option("--sync-pnl", "Refresh wallet PnL before recommending").option("--output <file>", "Write the recommendation JSON response to a local file").action(async (opts) => {
|
|
1611
1633
|
const globalOpts = program2.opts();
|
|
1612
1634
|
const client = new ApiClient(globalOpts);
|
|
1613
|
-
|
|
1635
|
+
requireAuth5(client);
|
|
1614
1636
|
const result = await client.post(
|
|
1615
1637
|
"/api/lp/recommend",
|
|
1616
1638
|
buildLpRecommendPayload(opts)
|
|
@@ -1618,10 +1640,10 @@ function registerLpCommands(program2) {
|
|
|
1618
1640
|
await writeArtifact(opts.output, result, Boolean(globalOpts.json));
|
|
1619
1641
|
displayLpRecommendResult(result, globalOpts);
|
|
1620
1642
|
});
|
|
1621
|
-
lp.command("evaluate").description("Evaluate LP performance and return compact lessons").option("--strategy-id <uuid>", "LP strategy id").option("--wallet-address <address>", "Wallet address to sync PnL from").option("--no-sync-pnl", "Use existing PnL snapshots without refreshing").option("--limit <n>", "Max PnL snapshots to evaluate (1-100, default 50)",
|
|
1643
|
+
lp.command("evaluate").description("Evaluate LP performance and return compact lessons").option("--strategy-id <uuid>", "LP strategy id").option("--wallet-address <address>", "Wallet address to sync PnL from").option("--no-sync-pnl", "Use existing PnL snapshots without refreshing").option("--limit <n>", "Max PnL snapshots to evaluate (1-100, default 50)", parsePositiveInt, 50).option("--output <file>", "Write the evaluation JSON response to a local file").action(async (opts) => {
|
|
1622
1644
|
const globalOpts = program2.opts();
|
|
1623
1645
|
const client = new ApiClient(globalOpts);
|
|
1624
|
-
|
|
1646
|
+
requireAuth5(client);
|
|
1625
1647
|
const result = await client.post(
|
|
1626
1648
|
"/api/lp/evaluate",
|
|
1627
1649
|
buildLpEvaluatePayload(opts)
|
|
@@ -1629,31 +1651,6 @@ function registerLpCommands(program2) {
|
|
|
1629
1651
|
await writeArtifact(opts.output, result, Boolean(globalOpts.json));
|
|
1630
1652
|
displayLpEvaluateResult(result, globalOpts);
|
|
1631
1653
|
});
|
|
1632
|
-
lp.command("run").description("Run scan, recommendation, and evaluation as one dry-run chain").option("--strategy-id <uuid>", "LP strategy id").option("--limit <n>", "Max candidates to scan/recommend (1-25, default 15)", parsePositiveInt2, 15).option("--no-sync-pnl", "Use existing PnL snapshots without refreshing").option("--output <file>", "Write the chained run JSON response to a local file").action(async (opts) => {
|
|
1633
|
-
const globalOpts = program2.opts();
|
|
1634
|
-
const client = new ApiClient(globalOpts);
|
|
1635
|
-
requireAuth6(client);
|
|
1636
|
-
const run = await client.post(
|
|
1637
|
-
"/api/lp/run",
|
|
1638
|
-
buildLpRunPayload(opts)
|
|
1639
|
-
);
|
|
1640
|
-
let evaluation = null;
|
|
1641
|
-
try {
|
|
1642
|
-
evaluation = await client.post("/api/lp/evaluate", {
|
|
1643
|
-
strategyId: run.strategyId,
|
|
1644
|
-
syncPnl: false
|
|
1645
|
-
});
|
|
1646
|
-
} catch (error2) {
|
|
1647
|
-
if (!globalOpts.json) {
|
|
1648
|
-
warn(
|
|
1649
|
-
`Could not load evaluation summary: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
const combined = { run, evaluation };
|
|
1654
|
-
await writeArtifact(opts.output, combined, Boolean(globalOpts.json));
|
|
1655
|
-
displayLpRunResult(combined, globalOpts);
|
|
1656
|
-
});
|
|
1657
1654
|
}
|
|
1658
1655
|
|
|
1659
1656
|
// src/commands/wallet.ts
|
|
@@ -1664,7 +1661,7 @@ var POLYGON_NATIVE_USDC = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
|
|
|
1664
1661
|
var SUPPORTED_ASSETS = ["pUsd", "usdcE", "matic"];
|
|
1665
1662
|
var POLYGON_NATIVE_ASSET_LABEL = "POL";
|
|
1666
1663
|
var TERMINAL_WITHDRAWAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired"]);
|
|
1667
|
-
function
|
|
1664
|
+
function requireAuth6(client) {
|
|
1668
1665
|
if (!client.isAuthenticated) {
|
|
1669
1666
|
error("Not authenticated. Run `hl auth login` first.");
|
|
1670
1667
|
process.exit(1);
|
|
@@ -1849,7 +1846,7 @@ function registerWalletCommands(program2) {
|
|
|
1849
1846
|
wallet.command("status").description("Show linked owner and Polymarket deposit wallet status").action(async () => {
|
|
1850
1847
|
const globalOpts = program2.opts();
|
|
1851
1848
|
const client = new ApiClient(globalOpts);
|
|
1852
|
-
|
|
1849
|
+
requireAuth6(client);
|
|
1853
1850
|
const status = await client.get("/api/wallet/status");
|
|
1854
1851
|
if (globalOpts.json) {
|
|
1855
1852
|
json(status);
|
|
@@ -1860,7 +1857,7 @@ function registerWalletCommands(program2) {
|
|
|
1860
1857
|
wallet.command("balances").alias("funds").description("Show available wallet funds").action(async () => {
|
|
1861
1858
|
const globalOpts = program2.opts();
|
|
1862
1859
|
const client = new ApiClient(globalOpts);
|
|
1863
|
-
|
|
1860
|
+
requireAuth6(client);
|
|
1864
1861
|
const balances = await client.get("/api/wallet/balances");
|
|
1865
1862
|
if (globalOpts.json) {
|
|
1866
1863
|
json(balances);
|
|
@@ -1871,7 +1868,7 @@ function registerWalletCommands(program2) {
|
|
|
1871
1868
|
wallet.command("deposit").description("Show deposit address and supported Polygon assets").option("--asset <asset>", "pUSD | USDC.e | POL").option("--bridge", "Also request Polymarket Bridge deposit addresses").action(async (opts) => {
|
|
1872
1869
|
const globalOpts = program2.opts();
|
|
1873
1870
|
const client = new ApiClient(globalOpts);
|
|
1874
|
-
|
|
1871
|
+
requireAuth6(client);
|
|
1875
1872
|
const asset = normalizeWalletAsset(opts.asset);
|
|
1876
1873
|
const result = await client.post("/api/wallet/deposit", {
|
|
1877
1874
|
bridge: Boolean(opts.bridge)
|
|
@@ -1894,7 +1891,7 @@ function registerWalletCommands(program2) {
|
|
|
1894
1891
|
wallet.command("withdraw").description("Create a browser-signed withdrawal intent and poll for completion").requiredOption("--to <address>", "Recipient EVM wallet address", parseWalletAddress).requiredOption("--amount <amount>", "Amount to withdraw", parsePositiveNumber2).option("--asset <asset>", "Source asset; currently only pUSD is supported", "pUSD").option("--to-chain-id <id>", "Destination chain id", "137").option("--to-token-address <address>", "Destination token address", parseWalletAddress, POLYGON_NATIVE_USDC).option("--no-open", "Do not open the browser signing URL").option("--no-wait", "Do not poll for completion after creating the intent").option("--poll-interval <seconds>", "Polling interval in seconds", parsePositiveNumber2, 5).option("--timeout <seconds>", "Maximum seconds to wait for completion", parsePositiveNumber2, 600).action(async (opts) => {
|
|
1895
1892
|
const globalOpts = program2.opts();
|
|
1896
1893
|
const client = new ApiClient(globalOpts);
|
|
1897
|
-
|
|
1894
|
+
requireAuth6(client);
|
|
1898
1895
|
const created = await client.post(
|
|
1899
1896
|
"/api/wallet/withdraw/intents",
|
|
1900
1897
|
buildWithdrawRequestPayload(opts)
|
|
@@ -1924,7 +1921,7 @@ import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
|
1924
1921
|
import { readFile as readFile2 } from "fs/promises";
|
|
1925
1922
|
|
|
1926
1923
|
// src/signal-display.ts
|
|
1927
|
-
import
|
|
1924
|
+
import chalk7 from "chalk";
|
|
1928
1925
|
function pctFromSignalValue(value) {
|
|
1929
1926
|
if (value === null || value === void 0 || Number.isNaN(value)) return null;
|
|
1930
1927
|
return Math.abs(value) <= 1 ? value * 100 : value;
|
|
@@ -1937,14 +1934,14 @@ function formatGap(value) {
|
|
|
1937
1934
|
if (value === null || value === void 0 || Number.isNaN(value)) return "n/a";
|
|
1938
1935
|
const points = Math.abs(value) <= 1 ? value * 100 : value;
|
|
1939
1936
|
const formatted = `${points >= 0 ? "+" : ""}${points.toFixed(1)}pp`;
|
|
1940
|
-
if (points > 0) return
|
|
1941
|
-
if (points < 0) return
|
|
1942
|
-
return
|
|
1937
|
+
if (points > 0) return chalk7.green(formatted);
|
|
1938
|
+
if (points < 0) return chalk7.red(formatted);
|
|
1939
|
+
return chalk7.dim(formatted);
|
|
1943
1940
|
}
|
|
1944
1941
|
function formatStrength(value) {
|
|
1945
|
-
if (value === "strong") return
|
|
1946
|
-
if (value === "weak") return
|
|
1947
|
-
return value ?
|
|
1942
|
+
if (value === "strong") return chalk7.green("strong");
|
|
1943
|
+
if (value === "weak") return chalk7.yellow("weak");
|
|
1944
|
+
return value ? chalk7.dim(value) : "n/a";
|
|
1948
1945
|
}
|
|
1949
1946
|
function analysisItems(result) {
|
|
1950
1947
|
if (!result) return [];
|
|
@@ -1991,27 +1988,27 @@ function displaySignalAnalysis(response, globalOpts) {
|
|
|
1991
1988
|
for (const item of items.slice(0, 3)) {
|
|
1992
1989
|
const analysis = item.analysis;
|
|
1993
1990
|
if (!analysis) continue;
|
|
1994
|
-
process.stdout.write("\n " +
|
|
1991
|
+
process.stdout.write("\n " + chalk7.bold(truncate(titleFor(analysis), 76)) + "\n");
|
|
1995
1992
|
if (analysis.market_link) {
|
|
1996
|
-
process.stdout.write(" " +
|
|
1993
|
+
process.stdout.write(" " + chalk7.dim("Polymarket: ") + analysis.market_link + "\n");
|
|
1997
1994
|
}
|
|
1998
1995
|
if (analysis.key_factors && analysis.key_factors.length > 0) {
|
|
1999
|
-
process.stdout.write(" " +
|
|
1996
|
+
process.stdout.write(" " + chalk7.dim("Key factors: ") + analysis.key_factors.slice(0, 4).join("; ") + "\n");
|
|
2000
1997
|
}
|
|
2001
1998
|
if (analysis.research_findings) {
|
|
2002
1999
|
process.stdout.write(
|
|
2003
|
-
" " +
|
|
2000
|
+
" " + chalk7.dim("Research: ") + truncate(analysis.research_findings, 180) + "\n"
|
|
2004
2001
|
);
|
|
2005
2002
|
}
|
|
2006
2003
|
}
|
|
2007
2004
|
if (items.length > 3) {
|
|
2008
|
-
process.stdout.write(
|
|
2005
|
+
process.stdout.write(chalk7.dim(`
|
|
2009
2006
|
... and ${items.length - 3} more
|
|
2010
2007
|
`));
|
|
2011
2008
|
}
|
|
2012
2009
|
if (result?.strong_signal_count !== void 0) {
|
|
2013
2010
|
process.stdout.write(
|
|
2014
|
-
|
|
2011
|
+
chalk7.dim(`
|
|
2015
2012
|
Strong signals: ${result.strong_signal_count}
|
|
2016
2013
|
`)
|
|
2017
2014
|
);
|
|
@@ -2019,7 +2016,7 @@ function displaySignalAnalysis(response, globalOpts) {
|
|
|
2019
2016
|
}
|
|
2020
2017
|
|
|
2021
2018
|
// src/commands/signal.ts
|
|
2022
|
-
function
|
|
2019
|
+
function requireAuth7(client) {
|
|
2023
2020
|
if (!client.isAuthenticated) {
|
|
2024
2021
|
error("Not authenticated. Run `hl auth login` first.");
|
|
2025
2022
|
process.exit(1);
|
|
@@ -2042,7 +2039,7 @@ async function readStdin2() {
|
|
|
2042
2039
|
}
|
|
2043
2040
|
return Buffer.concat(chunks).toString("utf8");
|
|
2044
2041
|
}
|
|
2045
|
-
async function
|
|
2042
|
+
async function readMarketPayload2(path) {
|
|
2046
2043
|
if (!path) return {};
|
|
2047
2044
|
const raw = path === "-" ? await readStdin2() : await readFile2(path, "utf8");
|
|
2048
2045
|
const parsed = JSON.parse(raw);
|
|
@@ -2074,7 +2071,7 @@ async function buildSignalPayload(positionalUrl, opts) {
|
|
|
2074
2071
|
const urls = [positionalUrl, ...opts.url ?? []].filter(
|
|
2075
2072
|
(value) => Boolean(value)
|
|
2076
2073
|
);
|
|
2077
|
-
const filePayload = await
|
|
2074
|
+
const filePayload = await readMarketPayload2(opts.market);
|
|
2078
2075
|
const inlineMarket = inlineMarketFromOptions(opts);
|
|
2079
2076
|
const hasMarketInput = Boolean(filePayload.market || filePayload.markets || inlineMarket);
|
|
2080
2077
|
if (urls.length > 0 && hasMarketInput) {
|
|
@@ -2103,7 +2100,7 @@ function registerSignalCommands(program2) {
|
|
|
2103
2100
|
signal.command("analyze").description("Estimate true YES probability and compare it with market pricing").argument("[url]", "Polymarket market/event URL to analyze").option("--url <url>", "Additional Polymarket URL; repeat for multiple markets", collect, []).option("--market <file>", "Inline market JSON object/array; use '-' to read stdin").option("--context <text>", "Prior search notes or analysis context for the agent").option("--question <text>", "Inline market question when not using a URL").option("--description <text>", "Inline market description or resolution criteria").option("--yes-prob <prob>", "Current YES probability or price, e.g. 0.52 or 52", parseProbability).option("--no-prob <prob>", "Current NO probability or price, e.g. 0.48 or 48", parseProbability).option("--slug <slug>", "Inline market slug").option("--link <url>", "Inline market link").action(async (url, o) => {
|
|
2104
2101
|
const globalOpts = program2.opts();
|
|
2105
2102
|
const client = new ApiClient(globalOpts);
|
|
2106
|
-
|
|
2103
|
+
requireAuth7(client);
|
|
2107
2104
|
let payload;
|
|
2108
2105
|
try {
|
|
2109
2106
|
payload = await buildSignalPayload(url, o);
|
|
@@ -2120,8 +2117,8 @@ function registerSignalCommands(program2) {
|
|
|
2120
2117
|
}
|
|
2121
2118
|
|
|
2122
2119
|
// src/index.ts
|
|
2123
|
-
var program = new
|
|
2124
|
-
program.name("hl").description("Hedge Layer CLI \u2014 prediction market intelligence from the terminal").version("1.
|
|
2120
|
+
var program = new Command4();
|
|
2121
|
+
program.name("hl").description("Hedge Layer CLI \u2014 prediction market intelligence from the terminal").version("2.1.0").option("--json", "Output as JSON (machine-readable)").option("--api-url <url>", "Override API base URL").option("--token <token>", "Override stored API token").option("--verbose", "Show HTTP request details").option("--no-color", "Disable colored output");
|
|
2125
2122
|
registerAuthCommands(program);
|
|
2126
2123
|
registerBriefCommands(program);
|
|
2127
2124
|
registerProfileCommand(program);
|
|
@@ -2129,7 +2126,6 @@ registerResearchCommands(program);
|
|
|
2129
2126
|
registerFeedCommand(program);
|
|
2130
2127
|
registerWalletCommands(program);
|
|
2131
2128
|
registerLpCommands(program);
|
|
2132
|
-
registerAllocatorCommands(program);
|
|
2133
2129
|
registerSignalCommands(program);
|
|
2134
2130
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
2135
2131
|
const opts = program.opts();
|