@potswap_io/mcp 0.1.0 → 0.3.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.
@@ -0,0 +1,117 @@
1
+ export const SOL_MINT = 'So11111111111111111111111111111111111111112';
2
+ const MIN = 60_000;
3
+ /** True when a time-based strategy should run at `now`. */
4
+ export function isDue(s, now) {
5
+ if (s.status !== 'active' || !s.nextRunAt)
6
+ return false;
7
+ return Date.parse(s.nextRunAt) <= now;
8
+ }
9
+ /**
10
+ * Next run after a run at `now`. A missed window runs once; the next slot is
11
+ * always in the future (P11.2), never a catch-up burst.
12
+ */
13
+ export function nextRunAfter(s, now) {
14
+ const interval = (s.kind === 'dca' ? s.intervalMinutes : s.checkIntervalMinutes) * MIN;
15
+ return new Date(now + interval).toISOString();
16
+ }
17
+ export function dcaIsComplete(s) {
18
+ return s.totalRuns !== undefined && s.runsDone >= s.totalRuns;
19
+ }
20
+ /**
21
+ * Rebalance plan (P11.4): weights from balances × prices; assets drifting more than
22
+ * `driftPct` points are brought back via SOL. Returns [] when nothing drifts.
23
+ * Unpriced targets are excluded and reported in `warnings`.
24
+ */
25
+ export function rebalancePlan(s, balances, prices) {
26
+ const warnings = [];
27
+ const mints = Object.keys(s.targets);
28
+ const priced = mints.filter((m) => prices[m] > 0);
29
+ for (const m of mints)
30
+ if (!(prices[m] > 0))
31
+ warnings.push(`No price for ${m}; it is excluded until one is available.`);
32
+ const values = Object.fromEntries(priced.map((m) => [m, (balances[m] ?? 0) * prices[m]]));
33
+ const total = Object.values(values).reduce((a, b) => a + b, 0);
34
+ const weights = {};
35
+ if (total <= 0)
36
+ return { legs: [], warnings, weights };
37
+ for (const m of priced)
38
+ weights[m] = (values[m] / total) * 100;
39
+ // Re-normalise targets over priced assets only.
40
+ const targetSum = priced.reduce((a, m) => a + s.targets[m], 0);
41
+ const targets = Object.fromEntries(priced.map((m) => [m, (s.targets[m] / targetSum) * 100]));
42
+ const drifted = priced.some((m) => Math.abs(weights[m] - targets[m]) > s.driftPct);
43
+ if (!drifted)
44
+ return { legs: [], warnings, weights };
45
+ const legs = [];
46
+ const deltaUsd = Object.fromEntries(priced.map((m) => [m, (targets[m] / 100) * total - values[m]])); // + = buy, − = sell
47
+ // Sells first (overweight non-SOL → SOL), then buys (SOL → underweight).
48
+ for (const m of priced) {
49
+ if (m === SOL_MINT)
50
+ continue;
51
+ const d = deltaUsd[m];
52
+ if (d < 0 && -d >= s.minTradeUsd)
53
+ legs.push({ inputMint: m, outputMint: SOL_MINT, amount: round(-d / prices[m]), reason: `sell overweight ${pct(weights[m])}% → ${pct(targets[m])}%` });
54
+ }
55
+ for (const m of priced) {
56
+ if (m === SOL_MINT)
57
+ continue;
58
+ const d = deltaUsd[m];
59
+ if (d > 0 && d >= s.minTradeUsd)
60
+ legs.push({ inputMint: SOL_MINT, outputMint: m, amount: round(d / prices[SOL_MINT]), reason: `buy underweight ${pct(weights[m])}% → ${pct(targets[m])}%` });
61
+ }
62
+ return { legs, warnings, weights };
63
+ }
64
+ /** TP/SL decision (P11.5). `price` undefined = no decision. Returns the updated high-water mark too. */
65
+ export function tpslDecision(s, price) {
66
+ if (price === undefined || !(price > 0))
67
+ return { trigger: null, highWater: s.highWater };
68
+ const highWater = Math.max(s.highWater, price);
69
+ if (s.takeProfitPct !== undefined) {
70
+ const t = s.entryPrice * (1 + s.takeProfitPct / 100);
71
+ if (price >= t)
72
+ return { trigger: { trigger: 'take-profit', threshold: t }, highWater };
73
+ }
74
+ if (s.trailingPct !== undefined) {
75
+ const t = highWater * (1 - s.trailingPct / 100);
76
+ if (price <= t && highWater > s.entryPrice)
77
+ return { trigger: { trigger: 'trailing-stop', threshold: t }, highWater };
78
+ }
79
+ if (s.stopLossPct !== undefined) {
80
+ const t = s.entryPrice * (1 - s.stopLossPct / 100);
81
+ if (price <= t)
82
+ return { trigger: { trigger: 'stop-loss', threshold: t }, highWater };
83
+ }
84
+ return { trigger: null, highWater };
85
+ }
86
+ /** SOL-equivalent spent by all strategies in the UTC day containing `now` (P11.6). */
87
+ export function solSpentToday(runs, now) {
88
+ const dayStart = new Date(now);
89
+ dayStart.setUTCHours(0, 0, 0, 0);
90
+ return runs.filter((r) => r.outcome === 'ok' && Date.parse(r.at) >= dayStart.getTime()).reduce((a, r) => a + (r.solEquivalent ?? 0), 0);
91
+ }
92
+ export function dcaLegFor(s) {
93
+ return { inputMint: s.inputMint, outputMint: s.outputMint, amount: s.amountPerRun, reason: `DCA run ${s.runsDone + 1}${s.totalRuns ? `/${s.totalRuns}` : ''}` };
94
+ }
95
+ /** Limit order decision (P13.10): 'fill' when the condition holds, 'expired' past expiresAt, else null. */
96
+ export function limitDecision(s, price, now) {
97
+ if (s.expiresAt && Date.parse(s.expiresAt) <= now)
98
+ return 'expired';
99
+ if (price === undefined || !(price > 0))
100
+ return null;
101
+ if (s.condition === 'below' && price <= s.triggerPriceUsd)
102
+ return 'fill';
103
+ if (s.condition === 'above' && price >= s.triggerPriceUsd)
104
+ return 'fill';
105
+ return null;
106
+ }
107
+ export function limitLegFor(s) {
108
+ return s.side === 'buy'
109
+ ? { inputMint: s.quoteMint, outputMint: s.mint, amount: s.amount, reason: `limit buy (${s.condition} ${s.triggerPriceUsd})` }
110
+ : { inputMint: s.mint, outputMint: s.quoteMint, amount: s.amount, reason: `limit sell (${s.condition} ${s.triggerPriceUsd})` };
111
+ }
112
+ export const MAX_CONSECUTIVE_FAILURES = 3;
113
+ const round = (n) => Math.floor(n * 1e9) / 1e9;
114
+ const pct = (n) => n.toFixed(1);
115
+ export function newId(prefix, now = Date.now()) {
116
+ return `${prefix}_${now.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
117
+ }
@@ -0,0 +1,7 @@
1
+ export interface Lock {
2
+ acquired: boolean;
3
+ ownerPid: number | null;
4
+ release(): void;
5
+ }
6
+ export declare function pidAlive(pid: number, kill?: (pid: number, sig: 0) => void): boolean;
7
+ export declare function acquireLock(dir: string, pid?: number, alive?: (pid: number) => boolean): Lock;
@@ -0,0 +1,30 @@
1
+ // PRD P11.11/P11.12 — one scheduler per state directory. The lock is a file holding the owner PID;
2
+ // a lock whose PID is no longer alive is stale and may be reclaimed.
3
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ export function pidAlive(pid, kill = process.kill) {
6
+ try {
7
+ kill(pid, 0);
8
+ return true;
9
+ }
10
+ catch (e) {
11
+ return e.code === 'EPERM';
12
+ }
13
+ }
14
+ export function acquireLock(dir, pid = process.pid, alive = pidAlive) {
15
+ const file = join(dir, 'scheduler.lock');
16
+ if (existsSync(file)) {
17
+ const owner = Number(readFileSync(file, 'utf8').trim());
18
+ if (owner && owner !== pid && alive(owner))
19
+ return { acquired: false, ownerPid: owner, release() { } };
20
+ }
21
+ writeFileSync(file, String(pid));
22
+ return {
23
+ acquired: true, ownerPid: pid,
24
+ release() { try {
25
+ if (Number(readFileSync(file, 'utf8').trim()) === pid)
26
+ unlinkSync(file);
27
+ }
28
+ catch { /* already gone */ } },
29
+ };
30
+ }
@@ -0,0 +1,39 @@
1
+ import type { TradeContext } from '../trade.js';
2
+ import type { StrategyStore } from './store.js';
3
+ import type { Run } from './types.js';
4
+ export interface SchedulerOptions {
5
+ store: StrategyStore;
6
+ trade: TradeContext;
7
+ maxDailySol: number;
8
+ now?: () => number;
9
+ tickMs?: number;
10
+ onRun?: (run: Run) => void;
11
+ }
12
+ export declare class Scheduler {
13
+ private store;
14
+ private trade;
15
+ private maxDailySol;
16
+ private now;
17
+ private tickMs;
18
+ private onRun?;
19
+ private timer;
20
+ private running;
21
+ /** Set while a leg executes so a stop() can wait for it (P11.11). */
22
+ inFlight: Promise<void> | null;
23
+ active: boolean;
24
+ constructor(o: SchedulerOptions);
25
+ start(): void;
26
+ stop(): Promise<void>;
27
+ /** One pass over all strategies. Safe to call concurrently; overlapping calls are skipped. */
28
+ tick(): Promise<void>;
29
+ private runTick;
30
+ private runDca;
31
+ private runRebalance;
32
+ private checkTpSl;
33
+ private checkLimit;
34
+ private execute;
35
+ private record;
36
+ private bumpFailure;
37
+ private advance;
38
+ private complete;
39
+ }
@@ -0,0 +1,242 @@
1
+ import { executeSwap } from '../trade.js';
2
+ import { log } from '../redact.js';
3
+ import { dcaIsComplete, dcaLegFor, isDue, limitDecision, limitLegFor, MAX_CONSECUTIVE_FAILURES, nextRunAfter, newId, rebalancePlan, solSpentToday, tpslDecision } from './engine.js';
4
+ export class Scheduler {
5
+ store;
6
+ trade;
7
+ maxDailySol;
8
+ now;
9
+ tickMs;
10
+ onRun;
11
+ timer = null;
12
+ running = false;
13
+ /** Set while a leg executes so a stop() can wait for it (P11.11). */
14
+ inFlight = null;
15
+ active = false;
16
+ constructor(o) {
17
+ this.store = o.store;
18
+ this.trade = o.trade;
19
+ this.maxDailySol = o.maxDailySol;
20
+ this.now = o.now ?? Date.now;
21
+ this.tickMs = o.tickMs ?? 30_000;
22
+ this.onRun = o.onRun;
23
+ }
24
+ start() {
25
+ if (this.timer)
26
+ return;
27
+ this.active = true;
28
+ this.timer = setInterval(() => { void this.tick(); }, this.tickMs);
29
+ void this.tick();
30
+ }
31
+ async stop() {
32
+ if (this.timer)
33
+ clearInterval(this.timer);
34
+ this.timer = null;
35
+ this.active = false;
36
+ if (this.inFlight)
37
+ await this.inFlight;
38
+ }
39
+ /** One pass over all strategies. Safe to call concurrently; overlapping calls are skipped. */
40
+ async tick() {
41
+ if (this.running)
42
+ return;
43
+ this.running = true;
44
+ const p = this.runTick().catch((e) => log('scheduler tick error', e instanceof Error ? e.message : String(e)));
45
+ this.inFlight = p;
46
+ try {
47
+ await p;
48
+ }
49
+ finally {
50
+ this.running = false;
51
+ this.inFlight = null;
52
+ }
53
+ }
54
+ async runTick() {
55
+ const now = this.now();
56
+ const state = this.store.load();
57
+ for (const s of state.strategies) {
58
+ if (s.status !== 'active')
59
+ continue;
60
+ if (s.kind === 'tpsl')
61
+ await this.checkTpSl(s);
62
+ else if (s.kind === 'limit')
63
+ await this.checkLimit(s);
64
+ else if (isDue(s, now))
65
+ await (s.kind === 'dca' ? this.runDca(s) : this.runRebalance(s));
66
+ }
67
+ }
68
+ // ---- DCA (P11.1–P11.3)
69
+ async runDca(s) {
70
+ const leg = dcaLegFor(s);
71
+ const out = await this.execute(s, leg);
72
+ this.store.update((st) => {
73
+ const cur = st.strategies.find((x) => x.id === s.id);
74
+ if (!cur || cur.status !== 'active')
75
+ return;
76
+ const now = this.now();
77
+ if (out.kind === 'ok' || out.kind === 'dry-run')
78
+ cur.runsDone++;
79
+ cur.nextRunAt = nextRunAfter(cur, now);
80
+ if (dcaIsComplete(cur)) {
81
+ cur.status = 'completed';
82
+ cur.nextRunAt = null;
83
+ }
84
+ });
85
+ }
86
+ // ---- Rebalance (P11.4)
87
+ async runRebalance(s) {
88
+ const wallet = this.trade.signer?.publicKey;
89
+ let balances = {}, prices = {};
90
+ try {
91
+ balances = wallet ? await this.trade.balances(wallet) : {};
92
+ prices = await this.trade.api.prices(Object.keys(s.targets));
93
+ }
94
+ catch (e) {
95
+ this.record(s, 'check', 'failed', `Could not read balances or prices: ${e instanceof Error ? e.message : String(e)}. No funds were debited.`);
96
+ this.bumpFailure(s.id);
97
+ this.advance(s.id);
98
+ return;
99
+ }
100
+ const plan = rebalancePlan(s, balances, prices);
101
+ for (const w of plan.warnings)
102
+ this.record(s, 'check', 'skipped', w);
103
+ if (plan.legs.length === 0) {
104
+ this.record(s, 'check', 'no-op', `Within ${s.driftPct}% of target: ${Object.entries(plan.weights).map(([m, w]) => `${short(m)} ${w.toFixed(1)}%`).join(', ')}`);
105
+ }
106
+ else {
107
+ for (const leg of plan.legs)
108
+ await this.execute(s, leg);
109
+ }
110
+ this.advance(s.id);
111
+ }
112
+ // ---- TP/SL (P11.5)
113
+ async checkTpSl(s) {
114
+ let price;
115
+ try {
116
+ price = (await this.trade.api.prices([s.mint]))[s.mint];
117
+ }
118
+ catch {
119
+ price = undefined;
120
+ }
121
+ if (price === undefined) {
122
+ this.record(s, 'check', 'skipped', 'price unavailable');
123
+ return;
124
+ }
125
+ const { trigger, highWater } = tpslDecision(s, price);
126
+ this.store.update((st) => { const cur = st.strategies.find((x) => x.id === s.id); if (cur)
127
+ cur.highWater = highWater; });
128
+ if (!trigger)
129
+ return;
130
+ const wallet = this.trade.signer?.publicKey;
131
+ const held = wallet ? (await this.trade.balances(wallet).catch(() => ({})))[s.mint] ?? 0 : 0;
132
+ const amount = Math.floor(held * (s.sellPct / 100) * 1e9) / 1e9;
133
+ if (amount <= 0) {
134
+ this.record(s, `sell ${s.sellPct}% (${trigger.trigger})`, 'completed', 'Nothing to sell; completing.');
135
+ this.complete(s.id);
136
+ return;
137
+ }
138
+ const out = await this.execute(s, { inputMint: s.mint, outputMint: s.exitMint, amount, reason: `${trigger.trigger} at ${price} (threshold ${trigger.threshold.toPrecision(6)})` });
139
+ if (out.kind === 'ok' || out.kind === 'dry-run')
140
+ this.complete(s.id);
141
+ }
142
+ // ---- Limit orders (P13.10)
143
+ async checkLimit(s) {
144
+ let price;
145
+ try {
146
+ price = (await this.trade.api.prices([s.mint]))[s.mint];
147
+ }
148
+ catch {
149
+ price = undefined;
150
+ }
151
+ const d = limitDecision(s, price, this.now());
152
+ if (d === 'expired') {
153
+ this.record(s, 'check', 'completed', 'Expired unfilled.');
154
+ this.complete(s.id);
155
+ return;
156
+ }
157
+ if (d !== 'fill')
158
+ return;
159
+ const out = await this.execute(s, limitLegFor(s));
160
+ if (out.kind === 'ok' || out.kind === 'dry-run')
161
+ this.complete(s.id);
162
+ }
163
+ // ---- shared leg execution with caps, run log, failure counting (P11.6–P11.8, P11.13)
164
+ async execute(s, leg) {
165
+ const now = this.now();
166
+ const spent = solSpentToday(this.store.load().runs, now);
167
+ const remainingSol = Math.max(0, this.maxDailySol - spent);
168
+ const out = await executeSwap(this.trade, {
169
+ inputMint: leg.inputMint, outputMint: leg.outputMint, amount: leg.amount, slippageBps: s.maxSlippageBps, confirm: true,
170
+ extraSolCap: { remainingSol, label: `Daily cap reached (${this.maxDailySol} SOL). Next legs resume at 00:00 UTC.` },
171
+ });
172
+ const action = `${leg.amount} ${short(leg.inputMint)} → ${short(leg.outputMint)} (${leg.reason})`;
173
+ switch (out.kind) {
174
+ case 'ok': {
175
+ const r = out.result;
176
+ this.record(s, action, 'ok', undefined, { signature: r.signature, inAmount: r.inAmount, inputSymbol: r.inputSymbol, outAmount: r.outAmount, outputSymbol: r.outputSymbol, feeUsd: r.feeUsd, pointsAdded: r.pointsAdded, solEquivalent: r.solEquivalent });
177
+ this.store.update((st) => {
178
+ const cur = st.strategies.find((x) => x.id === s.id);
179
+ if (!cur)
180
+ return;
181
+ cur.consecutiveFailures = 0;
182
+ cur.totals.runs++;
183
+ cur.totals.feeUsd += r.feeUsd ?? 0;
184
+ cur.totals.pointsAdded += r.pointsAdded ?? 0;
185
+ cur.totals.volumeUsd += out.quote.inputUsd ?? 0;
186
+ });
187
+ break;
188
+ }
189
+ case 'dry-run':
190
+ this.record(s, action, 'dry-run', `Would swap for ${out.quote.outAmount} ${out.quote.outputSymbol} via ${out.quote.router}`);
191
+ break;
192
+ case 'refused':
193
+ this.record(s, action, 'skipped', out.message);
194
+ break;
195
+ case 'quote-only':
196
+ this.record(s, action, 'skipped', 'internal: confirm missing');
197
+ break;
198
+ case 'no-wallet':
199
+ this.record(s, action, 'failed', out.message);
200
+ this.bumpFailure(s.id);
201
+ break;
202
+ case 'failed':
203
+ this.record(s, action, 'failed', out.message, { signature: out.signature });
204
+ this.bumpFailure(s.id);
205
+ break;
206
+ }
207
+ return out;
208
+ }
209
+ record(s, action, outcome, message, extra = {}) {
210
+ const run = { id: newId('run', this.now()), strategyId: s.id, at: new Date(this.now()).toISOString(), action, outcome, message, ...extra };
211
+ this.store.update((st) => { st.runs.push(run); });
212
+ this.onRun?.(run);
213
+ log(`[${s.kind} ${s.id}] ${outcome}: ${action}${message ? ` — ${message}` : ''}`);
214
+ }
215
+ bumpFailure(id) {
216
+ this.store.update((st) => {
217
+ const cur = st.strategies.find((x) => x.id === id);
218
+ if (!cur)
219
+ return;
220
+ cur.consecutiveFailures++;
221
+ if (cur.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
222
+ cur.status = 'paused';
223
+ cur.pausedReason = `${MAX_CONSECUTIVE_FAILURES} consecutive failures; resume after checking the wallet and route.`;
224
+ cur.nextRunAt = null;
225
+ }
226
+ });
227
+ }
228
+ advance(id) {
229
+ this.store.update((st) => {
230
+ const cur = st.strategies.find((x) => x.id === id);
231
+ if (cur && cur.status === 'active' && cur.kind !== 'tpsl' && cur.kind !== 'limit')
232
+ cur.nextRunAt = nextRunAfter(cur, this.now());
233
+ });
234
+ }
235
+ complete(id) {
236
+ this.store.update((st) => { const cur = st.strategies.find((x) => x.id === id); if (cur) {
237
+ cur.status = 'completed';
238
+ cur.nextRunAt = null;
239
+ } });
240
+ }
241
+ }
242
+ const short = (m) => (m === 'So11111111111111111111111111111111111111112' ? 'SOL' : m.slice(0, 4) + '…' + m.slice(-4));
@@ -0,0 +1,26 @@
1
+ import { type Run, type Strategy, type StrategyState } from './types.js';
2
+ export interface StrategyStore {
3
+ load(): StrategyState;
4
+ save(state: StrategyState): void;
5
+ /** Convenience: read-modify-write. */
6
+ update<T>(fn: (state: StrategyState) => T): T;
7
+ readonly location: string;
8
+ }
9
+ export declare const MAX_RUNS_KEPT = 2000;
10
+ export declare class MemoryStore implements StrategyStore {
11
+ private state;
12
+ location: string;
13
+ load(): StrategyState;
14
+ save(state: StrategyState): void;
15
+ update<T>(fn: (state: StrategyState) => T): T;
16
+ }
17
+ export declare class FileStore implements StrategyStore {
18
+ location: string;
19
+ constructor(dir?: string);
20
+ load(): StrategyState;
21
+ save(state: StrategyState): void;
22
+ update<T>(fn: (state: StrategyState) => T): T;
23
+ }
24
+ export declare function defaultStateDir(env?: NodeJS.ProcessEnv): string;
25
+ export declare const findStrategy: (state: StrategyState, id: string) => Strategy | undefined;
26
+ export declare const runsOf: (state: StrategyState, id: string, limit?: number) => Run[];
@@ -0,0 +1,53 @@
1
+ // PRD P11 — persistence: a JSON file with atomic writes (tmp + rename), plus an in-memory store for tests.
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { redact } from '../redact.js';
6
+ import { EMPTY_STATE } from './types.js';
7
+ export const MAX_RUNS_KEPT = 2000;
8
+ export class MemoryStore {
9
+ state = structuredClone(EMPTY_STATE);
10
+ location = 'memory';
11
+ load() { return structuredClone(this.state); }
12
+ save(state) { this.state = structuredClone(trim(state)); }
13
+ update(fn) { const s = this.load(); const r = fn(s); this.save(s); return r; }
14
+ }
15
+ export class FileStore {
16
+ location;
17
+ constructor(dir = defaultStateDir()) {
18
+ mkdirSync(dir, { recursive: true });
19
+ this.location = join(dir, 'strategies.json');
20
+ }
21
+ load() {
22
+ if (!existsSync(this.location))
23
+ return structuredClone(EMPTY_STATE);
24
+ try {
25
+ const parsed = JSON.parse(readFileSync(this.location, 'utf8'));
26
+ if (parsed.version !== 1 || !Array.isArray(parsed.strategies))
27
+ return structuredClone(EMPTY_STATE);
28
+ parsed.runs ??= [];
29
+ return parsed;
30
+ }
31
+ catch {
32
+ return structuredClone(EMPTY_STATE);
33
+ }
34
+ }
35
+ save(state) {
36
+ // Redaction is belt and braces: nothing here should hold a key, but the file is on disk forever.
37
+ const body = redact(JSON.stringify(trim(state), null, 2));
38
+ const tmp = `${this.location}.${process.pid}.tmp`;
39
+ writeFileSync(tmp, body);
40
+ renameSync(tmp, this.location);
41
+ }
42
+ update(fn) { const s = this.load(); const r = fn(s); this.save(s); return r; }
43
+ }
44
+ export function defaultStateDir(env = process.env) {
45
+ return env.POTSWAP_STATE_DIR || join(homedir(), '.potswap');
46
+ }
47
+ function trim(state) {
48
+ if (state.runs.length > MAX_RUNS_KEPT)
49
+ state.runs = state.runs.slice(-MAX_RUNS_KEPT);
50
+ return state;
51
+ }
52
+ export const findStrategy = (state, id) => state.strategies.find((s) => s.id === id);
53
+ export const runsOf = (state, id, limit = 50) => state.runs.filter((r) => r.strategyId === id).slice(-limit).reverse();
@@ -0,0 +1,10 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { TradeContext } from '../trade.js';
3
+ import { type StrategyStore } from './store.js';
4
+ import type { Scheduler } from './scheduler.js';
5
+ export interface StrategyDeps {
6
+ trade: TradeContext;
7
+ store?: StrategyStore;
8
+ scheduler?: Scheduler;
9
+ }
10
+ export declare function registerStrategyTools(server: McpServer, deps: StrategyDeps): void;