@coinrithm/mcp-trading 0.7.6 → 0.7.8
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 +134 -0
- package/README.md +37 -8
- package/dist/agent/act.js +19 -6
- package/dist/agent/capitalSizing.d.ts +32 -0
- package/dist/agent/capitalSizing.js +257 -0
- package/dist/agent/client.d.ts +2 -0
- package/dist/agent/client.js +4 -0
- package/dist/agent/decision.d.ts +392 -0
- package/dist/agent/decision.js +177 -0
- package/dist/agent/decisionProbe.d.ts +17 -0
- package/dist/agent/decisionProbe.js +70 -0
- package/dist/agent/decisionReceipt.d.ts +45 -0
- package/dist/agent/decisionReceipt.js +595 -0
- package/dist/agent/decisionValidator.d.ts +19 -2
- package/dist/agent/decisionValidator.js +97 -3
- package/dist/agent/engine.d.ts +5 -1
- package/dist/agent/engine.js +8 -1
- package/dist/agent/observe.js +194 -35
- package/dist/agent/pmContext.d.ts +13 -0
- package/dist/agent/pmContext.js +136 -0
- package/dist/agent/prompt.d.ts +14 -2
- package/dist/agent/prompt.js +232 -35
- package/dist/agent/providerCapabilities.d.ts +23 -0
- package/dist/agent/providerCapabilities.js +105 -0
- package/dist/agent/providers.d.ts +29 -1
- package/dist/agent/providers.js +159 -88
- package/dist/agent/resolve.d.ts +1 -1
- package/dist/agent/resolve.js +21 -1
- package/dist/agent/runner.d.ts +4 -1
- package/dist/agent/runner.js +418 -47
- package/dist/agent/scorecard.js +7 -1
- package/dist/agent/skill.js +23 -0
- package/dist/agent/skillValidator.d.ts +1 -0
- package/dist/agent/skillValidator.js +63 -0
- package/dist/agent/state.js +7 -1
- package/dist/agent/strictLint.js +20 -0
- package/dist/agent/thesis.d.ts +40 -0
- package/dist/agent/thesis.js +319 -0
- package/dist/agent/types.d.ts +151 -0
- package/dist/http.js +21 -0
- package/dist/tools.js +10 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,140 @@ ships two binaries — `coinrithm-mcp` (the MCP server) and `coinrithm-agent` (t
|
|
|
5
5
|
self-host agent runner) — versioned together. The CoinRithm **API contract** is
|
|
6
6
|
versioned separately (see `openapi.yaml` `info.version`, currently `1.7.0`).
|
|
7
7
|
|
|
8
|
+
## 0.7.8
|
|
9
|
+
|
|
10
|
+
Runner decision-quality, evidence and paper-capital release. Additive: no MCP
|
|
11
|
+
tool was renamed or removed, and the API **contract stays 1.7.0**. This release
|
|
12
|
+
contains all package changes since published 0.7.7 (`gitHead` `80d0cae`), not
|
|
13
|
+
just the previously listed thesis work.
|
|
14
|
+
|
|
15
|
+
**Thesis exits.** Every opening action (`futures_open`, `spot_order`,
|
|
16
|
+
`pm_open`) now carries a `thesis`: a one-sentence summary plus an
|
|
17
|
+
`invalidation` with at least one machine-checkable condition (`priceBelow` /
|
|
18
|
+
`priceAbove` for coins, `probabilityBelow` / `probabilityAbove` for prediction
|
|
19
|
+
markets, a `maxHoldMinutes` time stop, and a free-text `catalyst` the model
|
|
20
|
+
re-judges itself). The runner binds the thesis to the position the server
|
|
21
|
+
returns, sanitized side-aware (a rising price never invalidates a long; a
|
|
22
|
+
wrong-side level is dropped rather than re-signed; the time stop is clamped to
|
|
23
|
+
60 minutes .. 30 days), persists it in the run state (`RunState.theses`, the
|
|
24
|
+
same state file / `agent_state` JSON as before, no schema change) and
|
|
25
|
+
re-evaluates it every cycle. A futures position whose price level or time stop
|
|
26
|
+
is breached is closed by the runner before the model is asked anything, logged
|
|
27
|
+
as a `thesis_invalidated` exit with its own idempotency key, after the
|
|
28
|
+
kill-switch and drawdown checks and never instead of them. Prediction-market
|
|
29
|
+
positions have no close endpoint, so a broken PM thesis is surfaced to the
|
|
30
|
+
model instead (do not add, let it settle). The parser is tolerant (a malformed
|
|
31
|
+
thesis never fails the open; a thesis copied onto a close is ignored) and the
|
|
32
|
+
structured-output schema requires it, so schema-enforced hosted models always
|
|
33
|
+
emit one.
|
|
34
|
+
|
|
35
|
+
**Fundamentals in the observation.** Each watch entry now carries
|
|
36
|
+
`fundamentals` sourced only from calls the runner already makes: `categories`,
|
|
37
|
+
`marketCapRank` and `marketCapUsd` from the market context; `volume24hUsd` from
|
|
38
|
+
the candles the `indicators` capability already fetches (live-probed
|
|
39
|
+
2026-09-02: each bar's `v` is a rolling 24h volume, so the latest bar is the
|
|
40
|
+
24h figure, never the sum); and up to three `headlines` with `publishedAt`
|
|
41
|
+
timestamps from the one `news` call, attributed through the curated coin-news
|
|
42
|
+
graph. Discovered PM markets carry `endDate` and `liquidityUsd`; open PM
|
|
43
|
+
positions carry their title, side, entry and current probability and
|
|
44
|
+
`openedAt`; open futures positions carry `openedAt`. The system prompt states
|
|
45
|
+
the thesis contract, the runner-enforced exit and how to grade a trade on the
|
|
46
|
+
fundamentals. Not carried, because no agent endpoint serves them: an "about"
|
|
47
|
+
text per coin, a 24h probability change and a cross-venue divergence per PM
|
|
48
|
+
market.
|
|
49
|
+
|
|
50
|
+
**Fix:** the public movers feed serializes `change24h` / `currentPrice` as
|
|
51
|
+
decimal strings; the universe-scan context rows read them strictly as numbers
|
|
52
|
+
and shipped `undefined` for every mover.
|
|
53
|
+
|
|
54
|
+
**Opt-in equity-based paper sizing.** A runner can size entries from a
|
|
55
|
+
conservative fraction of its independently attributed paper book instead of a
|
|
56
|
+
fixed stake/margin. The book is accepted only when wallet identity, cash
|
|
57
|
+
partitions, held-position attribution and spot-mark coverage reconcile. Quotes
|
|
58
|
+
then enforce per-entry, per-symbol, deployed-capital and daily-entry limits;
|
|
59
|
+
fee buffers and the API's fee-inclusive quote evidence are included. Any
|
|
60
|
+
missing or inconsistent evidence fails closed. Legacy positions on a different
|
|
61
|
+
book remain visible for management but never inflate the current book's buying
|
|
62
|
+
power.
|
|
63
|
+
|
|
64
|
+
**Prediction-market decisions use executable economics.** PM opens now reject
|
|
65
|
+
an invalid raw probability and a model forecast that does not clear the quoted
|
|
66
|
+
entry price. Forecast edge is measured against the actual fee/slippage-adjusted
|
|
67
|
+
fill, not the headline market probability. Quote-expiry outcomes are recorded
|
|
68
|
+
separately from risk/balance rejection, and futures risk/reward validation uses
|
|
69
|
+
fee-inclusive entry and stop economics.
|
|
70
|
+
|
|
71
|
+
**Decision evidence is structured and bounded.** Cycles can expose a sanitized,
|
|
72
|
+
partial private decision-input record: configuration and observation
|
|
73
|
+
fingerprints, daily budget and guard state, plus bounded observation rows with
|
|
74
|
+
explicit omission counts. It is not a prompt, transcript, raw model output or
|
|
75
|
+
hidden reasoning record. The runner also reports quote/validation evidence for
|
|
76
|
+
abstained, forecast-only and quote-expired PM opportunities. Hosted persistence
|
|
77
|
+
and retention remain the caller's responsibility.
|
|
78
|
+
|
|
79
|
+
**Runtime controls are more faithful.** The model sees the remaining daily
|
|
80
|
+
entry/add budget rather than only static maxima. Entry caps still block new
|
|
81
|
+
risk, while closes and other risk-reducing actions remain available. Direct
|
|
82
|
+
provider HTTP 429 responses are capacity skips rather than model failures, so
|
|
83
|
+
BYO agents do not build a failure streak during ordinary quota pressure.
|
|
84
|
+
Structured-tool decisions remain required where the provider supports that
|
|
85
|
+
contract.
|
|
86
|
+
|
|
87
|
+
**Scorecard fix.** Maximum drawdown now measures decline from starting equity,
|
|
88
|
+
so an immediate loss is no longer hidden by treating the first post-trade point
|
|
89
|
+
as the high-water mark.
|
|
90
|
+
|
|
91
|
+
## 0.7.7
|
|
92
|
+
|
|
93
|
+
Reliability release. Every change here came from a live production failure, not
|
|
94
|
+
from a roadmap. Additive: no tool renamed or removed, and the API **contract
|
|
95
|
+
stays 1.7.0** because nothing on the documented surface changed.
|
|
96
|
+
|
|
97
|
+
**Model requests are now built from a declared capability table, not
|
|
98
|
+
assumptions.** `providerCapabilities.ts` states, per model family, which
|
|
99
|
+
parameter carries the completion budget, whether a non-default temperature is
|
|
100
|
+
allowed, and what extra body fields the family needs. Two failures this fixes:
|
|
101
|
+
|
|
102
|
+
- **OpenAI's current models rejected our requests outright.** `gpt-5*` and
|
|
103
|
+
`o*` refuse `max_tokens` and any non-default `temperature`; they take
|
|
104
|
+
`max_completion_tokens`. The family is detected by MODEL id, not just the
|
|
105
|
+
provider name, so an OpenAI-compatible gateway serving `gpt-5` gets the same
|
|
106
|
+
shape. If you brought your own OpenAI key, this is why it now works.
|
|
107
|
+
- **NVIDIA Nemotron models emitted a think-chain where the JSON decision
|
|
108
|
+
belonged**, which failed every cycle. The `chat_template_kwargs.enable_thinking=false`
|
|
109
|
+
switch and the "detailed thinking off" system hint are now encoded as data
|
|
110
|
+
rather than re-learned by failing.
|
|
111
|
+
|
|
112
|
+
**New: `probeDecisionContract()`.** An HTTP 200 is not proof a route can run an
|
|
113
|
+
agent. Both production failure modes returned 200s: a think-chain in the JSON
|
|
114
|
+
slot, and an empty completion because a reasoning model spent its whole budget
|
|
115
|
+
before answering. The probe sends a canned mini-observation through the REAL
|
|
116
|
+
decision parser at a >=1024 completion allowance and classifies the result as
|
|
117
|
+
`http`, `empty` or `parse`. Use it before adopting any model id; provider
|
|
118
|
+
catalogs list ids that 404 on invoke.
|
|
119
|
+
|
|
120
|
+
**Provider trouble no longer disables an agent.** A permanent-looking model
|
|
121
|
+
error (404/410/decommissioned) used to disable the agent after a threshold. On
|
|
122
|
+
2026-08-26 NVIDIA end-of-lifed an entire model line and 35 agents died on that
|
|
123
|
+
path. The runner now reports a hold and keeps retrying each cadence, recovering
|
|
124
|
+
by itself when the provider does. Disables remain for what deserves them:
|
|
125
|
+
revoked credentials, drawdown, kill-switch, user action.
|
|
126
|
+
|
|
127
|
+
**Failures carry structured metadata.** A failed `decide()` now returns
|
|
128
|
+
`status` and, when the provider sends one, `retryAfterMs` (parsed from
|
|
129
|
+
`Retry-After` in both delta-seconds and HTTP-date form, capped at an hour), so
|
|
130
|
+
a caller can tell a 429 from a 5xx without parsing strings. Error text is
|
|
131
|
+
unchanged.
|
|
132
|
+
|
|
133
|
+
**`ClientConfig.extraHeaders`.** Headers attached to every request, spread
|
|
134
|
+
before auth so they can never clobber it. Self-host has nothing to put here;
|
|
135
|
+
it exists so CoinRithm's own hosted scheduler can present its attestation
|
|
136
|
+
channel.
|
|
137
|
+
|
|
138
|
+
**Model names corrected throughout.** The retired Llama 3.x line is gone from
|
|
139
|
+
the README, the runtime defaults and the `quant-reference` example, which is
|
|
140
|
+
relocked onto `nvidia/nemotron-3-nano-30b-a3b`.
|
|
141
|
+
|
|
8
142
|
## 0.7.6
|
|
9
143
|
|
|
10
144
|
Agent capability release: universe discovery, first-class behavioral guards,
|
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
spot, futures, and prediction markets on
|
|
6
6
|
[CoinRithm](https://coinrithm.com/agentic-trading). No real money, no exchange,
|
|
7
7
|
no risk — a proving ground to show an agent works *before* anything is on the
|
|
8
|
-
line, with a public **Agent Arena** leaderboard
|
|
8
|
+
line, with a public **Agent Arena** leaderboard using a versioned,
|
|
9
|
+
confidence-weighted realized-PnL methodology.
|
|
9
10
|
|
|
10
11
|
**Plus a free prediction-market data surface — no key at all.** The same server
|
|
11
12
|
ships ten keyless `pm_data_*` tools serving CoinRithm's public cross-venue
|
|
@@ -22,7 +23,7 @@ Agents are **OKF bundles** — an open, model-agnostic folder of markdown + YAML
|
|
|
22
23
|
|
|
23
24
|
- **Managed — nothing to install.** Build and deploy an agent in your browser
|
|
24
25
|
with the **Agent Studio** (CoinRithm → My Agents → Studio): fork a house agent
|
|
25
|
-
or write one from scratch, and CoinRithm runs it **free on
|
|
26
|
+
or write one from scratch, and CoinRithm runs it **free on Nemotron 3 Nano 30B**
|
|
26
27
|
(NVIDIA NIM) on an always-on scheduler. The fastest path to a live agent.
|
|
27
28
|
- **Self-host — this package.** Bring your own model key and run the
|
|
28
29
|
`observe→decide→validate→act` loop on your machine, or wire the MCP server
|
|
@@ -83,6 +84,33 @@ key upstream. The Authorization header is **optional** on the hosted endpoint
|
|
|
83
84
|
the ten keyless `pm_data_*` market-data tools work anonymously; every other
|
|
84
85
|
tool requires it. See [`DEPLOY.md`](./DEPLOY.md).
|
|
85
86
|
|
|
87
|
+
## Bring your own model key
|
|
88
|
+
|
|
89
|
+
The hosted Agent Studio runs your agent free on a shared pool of NVIDIA-hosted
|
|
90
|
+
models. That pool is a **fixed budget shared by every hosted agent**, so the
|
|
91
|
+
scheduler floors how often a shared agent may run, and the floor stretches as
|
|
92
|
+
more agents join. Bringing your own model key removes that floor entirely:
|
|
93
|
+
your quota is yours, so there is nothing for us to ration.
|
|
94
|
+
|
|
95
|
+
| | Shared free pool | Your own key |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| Models | the free hosted picks | any model your provider serves |
|
|
98
|
+
| Interval | floored by fleet size | exactly what you configure |
|
|
99
|
+
| Rerouting | we may serve a live alternate when a model is rate-limited | never rerouted, your route is pinned |
|
|
100
|
+
| Cost | free | you pay your provider, not CoinRithm |
|
|
101
|
+
|
|
102
|
+
Providers accepted: `nvidia`, `openai`, `groq`, `anthropic`, and any
|
|
103
|
+
`openai-compatible` endpoint (https base URL required). The key is validated by
|
|
104
|
+
a **live decision probe before the agent is accepted** — a model that cannot
|
|
105
|
+
return a parseable decision is rejected at deploy time rather than failing
|
|
106
|
+
every scheduled cycle. Keys are encrypted at rest and never logged or echoed.
|
|
107
|
+
|
|
108
|
+
Self-hosting through this package works the same way: set the provider's env
|
|
109
|
+
var (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `NVIDIA_API_KEY`, `GROQ_API_KEY`
|
|
110
|
+
or `MODEL_API_KEY`) and the runner builds the request in the shape that
|
|
111
|
+
provider's model family actually accepts. A model key is **never** read from an
|
|
112
|
+
agent file.
|
|
113
|
+
|
|
86
114
|
## Configure (stdio)
|
|
87
115
|
|
|
88
116
|
| Env var | Required | Default | Notes |
|
|
@@ -278,13 +306,14 @@ least that long before retrying.
|
|
|
278
306
|
|
|
279
307
|
## Agent Arena
|
|
280
308
|
|
|
281
|
-
Opted-in agents are publicly
|
|
282
|
-
decided (win/loss) trade is listed (a small-sample asterisk flags thin records;
|
|
283
|
-
the live gate is surfaced as `minDecidedTrades` in the response) at
|
|
309
|
+
Opted-in agents are publicly listed at
|
|
284
310
|
[coinrithm.com](https://coinrithm.com/agentic-trading) — set `agentName` /
|
|
285
311
|
`agentPublic` / `agentModel` on your key to join, then check your standing
|
|
286
|
-
with `get_arena_leaderboard` / `get_arena_agent`.
|
|
287
|
-
|
|
288
|
-
|
|
312
|
+
with `get_arena_leaderboard` / `get_arena_agent`. Under `arena-ranking-v1`,
|
|
313
|
+
five decided trades qualify an agent for normal ordering. Positive realized
|
|
314
|
+
PnL is weighted by the 95% Wilson win-confidence lower bound; non-positive PnL
|
|
315
|
+
is used directly. Agents below five remain listed after qualified agents, and
|
|
316
|
+
fewer than 20 decided trades carries a separate small-sample warning. The API
|
|
317
|
+
returns the full machine-readable `contract` with every board response.
|
|
289
318
|
|
|
290
319
|
stdout is the MCP JSON-RPC channel; this server logs only to stderr.
|
package/dist/agent/act.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
// Act phase: fetch the quote evidence for an open (the runner does this, never
|
|
2
2
|
// the model) and execute a validated action (futures / spot / PM) with an
|
|
3
3
|
// idempotency key.
|
|
4
|
-
import { asObj, asNum
|
|
4
|
+
import { asObj, asNum } from "./extract.js";
|
|
5
|
+
import { freshnessOf } from "./pmContext.js";
|
|
5
6
|
function coinIdFor(observation, symbol) {
|
|
6
7
|
return (observation.watch.find((w) => w.symbol.toUpperCase() === symbol.toUpperCase())?.coinId ?? undefined);
|
|
7
8
|
}
|
|
8
|
-
function freshnessOf(block) {
|
|
9
|
-
const fr = asObj(block.freshness);
|
|
10
|
-
const status = asStr(fr.status);
|
|
11
|
-
return status ? { status, ageSeconds: asNum(fr.ageSeconds) } : undefined;
|
|
12
|
-
}
|
|
13
9
|
// Read-only quote BEFORE any open. Returns ineligible (never throws) on error.
|
|
14
10
|
export async function fetchQuote(client, action, observation, trace) {
|
|
15
11
|
let r;
|
|
@@ -50,7 +46,24 @@ export async function fetchQuote(client, action, observation, trace) {
|
|
|
50
46
|
entryPrice: asNum(d.entryPrice), // futures
|
|
51
47
|
liquidationPrice: asNum(d.liquidationPrice), // futures
|
|
52
48
|
executionPrice: asNum(d.executionPrice), // spot live fill price
|
|
49
|
+
entryProbability: asNum(d.entryProbability), // pm raw probability POINTS
|
|
50
|
+
...(action.type === "pm_open"
|
|
51
|
+
? {
|
|
52
|
+
stakeMusd: asNum(d.stakeMusd),
|
|
53
|
+
sharesEstimate: asNum(d.sharesEstimate),
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
53
56
|
estimatedCostMusd: asNum(d.estimatedCostMusd), // spot gross notional
|
|
57
|
+
...(action.type === "spot_order"
|
|
58
|
+
? { estimatedFeeMusd: asNum(d.estimatedFeeMusd) }
|
|
59
|
+
: {}),
|
|
60
|
+
...(action.type === "futures_open"
|
|
61
|
+
? {
|
|
62
|
+
futuresFeeBps: asNum(asObj(d.executionModel).feeBps),
|
|
63
|
+
estimatedEntryFeeMusd: asNum(asObj(d.executionModel).estimatedEntryFeeMusd),
|
|
64
|
+
cashRequiredMusd: asNum(d.cashRequiredMusd),
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
54
67
|
// Freshness lives in the response's `observation` block (anti-look-ahead).
|
|
55
68
|
freshness: freshnessOf(asObj(d.observation)),
|
|
56
69
|
// PM open-time quality-gate preview (additive; older backends omit it → the
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentSpec, CapitalBook, CapitalSizingAdjustment, Observation, ProposedAction, QuoteEvidence } from "./types.js";
|
|
2
|
+
export declare const CAPITAL_VALUATION_BASIS = "wallet_assets_spot_marked_futures_pm_at_collateral";
|
|
3
|
+
export declare const CAPITAL_FEE_BUFFER_BPS = 10;
|
|
4
|
+
/** Reconcile the bounded open-position reads against the wallet's frozen
|
|
5
|
+
* buckets. A full page is not assumed complete: the collateral checksums must
|
|
6
|
+
* agree. No held mark, unknown status, missing bucket, or mismatched book can
|
|
7
|
+
* silently become zero exposure. Lists retain legacy positions on other books
|
|
8
|
+
* for management; only explicitly attributed current-book rows reconcile its
|
|
9
|
+
* cash and marks. Closed history never contributes unrealized. */
|
|
10
|
+
export declare function deriveCapitalBook(portfolio: unknown, wallet: unknown, futures: unknown, pm: unknown): CapitalBook;
|
|
11
|
+
export interface CapitalBudget {
|
|
12
|
+
cashAvailableMusd: number;
|
|
13
|
+
committedCapitalMusd: number;
|
|
14
|
+
openMarginMusd: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const usesCapitalSizing: (spec: AgentSpec, mechanical?: boolean) => boolean;
|
|
17
|
+
export declare function prepareCapitalAction(action: ProposedAction, spec: AgentSpec, observation: Observation, budget: CapitalBudget): {
|
|
18
|
+
action: ProposedAction;
|
|
19
|
+
adjustment?: CapitalSizingAdjustment;
|
|
20
|
+
rejection?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Quote-bound monetary checks. Drift fails closed; never requote a resized
|
|
23
|
+
* ticket, silently widen a cap, or credit uncertain close/sell proceeds. */
|
|
24
|
+
export declare function validateCapitalAction(action: ProposedAction, spec: AgentSpec, observation: Observation, budget: CapitalBudget, quote?: QuoteEvidence): string | undefined;
|
|
25
|
+
/** Fee-inclusive opt-in reservation; the legacy gross helper stays unchanged.
|
|
26
|
+
* The API quotes a market fill even for a pending limit/stop order. Reserve at
|
|
27
|
+
* least that fee, scaled up if the proposed price requires more notional.
|
|
28
|
+
* This is conservative captured quote evidence, not a future fill guarantee. */
|
|
29
|
+
export declare function capitalSpotBuyCost(action: Extract<ProposedAction, {
|
|
30
|
+
type: "spot_order";
|
|
31
|
+
}>, quote?: QuoteEvidence): number | undefined;
|
|
32
|
+
export declare function capitalCashCost(action: ProposedAction, quote?: QuoteEvidence): number;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Opt-in, deterministic PAPER sizing. No provider, price or database reads.
|
|
2
|
+
import { asNum, asObj } from "./extract.js";
|
|
3
|
+
import { spotBuyCost } from "./types.js";
|
|
4
|
+
import { validateCapitalSizingPolicy } from "./skillValidator.js";
|
|
5
|
+
export const CAPITAL_VALUATION_BASIS = "wallet_assets_spot_marked_futures_pm_at_collateral";
|
|
6
|
+
// Conservative pre-quote runner estimate: 10bp each way, with the exit fee
|
|
7
|
+
// charged on stop notional. Actual API fee evidence is checked after ONE quote.
|
|
8
|
+
// This is not an exchange-fill, funding or stop-execution guarantee.
|
|
9
|
+
export const CAPITAL_FEE_BUFFER_BPS = 10;
|
|
10
|
+
const CENT_TOLERANCE = 0.011;
|
|
11
|
+
const centsDown = (n) => Math.floor(n * 100) / 100;
|
|
12
|
+
const positive = (n) => asNum(n) !== undefined && n > 0;
|
|
13
|
+
const nonnegative = (n) => asNum(n) !== undefined && n >= 0;
|
|
14
|
+
/** Reconcile the bounded open-position reads against the wallet's frozen
|
|
15
|
+
* buckets. A full page is not assumed complete: the collateral checksums must
|
|
16
|
+
* agree. No held mark, unknown status, missing bucket, or mismatched book can
|
|
17
|
+
* silently become zero exposure. Lists retain legacy positions on other books
|
|
18
|
+
* for management; only explicitly attributed current-book rows reconcile its
|
|
19
|
+
* cash and marks. Closed history never contributes unrealized. */
|
|
20
|
+
export function deriveCapitalBook(portfolio, wallet, futures, pm) {
|
|
21
|
+
const p = asObj(portfolio), w = asObj(wallet), eq = asObj(p.equity), cash = asObj(w.usdt);
|
|
22
|
+
const unavailable = (reason) => ({
|
|
23
|
+
status: "unavailable",
|
|
24
|
+
reason,
|
|
25
|
+
});
|
|
26
|
+
if (p.bookScope !== "api_key")
|
|
27
|
+
return unavailable("independent_agent_book_unproven");
|
|
28
|
+
if (!Number.isSafeInteger(p.walletId) ||
|
|
29
|
+
p.walletId <= 0 ||
|
|
30
|
+
p.walletId !== w.walletId)
|
|
31
|
+
return unavailable("portfolio_wallet_identity_mismatch");
|
|
32
|
+
if (eq.valuationBasis !== CAPITAL_VALUATION_BASIS || !positive(eq.totalUsd))
|
|
33
|
+
return unavailable("portfolio_valuation_unavailable");
|
|
34
|
+
if (eq.spotValuationComplete !== true)
|
|
35
|
+
return unavailable("held_spot_valuation_unproven");
|
|
36
|
+
const buckets = ["available", "frozen", "frozenPm", "frozenFutures"];
|
|
37
|
+
for (const key of buckets) {
|
|
38
|
+
if (!nonnegative(cash[key]) ||
|
|
39
|
+
!nonnegative(eq[key]) ||
|
|
40
|
+
Math.abs(cash[key] - eq[key]) > CENT_TOLERANCE)
|
|
41
|
+
return unavailable("cash_partitions_incomplete_or_changed");
|
|
42
|
+
}
|
|
43
|
+
const cashTotal = buckets.reduce((sum, k) => sum + cash[k], 0);
|
|
44
|
+
if (eq.totalUsd + CENT_TOLERANCE < cashTotal)
|
|
45
|
+
return unavailable("wallet_asset_value_incoherent");
|
|
46
|
+
let negativeMarks = 0;
|
|
47
|
+
for (const [raw, bucket, amountKey, markKeys] of [
|
|
48
|
+
[futures, "frozenFutures", "marginMusd", ["unrealizedPnlMusd"]],
|
|
49
|
+
[pm, "frozenPm", "stakeMusd", ["unrealizedPnl", "unrealizedPnlMusd"]],
|
|
50
|
+
]) {
|
|
51
|
+
const rows = asObj(raw).positions;
|
|
52
|
+
if (!Array.isArray(rows)) {
|
|
53
|
+
// PM is not fetched for a futures-only legacy universe. Zero frozen PM
|
|
54
|
+
// proves no tied-up PM collateral; a nonzero bucket must have coverage.
|
|
55
|
+
if (bucket === "frozenPm" && raw === undefined && cash[bucket] === 0)
|
|
56
|
+
continue;
|
|
57
|
+
return unavailable("position_coverage_unavailable");
|
|
58
|
+
}
|
|
59
|
+
let held = 0;
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
const position = asObj(row);
|
|
62
|
+
if (typeof position.status !== "string")
|
|
63
|
+
return unavailable("position_status_unavailable");
|
|
64
|
+
if (position.status !== "open")
|
|
65
|
+
continue;
|
|
66
|
+
if (!Number.isSafeInteger(position.walletId) ||
|
|
67
|
+
position.walletId <= 0)
|
|
68
|
+
return unavailable("held_position_wallet_unavailable");
|
|
69
|
+
// The key-scoped API includes legacy/shared-book positions. Their
|
|
70
|
+
// liabilities settle to their originating wallet, not this active book.
|
|
71
|
+
// Do not filter the management observation or credit their close proceeds.
|
|
72
|
+
if (position.walletId !== p.walletId)
|
|
73
|
+
continue;
|
|
74
|
+
const amount = position[amountKey];
|
|
75
|
+
const mark = markKeys
|
|
76
|
+
.map((key) => asNum(position[key]))
|
|
77
|
+
.find((n) => n !== undefined);
|
|
78
|
+
if (!nonnegative(amount) || mark === undefined)
|
|
79
|
+
return unavailable("held_position_mark_unavailable");
|
|
80
|
+
held += amount;
|
|
81
|
+
negativeMarks += Math.min(0, mark);
|
|
82
|
+
}
|
|
83
|
+
if (Math.abs(held - cash[bucket]) > CENT_TOLERANCE)
|
|
84
|
+
return unavailable("held_collateral_coverage_mismatch");
|
|
85
|
+
}
|
|
86
|
+
const conservativeEquityMusd = eq.totalUsd + negativeMarks;
|
|
87
|
+
if (!positive(conservativeEquityMusd))
|
|
88
|
+
return unavailable("nonpositive_conservative_equity");
|
|
89
|
+
return {
|
|
90
|
+
status: "ready",
|
|
91
|
+
walletId: p.walletId,
|
|
92
|
+
conservativeEquityMusd,
|
|
93
|
+
cashAvailableMusd: Math.min(cash.available, eq.available),
|
|
94
|
+
committedCapitalMusd: Math.max(0, eq.totalUsd - cash.available),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export const usesCapitalSizing = (spec, mechanical = false) => spec.capitalSizing !== undefined &&
|
|
98
|
+
!mechanical &&
|
|
99
|
+
spec.model?.provider !== "mechanical";
|
|
100
|
+
const increases = (a) => a.type === "futures_open" ||
|
|
101
|
+
a.type === "pm_open" ||
|
|
102
|
+
(a.type === "spot_order" && a.side === "buy");
|
|
103
|
+
export function prepareCapitalAction(action, spec, observation, budget) {
|
|
104
|
+
if (!usesCapitalSizing(spec) || !increases(action))
|
|
105
|
+
return { action };
|
|
106
|
+
if (validateCapitalSizingPolicy(spec.capitalSizing).length > 0)
|
|
107
|
+
return { action, rejection: "capital_policy_invalid" };
|
|
108
|
+
const policy = spec.capitalSizing;
|
|
109
|
+
const adjustment = {
|
|
110
|
+
version: policy.version,
|
|
111
|
+
basis: "owned_collateral_spot_marked_negative_position_marks_only",
|
|
112
|
+
};
|
|
113
|
+
const reject = (rejection) => ({ action, adjustment, rejection });
|
|
114
|
+
const book = observation.capitalBook;
|
|
115
|
+
if (!book || book.status !== "ready")
|
|
116
|
+
return reject(book?.reason ?? "capital_book_unavailable");
|
|
117
|
+
const equity = book.conservativeEquityMusd;
|
|
118
|
+
if (!positive(equity) ||
|
|
119
|
+
!nonnegative(budget.cashAvailableMusd) ||
|
|
120
|
+
!nonnegative(budget.committedCapitalMusd) ||
|
|
121
|
+
!nonnegative(budget.openMarginMusd))
|
|
122
|
+
return reject("capital_budget_unavailable");
|
|
123
|
+
adjustment.conservativeEquityMusd = equity;
|
|
124
|
+
const ticket = Math.min(spec.risk.perTradeMarginMusd, (equity * policy.perTicketCapitalPct) / 100);
|
|
125
|
+
const room = Math.min(ticket, (equity * policy.totalCapitalPct) / 100 - budget.committedCapitalMusd, budget.cashAvailableMusd - (equity * policy.cashReservePct) / 100);
|
|
126
|
+
if (!positive(room))
|
|
127
|
+
return reject("capital_allocation_or_reserve_exhausted");
|
|
128
|
+
if (action.type === "spot_order")
|
|
129
|
+
return { action, adjustment }; // retain quantity; quote gate owns its cost
|
|
130
|
+
if (action.type === "pm_open") {
|
|
131
|
+
const stake = centsDown(Math.min(room, (equity * policy.pmMaxLossPct) / 100));
|
|
132
|
+
adjustment.proposedAmountMusd = action.stakeMusd;
|
|
133
|
+
adjustment.sizedAmountMusd = stake;
|
|
134
|
+
adjustment.riskBudgetMusd = (equity * policy.pmMaxLossPct) / 100;
|
|
135
|
+
if (stake < 10)
|
|
136
|
+
return reject("capital_ticket_below_minimum");
|
|
137
|
+
return { action: { ...action, stakeMusd: stake }, adjustment };
|
|
138
|
+
}
|
|
139
|
+
if (action.type !== "futures_open")
|
|
140
|
+
return { action };
|
|
141
|
+
const mark = observation.watch.find((w) => w.symbol.toUpperCase() === action.symbol.toUpperCase())?.priceUsd;
|
|
142
|
+
const stop = action.stopLossPrice;
|
|
143
|
+
if (!positive(mark) ||
|
|
144
|
+
!positive(stop) ||
|
|
145
|
+
!positive(action.leverage) ||
|
|
146
|
+
(action.side === "long" ? stop >= mark : stop <= mark))
|
|
147
|
+
return reject("capital_stop_or_mark_unavailable");
|
|
148
|
+
const distance = Math.abs(mark - stop) / mark;
|
|
149
|
+
const fee = CAPITAL_FEE_BUFFER_BPS / 10_000;
|
|
150
|
+
const riskPerMargin = action.leverage * (distance + fee * (1 + stop / mark));
|
|
151
|
+
const riskBudget = (equity * policy.futuresRiskPct) / 100;
|
|
152
|
+
const margin = centsDown(Math.min(ticket, riskBudget / riskPerMargin, spec.limits.maxOpenMarginMusd - budget.openMarginMusd, room / (1 + action.leverage * fee)));
|
|
153
|
+
Object.assign(adjustment, {
|
|
154
|
+
proposedAmountMusd: action.marginMusd,
|
|
155
|
+
sizedAmountMusd: margin,
|
|
156
|
+
riskBudgetMusd: riskBudget,
|
|
157
|
+
feeBufferBps: CAPITAL_FEE_BUFFER_BPS,
|
|
158
|
+
});
|
|
159
|
+
if (margin < 10)
|
|
160
|
+
return reject("capital_ticket_below_minimum");
|
|
161
|
+
return { action: { ...action, marginMusd: margin }, adjustment };
|
|
162
|
+
}
|
|
163
|
+
/** Quote-bound monetary checks. Drift fails closed; never requote a resized
|
|
164
|
+
* ticket, silently widen a cap, or credit uncertain close/sell proceeds. */
|
|
165
|
+
export function validateCapitalAction(action, spec, observation, budget, quote) {
|
|
166
|
+
if (!usesCapitalSizing(spec) || !increases(action))
|
|
167
|
+
return undefined;
|
|
168
|
+
if (validateCapitalSizingPolicy(spec.capitalSizing).length > 0)
|
|
169
|
+
return "capital_policy_invalid";
|
|
170
|
+
if (observation.capitalBook?.status !== "ready")
|
|
171
|
+
return "capital_book_unavailable";
|
|
172
|
+
const p = spec.capitalSizing, equity = observation.capitalBook.conservativeEquityMusd;
|
|
173
|
+
if (!positive(equity) ||
|
|
174
|
+
!nonnegative(budget.cashAvailableMusd) ||
|
|
175
|
+
!nonnegative(budget.committedCapitalMusd) ||
|
|
176
|
+
!nonnegative(budget.openMarginMusd))
|
|
177
|
+
return "capital_budget_unavailable";
|
|
178
|
+
let cost, allocated;
|
|
179
|
+
if (action.type === "pm_open") {
|
|
180
|
+
cost = allocated = action.stakeMusd;
|
|
181
|
+
if (cost > (equity * p.pmMaxLossPct) / 100 + 1e-8)
|
|
182
|
+
return "capital_pm_max_loss_exceeded";
|
|
183
|
+
}
|
|
184
|
+
else if (action.type === "spot_order") {
|
|
185
|
+
cost = allocated = capitalSpotBuyCost(action, quote);
|
|
186
|
+
// Without a stop model, the entire spot buy is the capital at risk.
|
|
187
|
+
if (positive(cost) && cost > (equity * p.futuresRiskPct) / 100 + 1e-8)
|
|
188
|
+
return "capital_spot_risk_exceeded";
|
|
189
|
+
}
|
|
190
|
+
else if (action.type === "futures_open") {
|
|
191
|
+
const entry = quote?.entryPrice, stop = action.stopLossPrice, target = action.takeProfitPrice;
|
|
192
|
+
const bps = quote?.futuresFeeBps, entryFee = quote?.estimatedEntryFeeMusd;
|
|
193
|
+
cost = quote?.cashRequiredMusd;
|
|
194
|
+
allocated = action.marginMusd;
|
|
195
|
+
if (!positive(entry) ||
|
|
196
|
+
!positive(stop) ||
|
|
197
|
+
!positive(target) ||
|
|
198
|
+
!nonnegative(bps) ||
|
|
199
|
+
!nonnegative(entryFee) ||
|
|
200
|
+
!positive(cost))
|
|
201
|
+
return "capital_quote_cost_evidence_missing";
|
|
202
|
+
const notional = action.marginMusd * action.leverage;
|
|
203
|
+
const feeRate = bps / 10_000;
|
|
204
|
+
if (Math.abs(entryFee - notional * feeRate) > CENT_TOLERANCE ||
|
|
205
|
+
Math.abs(cost - action.marginMusd - entryFee) > CENT_TOLERANCE)
|
|
206
|
+
return "capital_quote_cost_mismatch";
|
|
207
|
+
const adverse = action.side === "long" ? entry - stop : stop - entry;
|
|
208
|
+
const favorable = action.side === "long" ? target - entry : entry - target;
|
|
209
|
+
if (!(adverse > 0) || !(favorable > 0))
|
|
210
|
+
return "capital_stop_target_wrong_side";
|
|
211
|
+
const risk = (notional * adverse) / entry +
|
|
212
|
+
entryFee +
|
|
213
|
+
((notional * stop) / entry) * feeRate;
|
|
214
|
+
const reward = (notional * favorable) / entry -
|
|
215
|
+
entryFee -
|
|
216
|
+
((notional * target) / entry) * feeRate;
|
|
217
|
+
if (risk > (equity * p.futuresRiskPct) / 100 + 1e-8)
|
|
218
|
+
return "capital_quote_stop_risk_exceeded";
|
|
219
|
+
if (reward / risk + 1e-8 < p.minRewardRisk)
|
|
220
|
+
return "capital_quote_reward_risk_too_low";
|
|
221
|
+
}
|
|
222
|
+
if (!positive(cost) || !positive(allocated))
|
|
223
|
+
return "capital_quote_cost_evidence_missing";
|
|
224
|
+
if (cost >
|
|
225
|
+
Math.min(spec.risk.perTradeMarginMusd, (equity * p.perTicketCapitalPct) / 100) +
|
|
226
|
+
1e-8)
|
|
227
|
+
return "capital_ticket_cap_exceeded";
|
|
228
|
+
if (budget.committedCapitalMusd + cost >
|
|
229
|
+
(equity * p.totalCapitalPct) / 100 + 1e-8)
|
|
230
|
+
return "capital_combined_allocation_exceeded";
|
|
231
|
+
if (budget.cashAvailableMusd - cost <
|
|
232
|
+
(equity * p.cashReservePct) / 100 - 1e-8)
|
|
233
|
+
return "capital_cash_reserve_exceeded";
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
/** Fee-inclusive opt-in reservation; the legacy gross helper stays unchanged.
|
|
237
|
+
* The API quotes a market fill even for a pending limit/stop order. Reserve at
|
|
238
|
+
* least that fee, scaled up if the proposed price requires more notional.
|
|
239
|
+
* This is conservative captured quote evidence, not a future fill guarantee. */
|
|
240
|
+
export function capitalSpotBuyCost(action, quote) {
|
|
241
|
+
const gross = spotBuyCost(action, quote);
|
|
242
|
+
const quotedGross = quote?.estimatedCostMusd;
|
|
243
|
+
const fee = quote?.estimatedFeeMusd;
|
|
244
|
+
if (!positive(gross) || !positive(quotedGross) || !nonnegative(fee))
|
|
245
|
+
return undefined;
|
|
246
|
+
const cost = gross + fee * Math.max(1, gross / quotedGross);
|
|
247
|
+
return positive(cost) ? cost : undefined;
|
|
248
|
+
}
|
|
249
|
+
export function capitalCashCost(action, quote) {
|
|
250
|
+
if (action.type === "futures_open")
|
|
251
|
+
return quote?.cashRequiredMusd ?? action.marginMusd;
|
|
252
|
+
if (action.type === "pm_open")
|
|
253
|
+
return action.stakeMusd;
|
|
254
|
+
if (action.type === "spot_order" && action.side === "buy")
|
|
255
|
+
return capitalSpotBuyCost(action, quote) ?? Number.NaN;
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
package/dist/agent/client.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ClientConfig {
|
|
|
21
21
|
fetchFn?: typeof fetch;
|
|
22
22
|
sleepFn?: (ms: number) => Promise<void>;
|
|
23
23
|
maxRetries?: number;
|
|
24
|
+
extraHeaders?: Record<string, string>;
|
|
24
25
|
}
|
|
25
26
|
export declare class CoinRithmClient {
|
|
26
27
|
private readonly apiKey;
|
|
@@ -28,6 +29,7 @@ export declare class CoinRithmClient {
|
|
|
28
29
|
private readonly fetchFn;
|
|
29
30
|
private readonly sleepFn;
|
|
30
31
|
private readonly maxRetries;
|
|
32
|
+
private readonly extraHeaders?;
|
|
31
33
|
rateLimitHits: number;
|
|
32
34
|
constructor(cfg: ClientConfig);
|
|
33
35
|
private request;
|
package/dist/agent/client.js
CHANGED
|
@@ -30,6 +30,7 @@ export class CoinRithmClient {
|
|
|
30
30
|
fetchFn;
|
|
31
31
|
sleepFn;
|
|
32
32
|
maxRetries;
|
|
33
|
+
extraHeaders;
|
|
33
34
|
// Every 429 seen this session (read or write, retried or not) — feeds the
|
|
34
35
|
// rate-limit-pressure kill-switch, which a write-only counter would miss.
|
|
35
36
|
rateLimitHits = 0;
|
|
@@ -39,6 +40,7 @@ export class CoinRithmClient {
|
|
|
39
40
|
this.fetchFn = cfg.fetchFn ?? fetch;
|
|
40
41
|
this.sleepFn = cfg.sleepFn ?? realSleep;
|
|
41
42
|
this.maxRetries = cfg.maxRetries ?? 3;
|
|
43
|
+
this.extraHeaders = cfg.extraHeaders;
|
|
42
44
|
}
|
|
43
45
|
async request(method, path, opts = {}) {
|
|
44
46
|
const url = new URL(this.baseUrl + path);
|
|
@@ -48,7 +50,9 @@ export class CoinRithmClient {
|
|
|
48
50
|
url.searchParams.set(k, String(v));
|
|
49
51
|
}
|
|
50
52
|
}
|
|
53
|
+
// extraHeaders first: auth, accept and trace can never be clobbered by it.
|
|
51
54
|
const headers = {
|
|
55
|
+
...this.extraHeaders,
|
|
52
56
|
Authorization: `Bearer ${this.apiKey}`,
|
|
53
57
|
Accept: "application/json",
|
|
54
58
|
...traceHeaders(opts.trace),
|