@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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { asArr, asNum, asObj } from "./extract.js";
|
|
2
|
+
export const PM_BLOCK_REASONS = [
|
|
3
|
+
"structurally_invalid",
|
|
4
|
+
"stale_freshness",
|
|
5
|
+
"freshness_unknown",
|
|
6
|
+
"unpriced",
|
|
7
|
+
"quote_dead",
|
|
8
|
+
"dead_zero",
|
|
9
|
+
"not_open",
|
|
10
|
+
"source_degraded",
|
|
11
|
+
"settlement_limbo",
|
|
12
|
+
];
|
|
13
|
+
export const PM_WARNING_REASONS = [
|
|
14
|
+
"lagging_freshness",
|
|
15
|
+
"unproven_no_activity",
|
|
16
|
+
"untraded_default",
|
|
17
|
+
"play_money",
|
|
18
|
+
"anomaly_flagged",
|
|
19
|
+
"source_time_unverified",
|
|
20
|
+
"sum_atypical_independent",
|
|
21
|
+
];
|
|
22
|
+
export const PM_FLAGS = [
|
|
23
|
+
"thinMarket",
|
|
24
|
+
"inactiveMarket",
|
|
25
|
+
"highAmbiguity",
|
|
26
|
+
"nearResolution",
|
|
27
|
+
"staleData",
|
|
28
|
+
];
|
|
29
|
+
export const FRESHNESS_BASES = [
|
|
30
|
+
"latest_snapshot",
|
|
31
|
+
"source_update",
|
|
32
|
+
"processed",
|
|
33
|
+
"event_update",
|
|
34
|
+
"unknown",
|
|
35
|
+
];
|
|
36
|
+
export const PM_TIERS = ["high", "medium", "low", "unknown"];
|
|
37
|
+
export const PM_SPREAD_TIERS = [
|
|
38
|
+
"tight",
|
|
39
|
+
"moderate",
|
|
40
|
+
"wide",
|
|
41
|
+
"unknown",
|
|
42
|
+
];
|
|
43
|
+
export const PM_QUALITY_CAPS = [
|
|
44
|
+
"unassessable",
|
|
45
|
+
"raw_book",
|
|
46
|
+
"pinned_outcome",
|
|
47
|
+
];
|
|
48
|
+
export const knownCode = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : undefined;
|
|
49
|
+
export function sourceTimestamp(value) {
|
|
50
|
+
if (typeof value !== "string" ||
|
|
51
|
+
value.length > 30 ||
|
|
52
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value))
|
|
53
|
+
return undefined;
|
|
54
|
+
const ms = Date.parse(value);
|
|
55
|
+
if (!Number.isFinite(ms))
|
|
56
|
+
return undefined;
|
|
57
|
+
const iso = new Date(ms).toISOString();
|
|
58
|
+
const normalized = value.replace(/(?:\.(\d{1,3}))?Z$/, (_match, fraction) => `.${(fraction ?? "").padEnd(3, "0")}Z`);
|
|
59
|
+
// Date.parse otherwise silently rolls impossible calendar dates forward.
|
|
60
|
+
return iso === normalized ? iso : undefined;
|
|
61
|
+
}
|
|
62
|
+
export function freshnessOf(block) {
|
|
63
|
+
const fr = asObj(block.freshness);
|
|
64
|
+
const status = knownCode(fr.status, [
|
|
65
|
+
"fresh",
|
|
66
|
+
"stale",
|
|
67
|
+
"lagging",
|
|
68
|
+
"never_ingested",
|
|
69
|
+
"unknown",
|
|
70
|
+
]);
|
|
71
|
+
if (!status)
|
|
72
|
+
return undefined;
|
|
73
|
+
const seconds = asNum(fr.ageSeconds);
|
|
74
|
+
const minutes = asNum(fr.ageMinutes);
|
|
75
|
+
const age = seconds ?? (minutes == null ? undefined : minutes * 60);
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
...(age != null && Number.isFinite(age) && age >= 0
|
|
79
|
+
? { ageSeconds: age }
|
|
80
|
+
: {}),
|
|
81
|
+
...(sourceTimestamp(fr.asOf) ? { asOf: sourceTimestamp(fr.asOf) } : {}),
|
|
82
|
+
...(knownCode(fr.basis, FRESHNESS_BASES)
|
|
83
|
+
? { basis: knownCode(fr.basis, FRESHNESS_BASES) }
|
|
84
|
+
: {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function pmQualityOf(value) {
|
|
88
|
+
const raw = asObj(value);
|
|
89
|
+
if (Object.keys(raw).length === 0)
|
|
90
|
+
return undefined;
|
|
91
|
+
const reasons = (v, allowed) => [
|
|
92
|
+
...new Set(asArr(v)
|
|
93
|
+
.slice(0, 32)
|
|
94
|
+
.filter((r) => !!knownCode(r, allowed))),
|
|
95
|
+
];
|
|
96
|
+
const warningReasons = reasons(raw.warningReasons, PM_WARNING_REASONS);
|
|
97
|
+
const blockReasons = reasons(raw.blockReasons, PM_BLOCK_REASONS);
|
|
98
|
+
const omitted = (v, allowed) => !Array.isArray(v) || v.length > 32 || v.some((r) => !knownCode(r, allowed));
|
|
99
|
+
return {
|
|
100
|
+
...(typeof raw.decisionEligible === "boolean"
|
|
101
|
+
? { decisionEligible: raw.decisionEligible }
|
|
102
|
+
: {}),
|
|
103
|
+
warningReasons,
|
|
104
|
+
blockReasons,
|
|
105
|
+
...(typeof raw.policyVersion === "string" &&
|
|
106
|
+
/^pm-quality-\d{1,3}$/.test(raw.policyVersion)
|
|
107
|
+
? { policyVersion: raw.policyVersion }
|
|
108
|
+
: {}),
|
|
109
|
+
...(sourceTimestamp(raw.assessedAt)
|
|
110
|
+
? { assessedAt: sourceTimestamp(raw.assessedAt) }
|
|
111
|
+
: {}),
|
|
112
|
+
reasonsOmitted: raw.reasonsOmitted === true ||
|
|
113
|
+
omitted(raw.warningReasons, PM_WARNING_REASONS) ||
|
|
114
|
+
omitted(raw.blockReasons, PM_BLOCK_REASONS),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function pmDecisionSupportOf(value) {
|
|
118
|
+
const raw = asObj(value);
|
|
119
|
+
if (Object.keys(raw).length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
const score = asNum(raw.qualityScore);
|
|
122
|
+
const rawFlags = asObj(raw.flags);
|
|
123
|
+
return {
|
|
124
|
+
...(score != null && score >= 0 && score <= 100
|
|
125
|
+
? { qualityScore: score }
|
|
126
|
+
: {}),
|
|
127
|
+
qualityTier: knownCode(raw.qualityTier, PM_TIERS),
|
|
128
|
+
qualityCapReason: raw.qualityCapReason === null
|
|
129
|
+
? null
|
|
130
|
+
: knownCode(raw.qualityCapReason, PM_QUALITY_CAPS),
|
|
131
|
+
spreadTier: knownCode(raw.spreadTier, PM_SPREAD_TIERS),
|
|
132
|
+
liquidityTier: knownCode(raw.liquidityTier, PM_TIERS),
|
|
133
|
+
volumeTier: knownCode(raw.volumeTier, PM_TIERS),
|
|
134
|
+
flags: Object.fromEntries(PM_FLAGS.filter((key) => typeof rawFlags[key] === "boolean").map((key) => [key, rawFlags[key]])),
|
|
135
|
+
};
|
|
136
|
+
}
|
package/dist/agent/prompt.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { AgentSpec, Observation, PmResolution } from "./types.js";
|
|
1
|
+
import { AgentSpec, Observation, PmResolution, RunState } from "./types.js";
|
|
2
|
+
export interface DailyRiskBudget {
|
|
3
|
+
version: "coinrithm.daily-risk-budget.v1";
|
|
4
|
+
utcDay: string;
|
|
5
|
+
limit: number | null;
|
|
6
|
+
used: number;
|
|
7
|
+
remaining: number | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildDailyRiskBudget(spec: AgentSpec, state: Pick<RunState, "dayKey" | "riskIncreasesToday">): DailyRiskBudget;
|
|
2
10
|
export declare function formatPmResolutions(resolutions: PmResolution[]): string[];
|
|
3
11
|
export declare function buildSystemPrompt(spec: AgentSpec, mergedProse: string, opts?: {
|
|
4
12
|
includeForecast?: boolean;
|
|
@@ -6,4 +14,8 @@ export declare function buildSystemPrompt(spec: AgentSpec, mergedProse: string,
|
|
|
6
14
|
export declare function buildUserPrompt(obs: Observation, journal?: Array<{
|
|
7
15
|
at: string;
|
|
8
16
|
did: string;
|
|
9
|
-
}
|
|
17
|
+
}>, opts?: {
|
|
18
|
+
venues?: AgentSpec["venues"];
|
|
19
|
+
dailyRiskBudget?: DailyRiskBudget;
|
|
20
|
+
capitalSizing?: AgentSpec["capitalSizing"];
|
|
21
|
+
}): string;
|
package/dist/agent/prompt.js
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
// static character (cached prefix); the user prompt is the fresh observation.
|
|
3
3
|
// The model only PROPOSES — the runner re-checks every action against the caps,
|
|
4
4
|
// so the prompt states the caps but never relies on the model to honor them.
|
|
5
|
+
import { pmQualityOf, pmDecisionSupportOf } from "./pmContext.js";
|
|
6
|
+
import { usesCapitalSizing } from "./capitalSizing.js";
|
|
7
|
+
// The runner has already called rollDay. Use the same counter as validation,
|
|
8
|
+
// including conservative legacy-state migration; never infer it from writes,
|
|
9
|
+
// model calls, or the number of positions that remain open.
|
|
10
|
+
export function buildDailyRiskBudget(spec, state) {
|
|
11
|
+
const limit = spec.limits.maxTradesPerDay > 0 ? spec.limits.maxTradesPerDay : null;
|
|
12
|
+
return {
|
|
13
|
+
version: "coinrithm.daily-risk-budget.v1",
|
|
14
|
+
utcDay: state.dayKey,
|
|
15
|
+
limit,
|
|
16
|
+
used: state.riskIncreasesToday,
|
|
17
|
+
remaining: limit === null ? null : Math.max(0, limit - state.riskIncreasesToday),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Whole-dollar rendering for the compact PM rows (tokens, not precision).
|
|
21
|
+
const roundUsd = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v) : undefined;
|
|
5
22
|
// Format the settlement-feedback block: a concise, natural-language recap of the
|
|
6
23
|
// agent's OWN PM bets that resolved since the last cycle, so the model can REFLECT
|
|
7
24
|
// (reinforce what worked, avoid what didn't). Capped + compact — this is context,
|
|
@@ -41,16 +58,36 @@ export function buildSystemPrompt(spec, mergedProse,
|
|
|
41
58
|
opts = {}) {
|
|
42
59
|
const r = spec.risk;
|
|
43
60
|
const v = spec.venues;
|
|
44
|
-
const
|
|
61
|
+
const hasFutures = v.includes("futures");
|
|
62
|
+
const hasSpot = v.includes("spot");
|
|
63
|
+
const hasPm = v.includes("pm");
|
|
64
|
+
const hasCoinVenue = hasFutures || hasSpot;
|
|
65
|
+
const coinVenueLabel = [
|
|
66
|
+
...(hasSpot ? ["spot"] : []),
|
|
67
|
+
...(hasFutures ? ["futures"] : []),
|
|
68
|
+
].join(" + ");
|
|
69
|
+
const includeForecast = hasPm && opts.includeForecast === true;
|
|
70
|
+
const sizeKinds = [
|
|
71
|
+
...(hasFutures ? ["futures margin"] : []),
|
|
72
|
+
...(hasSpot ? ["spot buy notional"] : []),
|
|
73
|
+
...(hasPm ? ["PM stake"] : []),
|
|
74
|
+
];
|
|
75
|
+
const openKinds = [
|
|
76
|
+
...(hasFutures ? ["futures_open"] : []),
|
|
77
|
+
...(hasSpot ? ["spot_order"] : []),
|
|
78
|
+
...(hasPm ? ["pm_open"] : []),
|
|
79
|
+
];
|
|
80
|
+
const hasIndicators = spec.capabilities.includes("indicators");
|
|
81
|
+
const hasNews = spec.capabilities.includes("news");
|
|
45
82
|
const actions = [];
|
|
46
|
-
if (
|
|
47
|
-
actions.push('{"type":"futures_open","symbol","side":"long"|"short","leverage","marginMusd","stopLossPrice","takeProfitPrice","confidence":0..1}', '{"type":"futures_close","positionId","fraction"}', '{"type":"futures_set_sltp","positionId","stopLossPrice","takeProfitPrice"}', "FUTURES TRIGGER RULES (the server rejects the WHOLE open otherwise): a LONG's takeProfitPrice must be ABOVE the current mark and stopLossPrice BELOW it (and above liquidationPrice); a SHORT is inverted (TP below mark, SL above). Every open position in observation.openPositions shows entryPrice, markPrice, liquidationPrice, stopLossPrice, takeProfitPrice — read them and place triggers on the correct side. NEVER attach stopLossPrice/takeProfitPrice to a futures_open for a symbol you ALREADY hold (the server treats it as an add and rejects it) — adjust that position with futures_set_sltp on its positionId instead.");
|
|
83
|
+
if (hasFutures) {
|
|
84
|
+
actions.push('{"type":"futures_open","symbol","side":"long"|"short","leverage","marginMusd","stopLossPrice","takeProfitPrice","confidence":0..1,"thesis":{"summary","invalidation":{"priceBelow"|"priceAbove","maxHoldMinutes","catalyst"}}}', '{"type":"futures_close","positionId","fraction"}', '{"type":"futures_set_sltp","positionId","stopLossPrice","takeProfitPrice"}', "FUTURES TRIGGER RULES (the server rejects the WHOLE open otherwise): a LONG's takeProfitPrice must be ABOVE the current mark and stopLossPrice BELOW it (and above liquidationPrice); a SHORT is inverted (TP below mark, SL above). Every open position in observation.openPositions shows entryPrice, markPrice, liquidationPrice, stopLossPrice, takeProfitPrice — read them and place triggers on the correct side. NEVER attach stopLossPrice/takeProfitPrice to a futures_open for a symbol you ALREADY hold (the server treats it as an add and rejects it) — adjust that position with futures_set_sltp on its positionId instead.");
|
|
48
85
|
}
|
|
49
|
-
if (
|
|
50
|
-
actions.push('{"type":"spot_order","symbol","side":"buy"|"sell","orderType":"market"|"limit"|"stop","quantity","limitPrice","stopPrice","confidence":0..1}', '{"type":"spot_cancel","orderId"}');
|
|
86
|
+
if (hasSpot) {
|
|
87
|
+
actions.push('{"type":"spot_order","symbol","side":"buy"|"sell","orderType":"market"|"limit"|"stop","quantity","limitPrice","stopPrice","confidence":0..1,"thesis":{"summary","invalidation":{"priceBelow"|"priceAbove","maxHoldMinutes","catalyst"}}}', '{"type":"spot_cancel","orderId"}');
|
|
51
88
|
}
|
|
52
|
-
if (
|
|
53
|
-
actions.push(`{"type":"pm_open","ref":"pmN","stakeMusd","confidence":0..1${includeForecast ? ',"forecastProbability":1..99' : ""}} (set "ref" to one of the refs listed THIS cycle (pm1..pmN) — the \`ref\` of the ONE observation.pmMarkets entry you are betting, e.g. "pm3", copied EXACTLY; a ref NOT in this cycle's list is rejected as pm_ref_unknown and wastes the cycle; stakeMusd >= 10${includeForecast ? '; set "forecastProbability" to YOUR OWN probability 1-99 that this outcome wins — see the forecast rule below' : ""})`);
|
|
89
|
+
if (hasPm) {
|
|
90
|
+
actions.push(`{"type":"pm_open","ref":"pmN","stakeMusd","confidence":0..1${includeForecast ? ',"forecastProbability":1..99' : ""},"thesis":{"summary","invalidation":{"probabilityBelow"|"probabilityAbove","maxHoldMinutes","catalyst"}}} (set "ref" to one of the refs listed THIS cycle (pm1..pmN) — the \`ref\` of the ONE observation.pmMarkets entry you are betting, e.g. "pm3", copied EXACTLY; a ref NOT in this cycle's list is rejected as pm_ref_unknown and wastes the cycle; stakeMusd >= 10${includeForecast ? '; set "forecastProbability" to YOUR OWN probability 1-99 that this outcome wins — see the forecast rule below' : ""})`);
|
|
54
91
|
}
|
|
55
92
|
return [
|
|
56
93
|
"You operate a CoinRithm PAPER-TRADING agent (simulated 50,000 mUSD; not real money, not financial advice).",
|
|
@@ -60,29 +97,54 @@ opts = {}) {
|
|
|
60
97
|
mergedProse.trim() || "(no strategy prose provided)",
|
|
61
98
|
"",
|
|
62
99
|
"## Hard caps the runner enforces (do not exceed; proposing over a cap wastes the cycle)",
|
|
100
|
+
"- When supplied, the user prompt's dailyRiskBudget is the remaining UTC-day entry/add allowance. It outranks setup/entry pressure: exhaustion is a legitimate skip for new risk, never a reason to skip otherwise-valid closes or protection. All other caps still apply, even when this daily count is unlimited.",
|
|
63
101
|
`- venues you may act in: ${v.join(", ")}`,
|
|
64
|
-
`- perTradeMarginMusd ${r.perTradeMarginMusd} is the per-trade SIZE cap (
|
|
65
|
-
|
|
102
|
+
`- perTradeMarginMusd ${r.perTradeMarginMusd} is the per-trade SIZE cap (${sizeKinds.join(" / ")})`,
|
|
103
|
+
...(usesCapitalSizing(spec)
|
|
104
|
+
? [
|
|
105
|
+
`- Opt-in paper capital policy ${spec.capitalSizing?.version ?? "invalid"}: the runner REPLACES proposed futures margins and PM stakes using current owned-book evidence, stops and fixed policy limits; it does not treat your confidence or the nominal starting grant as a sizing instruction. Choose the market, direction and meaningful protection; invalid policy, quoted costs, shared allocation and cash reserve can still reject an entry.`,
|
|
106
|
+
]
|
|
107
|
+
: []),
|
|
108
|
+
...(hasFutures
|
|
109
|
+
? [
|
|
110
|
+
`- futures: maxLeverage ${r.maxLeverage}, maxConcurrentPositions ${r.maxConcurrentPositions}, requireStopLoss ${r.requireStopLoss} (long stop below entry, short stop above)`,
|
|
111
|
+
]
|
|
112
|
+
: []),
|
|
113
|
+
...(r.direction
|
|
114
|
+
? [
|
|
115
|
+
r.direction === "short_only"
|
|
116
|
+
? '- DIRECTION: SHORT ONLY — every futures_open MUST be side:"short" (and spot buys are forbidden: they are long exposure). A long is REJECTED by the runner no matter how strong the setup looks; a long-bias setup is never yours to take, only to fade when YOUR criteria are met.'
|
|
117
|
+
: '- DIRECTION: LONG ONLY — every futures_open MUST be side:"long". A short is REJECTED by the runner no matter how strong the setup looks.',
|
|
118
|
+
]
|
|
119
|
+
: []),
|
|
66
120
|
// With universe_scan, the validator's gate is WATCH-membership (manual
|
|
67
121
|
// watchlist ∪ this cycle's discovered entries) — saying "ONLY these" here
|
|
68
122
|
// while the universe-scan section below calls discovered movers tradable
|
|
69
123
|
// made cap-obedient models refuse every discovered candidate (the caps
|
|
70
124
|
// header says proposing outside a cap wastes the cycle). Keep the two
|
|
71
125
|
// sections telling one story.
|
|
72
|
-
|
|
73
|
-
?
|
|
74
|
-
|
|
75
|
-
|
|
126
|
+
...(hasCoinVenue
|
|
127
|
+
? [
|
|
128
|
+
spec.capabilities.includes("universe_scan")
|
|
129
|
+
? `- tradable symbols (${coinVenueLabel}): your watchlist (${r.watchlist.join(", ")}) PLUS this cycle's watch entries marked \`discovered: true\` — nothing outside those`
|
|
130
|
+
: `- watchlist (${coinVenueLabel} use ONLY these): ${r.watchlist.join(", ")}`,
|
|
131
|
+
]
|
|
132
|
+
: []),
|
|
133
|
+
...(hasCoinVenue && r.blocklist && r.blocklist.length > 0
|
|
76
134
|
? [
|
|
77
135
|
`- deny-list (NEVER open these, even if on the watchlist): ${r.blocklist.join(", ")}`,
|
|
78
136
|
]
|
|
79
137
|
: []),
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
138
|
+
...(hasPm
|
|
139
|
+
? [
|
|
140
|
+
"- prediction markets are a FIRST-CLASS venue for you. Each observation.pmMarkets entry carries a short `ref` (pm1, pm2, ...), an `outcome` label, and `prob` (0..1, the market's current odds). BET (pm_open) when your independently formed estimate differs materially from the market's, after costs. Discovery filters known ineligible candidates but is NOT an execution promise: fresh quote and open-time guards still apply. `quality` contains eligibility and warning evidence; `decisionSupport` describes liquidity/activity/structure, NOT winning probability or forecast accuracy. Missing quality is unknown, not approval. Check warning reasons, freshness age and flags before deciding. Pick ONLY a listed market by its `ref`; min stake 10 mUSD. Do NOT re-bet a market+outcome already held (check observation.pmPositions); choose a different market or skip.",
|
|
141
|
+
"- PM stake is a SEPARATE budget from your futures margin: the futures margin cap (maxOpenMarginMusd) does NOT limit pm_open. So when your futures are at the margin/position cap — you hold the max, or a futures_open keeps getting REJECTED with open_margin_exceeds_cap — prediction markets are STILL fully open to you. PIVOT to pm_open on a mispriced market instead of re-proposing a futures_open that will just be rejected: a rejected open wastes the entire cycle, an eligible PM bet does not.",
|
|
142
|
+
"- YOUR SHARPEST PM EDGE is the crypto price view you JUST formed: crypto PM markets resolve on the very prices you analyse, so you have a genuine information edge there that you do NOT have on coin futures alone. EVERY cycle you reach a price conviction, it is REQUIRED that you scan observation.pmMarkets for a LISTED crypto market that same view prices wrong and, if one is materially mispriced, open it with pm_open by its `ref` — treat that mispricing exactly like a flagged coin setup (an ACT, not a skip). If you are bearish BTC, a 'BTC above $X by <date>' priced high is a NO; if bullish ETH, an 'ETH above $Y' priced low is a YES. ESCAPE HATCH — only the markets actually listed in observation.pmMarkets THIS cycle (pm1..pmN) are bettable: if NONE of them matches the coin or view you formed, that is a legitimate SKIP for PM (say so in one clause and move on) — do NOT invent, guess, or increment a ref for a market you wish existed, because a made-up ref is rejected (pm_ref_unknown) and wastes the whole cycle exactly like a rejected open. The mistake to avoid is leaving a LISTED, clearly mispriced crypto market untraded — a mispricing that is NOT on this cycle's board is simply not actionable now, not a miss. (For non-crypto events you have no special edge; skip unless the odds are obviously off.)",
|
|
143
|
+
]
|
|
144
|
+
: []),
|
|
83
145
|
...(includeForecast
|
|
84
146
|
? [
|
|
85
|
-
"- FORECAST RULE (pm_open forecastProbability): before you look at what the market is pricing, decide YOUR OWN probability the outcome you are backing actually WINS — reason ONLY from the question, its resolution criteria, and the deadline. Put that number (1-99, whole or one decimal) in `forecastProbability`. This is graded against reality as your PUBLIC calibration record, so it must be YOUR judgement, NOT the market's: do NOT copy, round, or anchor it to the observation.pmMarkets `prob`. It is FINE if your honest forecast happens to land on the market's number — but reaching that by echoing the price defeats the point. If you genuinely cannot form an independent view, OMIT the field rather than parroting the market (an absent forecast is better than a fake one, and it never blocks the bet).",
|
|
147
|
+
"- FORECAST RULE (pm_open forecastProbability): before you look at what the market is pricing, decide YOUR OWN probability the outcome you are backing actually WINS — reason ONLY from the question, its resolution criteria, and the deadline. Put that number (1-99, whole or one decimal) in `forecastProbability`. This is graded against reality as your PUBLIC calibration record, so it must be YOUR judgement, NOT the market's: do NOT copy, round, or anchor it to the observation.pmMarkets `prob`. It is FINE if your honest forecast happens to land on the market's number — but reaching that by echoing the price defeats the point. If you genuinely cannot form an independent view, OMIT the field rather than parroting the market (an absent forecast is better than a fake one, and it never blocks the bet). A forecast you DO give is enforced: if it is not above what the outcome currently costs, the open is rejected, because buying something you price below the market is a losing trade by your own numbers.",
|
|
86
148
|
]
|
|
87
149
|
: []),
|
|
88
150
|
`- abstention.minConfidence ${spec.abstention.minConfidence}: opens below this are rejected, so act with genuine conviction — but routine caution is no reason to sit out a clear setup`,
|
|
@@ -111,25 +173,82 @@ opts = {}) {
|
|
|
111
173
|
"Each item has `importance` (0..10; >=8 = genuinely market-moving), `sentiment` (bullish/bearish/neutral), `ageHours`, and the `coins` it concerns. Use it to CONFIRM or VETO the price read, never to trade on alone:",
|
|
112
174
|
"- A fresh high-importance (>=8) bullish story on a coin you're watching strengthens a long and warns against shorting into it; a bearish >=8 is the reverse. A surprise catalyst can matter more than the chart.",
|
|
113
175
|
"- Weight by importance AND freshness: a 9 from 30 min ago outweighs a stale 4 from yesterday. Old or low-importance news is noise — don't over-react.",
|
|
114
|
-
|
|
176
|
+
...(hasPm
|
|
177
|
+
? [
|
|
178
|
+
"- For PM: a high-importance catalyst is exactly the kind of mispricing edge to act on if the market hasn't repriced it yet.",
|
|
179
|
+
]
|
|
180
|
+
: []),
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
"",
|
|
184
|
+
`## Fundamentals (observation.watch[].fundamentals${hasPm ? ", observation.pmMarkets" : ""}): the fundamental leg of every decision`,
|
|
185
|
+
...(hasCoinVenue
|
|
186
|
+
? [
|
|
187
|
+
`Each watch entry carries \`fundamentals\`: \`categories\` (sector tags), \`marketCapRank\`, \`marketCapUsd\`${hasIndicators ? ", `volume24hUsd` (24h volume on the tracked exchanges)" : ""}${hasNews ? ", and `headlines` (up to 3 recent stories about that coin, each with an `at` timestamp, `importance` 0..10 and `sentiment`)" : ""}. Next to \`change24h\` / \`change7d\` this is your fundamental read; it GRADES the trade, it never replaces your technical rules:`,
|
|
188
|
+
"- A fresh, high-importance headline that explains the move is what turns a B-grade setup into A-grade size; a big move with no headline and thin volume is more often exhaustion than a beginning.",
|
|
189
|
+
"- Rank and volume set the size ceiling: a top-20 coin with deep volume can take your full per-trade margin; a rank-300 name on thin volume gets half at most, a wider stop and a shorter time stop.",
|
|
190
|
+
"- Categories tell you what else moves with it: a sector-wide story (an L2 narrative, an exchange listing wave, a regulatory hit) applies to peers on your watchlist too; a coin whose only story is its own pump has no fundamental leg.",
|
|
191
|
+
]
|
|
192
|
+
: []),
|
|
193
|
+
...(hasPm
|
|
194
|
+
? [
|
|
195
|
+
"- Each pmMarkets row carries `end` (resolution date), `vol24h` and `liq` (USD): thin liquidity means a smaller stake and a wider required edge; your time stop must sit before `end`; a probability that moved on heavy volume is information, one that moved on none is noise.",
|
|
115
196
|
]
|
|
116
197
|
: []),
|
|
117
198
|
"",
|
|
118
199
|
"## Output contract — return ONLY this JSON object, nothing else:",
|
|
119
|
-
'{"decision":"skip"|"act","confidence":0..1,"reason":"
|
|
200
|
+
'{"decision":"skip"|"act","confidence":0..1,"reason":"brief label","rationale":"1-2 sentences","actions":[]}',
|
|
201
|
+
'Decision/action consistency is mandatory: decision="act" requires at least one complete action object; decision="skip" requires actions=[]. Never describe entering or managing a trade while returning an empty actions array.',
|
|
120
202
|
"Each action is one of:",
|
|
121
203
|
...actions.map((a) => `- ${a}`),
|
|
122
204
|
`Set each opening action's "confidence" (0..1) to your honest conviction — the runner REJECTS any open below abstention.minConfidence (${spec.abstention.minConfidence}). The decision-level "confidence" is the fallback when an action omits its own.`,
|
|
123
205
|
"",
|
|
206
|
+
"## Thesis on every open, and thesis exits (the runner enforces the exit)",
|
|
207
|
+
`Every opening action (${openKinds.join(" / ")}) MUST carry a \`thesis\`: \`summary\` = one sentence with the edge and why NOW; \`invalidation\` = what proves it wrong, with at least ONE machine-checkable condition:`,
|
|
208
|
+
...(hasCoinVenue
|
|
209
|
+
? [
|
|
210
|
+
"- coins: `priceBelow` for a long or `priceAbove` for a short = the level at which the idea is dead (a real structure level inside your stop-loss); and/or `maxHoldMinutes` = a time stop (minimum 60, at most 43200) after which an idea that has not worked is closed.",
|
|
211
|
+
]
|
|
212
|
+
: []),
|
|
213
|
+
...(hasPm
|
|
214
|
+
? [
|
|
215
|
+
"- prediction markets: `probabilityBelow` for a YES or `probabilityAbove` for a NO, in 0..100 points of the outcome's market probability (the `currentProbability` shown on the position) = the odds at which your read is wrong; and/or `maxHoldMinutes`. Never set a time stop past the market's `end` date.",
|
|
216
|
+
]
|
|
217
|
+
: []),
|
|
218
|
+
'- `catalyst`: free text naming the event whose outcome kills the idea (e.g. "CPI prints hot", "the ETF decision slips"). The runner never evaluates it; YOU re-judge it every cycle you manage the position.',
|
|
219
|
+
...(hasFutures
|
|
220
|
+
? [
|
|
221
|
+
"The runner re-checks every open futures position each cycle: when its price level or time stop is breached, the position is CLOSED automatically (logged as a thesis exit) on top of your stop-loss / take-profit. A wrong-side level (a long's priceBelow above entry) is dropped at open, so place it properly.",
|
|
222
|
+
]
|
|
223
|
+
: []),
|
|
224
|
+
...(hasPm
|
|
225
|
+
? [
|
|
226
|
+
"Prediction-market positions cannot be closed before settlement: an invalidated PM thesis is shown to you so you do not add to it.",
|
|
227
|
+
]
|
|
228
|
+
: []),
|
|
229
|
+
"Each open position shows its `thesis` with `status` (intact | invalidated), `holdMinutes` and, when broken, `invalidatedBy`. While the status is intact, HOLD: a discretionary close must name the broken condition or the resolved catalyst in its `rationaleSummary`. A small loss, an early profit below your target or a wiggle against you is not an exit. A position with no thesis (opened before this rule) is managed by its stop and target only.",
|
|
230
|
+
"",
|
|
124
231
|
"## How to act — a decisive trader in character, not a bystander",
|
|
125
232
|
"You ARE the character in the strategy above; trade like it. When you have a clear read — even a moderate-confidence one — TAKE THE POSITION, sized within your caps and protected with a stop. You wake every cycle and people watch you live: an agent that watches forever and never commits is useless to them and to itself.",
|
|
126
233
|
"Skip ONLY when the read is genuinely contradictory (signals fight each other), the data is stale, or you truly have no edge this cycle. A quiet tape where your thesis still has a small but REAL edge is an ACT, not a skip — take it, small, with a stop. Do not confuse caution with paralysis.",
|
|
127
|
-
'In "rationale" (shown LIVE in your public terminal) speak in YOUR voice and commit to a view in 1-2 vivid, specific sentences — what you see and what you are DOING about it, like a trader posting their move, not a risk report. Good: "ETH
|
|
234
|
+
'In "rationale" (shown LIVE in your public terminal) speak in YOUR voice and commit to a view in 1-2 vivid, specific sentences — what you see and what you are DOING about it, like a trader posting their move, not a risk report. Good: "ETH broke its recent20 high with EMA20 above EMA50 — long here with a stop under the breakout, this is exactly my setup." Weak: "conditions are mixed, waiting for clarity." Keep "reason" a short label.',
|
|
128
235
|
"",
|
|
129
236
|
"## Flagged setups this cycle — your wake-up list (observation.setups)",
|
|
130
237
|
"A deterministic scan already checked every watchlist coin and put the ones with real, tradeable structure RIGHT NOW into observation.setups — each has symbol, kind, bias, strength, and a factual note (trend / RSI / breakout / ATR reads). This is your shortlist; you do NOT need to re-derive whether a setup exists.",
|
|
131
238
|
'- If observation.setups is NON-EMPTY: act on the strongest one that fits YOUR strategy. The `bias` is the trend-following read; if you are a contrarian / mean-reversion trader, FADE it with the same facts (e.g. a downtrend that is also "RSI oversold" is YOUR long). Skipping a flagged setup needs a SPECIFIC reason tied to your thesis — "no clear setup" is NOT a valid skip when setups are listed.',
|
|
132
|
-
|
|
239
|
+
// The act-pressure above must never outrank a hard cap: without this
|
|
240
|
+
// release valve a direction-constrained agent, staring at only wrong-way
|
|
241
|
+
// setups, is squeezed between "skipping needs a specific reason" and a
|
|
242
|
+
// constraint the runner enforces — that squeeze is how a short-only agent
|
|
243
|
+
// opened momentum longs on 2026-08-24.
|
|
244
|
+
...(r.direction
|
|
245
|
+
? [
|
|
246
|
+
`- Your DIRECTION cap outranks this list: a setup whose only actionable read violates it (${r.direction === "short_only" ? "long" : "short"}-side) is a LEGITIMATE skip — name the constraint in one clause and move on. Never take the wrong side to avoid skipping.`,
|
|
247
|
+
]
|
|
248
|
+
: []),
|
|
249
|
+
hasPm
|
|
250
|
+
? "- If observation.setups is EMPTY: no coin has a flagged structure right now — but BEFORE you skip, check observation.pmMarkets for a crypto market your current read prices wrong (a PM mispricing is a valid ACT even with zero coin setups). Only then, if nothing is mispriced, skip new entries and just manage any open positions."
|
|
251
|
+
: "- If observation.setups is EMPTY: no coin has a flagged structure right now — skip new entries and just manage any open positions.",
|
|
133
252
|
"- A setup tagged `held` (held: long|short) is a position you ALREADY hold. Do NOT propose a new open on it — that only hits the margin cap and wastes the cycle. MANAGE it instead: trail the stop toward your target, ADD only if you have margin room AND fresh conviction, or cut if the thesis broke.",
|
|
134
253
|
"",
|
|
135
254
|
"## After you act — hold with conviction, do not churn",
|
|
@@ -137,21 +256,60 @@ opts = {}) {
|
|
|
137
256
|
"Place each stop at a real structural level with ROOM to breathe — past the swing or extreme by a sensible margin — and size the position DOWN to keep the risk small. A stop hugging your entry gets clipped by normal volatility and bleeds you a cut at a time. After a stop-out, do not immediately re-enter the same name and direction (that level is hot — wait for a genuinely fresh setup). Decisive entries, patient holds.",
|
|
138
257
|
"",
|
|
139
258
|
"## Manage your open positions — ride winners, cut losers",
|
|
140
|
-
"Each cycle, look at your OPEN positions FIRST, not just new entries. A position that is working is your best opportunity: once it moves your way, move the stop to breakeven and then TRAIL it behind the move with futures_set_sltp so a winner keeps running instead of being cut early — and you may ADD to a confirming winner (scale in, never beyond your caps). A position that is clearly wrong
|
|
259
|
+
"Each cycle, look at your OPEN positions FIRST, not just new entries. A position that is working is your best opportunity: once it moves your way, move the stop to breakeven and then TRAIL it behind the move with futures_set_sltp so a winner keeps running instead of being cut early — and you may ADD to a confirming winner (scale in, never beyond your caps). A position that is clearly wrong (its `thesis.status` reads invalidated, the level broke, the catalyst resolved against you) is cut cleanly instead of nursed; a position whose thesis is intact is held. Riding one good trade beats opening ten fresh ones.",
|
|
141
260
|
].join("\n");
|
|
142
261
|
}
|
|
143
|
-
export function buildUserPrompt(obs, journal) {
|
|
262
|
+
export function buildUserPrompt(obs, journal, opts = {}) {
|
|
263
|
+
// Default to every venue for backwards-compatible direct callers and probes.
|
|
264
|
+
// The runner always supplies the real spec, so disabled venue instructions and
|
|
265
|
+
// empty observation blocks never consume prompt space or invite invalid acts.
|
|
266
|
+
const venues = opts.venues ?? ["futures", "spot", "pm"];
|
|
267
|
+
const hasFutures = venues.includes("futures");
|
|
268
|
+
const hasSpot = venues.includes("spot");
|
|
269
|
+
const hasPm = venues.includes("pm");
|
|
144
270
|
const lines = [
|
|
145
271
|
"Decide for THIS cycle using only the observation below (data available now — no look-ahead).",
|
|
146
272
|
];
|
|
273
|
+
if (opts.capitalSizing) {
|
|
274
|
+
lines.push("capitalSizingPolicy is the opt-in paper sizing policy (percent fields use percentage points). capitalBook is captured owned-book collateral plus marked spot, reduced only by negative futures/PM marks on its walletId; positive open-position gains are excluded, so this is NOT complete marked equity. Positions on other walletIds remain visible for management but their collateral, marks and close proceeds do not fund this book. Missing/unavailable capitalBook means no new entries; otherwise-valid closes, protection, cancellations and spot sells remain available.");
|
|
275
|
+
}
|
|
276
|
+
if (opts.dailyRiskBudget) {
|
|
277
|
+
const entryActions = [
|
|
278
|
+
...(hasFutures ? ["futures_open (including adds)"] : []),
|
|
279
|
+
...(hasSpot ? ["spot_order buys"] : []),
|
|
280
|
+
...(hasPm ? ["pm_open"] : []),
|
|
281
|
+
];
|
|
282
|
+
const protectiveActions = [
|
|
283
|
+
...(hasFutures ? ["futures_close", "futures_set_sltp"] : []),
|
|
284
|
+
...(hasSpot ? ["spot_order sells", "spot_cancel"] : []),
|
|
285
|
+
];
|
|
286
|
+
lines.push(`dailyRiskBudget below is a runtime-state snapshot: each successful ${entryActions.join(" / ")} uses one slot. It is NOT a model-call, API-call or total-write budget. Multiple entries/adds in one decision share the remaining slots; propose no more than remain. Closing does not restore a used slot. A null limit/remaining means no daily count cap; other risk caps still apply.`, ...(protectiveActions.length > 0
|
|
287
|
+
? [
|
|
288
|
+
`Otherwise-valid ${protectiveActions.join(" / ")} do not consume these slots and remain available when the entry/add budget is exhausted.`,
|
|
289
|
+
]
|
|
290
|
+
: []), ...(opts.dailyRiskBudget.remaining === 0
|
|
291
|
+
? [
|
|
292
|
+
"Today's entry/add budget is EXHAUSTED until the next UTC day: propose no new entries or adds. Manage/protect existing positions and orders where valid, or skip; a flagged setup does not override this budget.",
|
|
293
|
+
]
|
|
294
|
+
: []));
|
|
295
|
+
}
|
|
147
296
|
// Flat-state steer: when the agent holds NOTHING, weaker models (Llama 3.1 8B)
|
|
148
297
|
// still emit futures_close / futures_set_sltp / spot_cancel with a hallucinated
|
|
149
298
|
// positionId/orderId — which fails the whole cycle's strict parse (one bad id
|
|
150
299
|
// zeroes the cycle). There is nothing to manage when flat, so say so plainly and
|
|
151
300
|
// point the model at OPENING. (Observed: an 8B agent dead 36/60 cycles this way.)
|
|
152
301
|
if ((obs.openPositions?.length ?? 0) === 0 &&
|
|
153
|
-
(obs.pmPositions?.length ?? 0) === 0) {
|
|
154
|
-
|
|
302
|
+
(!hasPm || (obs.pmPositions?.length ?? 0) === 0)) {
|
|
303
|
+
const openingActions = [
|
|
304
|
+
...(hasFutures ? ["futures_open"] : []),
|
|
305
|
+
...(hasSpot ? ["spot_order"] : []),
|
|
306
|
+
...(hasPm ? ["pm_open"] : []),
|
|
307
|
+
];
|
|
308
|
+
const forbiddenActions = [
|
|
309
|
+
...(hasFutures ? ["futures_close", "futures_set_sltp"] : []),
|
|
310
|
+
...(hasSpot ? ["spot_cancel"] : []),
|
|
311
|
+
];
|
|
312
|
+
lines.push(`You currently hold NO open positions${hasPm ? " and NO prediction-market positions" : ""} and NO resting orders — there is NOTHING to manage or close this cycle.${forbiddenActions.length > 0 ? ` Do NOT emit any ${forbiddenActions.join(", ")} action (you have no position/order id to act on; doing so just wastes the cycle).` : ""} Your ONLY moves are to OPEN the best available setup (${openingActions.join(" / ")}) or to skip.`);
|
|
155
313
|
}
|
|
156
314
|
// Slice-3 memory: the agent's own recent moves, so it manages with continuity —
|
|
157
315
|
// remembers the thesis behind each open position and does not re-open an idea it
|
|
@@ -159,30 +317,69 @@ export function buildUserPrompt(obs, journal) {
|
|
|
159
317
|
if (journal && journal.length > 0) {
|
|
160
318
|
lines.push("", "## Your recent moves (memory, newest last) — manage these with continuity; do NOT churn by re-opening an idea you just acted on:", ...journal.slice(-6).map((j) => `- ${j.did}`));
|
|
161
319
|
}
|
|
320
|
+
// Slice 2: name the positions whose stated thesis broke this cycle. On a live
|
|
321
|
+
// run the runner has already closed the futures ones (they are no longer in
|
|
322
|
+
// openPositions); whatever is listed here is for the MODEL to act on.
|
|
323
|
+
const brokenTheses = [
|
|
324
|
+
...obs.openPositions
|
|
325
|
+
.filter((p) => p.thesis?.status === "invalidated")
|
|
326
|
+
.map((p) => `futures pos#${p.id} ${p.side ?? ""} ${p.symbol ?? ""}: ${p.thesis?.invalidatedBy ?? "invalidated"} (close it with futures_close)`),
|
|
327
|
+
...(hasPm
|
|
328
|
+
? (obs.pmPositions ?? [])
|
|
329
|
+
.filter((p) => p.thesis?.status === "invalidated")
|
|
330
|
+
.map((p) => `PM pos#${p.id} "${(p.title ?? p.slug ?? "").slice(0, 50)}": ${p.thesis?.invalidatedBy ?? "invalidated"} (no close endpoint: do NOT add, let it settle)`)
|
|
331
|
+
: []),
|
|
332
|
+
];
|
|
333
|
+
if (brokenTheses.length > 0) {
|
|
334
|
+
lines.push("", "## Positions whose thesis is INVALIDATED this cycle", ...brokenTheses.map((b) => `- ${b}`));
|
|
335
|
+
}
|
|
162
336
|
// Settlement-feedback loop: surface the agent's recently-RESOLVED PM bets so the
|
|
163
337
|
// model can reflect and adapt. Reflective context only — never a new action.
|
|
164
|
-
|
|
338
|
+
if (hasPm)
|
|
339
|
+
lines.push(...formatPmResolutions(obs.pmResolutions ?? []));
|
|
165
340
|
lines.push("", "```json",
|
|
166
341
|
// Compact (no pretty-print indentation — ~40% fewer tokens, still valid JSON)
|
|
167
342
|
// and the trade ledger is capped so a busy shared book can't bloat the prompt.
|
|
168
343
|
JSON.stringify({
|
|
169
344
|
asOf: obs.asOf,
|
|
345
|
+
...(opts.dailyRiskBudget
|
|
346
|
+
? { dailyRiskBudget: opts.dailyRiskBudget }
|
|
347
|
+
: {}),
|
|
170
348
|
cashAvailableMusd: obs.cashAvailableMusd,
|
|
171
349
|
equityMusd: obs.equityMusd,
|
|
350
|
+
...(opts.capitalSizing
|
|
351
|
+
? {
|
|
352
|
+
capitalSizingPolicy: opts.capitalSizing,
|
|
353
|
+
...(obs.capitalBook ? { capitalBook: obs.capitalBook } : {}),
|
|
354
|
+
}
|
|
355
|
+
: {}),
|
|
172
356
|
openPositions: obs.openPositions,
|
|
173
357
|
openOrders: obs.openOrders,
|
|
174
|
-
pmPositions: obs.pmPositions,
|
|
358
|
+
...(hasPm ? { pmPositions: obs.pmPositions } : {}),
|
|
175
359
|
// Compact display: the model picks a market by its short `ref` and never
|
|
176
360
|
// sees (or mis-copies) the long source/slug/outcomeExternalMarketId — the
|
|
177
361
|
// runner resolves the ref back to those. Also ~halves the PM block's tokens.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
362
|
+
...(hasPm
|
|
363
|
+
? {
|
|
364
|
+
pmMarkets: obs.pmMarkets.map((m) => ({
|
|
365
|
+
ref: m.ref,
|
|
366
|
+
source: m.source,
|
|
367
|
+
title: m.title,
|
|
368
|
+
outcome: m.outcomeName,
|
|
369
|
+
prob: m.probability,
|
|
370
|
+
freshness: m.freshness?.status,
|
|
371
|
+
ageSeconds: m.freshness?.ageSeconds,
|
|
372
|
+
sourceAsOf: m.freshness?.asOf,
|
|
373
|
+
freshnessBasis: m.freshness?.basis,
|
|
374
|
+
quality: pmQualityOf(m.quality),
|
|
375
|
+
decisionSupport: pmDecisionSupportOf(m.decisionSupport),
|
|
376
|
+
// Slice 2 fundamentals: resolution date, 24h volume, liquidity.
|
|
377
|
+
end: m.endDate,
|
|
378
|
+
vol24h: roundUsd(m.volumeUsd),
|
|
379
|
+
liq: roundUsd(m.liquidityUsd),
|
|
380
|
+
})),
|
|
381
|
+
}
|
|
382
|
+
: {}),
|
|
186
383
|
watch: obs.watch,
|
|
187
384
|
setups: obs.setups,
|
|
188
385
|
news: obs.news,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ProviderName } from "./types.js";
|
|
2
|
+
export declare const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1";
|
|
3
|
+
export declare const DECISION_TOOL_NAME = "submit_trading_decision";
|
|
4
|
+
export interface ChatShape {
|
|
5
|
+
family: "openai-reasoning" | "nvidia-nemotron" | "anthropic" | "openai-compat";
|
|
6
|
+
tokenParam: "max_tokens" | "max_completion_tokens";
|
|
7
|
+
allowsTemperature: boolean;
|
|
8
|
+
jsonResponseFormat: boolean;
|
|
9
|
+
jsonSchema?: Record<string, unknown>;
|
|
10
|
+
jsonSchemaTransport?: "tool_call" | "response_format";
|
|
11
|
+
extraBody?: Record<string, unknown>;
|
|
12
|
+
systemHint?: string;
|
|
13
|
+
minProbeCompletionTokens: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function chatShapeFor(provider: ProviderName, model: string, baseUrl?: string): ChatShape;
|
|
16
|
+
/** Build the chat-completions body for a route from its capability shape. */
|
|
17
|
+
export declare function buildChatBody(shape: ChatShape, args: {
|
|
18
|
+
model: string;
|
|
19
|
+
system: string;
|
|
20
|
+
user: string;
|
|
21
|
+
maxTokens: number;
|
|
22
|
+
temperature?: number;
|
|
23
|
+
}): Record<string, unknown>;
|