hunch-cup 0.1.0 → 0.2.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 +36 -8
- package/dist/cli.js +107 -31
- package/dist/client.d.ts +65 -8
- package/dist/client.js +95 -13
- package/dist/fleet.d.ts +43 -0
- package/dist/fleet.js +129 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/strategy.d.ts +21 -5
- package/dist/strategy.js +39 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# hunch-cup
|
|
2
2
|
|
|
3
3
|
One-line CLI + typed SDK to play the **Hunch Cup** — a risk-free, **paper-trading**
|
|
4
|
-
prediction-market tournament. Claim **
|
|
4
|
+
prediction-market tournament. Claim **10,000 $pHUNCH** (paper money, zero real-funds
|
|
5
5
|
risk), trade the live Hunch catalog, run a 24/7 agent, and climb a public leaderboard
|
|
6
6
|
scored on realized PnL. The top 50 wallets split a real **$5,000**.
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ scored on realized PnL. The top 50 wallets split a real **$5,000**.
|
|
|
11
11
|
## CLI
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
# 1. Generate a wallet and claim
|
|
14
|
+
# 1. Generate a wallet and claim 10,000 $pHUNCH (prints a private key — save it!)
|
|
15
15
|
npx hunch-cup wallet new
|
|
16
16
|
|
|
17
17
|
export HUNCH_CUP_PRIVATE_KEY=0x... # the key it printed
|
|
@@ -30,13 +30,32 @@ npx hunch-cup trade <slug> yes 25
|
|
|
30
30
|
|
|
31
31
|
# 4. Or let an agent do it — momentum | contrarian | news-reactive
|
|
32
32
|
npx hunch-cup run momentum 25 # dry-run (simulated)
|
|
33
|
-
npx hunch-cup run contrarian 25 --live # actually place trades
|
|
33
|
+
npx hunch-cup run contrarian 25 --live # one round, actually place trades
|
|
34
34
|
|
|
35
|
+
# 24×7, built in — no shell loop needed. It reads your balance first, skips
|
|
36
|
+
# markets you already hold, caps each round to a fraction of your bankroll, and
|
|
37
|
+
# stops before it goes broke (the old `while true` loop went insolvent in ~20m).
|
|
38
|
+
npx hunch-cup run momentum 25 --live --loop --interval 300
|
|
39
|
+
|
|
40
|
+
npx hunch-cup positions # balance + open/realized positions (watch for insolvency)
|
|
41
|
+
npx hunch-cup balance # just the $pHUNCH number
|
|
35
42
|
npx hunch-cup board
|
|
36
43
|
```
|
|
37
44
|
|
|
45
|
+
### Run a fleet (one person, many agents)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx hunch-cup fleet new 10 # mint 10 wallets, claim each, save to ~/.hunch-cup/fleet.json
|
|
49
|
+
npx hunch-cup fleet run momentum # run all 10 concurrently, 24×7 (each with a strategy)
|
|
50
|
+
npx hunch-cup fleet board # every wallet's live rank
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`fleet new` assigns a rotating strategy (momentum / contrarian / news-reactive) per
|
|
54
|
+
wallet so a demo fleet is visibly diverse. Keys live only in the local file.
|
|
55
|
+
|
|
38
56
|
Env: `HUNCH_CUP_PRIVATE_KEY`, `HUNCH_CUP_BASE_URL` (default `https://www.playhunch.xyz`),
|
|
39
|
-
`HUNCH_CUP_SIZE` (default 25), `HUNCH_CUP_SENTIMENT` (for `news-reactive`)
|
|
57
|
+
`HUNCH_CUP_SIZE` (default 25), `HUNCH_CUP_SENTIMENT` (for `news-reactive`),
|
|
58
|
+
`HUNCH_CUP_FLEET_FILE` (default `~/.hunch-cup/fleet.json`).
|
|
40
59
|
|
|
41
60
|
## SDK
|
|
42
61
|
|
|
@@ -45,15 +64,24 @@ import { CupClient, fromPrivateKey } from "hunch-cup";
|
|
|
45
64
|
|
|
46
65
|
const client = new CupClient({ signer: fromPrivateKey(process.env.HUNCH_CUP_PRIVATE_KEY!) });
|
|
47
66
|
|
|
48
|
-
await client.createWallet(); // claim
|
|
49
|
-
const markets = await client.listMarkets();
|
|
67
|
+
await client.createWallet(); // claim 10,000 $pHUNCH (once)
|
|
68
|
+
const markets = await client.listMarkets(25, 0); // paginated: limit, offset
|
|
50
69
|
await client.simulateTrade(markets[0].slug, "yes", 25); // dry run, no signature
|
|
51
70
|
await client.trade({ slug: markets[0].slug, side: "yes", sizePhunch: 25 }); // signed
|
|
52
|
-
await client.
|
|
71
|
+
await client.getWallet(); // balance + positions (typed)
|
|
53
72
|
await client.getLeaderboard({ wallet: client.address });
|
|
54
73
|
|
|
55
|
-
// Built-in strategy templates (pure deciders, you control execution)
|
|
74
|
+
// Built-in strategy templates (pure deciders, you control execution). N-way
|
|
75
|
+
// markets (ladders, THE FLIP) pick a real outcome key, never phantom yes/no.
|
|
56
76
|
await client.runStrategy({ strategy: "momentum", sizePhunch: 25, live: true });
|
|
77
|
+
|
|
78
|
+
// Bankroll-managed 24×7 loop (survives insolvency + transient errors):
|
|
79
|
+
await client.runLoop({ strategy: "momentum", sizePhunch: 25, intervalMs: 300_000, live: true });
|
|
80
|
+
|
|
81
|
+
// A whole fleet from one process:
|
|
82
|
+
import { fleetNew, fleetRun } from "hunch-cup";
|
|
83
|
+
await fleetNew(10);
|
|
84
|
+
await fleetRun({ defaultStrategy: "momentum", sizePhunch: 25, intervalMs: 300_000 });
|
|
57
85
|
```
|
|
58
86
|
|
|
59
87
|
## How auth works
|
package/dist/cli.js
CHANGED
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `npx hunch-cup` — one-line Hunch Cup paper-trading agent (S8, scope §8.2.3).
|
|
4
4
|
*
|
|
5
|
-
* hunch-cup wallet new
|
|
6
|
-
* hunch-cup markets
|
|
7
|
-
* hunch-cup quote <slug> <side> <n>
|
|
8
|
-
* hunch-cup trade <slug> <side> <n>
|
|
9
|
-
* hunch-cup run <strategy> [n]
|
|
10
|
-
* hunch-cup
|
|
5
|
+
* hunch-cup wallet new generate a FRESH wallet + claim 10,000 $pHUNCH
|
|
6
|
+
* hunch-cup markets [n] [offset] list tradable paper markets
|
|
7
|
+
* hunch-cup quote <slug> <side> <n> price a paper trade
|
|
8
|
+
* hunch-cup trade <slug> <side> <n> place a REAL signed paper trade
|
|
9
|
+
* hunch-cup run <strategy> [n] [--live] [--loop] [--interval <sec>]
|
|
10
|
+
* hunch-cup positions your balance + open/realized positions
|
|
11
|
+
* hunch-cup balance just your $pHUNCH balance
|
|
12
|
+
* hunch-cup board [n] show the leaderboard
|
|
13
|
+
* hunch-cup fleet new <n> mint N wallets + claim each (→ ~/.hunch-cup/fleet.json)
|
|
14
|
+
* hunch-cup fleet run <strategy> [--interval <sec>] run all fleet wallets 24×7
|
|
15
|
+
* hunch-cup fleet board every fleet wallet's rank
|
|
11
16
|
*
|
|
12
17
|
* Config via env: HUNCH_CUP_PRIVATE_KEY (0x…), HUNCH_CUP_BASE_URL,
|
|
13
18
|
* HUNCH_CUP_SIZE (default 25). `run` simulates by default; pass --live to trade.
|
|
14
19
|
*/
|
|
15
20
|
import { CupClient } from "./client.js";
|
|
16
21
|
import { fromPrivateKey, newWallet } from "./signer.js";
|
|
22
|
+
import { fleetNew, fleetRun, fleetBoard } from "./fleet.js";
|
|
23
|
+
const STRATEGIES = ["momentum", "contrarian", "news-reactive"];
|
|
17
24
|
function baseUrl() {
|
|
18
25
|
return process.env.HUNCH_CUP_BASE_URL;
|
|
19
26
|
}
|
|
@@ -25,6 +32,22 @@ function size(arg) {
|
|
|
25
32
|
const n = Number(arg ?? process.env.HUNCH_CUP_SIZE ?? 25);
|
|
26
33
|
return Number.isFinite(n) && n > 0 ? n : 25;
|
|
27
34
|
}
|
|
35
|
+
/** Read `--flag value` (or `--flag=value`) from an args array. */
|
|
36
|
+
function flagValue(args, flag) {
|
|
37
|
+
const eq = args.find((a) => a.startsWith(`${flag}=`));
|
|
38
|
+
if (eq)
|
|
39
|
+
return eq.slice(flag.length + 1);
|
|
40
|
+
const i = args.indexOf(flag);
|
|
41
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
42
|
+
}
|
|
43
|
+
function intervalMs(args) {
|
|
44
|
+
const sec = Number(flagValue(args, "--interval") ?? 300);
|
|
45
|
+
return (Number.isFinite(sec) && sec > 0 ? sec : 300) * 1000;
|
|
46
|
+
}
|
|
47
|
+
/** The first positional (non-flag) arg after index 0. */
|
|
48
|
+
function firstPositional(args) {
|
|
49
|
+
return args.find((a, i) => i > 0 && !a.startsWith("--"));
|
|
50
|
+
}
|
|
28
51
|
function out(value) {
|
|
29
52
|
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
30
53
|
}
|
|
@@ -34,34 +57,36 @@ async function main(argv) {
|
|
|
34
57
|
out([
|
|
35
58
|
"hunch-cup — paper-trade the Hunch Cup from your terminal.",
|
|
36
59
|
"",
|
|
37
|
-
" wallet new
|
|
38
|
-
" markets
|
|
39
|
-
" quote <slug> <side> <n>
|
|
40
|
-
" trade <slug> <side> <n>
|
|
41
|
-
" run <strategy> [n] [--live]
|
|
42
|
-
"
|
|
60
|
+
" wallet new generate a FRESH wallet + claim 10,000 $pHUNCH",
|
|
61
|
+
" markets [n] [offset] list tradable paper markets",
|
|
62
|
+
" quote <slug> <side> <n> price a paper trade",
|
|
63
|
+
" trade <slug> <side> <n> place a REAL signed paper trade",
|
|
64
|
+
" run <strategy> [n] [--live] [--loop] [--interval <sec>]",
|
|
65
|
+
" positions your balance + open/realized positions",
|
|
66
|
+
" balance just your $pHUNCH balance",
|
|
67
|
+
" board [n] show the leaderboard",
|
|
68
|
+
" fleet new <n> mint N wallets + claim each",
|
|
69
|
+
" fleet run <strategy> [--interval <sec>] run all fleet wallets 24×7",
|
|
70
|
+
" fleet board every fleet wallet's rank",
|
|
43
71
|
"",
|
|
72
|
+
"Strategies: momentum | contrarian | news-reactive",
|
|
44
73
|
"Env: HUNCH_CUP_PRIVATE_KEY, HUNCH_CUP_BASE_URL, HUNCH_CUP_SIZE",
|
|
45
74
|
].join("\n"));
|
|
46
75
|
return 0;
|
|
47
76
|
}
|
|
48
77
|
if (cmd === "wallet" && args[0] === "new") {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
privateKey = w.privateKey;
|
|
55
|
-
out(`Generated a new wallet. SAVE THIS PRIVATE KEY:\n ${privateKey}`);
|
|
56
|
-
out(`export HUNCH_CUP_PRIVATE_KEY=${privateKey}`);
|
|
57
|
-
}
|
|
78
|
+
// Always mint a FRESH wallet so `wallet new` xN builds a roster (the old
|
|
79
|
+
// behavior silently re-claimed the env key). Use `fleet new` for many.
|
|
80
|
+
const { privateKey, signer } = newWallet();
|
|
81
|
+
out(`Generated a new wallet. SAVE THIS PRIVATE KEY:\n ${privateKey}`);
|
|
82
|
+
out(`export HUNCH_CUP_PRIVATE_KEY=${privateKey}`);
|
|
58
83
|
const client = new CupClient({ baseUrl: baseUrl(), signer });
|
|
59
84
|
out(await client.createWallet());
|
|
60
85
|
return 0;
|
|
61
86
|
}
|
|
62
87
|
if (cmd === "markets") {
|
|
63
88
|
const client = new CupClient({ baseUrl: baseUrl() });
|
|
64
|
-
out(await client.listMarkets(Number(args[0] ?? 25)));
|
|
89
|
+
out(await client.listMarkets(Number(args[0] ?? 25), Number(args[1] ?? 0)));
|
|
65
90
|
return 0;
|
|
66
91
|
}
|
|
67
92
|
if (cmd === "quote") {
|
|
@@ -83,23 +108,43 @@ async function main(argv) {
|
|
|
83
108
|
out(await client.trade({ slug, side, sizePhunch: size(n) }));
|
|
84
109
|
return 0;
|
|
85
110
|
}
|
|
111
|
+
if (cmd === "positions" || cmd === "balance") {
|
|
112
|
+
const signer = signerFromEnv();
|
|
113
|
+
const addr = args[0] ?? signer?.address;
|
|
114
|
+
if (!addr)
|
|
115
|
+
return usage(`${cmd} [address] (or set HUNCH_CUP_PRIVATE_KEY)`);
|
|
116
|
+
const client = new CupClient({ baseUrl: baseUrl() });
|
|
117
|
+
const wallet = await client.getWallet(addr);
|
|
118
|
+
out(cmd === "balance" ? { balancePhunch: wallet.balancePhunch } : wallet);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
86
121
|
if (cmd === "run") {
|
|
87
122
|
const strategy = args[0];
|
|
88
|
-
if (!strategy || !
|
|
89
|
-
return usage("run <momentum|contrarian|news-reactive> [size] [--live]");
|
|
123
|
+
if (!strategy || !STRATEGIES.includes(strategy)) {
|
|
124
|
+
return usage("run <momentum|contrarian|news-reactive> [size] [--live] [--loop] [--interval <sec>]");
|
|
90
125
|
}
|
|
91
126
|
const live = args.includes("--live");
|
|
92
|
-
const
|
|
127
|
+
const loop = args.includes("--loop");
|
|
128
|
+
const n = firstPositional(args);
|
|
93
129
|
const signer = signerFromEnv();
|
|
94
130
|
if (live && !signer)
|
|
95
131
|
return usage("set HUNCH_CUP_PRIVATE_KEY to run --live");
|
|
96
132
|
const client = new CupClient({ baseUrl: baseUrl(), signer });
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
const sentiment = Number(process.env.HUNCH_CUP_SENTIMENT ?? 0);
|
|
134
|
+
if (loop) {
|
|
135
|
+
out(`Looping ${strategy} every ${intervalMs(args) / 1000}s (Ctrl-C to stop)…`);
|
|
136
|
+
await client.runLoop({
|
|
137
|
+
strategy,
|
|
138
|
+
sizePhunch: size(n),
|
|
139
|
+
intervalMs: intervalMs(args),
|
|
140
|
+
sentiment,
|
|
141
|
+
live,
|
|
142
|
+
onRound: (round, receipts) => out(`round ${round}: ${receipts.length} trade(s)`),
|
|
143
|
+
onError: (round, err) => console.error(`round ${round} error: ${err instanceof Error ? err.message : err}`),
|
|
144
|
+
});
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
out(await client.runStrategy({ strategy, sizePhunch: size(n), sentiment, live }));
|
|
103
148
|
return 0;
|
|
104
149
|
}
|
|
105
150
|
if (cmd === "board") {
|
|
@@ -107,6 +152,37 @@ async function main(argv) {
|
|
|
107
152
|
out(await client.getLeaderboard({ limit: Number(args[0] ?? 50) }));
|
|
108
153
|
return 0;
|
|
109
154
|
}
|
|
155
|
+
if (cmd === "fleet") {
|
|
156
|
+
const sub = args[0];
|
|
157
|
+
if (sub === "new") {
|
|
158
|
+
const count = Number(args[1] ?? 0);
|
|
159
|
+
if (!Number.isFinite(count) || count < 1)
|
|
160
|
+
return usage("fleet new <count>");
|
|
161
|
+
const created = await fleetNew(count, baseUrl());
|
|
162
|
+
out(`Minted ${created.length} wallet(s) + claimed each. Saved to ~/.hunch-cup/fleet.json`);
|
|
163
|
+
out(created.map((w) => ({ name: w.name, address: w.address, strategy: w.strategy })));
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (sub === "run") {
|
|
167
|
+
const strategy = args[1] ?? "momentum";
|
|
168
|
+
if (!STRATEGIES.includes(strategy))
|
|
169
|
+
return usage("fleet run <strategy> [--interval <sec>]");
|
|
170
|
+
out(`Running the fleet on ${strategy} every ${intervalMs(args) / 1000}s (Ctrl-C to stop)…`);
|
|
171
|
+
await fleetRun({
|
|
172
|
+
defaultStrategy: strategy,
|
|
173
|
+
sizePhunch: size(process.env.HUNCH_CUP_SIZE),
|
|
174
|
+
intervalMs: intervalMs(args),
|
|
175
|
+
baseUrl: baseUrl(),
|
|
176
|
+
onEvent: (line) => out(line),
|
|
177
|
+
});
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
if (sub === "board") {
|
|
181
|
+
out(await fleetBoard(baseUrl()));
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
return usage("fleet <new|run|board>");
|
|
185
|
+
}
|
|
110
186
|
return usage(`unknown command: ${cmd}`);
|
|
111
187
|
}
|
|
112
188
|
function usage(message) {
|
package/dist/client.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export interface CupSigner {
|
|
|
5
5
|
address: string;
|
|
6
6
|
signMessage: (message: string) => Promise<string>;
|
|
7
7
|
}
|
|
8
|
+
/** A tradeable outcome key + its live paper pool-implied odds. */
|
|
9
|
+
export interface CupOutcome {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
shortLabel: string;
|
|
13
|
+
impliedPct: number;
|
|
14
|
+
}
|
|
8
15
|
export interface CupMarket {
|
|
9
16
|
slug: string;
|
|
10
17
|
marketId: string;
|
|
@@ -12,11 +19,31 @@ export interface CupMarket {
|
|
|
12
19
|
shortTitle: string;
|
|
13
20
|
tokenSymbol: string;
|
|
14
21
|
direction: boolean;
|
|
22
|
+
/** True only for a real binary YES/NO market (drives yesCents/noCents). */
|
|
23
|
+
binary: boolean;
|
|
15
24
|
deadlineAt: string;
|
|
16
|
-
|
|
17
|
-
|
|
25
|
+
/** Binary cents; `null` for N-way markets (use `outcomes` instead). */
|
|
26
|
+
yesCents: number | null;
|
|
27
|
+
noCents: number | null;
|
|
18
28
|
poolPhunch: number;
|
|
19
29
|
tradeCount: number;
|
|
30
|
+
/** Every tradeable side (keys + odds) — the source of truth for N-way `side`. */
|
|
31
|
+
outcomes: CupOutcome[];
|
|
32
|
+
}
|
|
33
|
+
export interface CupPosition {
|
|
34
|
+
marketId: string;
|
|
35
|
+
side: string;
|
|
36
|
+
costPhunch: number;
|
|
37
|
+
status: "open" | "resolved";
|
|
38
|
+
realizedPnlPhunch: number;
|
|
39
|
+
projectedPayoutPhunch: number;
|
|
40
|
+
}
|
|
41
|
+
export interface CupWalletView {
|
|
42
|
+
walletAddress: string;
|
|
43
|
+
balancePhunch: number;
|
|
44
|
+
realizedPnlPhunch: number;
|
|
45
|
+
openCount: number;
|
|
46
|
+
positions: CupPosition[];
|
|
20
47
|
}
|
|
21
48
|
export interface CupTradeReceipt {
|
|
22
49
|
simulated: boolean;
|
|
@@ -42,13 +69,13 @@ export declare class CupClient {
|
|
|
42
69
|
get address(): string | undefined;
|
|
43
70
|
private json;
|
|
44
71
|
private requireSigner;
|
|
45
|
-
/** Claim
|
|
72
|
+
/** Claim 10,000 $pHUNCH once for the signer's wallet. */
|
|
46
73
|
createWallet(): Promise<{
|
|
47
74
|
walletAddress: string;
|
|
48
75
|
balancePhunch: number;
|
|
49
76
|
minted: boolean;
|
|
50
77
|
}>;
|
|
51
|
-
listMarkets(limit?: number): Promise<CupMarket[]>;
|
|
78
|
+
listMarkets(limit?: number, offset?: number): Promise<CupMarket[]>;
|
|
52
79
|
getQuote(slug: string, side: string, sizePhunch: number): Promise<{
|
|
53
80
|
shares: number;
|
|
54
81
|
projectedPayoutPhunch: number;
|
|
@@ -63,16 +90,27 @@ export declare class CupClient {
|
|
|
63
90
|
sizePhunch: number;
|
|
64
91
|
tradeId?: string;
|
|
65
92
|
}): Promise<CupTradeReceipt>;
|
|
66
|
-
|
|
93
|
+
/** Typed wallet summary: balance + realized PnL + positions, all in $pHUNCH. */
|
|
94
|
+
getWallet(address?: string): Promise<CupWalletView>;
|
|
95
|
+
/** @deprecated use {@link getWallet}. Kept for back-compat. */
|
|
96
|
+
getPositions(address?: string): Promise<CupWalletView>;
|
|
67
97
|
getLeaderboard(opts?: {
|
|
68
98
|
limit?: number;
|
|
69
99
|
week?: number;
|
|
70
100
|
wallet?: string;
|
|
71
101
|
}): Promise<unknown>;
|
|
72
102
|
/**
|
|
73
|
-
* One round of a built-in strategy
|
|
74
|
-
*
|
|
75
|
-
*
|
|
103
|
+
* One round of a built-in strategy WITH bankroll survival — the un-managed
|
|
104
|
+
* loop in the hosting docs went insolvent in ~20 minutes (250 $pHUNCH/round from
|
|
105
|
+
* a finite no-top-up grant). This:
|
|
106
|
+
* • reads the live balance + open positions first (a `live` run);
|
|
107
|
+
* • skips markets the wallet is ALREADY positioned in (no re-piling);
|
|
108
|
+
* • caps the round to `min(size, maxRoundFraction × balance)` per market and
|
|
109
|
+
* stops when the balance can't cover another ticket;
|
|
110
|
+
* • uses a STABLE tradeId (`cup:<roundBucket>:<slug>:<side>`) so an accidental
|
|
111
|
+
* double-fire in the same 5-min bucket is idempotent, not a double-spend;
|
|
112
|
+
* • picks over the market's REAL outcome keys (N-way safe).
|
|
113
|
+
* The decision is pure ({@link pickFor}); placement is deterministic code.
|
|
76
114
|
*/
|
|
77
115
|
runStrategy(opts: {
|
|
78
116
|
strategy: CupStrategy;
|
|
@@ -80,5 +118,24 @@ export declare class CupClient {
|
|
|
80
118
|
limit?: number;
|
|
81
119
|
sentiment?: number;
|
|
82
120
|
live?: boolean;
|
|
121
|
+
/** Max fraction of the current balance to deploy per round (default 25%). */
|
|
122
|
+
maxRoundFraction?: number;
|
|
83
123
|
}): Promise<CupTradeReceipt[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Run a strategy on a fixed interval, forever (or `rounds` times), catching
|
|
126
|
+
* per-round errors so a transient failure never kills the agent — the built-in
|
|
127
|
+
* 24×7 loop the CLI/hosting docs previously punted to a `while true` shell hack.
|
|
128
|
+
*/
|
|
129
|
+
runLoop(opts: {
|
|
130
|
+
strategy: CupStrategy;
|
|
131
|
+
sizePhunch: number;
|
|
132
|
+
intervalMs: number;
|
|
133
|
+
limit?: number;
|
|
134
|
+
sentiment?: number;
|
|
135
|
+
live?: boolean;
|
|
136
|
+
maxRoundFraction?: number;
|
|
137
|
+
rounds?: number;
|
|
138
|
+
onRound?: (round: number, receipts: CupTradeReceipt[]) => void;
|
|
139
|
+
onError?: (round: number, err: unknown) => void;
|
|
140
|
+
}): Promise<void>;
|
|
84
141
|
}
|
package/dist/client.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Hunch Cup — thin typed SDK client over `/api/cup/v1/*` (S8).
|
|
3
3
|
*
|
|
4
4
|
* Wallet-native + keyless: pass a viem `LocalAccount` (or any `CupSigner`) and
|
|
5
|
-
* the client claims your
|
|
5
|
+
* the client claims your 10,000 $pHUNCH, lists/quotes paper markets, and — for a
|
|
6
6
|
* REAL trade — signs the canonical message for you (the only thing that proves
|
|
7
7
|
* wallet ownership on the paper path). Reads need no signer. Uses global `fetch`
|
|
8
8
|
* (Node ≥ 20). `viem` is an optional peer used only by {@link fromPrivateKey}.
|
|
@@ -36,7 +36,7 @@ export class CupClient {
|
|
|
36
36
|
throw new Error("A signer is required for this action (claim/trade).");
|
|
37
37
|
return this.signer;
|
|
38
38
|
}
|
|
39
|
-
/** Claim
|
|
39
|
+
/** Claim 10,000 $pHUNCH once for the signer's wallet. */
|
|
40
40
|
async createWallet() {
|
|
41
41
|
const signer = this.requireSigner();
|
|
42
42
|
return this.json("/api/cup/v1/wallet", {
|
|
@@ -45,8 +45,11 @@ export class CupClient {
|
|
|
45
45
|
body: JSON.stringify({ walletAddress: signer.address }),
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
async listMarkets(limit = 25) {
|
|
49
|
-
const
|
|
48
|
+
async listMarkets(limit = 25, offset = 0) {
|
|
49
|
+
const qs = new URLSearchParams({ limit: String(limit) });
|
|
50
|
+
if (offset > 0)
|
|
51
|
+
qs.set("offset", String(offset));
|
|
52
|
+
const out = await this.json(`/api/cup/v1/markets?${qs}`);
|
|
50
53
|
return out.markets;
|
|
51
54
|
}
|
|
52
55
|
async getQuote(slug, side, sizePhunch) {
|
|
@@ -97,12 +100,17 @@ export class CupClient {
|
|
|
97
100
|
});
|
|
98
101
|
return out.receipt;
|
|
99
102
|
}
|
|
100
|
-
|
|
103
|
+
/** Typed wallet summary: balance + realized PnL + positions, all in $pHUNCH. */
|
|
104
|
+
async getWallet(address) {
|
|
101
105
|
const addr = address ?? this.signer?.address;
|
|
102
106
|
if (!addr)
|
|
103
107
|
throw new Error("No wallet address (pass one or construct with a signer).");
|
|
104
108
|
return this.json(`/api/cup/v1/wallet/${addr}`);
|
|
105
109
|
}
|
|
110
|
+
/** @deprecated use {@link getWallet}. Kept for back-compat. */
|
|
111
|
+
async getPositions(address) {
|
|
112
|
+
return this.getWallet(address);
|
|
113
|
+
}
|
|
106
114
|
async getLeaderboard(opts = {}) {
|
|
107
115
|
const qs = new URLSearchParams();
|
|
108
116
|
if (opts.limit)
|
|
@@ -115,27 +123,101 @@ export class CupClient {
|
|
|
115
123
|
return this.json(`/api/cup/v1/leaderboard${suffix}`);
|
|
116
124
|
}
|
|
117
125
|
/**
|
|
118
|
-
* One round of a built-in strategy
|
|
119
|
-
*
|
|
120
|
-
*
|
|
126
|
+
* One round of a built-in strategy WITH bankroll survival — the un-managed
|
|
127
|
+
* loop in the hosting docs went insolvent in ~20 minutes (250 $pHUNCH/round from
|
|
128
|
+
* a finite no-top-up grant). This:
|
|
129
|
+
* • reads the live balance + open positions first (a `live` run);
|
|
130
|
+
* • skips markets the wallet is ALREADY positioned in (no re-piling);
|
|
131
|
+
* • caps the round to `min(size, maxRoundFraction × balance)` per market and
|
|
132
|
+
* stops when the balance can't cover another ticket;
|
|
133
|
+
* • uses a STABLE tradeId (`cup:<roundBucket>:<slug>:<side>`) so an accidental
|
|
134
|
+
* double-fire in the same 5-min bucket is idempotent, not a double-spend;
|
|
135
|
+
* • picks over the market's REAL outcome keys (N-way safe).
|
|
136
|
+
* The decision is pure ({@link pickFor}); placement is deterministic code.
|
|
121
137
|
*/
|
|
122
138
|
async runStrategy(opts) {
|
|
123
139
|
const markets = await this.listMarkets(opts.limit ?? 10);
|
|
124
140
|
const receipts = [];
|
|
141
|
+
// Bankroll snapshot (live only — a simulate run writes nothing, so it can
|
|
142
|
+
// fan out over every market without spending).
|
|
143
|
+
let balance = Infinity;
|
|
144
|
+
const held = new Set();
|
|
145
|
+
if (opts.live && this.signer) {
|
|
146
|
+
try {
|
|
147
|
+
const wallet = await this.getWallet();
|
|
148
|
+
balance = wallet.balancePhunch;
|
|
149
|
+
for (const p of wallet.positions)
|
|
150
|
+
if (p.status === "open")
|
|
151
|
+
held.add(p.marketId);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* fail-soft: no snapshot → behave like the unmanaged run but still capped below */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const fraction = opts.maxRoundFraction ?? 0.25;
|
|
158
|
+
let roundBudget = opts.live ? Math.max(0, balance * fraction) : Infinity;
|
|
159
|
+
const bucket = roundBucket();
|
|
125
160
|
for (const m of markets) {
|
|
161
|
+
if (opts.live && held.has(m.marketId))
|
|
162
|
+
continue; // already positioned
|
|
163
|
+
const size = opts.sizePhunch;
|
|
164
|
+
if (opts.live && (balance < size || roundBudget < size))
|
|
165
|
+
break; // insolvent / budget spent
|
|
126
166
|
const card = {
|
|
127
167
|
slug: m.slug,
|
|
128
|
-
yesCents: m.yesCents,
|
|
129
|
-
noCents: m.noCents,
|
|
168
|
+
yesCents: m.yesCents ?? 50,
|
|
169
|
+
noCents: m.noCents ?? 50,
|
|
130
170
|
direction: m.direction,
|
|
171
|
+
outcomes: m.outcomes?.map((o) => ({ key: o.key, impliedPct: o.impliedPct })),
|
|
131
172
|
};
|
|
132
173
|
const pick = pickFor(opts.strategy, card, opts.sentiment ?? 0);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
if (opts.live) {
|
|
175
|
+
const receipt = await this.trade({
|
|
176
|
+
slug: pick.slug,
|
|
177
|
+
side: pick.side,
|
|
178
|
+
sizePhunch: size,
|
|
179
|
+
tradeId: `cup:${bucket}:${pick.slug}:${pick.side}`,
|
|
180
|
+
});
|
|
181
|
+
receipts.push(receipt);
|
|
182
|
+
if (typeof receipt.balancePhunch === "number")
|
|
183
|
+
balance = receipt.balancePhunch;
|
|
184
|
+
else
|
|
185
|
+
balance -= size;
|
|
186
|
+
roundBudget -= size;
|
|
187
|
+
held.add(m.marketId);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
receipts.push(await this.simulateTrade(pick.slug, pick.side, size));
|
|
191
|
+
}
|
|
136
192
|
}
|
|
137
193
|
return receipts;
|
|
138
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Run a strategy on a fixed interval, forever (or `rounds` times), catching
|
|
197
|
+
* per-round errors so a transient failure never kills the agent — the built-in
|
|
198
|
+
* 24×7 loop the CLI/hosting docs previously punted to a `while true` shell hack.
|
|
199
|
+
*/
|
|
200
|
+
async runLoop(opts) {
|
|
201
|
+
for (let round = 1; !opts.rounds || round <= opts.rounds; round += 1) {
|
|
202
|
+
try {
|
|
203
|
+
const receipts = await this.runStrategy(opts);
|
|
204
|
+
opts.onRound?.(round, receipts);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
opts.onError?.(round, err);
|
|
208
|
+
}
|
|
209
|
+
if (opts.rounds && round >= opts.rounds)
|
|
210
|
+
break;
|
|
211
|
+
await sleep(opts.intervalMs);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** A 5-minute time bucket, so a same-bucket re-run reuses the same tradeIds. */
|
|
216
|
+
function roundBucket() {
|
|
217
|
+
return Math.floor(Date.now() / (5 * 60 * 1000)).toString(36);
|
|
218
|
+
}
|
|
219
|
+
function sleep(ms) {
|
|
220
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
221
|
}
|
|
140
222
|
function randomId() {
|
|
141
223
|
// Best-effort unique id; the server idempotency key is the full tradeId.
|
package/dist/fleet.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CupStrategy } from "./strategy.js";
|
|
2
|
+
export interface FleetWallet {
|
|
3
|
+
name: string;
|
|
4
|
+
privateKey: string;
|
|
5
|
+
address: string;
|
|
6
|
+
/** Optional per-wallet strategy override (else the fleet default is used). */
|
|
7
|
+
strategy?: CupStrategy;
|
|
8
|
+
}
|
|
9
|
+
export interface Fleet {
|
|
10
|
+
wallets: FleetWallet[];
|
|
11
|
+
}
|
|
12
|
+
export declare function fleetFilePath(): string;
|
|
13
|
+
export declare function loadFleet(): Fleet;
|
|
14
|
+
export declare function saveFleet(fleet: Fleet): string;
|
|
15
|
+
/**
|
|
16
|
+
* Mint `count` fresh wallets, claim each one's 10,000 $pHUNCH, and append them to
|
|
17
|
+
* the fleet file with a rotating strategy. Idempotent claim (the server grants
|
|
18
|
+
* once), so re-running only tops up the roster.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fleetNew(count: number, baseUrl?: string): Promise<FleetWallet[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Run a strategy loop across every fleet wallet concurrently. Each wallet uses
|
|
23
|
+
* its own per-wallet `strategy` if set, else `defaultStrategy`. Rounds are
|
|
24
|
+
* staggered so N wallets don't all fire on the exact same instant. Never throws —
|
|
25
|
+
* per-round errors are reported via `onEvent`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function fleetRun(opts: {
|
|
28
|
+
defaultStrategy: CupStrategy;
|
|
29
|
+
sizePhunch: number;
|
|
30
|
+
intervalMs: number;
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
rounds?: number;
|
|
34
|
+
onEvent?: (line: string) => void;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/** Each fleet wallet's live rank + balance + realized PnL, best rank first. */
|
|
37
|
+
export declare function fleetBoard(baseUrl?: string): Promise<Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
address: string;
|
|
40
|
+
rank: number | null;
|
|
41
|
+
balancePhunch: number;
|
|
42
|
+
realizedPnlPhunch: number;
|
|
43
|
+
}>>;
|
package/dist/fleet.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hunch Cup — run a FLEET of agents from one machine (founder goal 2: "one
|
|
3
|
+
* person spins up 5–10 agents easily").
|
|
4
|
+
*
|
|
5
|
+
* A fleet is a set of throwaway wallets persisted to `~/.hunch-cup/fleet.json`
|
|
6
|
+
* (override with `HUNCH_CUP_FLEET_FILE`). `fleetNew` mints N fresh wallets and
|
|
7
|
+
* claims each one's 10,000 $pHUNCH; `fleetRun` runs a (possibly per-wallet mixed)
|
|
8
|
+
* strategy loop across all of them concurrently; `fleetBoard` shows each wallet's
|
|
9
|
+
* live rank. Keys never leave the local file.
|
|
10
|
+
*
|
|
11
|
+
* SECURITY: the file holds real (paper-only) private keys in plaintext — same
|
|
12
|
+
* trust model as `HUNCH_CUP_PRIVATE_KEY`. These wallets only ever touch paper
|
|
13
|
+
* $pHUNCH, never real funds.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { CupClient } from "./client.js";
|
|
19
|
+
import { fromPrivateKey, newWallet } from "./signer.js";
|
|
20
|
+
export function fleetFilePath() {
|
|
21
|
+
return process.env.HUNCH_CUP_FLEET_FILE ?? join(homedir(), ".hunch-cup", "fleet.json");
|
|
22
|
+
}
|
|
23
|
+
export function loadFleet() {
|
|
24
|
+
const path = fleetFilePath();
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return { wallets: [] };
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
+
return { wallets: Array.isArray(parsed.wallets) ? parsed.wallets : [] };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { wallets: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function saveFleet(fleet) {
|
|
36
|
+
const path = fleetFilePath();
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
38
|
+
writeFileSync(path, JSON.stringify(fleet, null, 2), { mode: 0o600 });
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
/** A rotation of the built-in strategies, so a demo fleet is visibly diverse. */
|
|
42
|
+
const STRATEGY_CYCLE = ["momentum", "contrarian", "news-reactive"];
|
|
43
|
+
/**
|
|
44
|
+
* Mint `count` fresh wallets, claim each one's 10,000 $pHUNCH, and append them to
|
|
45
|
+
* the fleet file with a rotating strategy. Idempotent claim (the server grants
|
|
46
|
+
* once), so re-running only tops up the roster.
|
|
47
|
+
*/
|
|
48
|
+
export async function fleetNew(count, baseUrl) {
|
|
49
|
+
const fleet = loadFleet();
|
|
50
|
+
const created = [];
|
|
51
|
+
for (let i = 0; i < count; i += 1) {
|
|
52
|
+
const { privateKey, signer } = newWallet();
|
|
53
|
+
const idx = fleet.wallets.length + created.length;
|
|
54
|
+
const wallet = {
|
|
55
|
+
name: `bot-${idx + 1}`,
|
|
56
|
+
privateKey,
|
|
57
|
+
address: signer.address,
|
|
58
|
+
strategy: STRATEGY_CYCLE[idx % STRATEGY_CYCLE.length],
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
await new CupClient({ baseUrl, signer }).createWallet();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* claim is dormant/closed or offline — the wallet is saved regardless */
|
|
65
|
+
}
|
|
66
|
+
created.push(wallet);
|
|
67
|
+
}
|
|
68
|
+
saveFleet({ wallets: [...fleet.wallets, ...created] });
|
|
69
|
+
return created;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run a strategy loop across every fleet wallet concurrently. Each wallet uses
|
|
73
|
+
* its own per-wallet `strategy` if set, else `defaultStrategy`. Rounds are
|
|
74
|
+
* staggered so N wallets don't all fire on the exact same instant. Never throws —
|
|
75
|
+
* per-round errors are reported via `onEvent`.
|
|
76
|
+
*/
|
|
77
|
+
export async function fleetRun(opts) {
|
|
78
|
+
const fleet = loadFleet();
|
|
79
|
+
if (fleet.wallets.length === 0) {
|
|
80
|
+
opts.onEvent?.("No fleet wallets. Run `hunch-cup fleet new <n>` first.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const stagger = Math.floor(opts.intervalMs / Math.max(1, fleet.wallets.length));
|
|
84
|
+
await Promise.all(fleet.wallets.map(async (w, i) => {
|
|
85
|
+
await sleep(stagger * i); // spread the first fire across the interval
|
|
86
|
+
const client = new CupClient({ baseUrl: opts.baseUrl, signer: fromPrivateKey(w.privateKey) });
|
|
87
|
+
const strategy = w.strategy ?? opts.defaultStrategy;
|
|
88
|
+
await client.runLoop({
|
|
89
|
+
strategy,
|
|
90
|
+
sizePhunch: opts.sizePhunch,
|
|
91
|
+
intervalMs: opts.intervalMs,
|
|
92
|
+
limit: opts.limit,
|
|
93
|
+
live: true,
|
|
94
|
+
rounds: opts.rounds,
|
|
95
|
+
onRound: (round, receipts) => opts.onEvent?.(`[${w.name}·${strategy}] round ${round}: ${receipts.length} trade(s)`),
|
|
96
|
+
onError: (round, err) => opts.onEvent?.(`[${w.name}] round ${round} error: ${err instanceof Error ? err.message : String(err)}`),
|
|
97
|
+
});
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
/** Each fleet wallet's live rank + balance + realized PnL, best rank first. */
|
|
101
|
+
export async function fleetBoard(baseUrl) {
|
|
102
|
+
const fleet = loadFleet();
|
|
103
|
+
const rows = await Promise.all(fleet.wallets.map(async (w) => {
|
|
104
|
+
const client = new CupClient({ baseUrl });
|
|
105
|
+
let balancePhunch = 0;
|
|
106
|
+
let realizedPnlPhunch = 0;
|
|
107
|
+
let rank = null;
|
|
108
|
+
try {
|
|
109
|
+
const wallet = await client.getWallet(w.address);
|
|
110
|
+
balancePhunch = wallet.balancePhunch;
|
|
111
|
+
realizedPnlPhunch = wallet.realizedPnlPhunch;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* dormant/offline */
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const board = (await client.getLeaderboard({ wallet: w.address, limit: 1 }));
|
|
118
|
+
rank = board.you?.rank ?? null;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* no rank yet */
|
|
122
|
+
}
|
|
123
|
+
return { name: w.name, address: w.address, rank, balancePhunch, realizedPnlPhunch };
|
|
124
|
+
}));
|
|
125
|
+
return rows.sort((a, b) => (a.rank ?? 1e9) - (b.rank ?? 1e9));
|
|
126
|
+
}
|
|
127
|
+
function sleep(ms) {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { CupClient, DEFAULT_BASE_URL } from "./client.js";
|
|
2
|
-
export type { CupClientOptions, CupSigner, CupMarket, CupTradeReceipt, } from "./client.js";
|
|
2
|
+
export type { CupClientOptions, CupSigner, CupMarket, CupOutcome, CupPosition, CupWalletView, CupTradeReceipt, } from "./client.js";
|
|
3
3
|
export { fromViemAccount, fromPrivateKey, newWallet } from "./signer.js";
|
|
4
4
|
export { buildCupTradeMessage } from "./message.js";
|
|
5
5
|
export type { CupTradeMessageParams } from "./message.js";
|
|
6
6
|
export { pickFor, momentumPick, contrarianPick, newsReactivePick, } from "./strategy.js";
|
|
7
|
-
export type { CupStrategy, StrategyCard, StrategyPick } from "./strategy.js";
|
|
7
|
+
export type { CupStrategy, StrategyCard, StrategyOutcome, StrategyPick, } from "./strategy.js";
|
|
8
|
+
export { fleetNew, fleetRun, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
|
|
9
|
+
export type { Fleet, FleetWallet } from "./fleet.js";
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { CupClient, DEFAULT_BASE_URL } from "./client.js";
|
|
|
2
2
|
export { fromViemAccount, fromPrivateKey, newWallet } from "./signer.js";
|
|
3
3
|
export { buildCupTradeMessage } from "./message.js";
|
|
4
4
|
export { pickFor, momentumPick, contrarianPick, newsReactivePick, } from "./strategy.js";
|
|
5
|
+
export { fleetNew, fleetRun, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
|
package/dist/strategy.d.ts
CHANGED
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure, deterministic deciders. The LLM (if any) NEVER executes — a strategy
|
|
5
5
|
* maps a market card to a concrete `{ side }` and order placement is plain code.
|
|
6
|
-
*
|
|
6
|
+
* Binary markets resolve to YES/NO, direction markets to UP/DOWN, and N-way
|
|
7
|
+
* markets (ladders, outperform books, THE FLIP: heads/tails/tie) pick over their
|
|
8
|
+
* REAL outcome keys — never yes/no, which on an N-way market records a bet that
|
|
9
|
+
* can never win (the server now 422s it; the strategy must not emit it).
|
|
7
10
|
*/
|
|
11
|
+
/** One tradeable outcome with its live pool-implied probability (0–100). */
|
|
12
|
+
export interface StrategyOutcome {
|
|
13
|
+
key: string;
|
|
14
|
+
impliedPct: number;
|
|
15
|
+
}
|
|
8
16
|
export interface StrategyCard {
|
|
9
17
|
slug: string;
|
|
10
18
|
/** Implied paper-pool probability of the positive outcome, in cents (0–100). */
|
|
@@ -13,20 +21,28 @@ export interface StrategyCard {
|
|
|
13
21
|
noCents: number;
|
|
14
22
|
/** True → the positive/negative outcomes are UP/DOWN, not YES/NO. */
|
|
15
23
|
direction: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The market's real outcome keys + odds. Present for N-way markets (and
|
|
26
|
+
* optionally binary); when set, strategies pick over THESE keys so an
|
|
27
|
+
* N-way bet never lands on a phantom yes/no side.
|
|
28
|
+
*/
|
|
29
|
+
outcomes?: StrategyOutcome[];
|
|
16
30
|
}
|
|
17
31
|
export interface StrategyPick {
|
|
18
32
|
slug: string;
|
|
19
33
|
side: string;
|
|
20
34
|
}
|
|
21
35
|
export type CupStrategy = "momentum" | "contrarian" | "news-reactive";
|
|
22
|
-
/** Momentum: trade WITH the crowd — back the pool's favorite. */
|
|
36
|
+
/** Momentum: trade WITH the crowd — back the pool's favorite (max odds). */
|
|
23
37
|
export declare function momentumPick(card: StrategyCard): StrategyPick;
|
|
24
|
-
/** Contrarian: fade the crowd — back the
|
|
38
|
+
/** Contrarian: fade the crowd — back the longest-odds outcome. */
|
|
25
39
|
export declare function contrarianPick(card: StrategyCard): StrategyPick;
|
|
26
40
|
/**
|
|
27
41
|
* News-reactive: trade off an external sentiment signal in [-1, 1] (e.g. a
|
|
28
|
-
* headline classifier).
|
|
29
|
-
* negative
|
|
42
|
+
* headline classifier). On a binary/direction market positive → the positive
|
|
43
|
+
* outcome (YES/UP), negative → the negative (NO/DOWN); neutral 0 → momentum.
|
|
44
|
+
* N-way markets have no natural positive pole, so a signal maps onto the
|
|
45
|
+
* crowd favorite (positive) or underdog (negative) — always a real key.
|
|
30
46
|
*/
|
|
31
47
|
export declare function newsReactivePick(card: StrategyCard, sentiment: number): StrategyPick;
|
|
32
48
|
/** Dispatch a named strategy. `news-reactive` uses `sentiment` (default 0). */
|
package/dist/strategy.js
CHANGED
|
@@ -3,9 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pure, deterministic deciders. The LLM (if any) NEVER executes — a strategy
|
|
5
5
|
* maps a market card to a concrete `{ side }` and order placement is plain code.
|
|
6
|
-
*
|
|
6
|
+
* Binary markets resolve to YES/NO, direction markets to UP/DOWN, and N-way
|
|
7
|
+
* markets (ladders, outperform books, THE FLIP: heads/tails/tie) pick over their
|
|
8
|
+
* REAL outcome keys — never yes/no, which on an N-way market records a bet that
|
|
9
|
+
* can never win (the server now 422s it; the strategy must not emit it).
|
|
7
10
|
*/
|
|
8
|
-
/**
|
|
11
|
+
/** Is this an N-way market we should pick over its own outcome keys? */
|
|
12
|
+
function isNway(card) {
|
|
13
|
+
return (Array.isArray(card.outcomes) &&
|
|
14
|
+
card.outcomes.length > 0 &&
|
|
15
|
+
// A true binary yes/no pair is handled by the yesCents/noCents path below.
|
|
16
|
+
!(card.outcomes.length === 2 &&
|
|
17
|
+
card.outcomes.some((o) => o.key === "yes") &&
|
|
18
|
+
card.outcomes.some((o) => o.key === "no")));
|
|
19
|
+
}
|
|
20
|
+
/** The outcome key with the highest / lowest implied odds (ties → first listed). */
|
|
21
|
+
function extreme(outcomes, want) {
|
|
22
|
+
return outcomes.reduce((best, o) => want === "max"
|
|
23
|
+
? o.impliedPct > best.impliedPct
|
|
24
|
+
? o
|
|
25
|
+
: best
|
|
26
|
+
: o.impliedPct < best.impliedPct
|
|
27
|
+
? o
|
|
28
|
+
: best).key;
|
|
29
|
+
}
|
|
30
|
+
/** The outcome label for a binary/direction pole (UP/DOWN vs YES/NO). */
|
|
9
31
|
function sideFor(card, pole) {
|
|
10
32
|
if (card.direction)
|
|
11
33
|
return pole === "pos" ? "up" : "down";
|
|
@@ -15,21 +37,32 @@ function sideFor(card, pole) {
|
|
|
15
37
|
function favorite(card) {
|
|
16
38
|
return card.yesCents >= card.noCents ? "pos" : "neg";
|
|
17
39
|
}
|
|
18
|
-
/** Momentum: trade WITH the crowd — back the pool's favorite. */
|
|
40
|
+
/** Momentum: trade WITH the crowd — back the pool's favorite (max odds). */
|
|
19
41
|
export function momentumPick(card) {
|
|
42
|
+
if (isNway(card))
|
|
43
|
+
return { slug: card.slug, side: extreme(card.outcomes, "max") };
|
|
20
44
|
return { slug: card.slug, side: sideFor(card, favorite(card)) };
|
|
21
45
|
}
|
|
22
|
-
/** Contrarian: fade the crowd — back the
|
|
46
|
+
/** Contrarian: fade the crowd — back the longest-odds outcome. */
|
|
23
47
|
export function contrarianPick(card) {
|
|
48
|
+
if (isNway(card))
|
|
49
|
+
return { slug: card.slug, side: extreme(card.outcomes, "min") };
|
|
24
50
|
const pole = favorite(card) === "pos" ? "neg" : "pos";
|
|
25
51
|
return { slug: card.slug, side: sideFor(card, pole) };
|
|
26
52
|
}
|
|
27
53
|
/**
|
|
28
54
|
* News-reactive: trade off an external sentiment signal in [-1, 1] (e.g. a
|
|
29
|
-
* headline classifier).
|
|
30
|
-
* negative
|
|
55
|
+
* headline classifier). On a binary/direction market positive → the positive
|
|
56
|
+
* outcome (YES/UP), negative → the negative (NO/DOWN); neutral 0 → momentum.
|
|
57
|
+
* N-way markets have no natural positive pole, so a signal maps onto the
|
|
58
|
+
* crowd favorite (positive) or underdog (negative) — always a real key.
|
|
31
59
|
*/
|
|
32
60
|
export function newsReactivePick(card, sentiment) {
|
|
61
|
+
if (isNway(card)) {
|
|
62
|
+
if (sentiment < 0)
|
|
63
|
+
return contrarianPick(card);
|
|
64
|
+
return momentumPick(card);
|
|
65
|
+
}
|
|
33
66
|
if (sentiment > 0)
|
|
34
67
|
return { slug: card.slug, side: sideFor(card, "pos") };
|
|
35
68
|
if (sentiment < 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hunch-cup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "One-line CLI + typed SDK to paper-trade the Hunch Cup tournament: claim 1,000 $pHUNCH, trade live markets with zero real-funds risk, run momentum/contrarian/news-reactive agents, and climb the public leaderboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hunch",
|