@coinrithm/mcp-trading 0.7.7 → 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 CHANGED
@@ -5,6 +5,89 @@ 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
+
8
91
  ## 0.7.7
9
92
 
10
93
  Reliability release. Every change here came from a live production failure, not
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, asStr } from "./extract.js";
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
+ }