hunch-cup 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/client.d.ts +6 -0
- package/dist/client.js +9 -2
- package/dist/fleet.d.ts +26 -0
- package/dist/fleet.js +51 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/strategy.d.ts +8 -1
- package/dist/strategy.js +16 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { CupClient } from "./client.js";
|
|
21
21
|
import { fromPrivateKey, newWallet } from "./signer.js";
|
|
22
22
|
import { fleetNew, fleetRun, fleetBoard } from "./fleet.js";
|
|
23
|
-
const STRATEGIES = ["momentum", "contrarian", "news-reactive"];
|
|
23
|
+
const STRATEGIES = ["momentum", "contrarian", "news-reactive", "random"];
|
|
24
24
|
function baseUrl() {
|
|
25
25
|
return process.env.HUNCH_CUP_BASE_URL;
|
|
26
26
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -115,11 +115,15 @@ export declare class CupClient {
|
|
|
115
115
|
runStrategy(opts: {
|
|
116
116
|
strategy: CupStrategy;
|
|
117
117
|
sizePhunch: number;
|
|
118
|
+
/** When set, each bet's size is a random integer in [sizePhunch, sizePhunchMax]. */
|
|
119
|
+
sizePhunchMax?: number;
|
|
118
120
|
limit?: number;
|
|
119
121
|
sentiment?: number;
|
|
120
122
|
live?: boolean;
|
|
121
123
|
/** Max fraction of the current balance to deploy per round (default 25%). */
|
|
122
124
|
maxRoundFraction?: number;
|
|
125
|
+
/** Trade only markets this predicate keeps (e.g. only the flip + 5m/15m rounds). */
|
|
126
|
+
marketFilter?: (m: CupMarket) => boolean;
|
|
123
127
|
}): Promise<CupTradeReceipt[]>;
|
|
124
128
|
/**
|
|
125
129
|
* Run a strategy on a fixed interval, forever (or `rounds` times), catching
|
|
@@ -129,11 +133,13 @@ export declare class CupClient {
|
|
|
129
133
|
runLoop(opts: {
|
|
130
134
|
strategy: CupStrategy;
|
|
131
135
|
sizePhunch: number;
|
|
136
|
+
sizePhunchMax?: number;
|
|
132
137
|
intervalMs: number;
|
|
133
138
|
limit?: number;
|
|
134
139
|
sentiment?: number;
|
|
135
140
|
live?: boolean;
|
|
136
141
|
maxRoundFraction?: number;
|
|
142
|
+
marketFilter?: (m: CupMarket) => boolean;
|
|
137
143
|
rounds?: number;
|
|
138
144
|
onRound?: (round: number, receipts: CupTradeReceipt[]) => void;
|
|
139
145
|
onError?: (round: number, err: unknown) => void;
|
package/dist/client.js
CHANGED
|
@@ -136,7 +136,8 @@ export class CupClient {
|
|
|
136
136
|
* The decision is pure ({@link pickFor}); placement is deterministic code.
|
|
137
137
|
*/
|
|
138
138
|
async runStrategy(opts) {
|
|
139
|
-
const
|
|
139
|
+
const all = await this.listMarkets(opts.limit ?? 10);
|
|
140
|
+
const markets = opts.marketFilter ? all.filter(opts.marketFilter) : all;
|
|
140
141
|
const receipts = [];
|
|
141
142
|
// Bankroll snapshot (live only — a simulate run writes nothing, so it can
|
|
142
143
|
// fan out over every market without spending).
|
|
@@ -160,7 +161,9 @@ export class CupClient {
|
|
|
160
161
|
for (const m of markets) {
|
|
161
162
|
if (opts.live && held.has(m.marketId))
|
|
162
163
|
continue; // already positioned
|
|
163
|
-
const size = opts.sizePhunch
|
|
164
|
+
const size = opts.sizePhunchMax && opts.sizePhunchMax > opts.sizePhunch
|
|
165
|
+
? randInt(opts.sizePhunch, opts.sizePhunchMax)
|
|
166
|
+
: opts.sizePhunch;
|
|
164
167
|
if (opts.live && (balance < size || roundBudget < size))
|
|
165
168
|
break; // insolvent / budget spent
|
|
166
169
|
const card = {
|
|
@@ -219,6 +222,10 @@ function roundBucket() {
|
|
|
219
222
|
function sleep(ms) {
|
|
220
223
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
221
224
|
}
|
|
225
|
+
/** Uniform random integer in [min, max]. */
|
|
226
|
+
function randInt(min, max) {
|
|
227
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
228
|
+
}
|
|
222
229
|
function randomId() {
|
|
223
230
|
// Best-effort unique id; the server idempotency key is the full tradeId.
|
|
224
231
|
return Math.abs(Math.floor(Math.random() * 1e15)).toString(36) + Date.now().toString(36);
|
package/dist/fleet.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CupMarket } from "./client.js";
|
|
1
2
|
import type { CupStrategy } from "./strategy.js";
|
|
2
3
|
export interface FleetWallet {
|
|
3
4
|
name: string;
|
|
@@ -33,6 +34,31 @@ export declare function fleetRun(opts: {
|
|
|
33
34
|
rounds?: number;
|
|
34
35
|
onEvent?: (line: string) => void;
|
|
35
36
|
}): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Coordinated round loop: instead of every wallet running its own perpetual
|
|
39
|
+
* loop, each ROUND a random subset of `participants()` wallets acts once (via
|
|
40
|
+
* `runStrategy`). Models a live crowd where a variable slice of the roster
|
|
41
|
+
* trades each round, not all of them every time. Concurrency across the chosen
|
|
42
|
+
* wallets, staggered so they don't fire on the same instant. Never throws —
|
|
43
|
+
* per-wallet errors surface via `onEvent`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fleetRunRounds(opts: {
|
|
46
|
+
strategy: CupStrategy;
|
|
47
|
+
sizePhunch: number;
|
|
48
|
+
/** When set, each bet's size is a random int in [sizePhunch, sizePhunchMax]. */
|
|
49
|
+
sizePhunchMax?: number;
|
|
50
|
+
intervalMs: number;
|
|
51
|
+
/** How many wallets act this round (default: all). e.g. `() => 70 + rand(0..30)`. */
|
|
52
|
+
participants?: () => number;
|
|
53
|
+
marketFilter?: (m: CupMarket) => boolean;
|
|
54
|
+
maxRoundFraction?: number;
|
|
55
|
+
/** false → simulate (no writes), for a dry-run smoke. Default true (real trades). */
|
|
56
|
+
live?: boolean;
|
|
57
|
+
baseUrl?: string;
|
|
58
|
+
limit?: number;
|
|
59
|
+
rounds?: number;
|
|
60
|
+
onEvent?: (line: string) => void;
|
|
61
|
+
}): Promise<void>;
|
|
36
62
|
/** Each fleet wallet's live rank + balance + realized PnL, best rank first. */
|
|
37
63
|
export declare function fleetBoard(baseUrl?: string): Promise<Array<{
|
|
38
64
|
name: string;
|
package/dist/fleet.js
CHANGED
|
@@ -97,6 +97,57 @@ export async function fleetRun(opts) {
|
|
|
97
97
|
});
|
|
98
98
|
}));
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Coordinated round loop: instead of every wallet running its own perpetual
|
|
102
|
+
* loop, each ROUND a random subset of `participants()` wallets acts once (via
|
|
103
|
+
* `runStrategy`). Models a live crowd where a variable slice of the roster
|
|
104
|
+
* trades each round, not all of them every time. Concurrency across the chosen
|
|
105
|
+
* wallets, staggered so they don't fire on the same instant. Never throws —
|
|
106
|
+
* per-wallet errors surface via `onEvent`.
|
|
107
|
+
*/
|
|
108
|
+
export async function fleetRunRounds(opts) {
|
|
109
|
+
const fleet = loadFleet();
|
|
110
|
+
if (fleet.wallets.length === 0) {
|
|
111
|
+
opts.onEvent?.("No fleet wallets. Run `hunch-cup fleet new <n>` first.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (let round = 1; !opts.rounds || round <= opts.rounds; round += 1) {
|
|
115
|
+
const n = Math.max(1, Math.min(fleet.wallets.length, Math.floor(opts.participants?.() ?? fleet.wallets.length)));
|
|
116
|
+
const chosen = shuffle(fleet.wallets).slice(0, n);
|
|
117
|
+
const stagger = Math.floor(opts.intervalMs / Math.max(1, chosen.length));
|
|
118
|
+
await Promise.all(chosen.map(async (w, i) => {
|
|
119
|
+
await sleep(stagger * i);
|
|
120
|
+
try {
|
|
121
|
+
const client = new CupClient({ baseUrl: opts.baseUrl, signer: fromPrivateKey(w.privateKey) });
|
|
122
|
+
const receipts = await client.runStrategy({
|
|
123
|
+
strategy: w.strategy ?? opts.strategy,
|
|
124
|
+
sizePhunch: opts.sizePhunch,
|
|
125
|
+
sizePhunchMax: opts.sizePhunchMax,
|
|
126
|
+
limit: opts.limit,
|
|
127
|
+
live: opts.live ?? true,
|
|
128
|
+
maxRoundFraction: opts.maxRoundFraction,
|
|
129
|
+
marketFilter: opts.marketFilter,
|
|
130
|
+
});
|
|
131
|
+
opts.onEvent?.(`[${w.name}] round ${round}: ${receipts.length} trade(s)`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
opts.onEvent?.(`[${w.name}] round ${round} error: ${err instanceof Error ? err.message : String(err)}`);
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
if (opts.rounds && round >= opts.rounds)
|
|
138
|
+
break;
|
|
139
|
+
await sleep(opts.intervalMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Fisher–Yates shuffle (copy) — pick a random subset without bias. */
|
|
143
|
+
function shuffle(arr) {
|
|
144
|
+
const a = [...arr];
|
|
145
|
+
for (let i = a.length - 1; i > 0; i -= 1) {
|
|
146
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
147
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
148
|
+
}
|
|
149
|
+
return a;
|
|
150
|
+
}
|
|
100
151
|
/** Each fleet wallet's live rank + balance + realized PnL, best rank first. */
|
|
101
152
|
export async function fleetBoard(baseUrl) {
|
|
102
153
|
const fleet = loadFleet();
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { CupClientOptions, CupSigner, CupMarket, CupOutcome, CupPosition, C
|
|
|
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
|
-
export { pickFor, momentumPick, contrarianPick, newsReactivePick, } from "./strategy.js";
|
|
6
|
+
export { pickFor, momentumPick, contrarianPick, newsReactivePick, randomPick, } from "./strategy.js";
|
|
7
7
|
export type { CupStrategy, StrategyCard, StrategyOutcome, StrategyPick, } from "./strategy.js";
|
|
8
|
-
export { fleetNew, fleetRun, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
|
|
8
|
+
export { fleetNew, fleetRun, fleetRunRounds, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
|
|
9
9
|
export type { Fleet, FleetWallet } from "./fleet.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
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
|
-
export { pickFor, momentumPick, contrarianPick, newsReactivePick, } from "./strategy.js";
|
|
5
|
-
export { fleetNew, fleetRun, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
|
|
4
|
+
export { pickFor, momentumPick, contrarianPick, newsReactivePick, randomPick, } from "./strategy.js";
|
|
5
|
+
export { fleetNew, fleetRun, fleetRunRounds, fleetBoard, loadFleet, saveFleet, fleetFilePath, } from "./fleet.js";
|
package/dist/strategy.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface StrategyPick {
|
|
|
32
32
|
slug: string;
|
|
33
33
|
side: string;
|
|
34
34
|
}
|
|
35
|
-
export type CupStrategy = "momentum" | "contrarian" | "news-reactive";
|
|
35
|
+
export type CupStrategy = "momentum" | "contrarian" | "news-reactive" | "random";
|
|
36
36
|
/** Momentum: trade WITH the crowd — back the pool's favorite (max odds). */
|
|
37
37
|
export declare function momentumPick(card: StrategyCard): StrategyPick;
|
|
38
38
|
/** Contrarian: fade the crowd — back the longest-odds outcome. */
|
|
@@ -45,5 +45,12 @@ export declare function contrarianPick(card: StrategyCard): StrategyPick;
|
|
|
45
45
|
* crowd favorite (positive) or underdog (negative) — always a real key.
|
|
46
46
|
*/
|
|
47
47
|
export declare function newsReactivePick(card: StrategyCard, sentiment: number): StrategyPick;
|
|
48
|
+
/**
|
|
49
|
+
* Random: take a side with NO logic — uniformly pick one of the market's real
|
|
50
|
+
* outcome keys (N-way → over `outcomes`; binary/direction → yes/no, the
|
|
51
|
+
* settlement keys). A fleet of these produces liquidity and pool depth with no
|
|
52
|
+
* predictable bias; the `rng` is injectable so the pick is unit-testable.
|
|
53
|
+
*/
|
|
54
|
+
export declare function randomPick(card: StrategyCard, rng?: () => number): StrategyPick;
|
|
48
55
|
/** Dispatch a named strategy. `news-reactive` uses `sentiment` (default 0). */
|
|
49
56
|
export declare function pickFor(strategy: CupStrategy, card: StrategyCard, sentiment?: number): StrategyPick;
|
package/dist/strategy.js
CHANGED
|
@@ -69,6 +69,20 @@ export function newsReactivePick(card, sentiment) {
|
|
|
69
69
|
return { slug: card.slug, side: sideFor(card, "neg") };
|
|
70
70
|
return momentumPick(card);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Random: take a side with NO logic — uniformly pick one of the market's real
|
|
74
|
+
* outcome keys (N-way → over `outcomes`; binary/direction → yes/no, the
|
|
75
|
+
* settlement keys). A fleet of these produces liquidity and pool depth with no
|
|
76
|
+
* predictable bias; the `rng` is injectable so the pick is unit-testable.
|
|
77
|
+
*/
|
|
78
|
+
export function randomPick(card, rng = Math.random) {
|
|
79
|
+
if (isNway(card)) {
|
|
80
|
+
const keys = card.outcomes.map((o) => o.key);
|
|
81
|
+
const i = Math.min(Math.floor(rng() * keys.length), keys.length - 1);
|
|
82
|
+
return { slug: card.slug, side: keys[i] ?? keys[0] };
|
|
83
|
+
}
|
|
84
|
+
return { slug: card.slug, side: rng() < 0.5 ? "yes" : "no" };
|
|
85
|
+
}
|
|
72
86
|
/** Dispatch a named strategy. `news-reactive` uses `sentiment` (default 0). */
|
|
73
87
|
export function pickFor(strategy, card, sentiment = 0) {
|
|
74
88
|
switch (strategy) {
|
|
@@ -78,5 +92,7 @@ export function pickFor(strategy, card, sentiment = 0) {
|
|
|
78
92
|
return contrarianPick(card);
|
|
79
93
|
case "news-reactive":
|
|
80
94
|
return newsReactivePick(card, sentiment);
|
|
95
|
+
case "random":
|
|
96
|
+
return randomPick(card);
|
|
81
97
|
}
|
|
82
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hunch-cup",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "One-line CLI + typed SDK to paper-trade the Hunch Cup tournament: claim 10,000 $pUSDC, 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",
|