@coinrithm/mcp-trading 0.1.8 → 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.
Files changed (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +43 -19
  3. package/dist/agent/act.d.ts +4 -0
  4. package/dist/agent/act.js +114 -0
  5. package/dist/agent/capabilityGuard.d.ts +2 -0
  6. package/dist/agent/capabilityGuard.js +131 -0
  7. package/dist/agent/cli.d.ts +21 -0
  8. package/dist/agent/cli.js +382 -0
  9. package/dist/agent/client.d.ts +107 -0
  10. package/dist/agent/client.js +173 -0
  11. package/dist/agent/decision.d.ts +137 -0
  12. package/dist/agent/decision.js +118 -0
  13. package/dist/agent/decisionValidator.d.ts +16 -0
  14. package/dist/agent/decisionValidator.js +215 -0
  15. package/dist/agent/engine.d.ts +10 -0
  16. package/dist/agent/engine.js +16 -0
  17. package/dist/agent/extract.d.ts +4 -0
  18. package/dist/agent/extract.js +5 -0
  19. package/dist/agent/frontmatter.d.ts +5 -0
  20. package/dist/agent/frontmatter.js +19 -0
  21. package/dist/agent/index.d.ts +2 -0
  22. package/dist/agent/index.js +10 -0
  23. package/dist/agent/indicators.d.ts +44 -0
  24. package/dist/agent/indicators.js +135 -0
  25. package/dist/agent/manifest.d.ts +15 -0
  26. package/dist/agent/manifest.js +40 -0
  27. package/dist/agent/mergeRules.d.ts +11 -0
  28. package/dist/agent/mergeRules.js +82 -0
  29. package/dist/agent/observe.d.ts +7 -0
  30. package/dist/agent/observe.js +244 -0
  31. package/dist/agent/prompt.d.ts +3 -0
  32. package/dist/agent/prompt.js +76 -0
  33. package/dist/agent/providers.d.ts +25 -0
  34. package/dist/agent/providers.js +143 -0
  35. package/dist/agent/resolve.d.ts +11 -0
  36. package/dist/agent/resolve.js +499 -0
  37. package/dist/agent/runEvidence.d.ts +6 -0
  38. package/dist/agent/runEvidence.js +23 -0
  39. package/dist/agent/runner.d.ts +19 -0
  40. package/dist/agent/runner.js +280 -0
  41. package/dist/agent/skill.d.ts +12 -0
  42. package/dist/agent/skill.js +136 -0
  43. package/dist/agent/skillValidator.d.ts +7 -0
  44. package/dist/agent/skillValidator.js +123 -0
  45. package/dist/agent/state.d.ts +7 -0
  46. package/dist/agent/state.js +96 -0
  47. package/dist/agent/strictLint.d.ts +3 -0
  48. package/dist/agent/strictLint.js +165 -0
  49. package/dist/agent/templates.d.ts +14 -0
  50. package/dist/agent/templates.js +192 -0
  51. package/dist/agent/types.d.ts +286 -0
  52. package/dist/agent/types.js +88 -0
  53. package/dist/agent/util.d.ts +13 -0
  54. package/dist/agent/util.js +116 -0
  55. package/dist/agent/version.d.ts +11 -0
  56. package/dist/agent/version.js +16 -0
  57. package/dist/client.d.ts +162 -0
  58. package/dist/http.d.ts +2 -0
  59. package/dist/index.d.ts +2 -0
  60. package/dist/tools.d.ts +3 -0
  61. package/dist/version.d.ts +1 -0
  62. package/package.json +78 -67
@@ -0,0 +1,137 @@
1
+ import { z } from "zod";
2
+ import { Decision } from "./types.js";
3
+ export declare const actionSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
4
+ type: z.ZodLiteral<"futures_open">;
5
+ symbol: z.ZodString;
6
+ side: z.ZodEnum<["long", "short"]>;
7
+ leverage: z.ZodNumber;
8
+ marginMusd: z.ZodNumber;
9
+ stopLossPrice: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
10
+ takeProfitPrice: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
11
+ confidence: z.ZodOptional<z.ZodNumber>;
12
+ rationaleSummary: z.ZodOptional<z.ZodString>;
13
+ }, "strict", z.ZodTypeAny, {
14
+ symbol: string;
15
+ side: "long" | "short";
16
+ leverage: number;
17
+ marginMusd: number;
18
+ type: "futures_open";
19
+ stopLossPrice?: number | null | undefined;
20
+ takeProfitPrice?: number | null | undefined;
21
+ confidence?: number | undefined;
22
+ rationaleSummary?: string | undefined;
23
+ }, {
24
+ symbol: string;
25
+ side: "long" | "short";
26
+ leverage: number;
27
+ marginMusd: number;
28
+ type: "futures_open";
29
+ stopLossPrice?: number | null | undefined;
30
+ takeProfitPrice?: number | null | undefined;
31
+ confidence?: number | undefined;
32
+ rationaleSummary?: string | undefined;
33
+ }>, z.ZodObject<{
34
+ type: z.ZodLiteral<"futures_close">;
35
+ positionId: z.ZodNumber;
36
+ fraction: z.ZodOptional<z.ZodNumber>;
37
+ confidence: z.ZodOptional<z.ZodNumber>;
38
+ rationaleSummary: z.ZodOptional<z.ZodString>;
39
+ }, "strict", z.ZodTypeAny, {
40
+ positionId: number;
41
+ type: "futures_close";
42
+ fraction?: number | undefined;
43
+ confidence?: number | undefined;
44
+ rationaleSummary?: string | undefined;
45
+ }, {
46
+ positionId: number;
47
+ type: "futures_close";
48
+ fraction?: number | undefined;
49
+ confidence?: number | undefined;
50
+ rationaleSummary?: string | undefined;
51
+ }>, z.ZodObject<{
52
+ type: z.ZodLiteral<"futures_set_sltp">;
53
+ positionId: z.ZodNumber;
54
+ stopLossPrice: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
55
+ takeProfitPrice: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
56
+ }, "strict", z.ZodTypeAny, {
57
+ positionId: number;
58
+ type: "futures_set_sltp";
59
+ stopLossPrice?: number | null | undefined;
60
+ takeProfitPrice?: number | null | undefined;
61
+ }, {
62
+ positionId: number;
63
+ type: "futures_set_sltp";
64
+ stopLossPrice?: number | null | undefined;
65
+ takeProfitPrice?: number | null | undefined;
66
+ }>, z.ZodObject<{
67
+ type: z.ZodLiteral<"spot_order">;
68
+ symbol: z.ZodString;
69
+ side: z.ZodEnum<["buy", "sell"]>;
70
+ orderType: z.ZodEnum<["market", "limit", "stop"]>;
71
+ quantity: z.ZodNumber;
72
+ limitPrice: z.ZodOptional<z.ZodNumber>;
73
+ stopPrice: z.ZodOptional<z.ZodNumber>;
74
+ confidence: z.ZodOptional<z.ZodNumber>;
75
+ rationaleSummary: z.ZodOptional<z.ZodString>;
76
+ }, "strict", z.ZodTypeAny, {
77
+ symbol: string;
78
+ side: "buy" | "sell";
79
+ orderType: "limit" | "market" | "stop";
80
+ quantity: number;
81
+ type: "spot_order";
82
+ limitPrice?: number | undefined;
83
+ stopPrice?: number | undefined;
84
+ confidence?: number | undefined;
85
+ rationaleSummary?: string | undefined;
86
+ }, {
87
+ symbol: string;
88
+ side: "buy" | "sell";
89
+ orderType: "limit" | "market" | "stop";
90
+ quantity: number;
91
+ type: "spot_order";
92
+ limitPrice?: number | undefined;
93
+ stopPrice?: number | undefined;
94
+ confidence?: number | undefined;
95
+ rationaleSummary?: string | undefined;
96
+ }>, z.ZodObject<{
97
+ type: z.ZodLiteral<"spot_cancel">;
98
+ orderId: z.ZodNumber;
99
+ }, "strict", z.ZodTypeAny, {
100
+ type: "spot_cancel";
101
+ orderId: number;
102
+ }, {
103
+ type: "spot_cancel";
104
+ orderId: number;
105
+ }>, z.ZodObject<{
106
+ type: z.ZodLiteral<"pm_open">;
107
+ source: z.ZodString;
108
+ slug: z.ZodString;
109
+ outcomeExternalMarketId: z.ZodString;
110
+ stakeMusd: z.ZodNumber;
111
+ confidence: z.ZodOptional<z.ZodNumber>;
112
+ rationaleSummary: z.ZodOptional<z.ZodString>;
113
+ }, "strict", z.ZodTypeAny, {
114
+ source: string;
115
+ slug: string;
116
+ outcomeExternalMarketId: string;
117
+ stakeMusd: number;
118
+ type: "pm_open";
119
+ confidence?: number | undefined;
120
+ rationaleSummary?: string | undefined;
121
+ }, {
122
+ source: string;
123
+ slug: string;
124
+ outcomeExternalMarketId: string;
125
+ stakeMusd: number;
126
+ type: "pm_open";
127
+ confidence?: number | undefined;
128
+ rationaleSummary?: string | undefined;
129
+ }>]>;
130
+ export type ParseDecisionResult = {
131
+ ok: true;
132
+ decision: Decision;
133
+ } | {
134
+ ok: false;
135
+ error: string;
136
+ };
137
+ export declare function parseDecision(text: string): ParseDecisionResult;
@@ -0,0 +1,118 @@
1
+ // Parse the model's single text response into a strict, structured Decision.
2
+ // Accepts futures + spot + prediction-market actions. Anything else — invalid
3
+ // JSON, an unknown action type, a free-form endpoint/tool name, extra unknown
4
+ // fields, or a missing required field — fails closed (the runner skips).
5
+ import { z } from "zod";
6
+ const futuresOpen = z
7
+ .object({
8
+ type: z.literal("futures_open"),
9
+ symbol: z.string().min(1),
10
+ side: z.enum(["long", "short"]),
11
+ leverage: z.number().positive(),
12
+ marginMusd: z.number().positive(),
13
+ stopLossPrice: z.number().nullable().optional(),
14
+ takeProfitPrice: z.number().nullable().optional(),
15
+ confidence: z.number().min(0).max(1).optional(),
16
+ rationaleSummary: z.string().optional(),
17
+ })
18
+ .strict();
19
+ const futuresClose = z
20
+ .object({
21
+ type: z.literal("futures_close"),
22
+ positionId: z.number(),
23
+ fraction: z.number().positive().max(1).optional(),
24
+ confidence: z.number().min(0).max(1).optional(),
25
+ rationaleSummary: z.string().optional(),
26
+ })
27
+ .strict();
28
+ const futuresSetSltp = z
29
+ .object({
30
+ type: z.literal("futures_set_sltp"),
31
+ positionId: z.number(),
32
+ stopLossPrice: z.number().nullable().optional(),
33
+ takeProfitPrice: z.number().nullable().optional(),
34
+ })
35
+ .strict();
36
+ const spotOrder = z
37
+ .object({
38
+ type: z.literal("spot_order"),
39
+ symbol: z.string().min(1),
40
+ side: z.enum(["buy", "sell"]),
41
+ orderType: z.enum(["market", "limit", "stop"]),
42
+ quantity: z.number().positive(),
43
+ limitPrice: z.number().positive().optional(),
44
+ stopPrice: z.number().positive().optional(),
45
+ confidence: z.number().min(0).max(1).optional(),
46
+ rationaleSummary: z.string().optional(),
47
+ })
48
+ .strict();
49
+ const spotCancel = z
50
+ .object({
51
+ type: z.literal("spot_cancel"),
52
+ orderId: z.number(),
53
+ })
54
+ .strict();
55
+ const pmOpen = z
56
+ .object({
57
+ type: z.literal("pm_open"),
58
+ source: z.string().min(1),
59
+ slug: z.string().min(1),
60
+ outcomeExternalMarketId: z.string().min(1),
61
+ stakeMusd: z.number().positive(),
62
+ confidence: z.number().min(0).max(1).optional(),
63
+ rationaleSummary: z.string().optional(),
64
+ })
65
+ .strict();
66
+ export const actionSchema = z.discriminatedUnion("type", [
67
+ futuresOpen,
68
+ futuresClose,
69
+ futuresSetSltp,
70
+ spotOrder,
71
+ spotCancel,
72
+ pmOpen,
73
+ ]);
74
+ const decisionSchema = z
75
+ .object({
76
+ decision: z.enum(["skip", "act"]),
77
+ confidence: z.number().min(0).max(1).optional(),
78
+ reason: z.string().optional(),
79
+ actions: z.array(actionSchema).default([]),
80
+ })
81
+ .strict();
82
+ // Pull a JSON object out of a model response that may be fenced or wrapped.
83
+ function coerceJson(text) {
84
+ let s = text.trim();
85
+ const fence = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(s);
86
+ if (fence)
87
+ s = fence[1].trim();
88
+ if (!s.startsWith("{")) {
89
+ const i = s.indexOf("{");
90
+ const j = s.lastIndexOf("}");
91
+ if (i >= 0 && j > i)
92
+ s = s.slice(i, j + 1);
93
+ }
94
+ return JSON.parse(s); // throws on invalid JSON -> caller treats as fail-closed
95
+ }
96
+ export function parseDecision(text) {
97
+ let obj;
98
+ try {
99
+ obj = coerceJson(text);
100
+ }
101
+ catch (err) {
102
+ return { ok: false, error: `model output is not valid JSON: ${err instanceof Error ? err.message : String(err)}` };
103
+ }
104
+ const res = decisionSchema.safeParse(obj);
105
+ if (!res.success) {
106
+ return {
107
+ ok: false,
108
+ error: res.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
109
+ };
110
+ }
111
+ const d = res.data;
112
+ // A "skip" decision ignores any actions; an "act" with no actions is a skip.
113
+ const actions = d.decision === "act" ? d.actions : [];
114
+ return {
115
+ ok: true,
116
+ decision: { decision: d.decision, confidence: d.confidence, reason: d.reason, actions },
117
+ };
118
+ }
@@ -0,0 +1,16 @@
1
+ import { AgentSpec, Observation, ProposedAction, QuoteEvidence, ValidationResult } from "./types.js";
2
+ export interface DecisionContext {
3
+ spec: AgentSpec;
4
+ decisionConfidence?: number;
5
+ observation: Observation;
6
+ quote?: QuoteEvidence;
7
+ writesThisCycle: number;
8
+ writesToday: number;
9
+ openCount: number;
10
+ cashAvailableMusd: number | null;
11
+ openMarginMusd: number;
12
+ realizedLossTodayMusd: number;
13
+ targetedPositionIds: number[];
14
+ targetedOrderIds: number[];
15
+ }
16
+ export declare function validateAction(action: ProposedAction, ctx: DecisionContext): ValidationResult;
@@ -0,0 +1,215 @@
1
+ // The decision gate: re-check EVERY proposed action against the spec's hard caps
2
+ // BEFORE any write. The model only proposes; this disposes. Because the caps
3
+ // come from the spec (not the observation or the model), a prompt-injection in
4
+ // market text cannot widen a limit or force a trade. Covers futures + spot + PM.
5
+ import { ok, fail, actionVenue, spotBuyCost, } from "./types.js";
6
+ const SERVER_MAX_LEVERAGE = 20;
7
+ const PM_MIN_STAKE_MUSD = 10; // server minimum prediction-market stake
8
+ export function validateAction(action, ctx) {
9
+ const { spec, observation } = ctx;
10
+ const venue = actionVenue(action);
11
+ if (!spec.venues.includes(venue)) {
12
+ return fail("venue_not_allowed", `venue ${venue} not in [${spec.venues.join(", ")}]`);
13
+ }
14
+ if (spec.sync.requirePollBeforeWrite && !observation.polledBeforeWrite) {
15
+ return fail("no_poll_before_write", "must successfully poll /trades before writing");
16
+ }
17
+ if (ctx.writesThisCycle >= spec.limits.maxWritesPerCycle) {
18
+ return fail("write_budget_exceeded", `maxWritesPerCycle ${spec.limits.maxWritesPerCycle} reached`);
19
+ }
20
+ if (ctx.writesToday >= spec.limits.maxTradesPerDay) {
21
+ return fail("daily_trade_cap", `maxTradesPerDay ${spec.limits.maxTradesPerDay} reached`);
22
+ }
23
+ // Deny-list: an open on a blocked symbol is rejected up front (deny wins over
24
+ // the watchlist). PM opens use a market slug, not a coin symbol, so skip them.
25
+ if (action.type === "futures_open" || action.type === "spot_order") {
26
+ const blocklist = spec.risk.blocklist;
27
+ if (Array.isArray(blocklist) && blocklist.length > 0) {
28
+ const sym = action.symbol.toUpperCase();
29
+ if (blocklist.some((b) => b.toUpperCase() === sym)) {
30
+ return fail("blocked_symbol", `${action.symbol} is on the deny-list`);
31
+ }
32
+ }
33
+ }
34
+ if (action.type === "futures_open") {
35
+ // Daily realized-loss stop: once today's loss hits the cap, open no new risk.
36
+ if (spec.limits.maxDailyLossMusd > 0 &&
37
+ ctx.realizedLossTodayMusd >= spec.limits.maxDailyLossMusd) {
38
+ return fail("daily_loss_cap", `today's realized loss ${ctx.realizedLossTodayMusd} >= ${spec.limits.maxDailyLossMusd}`);
39
+ }
40
+ const entry = observation.watch.find((w) => w.symbol.toUpperCase() === action.symbol.toUpperCase());
41
+ if (!entry)
42
+ return fail("unknown_symbol", `${action.symbol} is not on the watchlist`);
43
+ if (!entry.coinId)
44
+ return fail("unresolved_symbol", `${action.symbol} did not resolve to a coin`);
45
+ if (action.leverage > spec.risk.maxLeverage) {
46
+ return fail("leverage_exceeds_cap", `leverage ${action.leverage} > cap ${spec.risk.maxLeverage}`);
47
+ }
48
+ if (action.leverage > SERVER_MAX_LEVERAGE) {
49
+ return fail("leverage_exceeds_server", `leverage ${action.leverage} > server cap ${SERVER_MAX_LEVERAGE}`);
50
+ }
51
+ if (action.marginMusd > spec.risk.perTradeMarginMusd) {
52
+ return fail("margin_exceeds_cap", `margin ${action.marginMusd} > cap ${spec.risk.perTradeMarginMusd}`);
53
+ }
54
+ // Aggregate exposure ceiling (existing open margin + this cycle) — the cap
55
+ // that perTradeMargin × maxConcurrentPositions would otherwise blow past.
56
+ if (ctx.openMarginMusd + action.marginMusd >
57
+ spec.limits.maxOpenMarginMusd) {
58
+ return fail("open_margin_exceeds_cap", `open margin ${ctx.openMarginMusd} + ${action.marginMusd} > ${spec.limits.maxOpenMarginMusd}`);
59
+ }
60
+ if (ctx.openCount >= spec.risk.maxConcurrentPositions) {
61
+ return fail("max_positions", `already ${ctx.openCount} open >= ${spec.risk.maxConcurrentPositions}`);
62
+ }
63
+ if (ctx.cashAvailableMusd != null &&
64
+ action.marginMusd > ctx.cashAvailableMusd) {
65
+ return fail("insufficient_balance", `margin ${action.marginMusd} > available ${ctx.cashAvailableMusd}`);
66
+ }
67
+ // NOTE: minConfidence keys on the model's SELF-REPORTED confidence — a
68
+ // cooperation hint, NOT an injection-resistant control. The hard caps above
69
+ // (which come from the spec, never the observation) are what actually bind.
70
+ if ((action.confidence ?? ctx.decisionConfidence ?? 0) <
71
+ spec.abstention.minConfidence) {
72
+ return fail("below_min_confidence", `confidence ${action.confidence ?? 0} < min ${spec.abstention.minConfidence}`);
73
+ }
74
+ if (spec.risk.requireStopLoss) {
75
+ const sl = action.stopLossPrice;
76
+ if (sl == null || !Number.isFinite(sl) || sl <= 0) {
77
+ return fail("missing_stop_loss", "requireStopLoss is set but no valid (finite, positive) stopLossPrice was proposed");
78
+ }
79
+ // Side-aware corridor: a long's stop must be BELOW entry, a short's ABOVE.
80
+ // A wrong-side "stop" is a dead trigger that never protects.
81
+ const e = ctx.quote?.entryPrice;
82
+ if (typeof e === "number" && Number.isFinite(e)) {
83
+ if (action.side === "long" && sl >= e) {
84
+ return fail("stop_loss_wrong_side", `long stop ${sl} must be below entry ${e}`);
85
+ }
86
+ if (action.side === "short" && sl <= e) {
87
+ return fail("stop_loss_wrong_side", `short stop ${sl} must be above entry ${e}`);
88
+ }
89
+ }
90
+ }
91
+ if (!ctx.quote)
92
+ return fail("missing_quote", "no quote evidence was fetched for this open");
93
+ if (!ctx.quote.eligible) {
94
+ return fail("quote_ineligible", `quote blocked: ${JSON.stringify(ctx.quote.blockReasons ?? [])}`);
95
+ }
96
+ // FAIL-CLOSED: a missing freshness block is treated as not-fresh.
97
+ if (!ctx.quote.freshness || ctx.quote.freshness.status !== "fresh") {
98
+ return fail("stale_quote", `quote freshness ${ctx.quote.freshness?.status ?? "missing"} (need fresh)`);
99
+ }
100
+ return ok();
101
+ }
102
+ if (action.type === "futures_close" || action.type === "futures_set_sltp") {
103
+ const pos = observation.openPositions.find((p) => p.id === action.positionId && p.venue === "futures");
104
+ if (!pos)
105
+ return fail("unknown_position", `no open futures position ${action.positionId}`);
106
+ // No double-acting on the same position within one cycle.
107
+ if (ctx.targetedPositionIds.includes(action.positionId)) {
108
+ return fail("position_already_targeted", `position ${action.positionId} already acted on this cycle`);
109
+ }
110
+ if (action.type === "futures_set_sltp") {
111
+ const hasTrigger = [action.stopLossPrice, action.takeProfitPrice].some((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
112
+ if (!hasTrigger) {
113
+ return fail("sltp_no_op", "futures_set_sltp must set at least one positive stopLossPrice or takeProfitPrice");
114
+ }
115
+ }
116
+ return ok();
117
+ }
118
+ if (action.type === "spot_order") {
119
+ const entry = observation.watch.find((w) => w.symbol.toUpperCase() === action.symbol.toUpperCase());
120
+ if (!entry)
121
+ return fail("unknown_symbol", `${action.symbol} is not on the watchlist`);
122
+ if (!entry.coinId)
123
+ return fail("unresolved_symbol", `${action.symbol} did not resolve to a coin`);
124
+ if (action.orderType === "limit" &&
125
+ !(typeof action.limitPrice === "number" && action.limitPrice > 0)) {
126
+ return fail("missing_limit_price", "a limit order needs a positive limitPrice");
127
+ }
128
+ if (action.orderType === "stop" &&
129
+ !(typeof action.stopPrice === "number" && action.stopPrice > 0)) {
130
+ return fail("missing_stop_price", "a stop order needs a positive stopPrice");
131
+ }
132
+ // A BUY opens new risk: daily-loss stop, confidence, per-trade notional, cash.
133
+ if (action.side === "buy") {
134
+ if (spec.limits.maxDailyLossMusd > 0 &&
135
+ ctx.realizedLossTodayMusd >= spec.limits.maxDailyLossMusd) {
136
+ return fail("daily_loss_cap", `today's realized loss ${ctx.realizedLossTodayMusd} >= ${spec.limits.maxDailyLossMusd}`);
137
+ }
138
+ if ((action.confidence ?? ctx.decisionConfidence ?? 0) <
139
+ spec.abstention.minConfidence) {
140
+ return fail("below_min_confidence", `confidence ${action.confidence ?? 0} < min ${spec.abstention.minConfidence}`);
141
+ }
142
+ // Size the BUY with the SAME helper the runner uses to decrement cash, so
143
+ // the gate and the running-cash accounting never diverge. FAIL CLOSED: an
144
+ // unpriced market buy (server quote omits executionPrice/estimatedCostMusd)
145
+ // yields undefined -> reject. The old `cost != null && ...` guards SKIPPED
146
+ // both caps in that case (notional + balance), a fail-open we must not keep.
147
+ const cost = spotBuyCost(action, ctx.quote);
148
+ if (!(typeof cost === "number" && cost > 0)) {
149
+ return fail("missing_quote_price", "cannot size spot buy notional without a price");
150
+ }
151
+ if (cost > spec.risk.perTradeMarginMusd) {
152
+ return fail("notional_exceeds_cap", `spot notional ${cost} > per-trade cap ${spec.risk.perTradeMarginMusd}`);
153
+ }
154
+ if (ctx.cashAvailableMusd != null && cost > ctx.cashAvailableMusd) {
155
+ return fail("insufficient_balance", `spot cost ${cost} > available ${ctx.cashAvailableMusd}`);
156
+ }
157
+ }
158
+ if (!ctx.quote)
159
+ return fail("missing_quote", "no quote evidence was fetched for this spot order");
160
+ if (!ctx.quote.eligible) {
161
+ return fail("quote_ineligible", `quote blocked: ${JSON.stringify(ctx.quote.blockReasons ?? [])}`);
162
+ }
163
+ if (!ctx.quote.freshness || ctx.quote.freshness.status !== "fresh") {
164
+ return fail("stale_quote", `quote freshness ${ctx.quote.freshness?.status ?? "missing"} (need fresh)`);
165
+ }
166
+ return ok();
167
+ }
168
+ if (action.type === "spot_cancel") {
169
+ const o = observation.openOrders.find((x) => x.id === action.orderId);
170
+ if (!o)
171
+ return fail("unknown_order", `no open spot order ${action.orderId}`);
172
+ if (ctx.targetedOrderIds.includes(action.orderId)) {
173
+ return fail("order_already_targeted", `order ${action.orderId} already cancelled this cycle`);
174
+ }
175
+ return ok();
176
+ }
177
+ if (action.type === "pm_open") {
178
+ if (spec.limits.maxDailyLossMusd > 0 &&
179
+ ctx.realizedLossTodayMusd >= spec.limits.maxDailyLossMusd) {
180
+ return fail("daily_loss_cap", `today's realized loss ${ctx.realizedLossTodayMusd} >= ${spec.limits.maxDailyLossMusd}`);
181
+ }
182
+ // The model may only open a market that DISCOVERY surfaced this cycle — no
183
+ // hallucinated source/slug. (source/slug are lowercased; outcome is exact.)
184
+ const mkt = observation.pmMarkets.find((m) => m.source === action.source.toLowerCase() &&
185
+ m.slug === action.slug.toLowerCase() &&
186
+ m.outcomeExternalMarketId === action.outcomeExternalMarketId);
187
+ if (!mkt) {
188
+ return fail("pm_market_not_discovered", `${action.source}/${action.slug} is not in the discovered PM markets`);
189
+ }
190
+ if (action.stakeMusd < PM_MIN_STAKE_MUSD) {
191
+ return fail("pm_stake_below_min", `stake ${action.stakeMusd} < $${PM_MIN_STAKE_MUSD} minimum`);
192
+ }
193
+ if (action.stakeMusd > spec.risk.perTradeMarginMusd) {
194
+ return fail("pm_stake_exceeds_cap", `stake ${action.stakeMusd} > per-trade cap ${spec.risk.perTradeMarginMusd}`);
195
+ }
196
+ if (ctx.cashAvailableMusd != null &&
197
+ action.stakeMusd > ctx.cashAvailableMusd) {
198
+ return fail("insufficient_balance", `stake ${action.stakeMusd} > available ${ctx.cashAvailableMusd}`);
199
+ }
200
+ if ((action.confidence ?? ctx.decisionConfidence ?? 0) <
201
+ spec.abstention.minConfidence) {
202
+ return fail("below_min_confidence", `confidence ${action.confidence ?? 0} < min ${spec.abstention.minConfidence}`);
203
+ }
204
+ if (!ctx.quote)
205
+ return fail("missing_quote", "no quote evidence was fetched for this PM open");
206
+ if (!ctx.quote.eligible) {
207
+ return fail("quote_ineligible", `quote blocked: ${JSON.stringify(ctx.quote.blockReasons ?? [])}`);
208
+ }
209
+ if (!ctx.quote.freshness || ctx.quote.freshness.status !== "fresh") {
210
+ return fail("stale_quote", `quote freshness ${ctx.quote.freshness?.status ?? "missing"} (need fresh)`);
211
+ }
212
+ return ok();
213
+ }
214
+ return fail("unknown_action", "unsupported action type");
215
+ }
@@ -0,0 +1,10 @@
1
+ export { runCycle, type RunnerDeps } from "./runner.js";
2
+ export { selectProvider, type ProviderEnv, type Provider } from "./providers.js";
3
+ export { CoinRithmClient } from "./client.js";
4
+ export { loadAgent, buildSpec, type LoadedAgent } from "./skill.js";
5
+ export { resolveAgent } from "./resolve.js";
6
+ export { validateSkill, type SkillValidationMode } from "./skillValidator.js";
7
+ export { newState, rollDay } from "./state.js";
8
+ export { makeRunId } from "./runEvidence.js";
9
+ export { parseCadenceMs } from "./util.js";
10
+ export type { AgentSpec, RunState, CycleResult, PlannedAction, Venue, ProviderName, ModelConfig, } from "./types.js";
@@ -0,0 +1,16 @@
1
+ // Public engine surface for an external scheduler (the hosted "free run").
2
+ // The runner is storage-agnostic: runCycle takes { spec, mergedProse, state } as
3
+ // plain objects and saveState(undefined) no-ops, so a scheduler can load state
4
+ // from a DB, run one cycle, and persist the mutated state itself — no file I/O.
5
+ //
6
+ // This barrel is the ONE import a host scheduler needs; it re-exports only the
7
+ // stable engine pieces, never the CLI.
8
+ export { runCycle } from "./runner.js";
9
+ export { selectProvider } from "./providers.js";
10
+ export { CoinRithmClient } from "./client.js";
11
+ export { loadAgent, buildSpec } from "./skill.js";
12
+ export { resolveAgent } from "./resolve.js";
13
+ export { validateSkill } from "./skillValidator.js";
14
+ export { newState, rollDay } from "./state.js";
15
+ export { makeRunId } from "./runEvidence.js";
16
+ export { parseCadenceMs } from "./util.js";
@@ -0,0 +1,4 @@
1
+ export declare const asObj: (v: unknown) => Record<string, unknown>;
2
+ export declare const asArr: (v: unknown) => unknown[];
3
+ export declare const asNum: (v: unknown) => number | undefined;
4
+ export declare const asStr: (v: unknown) => string | undefined;
@@ -0,0 +1,5 @@
1
+ // Defensive extractors for untyped API JSON.
2
+ export const asObj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
3
+ export const asArr = (v) => (Array.isArray(v) ? v : []);
4
+ export const asNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
5
+ export const asStr = (v) => typeof v === "string" ? v : undefined;
@@ -0,0 +1,5 @@
1
+ export interface Frontmatter {
2
+ data: Record<string, unknown>;
3
+ body: string;
4
+ }
5
+ export declare function parseFrontmatter(text: string): Frontmatter;
@@ -0,0 +1,19 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ export function parseFrontmatter(text) {
3
+ const src = text.replace(/\r\n/g, "\n");
4
+ const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(src);
5
+ if (!m) {
6
+ throw new Error("skill file has no YAML frontmatter (expected a `---` block at the top)");
7
+ }
8
+ let data;
9
+ try {
10
+ data = parseYaml(m[1]);
11
+ }
12
+ catch (err) {
13
+ throw new Error(`skill frontmatter is not valid YAML: ${err instanceof Error ? err.message : String(err)}`);
14
+ }
15
+ if (data == null || typeof data !== "object" || Array.isArray(data)) {
16
+ throw new Error("skill frontmatter must be a YAML mapping (key: value pairs)");
17
+ }
18
+ return { data: data, body: (m[2] ?? "").trim() };
19
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ // coinrithm-agent CLI entrypoint.
3
+ import { main } from "./cli.js";
4
+ main(process.argv.slice(2))
5
+ .then((code) => process.exit(code))
6
+ .catch((err) => {
7
+ // eslint-disable-next-line no-console
8
+ console.error(err instanceof Error ? err.message : String(err));
9
+ process.exit(1);
10
+ });
@@ -0,0 +1,44 @@
1
+ export interface Candle {
2
+ open: number;
3
+ high: number;
4
+ low: number;
5
+ close: number;
6
+ volume?: number;
7
+ }
8
+ export declare function sma(values: number[], period: number): number | null;
9
+ export declare function ema(values: number[], period: number): number | null;
10
+ export declare function rsi(closes: number[], period?: number): number | null;
11
+ export declare function atr(candles: Candle[], period?: number): number | null;
12
+ export interface Bollinger {
13
+ upper: number;
14
+ mid: number;
15
+ lower: number;
16
+ }
17
+ export declare function bollinger(closes: number[], period?: number, mult?: number): Bollinger | null;
18
+ export interface HighLow {
19
+ high: number;
20
+ low: number;
21
+ }
22
+ export declare function recentHighLow(candles: Candle[], lookback: number): HighLow | null;
23
+ export interface IndicatorSet {
24
+ asOfClose: number;
25
+ rsi14: number | null;
26
+ ema20: number | null;
27
+ ema50: number | null;
28
+ atr14: number | null;
29
+ bollinger: Bollinger | null;
30
+ recent20: HighLow | null;
31
+ aboveEma20: boolean | null;
32
+ ema20AboveEma50: boolean | null;
33
+ brokeRecentHigh: boolean | null;
34
+ brokeRecentLow: boolean | null;
35
+ }
36
+ export interface IndicatorOpts {
37
+ rsiPeriod?: number;
38
+ emaFast?: number;
39
+ emaSlow?: number;
40
+ atrPeriod?: number;
41
+ bbPeriod?: number;
42
+ breakoutLookback?: number;
43
+ }
44
+ export declare function computeIndicators(candles: Candle[], opts?: IndicatorOpts): IndicatorSet | null;