@coinrithm/mcp-trading 0.2.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.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -2
- package/dist/agent/act.d.ts +4 -0
- package/dist/agent/act.js +58 -9
- package/dist/agent/capabilityGuard.d.ts +2 -0
- package/dist/agent/capabilityGuard.js +131 -0
- package/dist/agent/cli.d.ts +21 -0
- package/dist/agent/cli.js +28 -6
- package/dist/agent/client.d.ts +107 -0
- package/dist/agent/client.js +26 -0
- package/dist/agent/decision.d.ts +137 -0
- package/dist/agent/decision.js +37 -4
- package/dist/agent/decisionValidator.d.ts +16 -0
- package/dist/agent/decisionValidator.js +118 -10
- package/dist/agent/engine.d.ts +10 -0
- package/dist/agent/engine.js +16 -0
- package/dist/agent/extract.d.ts +4 -0
- package/dist/agent/frontmatter.d.ts +5 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/indicators.d.ts +44 -0
- package/dist/agent/indicators.js +135 -0
- package/dist/agent/manifest.d.ts +15 -0
- package/dist/agent/mergeRules.d.ts +11 -0
- package/dist/agent/observe.d.ts +7 -0
- package/dist/agent/observe.js +142 -8
- package/dist/agent/prompt.d.ts +3 -0
- package/dist/agent/prompt.js +39 -8
- package/dist/agent/providers.d.ts +25 -0
- package/dist/agent/providers.js +10 -1
- package/dist/agent/resolve.d.ts +11 -0
- package/dist/agent/resolve.js +12 -3
- package/dist/agent/runEvidence.d.ts +6 -0
- package/dist/agent/runner.d.ts +19 -0
- package/dist/agent/runner.js +102 -19
- package/dist/agent/skill.d.ts +12 -0
- package/dist/agent/skill.js +6 -2
- package/dist/agent/skillValidator.d.ts +7 -0
- package/dist/agent/state.d.ts +7 -0
- package/dist/agent/state.js +3 -1
- package/dist/agent/strictLint.d.ts +3 -0
- package/dist/agent/strictLint.js +2 -1
- package/dist/agent/templates.d.ts +14 -0
- package/dist/agent/types.d.ts +286 -0
- package/dist/agent/types.js +39 -1
- package/dist/agent/util.d.ts +13 -0
- package/dist/agent/util.js +4 -0
- package/dist/agent/version.d.ts +11 -0
- package/dist/agent/version.js +1 -1
- package/dist/client.d.ts +162 -0
- package/dist/http.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/tools.d.ts +3 -0
- package/dist/version.d.ts +1 -0
- package/package.json +7 -2
package/dist/agent/decision.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Parse the model's single text response into a strict, structured Decision.
|
|
2
|
-
//
|
|
3
|
-
// action type, a free-form endpoint/tool name, extra unknown
|
|
4
|
-
// missing required field — fails closed (the runner
|
|
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
5
|
import { z } from "zod";
|
|
6
6
|
const futuresOpen = z
|
|
7
7
|
.object({
|
|
@@ -33,10 +33,43 @@ const futuresSetSltp = z
|
|
|
33
33
|
takeProfitPrice: z.number().nullable().optional(),
|
|
34
34
|
})
|
|
35
35
|
.strict();
|
|
36
|
-
const
|
|
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", [
|
|
37
67
|
futuresOpen,
|
|
38
68
|
futuresClose,
|
|
39
69
|
futuresSetSltp,
|
|
70
|
+
spotOrder,
|
|
71
|
+
spotCancel,
|
|
72
|
+
pmOpen,
|
|
40
73
|
]);
|
|
41
74
|
const decisionSchema = z
|
|
42
75
|
.object({
|
|
@@ -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;
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// The decision gate: re-check EVERY proposed action against the spec's hard caps
|
|
2
2
|
// BEFORE any write. The model only proposes; this disposes. Because the caps
|
|
3
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.
|
|
5
|
-
import { ok, fail, actionVenue, } from "./types.js";
|
|
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
6
|
const SERVER_MAX_LEVERAGE = 20;
|
|
7
|
+
const PM_MIN_STAKE_MUSD = 10; // server minimum prediction-market stake
|
|
7
8
|
export function validateAction(action, ctx) {
|
|
8
9
|
const { spec, observation } = ctx;
|
|
9
|
-
// v1 scope: futures only.
|
|
10
|
-
if (action.type === "spot_order" || action.type === "spot_cancel" || action.type === "pm_open") {
|
|
11
|
-
return fail("out_of_scope_v1", `action "${action.type}" is out of v1 scope (futures only)`);
|
|
12
|
-
}
|
|
13
10
|
const venue = actionVenue(action);
|
|
14
11
|
if (!spec.venues.includes(venue)) {
|
|
15
12
|
return fail("venue_not_allowed", `venue ${venue} not in [${spec.venues.join(", ")}]`);
|
|
@@ -23,9 +20,21 @@ export function validateAction(action, ctx) {
|
|
|
23
20
|
if (ctx.writesToday >= spec.limits.maxTradesPerDay) {
|
|
24
21
|
return fail("daily_trade_cap", `maxTradesPerDay ${spec.limits.maxTradesPerDay} reached`);
|
|
25
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
|
+
}
|
|
26
34
|
if (action.type === "futures_open") {
|
|
27
35
|
// Daily realized-loss stop: once today's loss hits the cap, open no new risk.
|
|
28
|
-
if (spec.limits.maxDailyLossMusd > 0 &&
|
|
36
|
+
if (spec.limits.maxDailyLossMusd > 0 &&
|
|
37
|
+
ctx.realizedLossTodayMusd >= spec.limits.maxDailyLossMusd) {
|
|
29
38
|
return fail("daily_loss_cap", `today's realized loss ${ctx.realizedLossTodayMusd} >= ${spec.limits.maxDailyLossMusd}`);
|
|
30
39
|
}
|
|
31
40
|
const entry = observation.watch.find((w) => w.symbol.toUpperCase() === action.symbol.toUpperCase());
|
|
@@ -44,19 +53,22 @@ export function validateAction(action, ctx) {
|
|
|
44
53
|
}
|
|
45
54
|
// Aggregate exposure ceiling (existing open margin + this cycle) — the cap
|
|
46
55
|
// that perTradeMargin × maxConcurrentPositions would otherwise blow past.
|
|
47
|
-
if (ctx.openMarginMusd + action.marginMusd >
|
|
56
|
+
if (ctx.openMarginMusd + action.marginMusd >
|
|
57
|
+
spec.limits.maxOpenMarginMusd) {
|
|
48
58
|
return fail("open_margin_exceeds_cap", `open margin ${ctx.openMarginMusd} + ${action.marginMusd} > ${spec.limits.maxOpenMarginMusd}`);
|
|
49
59
|
}
|
|
50
60
|
if (ctx.openCount >= spec.risk.maxConcurrentPositions) {
|
|
51
61
|
return fail("max_positions", `already ${ctx.openCount} open >= ${spec.risk.maxConcurrentPositions}`);
|
|
52
62
|
}
|
|
53
|
-
if (ctx.cashAvailableMusd != null &&
|
|
63
|
+
if (ctx.cashAvailableMusd != null &&
|
|
64
|
+
action.marginMusd > ctx.cashAvailableMusd) {
|
|
54
65
|
return fail("insufficient_balance", `margin ${action.marginMusd} > available ${ctx.cashAvailableMusd}`);
|
|
55
66
|
}
|
|
56
67
|
// NOTE: minConfidence keys on the model's SELF-REPORTED confidence — a
|
|
57
68
|
// cooperation hint, NOT an injection-resistant control. The hard caps above
|
|
58
69
|
// (which come from the spec, never the observation) are what actually bind.
|
|
59
|
-
if ((action.confidence ?? 0) <
|
|
70
|
+
if ((action.confidence ?? ctx.decisionConfidence ?? 0) <
|
|
71
|
+
spec.abstention.minConfidence) {
|
|
60
72
|
return fail("below_min_confidence", `confidence ${action.confidence ?? 0} < min ${spec.abstention.minConfidence}`);
|
|
61
73
|
}
|
|
62
74
|
if (spec.risk.requireStopLoss) {
|
|
@@ -103,5 +115,101 @@ export function validateAction(action, ctx) {
|
|
|
103
115
|
}
|
|
104
116
|
return ok();
|
|
105
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
|
+
}
|
|
106
214
|
return fail("unknown_action", "unsupported action type");
|
|
107
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,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;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Deterministic technical-indicator math — pure functions over OHLC candles.
|
|
2
|
+
//
|
|
3
|
+
// Probe-First note: this module is JUST math (no network). It is the
|
|
4
|
+
// runner-computed half of the `indicators` capability (types.ts) — an agent
|
|
5
|
+
// that declares it gets compact, model-friendly signal (RSI/EMA/ATR/Bollinger/
|
|
6
|
+
// breakout levels) instead of raw bars the free-tier brain cannot reason over.
|
|
7
|
+
// Wiring observe() to FETCH the candles is gated on a live probe of
|
|
8
|
+
// GET /api/agent/market/:coinId/candles (see DECISIONS D16); the math here is
|
|
9
|
+
// independently verifiable and shipped ahead of that.
|
|
10
|
+
//
|
|
11
|
+
// Every function returns null when there are too few candles, so callers can
|
|
12
|
+
// omit an indicator from the observation rather than emit a misleading number.
|
|
13
|
+
const closesOf = (c) => c.map((x) => x.close);
|
|
14
|
+
const finite = (n) => Number.isFinite(n);
|
|
15
|
+
// Simple moving average of the last `period` values.
|
|
16
|
+
export function sma(values, period) {
|
|
17
|
+
if (period <= 0 || values.length < period)
|
|
18
|
+
return null;
|
|
19
|
+
const slice = values.slice(-period);
|
|
20
|
+
const sum = slice.reduce((a, b) => a + b, 0);
|
|
21
|
+
return finite(sum) ? sum / period : null;
|
|
22
|
+
}
|
|
23
|
+
// Exponential moving average, seeded with the SMA of the first `period` values
|
|
24
|
+
// (the standard, deterministic seeding).
|
|
25
|
+
export function ema(values, period) {
|
|
26
|
+
if (period <= 0 || values.length < period)
|
|
27
|
+
return null;
|
|
28
|
+
const k = 2 / (period + 1);
|
|
29
|
+
let prev = values.slice(0, period).reduce((a, b) => a + b, 0) / period;
|
|
30
|
+
for (let i = period; i < values.length; i++) {
|
|
31
|
+
prev = values[i] * k + prev * (1 - k);
|
|
32
|
+
}
|
|
33
|
+
return finite(prev) ? prev : null;
|
|
34
|
+
}
|
|
35
|
+
// Wilder's RSI over `period` (default 14). 100 = only gains, 0 = only losses.
|
|
36
|
+
export function rsi(closes, period = 14) {
|
|
37
|
+
if (period <= 0 || closes.length < period + 1)
|
|
38
|
+
return null;
|
|
39
|
+
let gain = 0;
|
|
40
|
+
let loss = 0;
|
|
41
|
+
for (let i = 1; i <= period; i++) {
|
|
42
|
+
const ch = closes[i] - closes[i - 1];
|
|
43
|
+
if (ch >= 0)
|
|
44
|
+
gain += ch;
|
|
45
|
+
else
|
|
46
|
+
loss -= ch;
|
|
47
|
+
}
|
|
48
|
+
let avgGain = gain / period;
|
|
49
|
+
let avgLoss = loss / period;
|
|
50
|
+
for (let i = period + 1; i < closes.length; i++) {
|
|
51
|
+
const ch = closes[i] - closes[i - 1];
|
|
52
|
+
const g = ch >= 0 ? ch : 0;
|
|
53
|
+
const l = ch < 0 ? -ch : 0;
|
|
54
|
+
avgGain = (avgGain * (period - 1) + g) / period;
|
|
55
|
+
avgLoss = (avgLoss * (period - 1) + l) / period;
|
|
56
|
+
}
|
|
57
|
+
if (avgLoss === 0)
|
|
58
|
+
return avgGain === 0 ? 50 : 100;
|
|
59
|
+
const rs = avgGain / avgLoss;
|
|
60
|
+
return 100 - 100 / (1 + rs);
|
|
61
|
+
}
|
|
62
|
+
// Wilder's Average True Range over `period` (default 14) — a volatility gauge.
|
|
63
|
+
export function atr(candles, period = 14) {
|
|
64
|
+
if (period <= 0 || candles.length < period + 1)
|
|
65
|
+
return null;
|
|
66
|
+
const tr = [];
|
|
67
|
+
for (let i = 1; i < candles.length; i++) {
|
|
68
|
+
const h = candles[i].high;
|
|
69
|
+
const l = candles[i].low;
|
|
70
|
+
const pc = candles[i - 1].close;
|
|
71
|
+
tr.push(Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc)));
|
|
72
|
+
}
|
|
73
|
+
if (tr.length < period)
|
|
74
|
+
return null;
|
|
75
|
+
let prev = tr.slice(0, period).reduce((a, b) => a + b, 0) / period;
|
|
76
|
+
for (let i = period; i < tr.length; i++) {
|
|
77
|
+
prev = (prev * (period - 1) + tr[i]) / period;
|
|
78
|
+
}
|
|
79
|
+
return finite(prev) ? prev : null;
|
|
80
|
+
}
|
|
81
|
+
// Bollinger bands: SMA(period) ± mult·stddev over the last `period` closes.
|
|
82
|
+
export function bollinger(closes, period = 20, mult = 2) {
|
|
83
|
+
if (period <= 0 || closes.length < period)
|
|
84
|
+
return null;
|
|
85
|
+
const slice = closes.slice(-period);
|
|
86
|
+
const mid = slice.reduce((a, b) => a + b, 0) / period;
|
|
87
|
+
const variance = slice.reduce((a, b) => a + (b - mid) ** 2, 0) / period;
|
|
88
|
+
const sd = Math.sqrt(variance);
|
|
89
|
+
if (!finite(mid) || !finite(sd))
|
|
90
|
+
return null;
|
|
91
|
+
return { upper: mid + mult * sd, mid, lower: mid - mult * sd };
|
|
92
|
+
}
|
|
93
|
+
// Highest high / lowest low over the last `lookback` candles (breakout levels).
|
|
94
|
+
export function recentHighLow(candles, lookback) {
|
|
95
|
+
if (lookback <= 0 || candles.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
const slice = candles.slice(-lookback);
|
|
98
|
+
let high = -Infinity;
|
|
99
|
+
let low = Infinity;
|
|
100
|
+
for (const c of slice) {
|
|
101
|
+
if (c.high > high)
|
|
102
|
+
high = c.high;
|
|
103
|
+
if (c.low < low)
|
|
104
|
+
low = c.low;
|
|
105
|
+
}
|
|
106
|
+
return finite(high) && finite(low) ? { high, low } : null;
|
|
107
|
+
}
|
|
108
|
+
// Compute the compact indicator bundle the runner injects into the observation
|
|
109
|
+
// for an agent that declared the `indicators` capability. The breakout flags
|
|
110
|
+
// compare the latest close against the high/low of the PRECEDING window (so a
|
|
111
|
+
// candle closing at a new 20-bar high reads as a breakout, not a tautology).
|
|
112
|
+
export function computeIndicators(candles, opts = {}) {
|
|
113
|
+
if (candles.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
const closes = closesOf(candles);
|
|
116
|
+
const close = closes[closes.length - 1];
|
|
117
|
+
const { rsiPeriod = 14, emaFast = 20, emaSlow = 50, atrPeriod = 14, bbPeriod = 20, breakoutLookback = 20, } = opts;
|
|
118
|
+
const ema20 = ema(closes, emaFast);
|
|
119
|
+
const ema50 = ema(closes, emaSlow);
|
|
120
|
+
// Breakout vs the window BEFORE the latest candle (exclude the current bar).
|
|
121
|
+
const prior = recentHighLow(candles.slice(0, -1), breakoutLookback);
|
|
122
|
+
return {
|
|
123
|
+
asOfClose: close,
|
|
124
|
+
rsi14: rsi(closes, rsiPeriod),
|
|
125
|
+
ema20,
|
|
126
|
+
ema50,
|
|
127
|
+
atr14: atr(candles, atrPeriod),
|
|
128
|
+
bollinger: bollinger(closes, bbPeriod),
|
|
129
|
+
recent20: recentHighLow(candles, breakoutLookback),
|
|
130
|
+
aboveEma20: ema20 == null ? null : close > ema20,
|
|
131
|
+
ema20AboveEma50: ema20 == null || ema50 == null ? null : ema20 > ema50,
|
|
132
|
+
brokeRecentHigh: prior == null ? null : close >= prior.high,
|
|
133
|
+
brokeRecentLow: prior == null ? null : close <= prior.low,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AgentSpec, ResolvedAgent, Provenance } from "./types.js";
|
|
2
|
+
export interface AgentManifest {
|
|
3
|
+
schema: string;
|
|
4
|
+
spec: string;
|
|
5
|
+
resolverVersion: string;
|
|
6
|
+
runnerVersion: string;
|
|
7
|
+
resolvedConfig: Record<string, unknown>;
|
|
8
|
+
resolvedSpec: AgentSpec;
|
|
9
|
+
provenance: Provenance;
|
|
10
|
+
contentHashes: Record<string, string>;
|
|
11
|
+
configHash: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildManifest(resolved: ResolvedAgent, spec: AgentSpec): AgentManifest;
|
|
14
|
+
export declare function serializeManifest(manifest: AgentManifest): string;
|
|
15
|
+
export declare function writeManifest(agentDir: string, manifest: AgentManifest): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResolveIssue } from "./types.js";
|
|
2
|
+
export type CapDirection = "lower" | "higher" | "true";
|
|
3
|
+
export declare const RISK_CAPS: Record<string, CapDirection>;
|
|
4
|
+
export declare const LIMIT_CAPS: Record<string, CapDirection>;
|
|
5
|
+
export declare function isAtLeastAsRestrictive(dir: CapDirection, candidate: unknown, base: unknown): boolean;
|
|
6
|
+
export declare function mostRestrictive(dir: CapDirection, a: unknown, b: unknown): unknown;
|
|
7
|
+
export interface CapMergeResult {
|
|
8
|
+
merged: Record<string, unknown>;
|
|
9
|
+
issues: ResolveIssue[];
|
|
10
|
+
}
|
|
11
|
+
export declare function mergeCapPatch(base: Record<string, unknown>, patch: Record<string, unknown>, caps: Record<string, CapDirection>, sourceLabel: string): CapMergeResult;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CoinRithmClient } from "./client.js";
|
|
2
|
+
import { AgentSpec, RunState, Observation, AgentTrace } from "./types.js";
|
|
3
|
+
export interface ObserveOutput {
|
|
4
|
+
observation: Observation;
|
|
5
|
+
skip?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function observe(client: CoinRithmClient, spec: AgentSpec, state: RunState, trace?: AgentTrace): Promise<ObserveOutput>;
|