@coinrithm/mcp-trading 0.7.15 → 0.7.16
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 +127 -8
- package/README.md +18 -3
- package/dist/agent/bench/bench.d.ts +59 -0
- package/dist/agent/bench/bench.js +319 -0
- package/dist/agent/bench/cassette.d.ts +97 -0
- package/dist/agent/bench/cassette.js +181 -0
- package/dist/agent/bench/costs.d.ts +28 -0
- package/dist/agent/bench/costs.js +60 -0
- package/dist/agent/bench/labelBuilder.d.ts +123 -0
- package/dist/agent/bench/labelBuilder.js +282 -0
- package/dist/agent/bench/labels.d.ts +75 -0
- package/dist/agent/bench/labels.js +259 -0
- package/dist/agent/bench/recordingClient.d.ts +51 -0
- package/dist/agent/bench/recordingClient.js +193 -0
- package/dist/agent/bench/replayClient.d.ts +41 -0
- package/dist/agent/bench/replayClient.js +262 -0
- package/dist/agent/bench/report.d.ts +115 -0
- package/dist/agent/bench/report.js +435 -0
- package/dist/agent/cli.d.ts +25 -0
- package/dist/agent/cli.js +300 -2
- package/dist/agent/client.d.ts +1 -0
- package/dist/agent/client.js +6 -0
- package/dist/agent/configurationWarnings.js +11 -0
- package/dist/agent/decision.d.ts +3 -0
- package/dist/agent/decision.js +85 -3
- package/dist/agent/decisionValidator.d.ts +30 -0
- package/dist/agent/decisionValidator.js +147 -2
- package/dist/agent/engine.d.ts +3 -3
- package/dist/agent/engine.js +3 -3
- package/dist/agent/observe.d.ts +27 -2
- package/dist/agent/observe.js +529 -89
- package/dist/agent/prompt.d.ts +4 -1
- package/dist/agent/prompt.js +98 -7
- package/dist/agent/promptTables.d.ts +1 -0
- package/dist/agent/promptTables.js +33 -0
- package/dist/agent/providerCapabilities.d.ts +6 -0
- package/dist/agent/providerCapabilities.js +23 -0
- package/dist/agent/providers.d.ts +25 -7
- package/dist/agent/providers.js +188 -48
- package/dist/agent/resolve.js +3 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +59 -15
- package/dist/agent/setups.d.ts +2 -1
- package/dist/agent/setups.js +17 -16
- package/dist/agent/signals.d.ts +33 -0
- package/dist/agent/signals.js +89 -0
- package/dist/agent/skill.js +37 -0
- package/dist/agent/skillValidator.js +33 -0
- package/dist/agent/strictLint.js +29 -0
- package/dist/agent/types.d.ts +149 -12
- package/dist/agent/universe.d.ts +78 -0
- package/dist/agent/universe.js +215 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,11 +5,130 @@ 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.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
## 0.7.16 (unpublished)
|
|
9
|
+
|
|
10
|
+
- Preserve independently timed funding-by-venue context in watchlist and
|
|
11
|
+
discovered entries, including regular and compact prompts. Keep raw rates,
|
|
12
|
+
interval normalization and unknown source/next-funding times distinct from
|
|
13
|
+
settlement funding.
|
|
14
|
+
- Bench reports retain up to two safe route attempts with response source,
|
|
15
|
+
failure class, actions-string diagnostic and admission categories. Legacy
|
|
16
|
+
missing evidence remains unknown; error text, latency, prompts and raw model
|
|
17
|
+
output are excluded. Record explicit truncation for nonstandard providers.
|
|
18
|
+
- Carry open interest, positioning, sampled liquidations, macro proxies,
|
|
19
|
+
chain TVL, stablecoin supply and sampled order-book depth through the runner
|
|
20
|
+
with their separate freshness, coverage and provenance limits.
|
|
21
|
+
- Keep news source URLs/categories and every related PM coin in context; add
|
|
22
|
+
the opt-in per-coin-horizon PM cap. Objective guidance stays within the
|
|
23
|
+
authored strategy and hard caps. Use the supported MCP SDK 1.31 dependency.
|
|
24
|
+
|
|
25
|
+
- Bench: the PM baselines now preserve the recorded agent's PM board policy
|
|
26
|
+
(for house agents: depth 30, churn removed), the original opportunity set.
|
|
27
|
+
Before, they asked for the uncurated 12-row page a live mechanical agent
|
|
28
|
+
reads, and a cassette recorded without it (house Mia) reported the market
|
|
29
|
+
as missing. Originally mechanical recordings keep their uncurated board;
|
|
30
|
+
live mechanical agents are unchanged. New recordings need only the original
|
|
31
|
+
pass and mark PM baseline readiness after successful PM reads, rather than
|
|
32
|
+
assuming that an enabled PM venue guarantees recorded evidence.
|
|
33
|
+
- Bench: two more PM baselines, `baseline:base-rate` (uninformative 50%) and
|
|
34
|
+
`baseline:random` (seeded 20-80%), next to `baseline:skip` and
|
|
35
|
+
`baseline:market`. They replay the same recorded market pass, so existing
|
|
36
|
+
corpora need no re-recording; futures/spot-only corpora still get skip only.
|
|
37
|
+
- Add `coinrithm-agent label --corpus <dir> [--horizon-hours 24] [--overwrite]`:
|
|
38
|
+
writes `labels/<cassetteId>.json` price bars from candles published after
|
|
39
|
+
each cassette's asOf (reads only). It waits for the horizon to elapse, uses
|
|
40
|
+
the finest bar size the cassette's age still allows, keeps existing PM and
|
|
41
|
+
funding labels, and writes PM settlement only from the public event
|
|
42
|
+
verdict (settlement-eligible + "settle" + a provider won/lost outcome
|
|
43
|
+
result; anything else stays unlabelled). Only
|
|
44
|
+
bars that close by the horizon are stored, and an existing file for
|
|
45
|
+
another horizon is reported (`horizon_mismatch`), never kept silently.
|
|
46
|
+
- Bench futures scoring walks only bars that open after asOf and close by
|
|
47
|
+
the horizon end; a horizon off the bar grid stops at the last full bar
|
|
48
|
+
before it (`horizonFlooredToBar`), so no price after the horizon is used.
|
|
49
|
+
- Add optional per-agent `signals` thresholds (`rsiOversold`, `rsiOverbought`,
|
|
50
|
+
`strongMovePct`, `leanMovePct`, `minStrength`) and a `data.indicatorRange`
|
|
51
|
+
data diet (`1D` 5m bars, `1W` 15m, `1M` 1h, `3M` 4h). They change the setup
|
|
52
|
+
shortlist AND the gate that decides when the model is called, so personality
|
|
53
|
+
changes what an agent notices. Absent blocks keep the previous constants and
|
|
54
|
+
the 1D range exactly. Both are validated fail-closed, strict-linted and
|
|
55
|
+
`$ref`-able; the prompt names the agent's bar size and RSI levels.
|
|
56
|
+
|
|
57
|
+
- Every watch entry now carries perpetual open interest when the API serves it
|
|
58
|
+
(`openInterest`: single-side USD over Bybit and OKX, 1h and 24h change,
|
|
59
|
+
`asOf`, `stale`), read from the existing `/api/agent/market` call. The prompt
|
|
60
|
+
explains how to read it with price; it is context, never a trade rule. Older
|
|
61
|
+
APIs without the block leave entries unchanged.
|
|
62
|
+
|
|
63
|
+
- Add an agent test bench: `coinrithm-agent record` saves the read inputs of
|
|
64
|
+
dry-run cycles as cassettes (writes refused, no model call), and
|
|
65
|
+
`coinrithm-agent bench` replays them through the production runner for
|
|
66
|
+
several agent variants and repeats, next to skip and market-implied
|
|
67
|
+
baselines. The deterministic report gives reject codes, action overlap,
|
|
68
|
+
repeat consistency, seeded paired bootstrap CIs, a calibrated A/A null
|
|
69
|
+
rate, a chronological holdout and optional label scoring. Quotes are
|
|
70
|
+
synthesized and counted; see "Bench" in docs/agent-runner.md for the limits.
|
|
71
|
+
|
|
72
|
+
- Add an optional `universe` block (market boundaries): a market-cap rank
|
|
73
|
+
band, a minimum 24h volume, stablecoins in or out, curated sectors and chain
|
|
74
|
+
ecosystems in or out, and the sort that defines an opportunity. Each cycle
|
|
75
|
+
the runner asks `GET /api/agent/universe` for rows inside those boundaries,
|
|
76
|
+
resolves the top `resolveTop` (max 10) into full `discovered` watch entries
|
|
77
|
+
and passes the rest as context. Rows are re-checked client-side, so a server
|
|
78
|
+
can narrow the set but never widen it; the blocklist still wins and every
|
|
79
|
+
cap applies. A failed screener degrades to the watchlist only. The block
|
|
80
|
+
can live in its own file via `$ref`. Without it, `universe_scan` keeps the
|
|
81
|
+
previous top-gainers scan exactly. Needs backend-v2 with the screener.
|
|
82
|
+
|
|
83
|
+
- Add three optional, runner-enforced prediction-market policy fields, so
|
|
84
|
+
numbers that strategies stated only in prose become executable. Absent keeps
|
|
85
|
+
the previous behaviour exactly; malformed values fail closed.
|
|
86
|
+
- `risk.pmMinEdgeGapPct` (0..100): the forecast must beat the fee-inclusive
|
|
87
|
+
cost by that share of the room left to 100 (16 means cost 50 needs 58). It
|
|
88
|
+
never lowers the global minimum edge, and with it set a `pm_open` without
|
|
89
|
+
`forecastProbability` is rejected (`pm_forecast_required`,
|
|
90
|
+
`pm_edge_below_gap_rule`). Mechanical benchmarks are exempt.
|
|
91
|
+
- `risk.pmMaxEdgePoints` (0..100): overconfidence guard; the forecast may
|
|
92
|
+
beat the fee-inclusive cost by at most that many points, and a forecast
|
|
93
|
+
becomes mandatory (`pm_edge_overconfident`). This is an optional action
|
|
94
|
+
constraint; its effect on future performance requires separate evaluation.
|
|
95
|
+
- `risk.pmMaxOpenPerEvent` (1..50): open bets per event (source + slug),
|
|
96
|
+
counting held positions and opens accepted this cycle (`pm_event_cap`).
|
|
97
|
+
- `risk.pmMaxOpenPerCoinHorizon` (1..50): open bets on the same directly
|
|
98
|
+
linked coin whose events end within 24 h of each other, across different
|
|
99
|
+
events, counting held positions and opens accepted this cycle
|
|
100
|
+
(`pm_coin_horizon_cap`; a value outside 1..50 fails closed). Every linked
|
|
101
|
+
coin is kept. Reads the additive discover `relatedCoins` and
|
|
102
|
+
`/positions/pm` `relatedCoins` / `eventEndDate`; a missing coin, or an end
|
|
103
|
+
that is missing, past or over 5 years out, never blocks. A count cap, not
|
|
104
|
+
a correlation model, over the open positions the feed returns (at most
|
|
105
|
+
200, newest first).
|
|
106
|
+
- `risk.pmMinMinutesToClose` (0..10080): reject a market whose known close is
|
|
107
|
+
nearer than that to the later of `observation.asOf` and the validation
|
|
108
|
+
clock after the model call (`pm_closes_too_soon`). An unknown or
|
|
109
|
+
unparseable close never blocks.
|
|
110
|
+
|
|
111
|
+
- Keep repeated provider 404/410 availability failures out of the generic model
|
|
112
|
+
failure kill-switch. BYO and self-hosted agents retain their configured model
|
|
113
|
+
and provider holds without entering a disable/revive loop. Transient failures
|
|
114
|
+
and invalid decisions still count toward the existing kill-switch.
|
|
115
|
+
- Keep rejected model text out of retained parser errors: JSON syntax errors
|
|
116
|
+
omit input excerpts, and schema errors omit rejected enum values and unknown
|
|
117
|
+
field names. Useful field/type diagnostics remain; validation is unchanged.
|
|
118
|
+
- Reject OpenAI-compatible and Anthropic responses that explicitly end at the
|
|
119
|
+
output token limit, and Anthropic responses stopped at the context-window
|
|
120
|
+
limit, even if their partial decision text happens to be valid JSON. No action
|
|
121
|
+
from that incomplete response reaches execution. Preserve reported token usage
|
|
122
|
+
and the hosted router's existing bounded malformed-response fallback. A
|
|
123
|
+
truncated direct-provider retry also retains its malformed classification.
|
|
124
|
+
- Classify rejected actions strings with bounded, fixed diagnostic categories.
|
|
125
|
+
JSON5 is used only for diagnostics; string-valued actions remain rejected.
|
|
126
|
+
No raw model output is retained by this diagnostic path.
|
|
127
|
+
|
|
128
|
+
## 0.7.15 (published 2026-10-02)
|
|
129
|
+
|
|
130
|
+
The verified release includes source changes through 1 October 2026. Its public
|
|
131
|
+
archive and MCP Registry delivery are recorded in the repository publishing log.
|
|
13
132
|
|
|
14
133
|
- PM board rows carry event settlement context from discovery's optional
|
|
15
134
|
`resolution`. Where `scope` is `per_outcome`, each row also carries its own
|
|
@@ -203,9 +322,9 @@ Source changes following the published 0.7.9 release. The TypeScript SDK remains
|
|
|
203
322
|
- Pin workflow actions and verify release-tool checksums. Add installed-package
|
|
204
323
|
compatibility and restart smoke checks across operating systems and runtimes.
|
|
205
324
|
|
|
206
|
-
0.7.10 was published and deployed on 2026-09-15. Registry and GitHub downloads
|
|
207
|
-
matched the CI-tested archive; hosted MCP and scheduler deployments finished.
|
|
208
|
-
See the [release record](https://github.com/CoinRithm/coinrithm-agent-trading/releases/tag/mcp-trading-v0.7.10).
|
|
325
|
+
0.7.10 was published and deployed on 2026-09-15. Registry and GitHub downloads
|
|
326
|
+
matched the CI-tested archive; hosted MCP and scheduler deployments finished.
|
|
327
|
+
See the [release record](https://github.com/CoinRithm/coinrithm-agent-trading/releases/tag/mcp-trading-v0.7.10).
|
|
209
328
|
|
|
210
329
|
## 0.7.9 - 2026-09-15
|
|
211
330
|
|
package/README.md
CHANGED
|
@@ -44,9 +44,24 @@ This package ships two binaries:
|
|
|
44
44
|
|
|
45
45
|
> **Paper trading only** — virtual funds (50,000 mUSD). Not financial advice.
|
|
46
46
|
|
|
47
|
-
## Version 0.7.
|
|
47
|
+
## Version 0.7.16 (unpublished)
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
The runner rejects explicitly incomplete provider responses before execution,
|
|
50
|
+
including valid-looking prefixes, and preserves their token usage. Bounded
|
|
51
|
+
shape diagnostics still reject string-valued actions. These guards do not raise
|
|
52
|
+
completion limits, repair model proposals or change customer strategies.
|
|
53
|
+
|
|
54
|
+
This release also includes configurable universe and signal boundaries, PM
|
|
55
|
+
policies, objective guidance, separately timed market context and the
|
|
56
|
+
`record`/`bench`/`label` evaluation tools. Funding-by-venue context does not
|
|
57
|
+
change settlement; optional data retains source, coverage and timing limits.
|
|
58
|
+
Benchmarks preserve missing outcomes and bounded route diagnostics. They do not
|
|
59
|
+
establish a profitable strategy or a superior model. See the changelog and
|
|
60
|
+
[runner documentation](https://github.com/CoinRithm/coinrithm-agent-trading/blob/main/docs/agent-runner.md).
|
|
61
|
+
|
|
62
|
+
The preceding published version, 0.7.15, includes the following capabilities.
|
|
63
|
+
|
|
64
|
+
It preserves observed-price evidence in public market summaries
|
|
50
65
|
(`hasObservedPrice` and bounded `sourceObservation`, including both sides of
|
|
51
66
|
venue comparisons), keeps full public `walletAddress` values in whale summaries
|
|
52
67
|
for `pm_data_whale_wallet` follow-up, adds the opt-in `whale_context` runner
|
|
@@ -106,7 +121,7 @@ summaries to the model observation for non-mechanical providers. The read is bes
|
|
|
106
121
|
availability status; it never expands PM candidates or bypasses risk and write
|
|
107
122
|
gates. Owners can enable it in Agent Studio under Risk & caps; existing agents
|
|
108
123
|
remain opted out unless their owners choose it. It ships in package version
|
|
109
|
-
0.7.15;
|
|
124
|
+
0.7.15; earlier MCP `0.7.14` does not include it.
|
|
110
125
|
The CoinRithm hosted scheduler runs this same engine for you — see the
|
|
111
126
|
[scheduler README](https://github.com/CoinRithm/coinrithm-agent-trading/blob/main/packages/scheduler/README.md) for the built,
|
|
112
127
|
DB-driven runtime.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ObserveOptions } from "../observe.js";
|
|
2
|
+
import { Provider } from "../providers.js";
|
|
3
|
+
import { AgentSpec, ProposedAction } from "../types.js";
|
|
4
|
+
import { Cassette } from "./cassette.js";
|
|
5
|
+
import type { BenchmarkStrategy } from "../mechanical.js";
|
|
6
|
+
import { LabelFile } from "./labels.js";
|
|
7
|
+
import { BenchReport, CycleRow } from "./report.js";
|
|
8
|
+
export declare const BASELINE_SKIP = "baseline:skip";
|
|
9
|
+
export declare const BASELINE_MARKET = "baseline:market";
|
|
10
|
+
export declare const BASELINE_BASE_RATE = "baseline:base-rate";
|
|
11
|
+
export declare const BASELINE_RANDOM = "baseline:random";
|
|
12
|
+
/**
|
|
13
|
+
* Mechanical PM baselines, one deterministic run per cassette each: the
|
|
14
|
+
* market's own probability, an uninformative 50, and a seeded random
|
|
15
|
+
* forecast (mechanical.ts). They need the recorded PM observation, so a
|
|
16
|
+
* futures/spot-only corpus has only baseline:skip.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MECHANICAL_BASELINES: ReadonlyArray<readonly [name: string, strategy: BenchmarkStrategy]>;
|
|
19
|
+
export declare const DEFAULT_REPEATS = 3;
|
|
20
|
+
export declare const MAX_REPEATS = 50;
|
|
21
|
+
export declare const DEFAULT_SEED = 20261007;
|
|
22
|
+
export declare const DEFAULT_BOOTSTRAP_RESAMPLES = 2000;
|
|
23
|
+
export interface BenchVariant {
|
|
24
|
+
name: string;
|
|
25
|
+
spec: AgentSpec;
|
|
26
|
+
mergedProse: string;
|
|
27
|
+
provider: Provider;
|
|
28
|
+
}
|
|
29
|
+
export interface RunBenchOptions {
|
|
30
|
+
cassettes: Cassette[];
|
|
31
|
+
variants: BenchVariant[];
|
|
32
|
+
repeats?: number;
|
|
33
|
+
labels?: Record<string, LabelFile>;
|
|
34
|
+
seed?: number;
|
|
35
|
+
bootstrapResamples?: number;
|
|
36
|
+
/** Include the skip and market-implied baselines (default true). */
|
|
37
|
+
baselines?: boolean;
|
|
38
|
+
log?: (line: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run `fn` with Date.now() reading `clockMs` plus the real time elapsed since
|
|
42
|
+
* the call. Restored afterwards even when `fn` throws. `new Date()` with no
|
|
43
|
+
* argument is NOT shifted (it does not call Date.now); in the runner it only
|
|
44
|
+
* stamps the whale-context fetchedAt and fallback asOf values.
|
|
45
|
+
*/
|
|
46
|
+
export declare function withReplayClock<T>(clockMs: number, fn: () => Promise<T>): Promise<T>;
|
|
47
|
+
/**
|
|
48
|
+
* The identity of a move WITHOUT its size, for action-set overlap and repeat
|
|
49
|
+
* consistency: did the variant do the same thing, not the same amount.
|
|
50
|
+
*/
|
|
51
|
+
export declare function actionKey(action: ProposedAction): string;
|
|
52
|
+
/** Replay one cassette for one variant once, through the real runCycle. */
|
|
53
|
+
export declare function replayCycle(cassette: Cassette, variant: string, repeat: number, spec: AgentSpec, mergedProse: string, provider: Provider, labels?: LabelFile, observeOptions?: ObserveOptions): Promise<CycleRow>;
|
|
54
|
+
/**
|
|
55
|
+
* Every modelling assumption behind the numbers, written into each report so
|
|
56
|
+
* a reader never has to guess what a "stop" or a "cost" means here.
|
|
57
|
+
*/
|
|
58
|
+
export declare function benchAssumptions(): Record<string, unknown>;
|
|
59
|
+
export declare function runBench(opts: RunBenchOptions): Promise<BenchReport>;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// The bench: run variants of an agent on the SAME recorded inputs, K times
|
|
2
|
+
// each, next to built-in baselines, and report honest paired statistics.
|
|
3
|
+
//
|
|
4
|
+
// Every replayed cycle goes through the production runCycle in DRY-RUN with a
|
|
5
|
+
// ReplayClient, so prompt building, parsing, ref resolution, capital sizing,
|
|
6
|
+
// validation and every reject code are exactly the shipped code paths. There
|
|
7
|
+
// is no bench-only decision logic to drift from production.
|
|
8
|
+
//
|
|
9
|
+
// Each cycle starts from a fresh run state (the same state recording used),
|
|
10
|
+
// so daily counters, debounce and journal can never leak between cassettes or
|
|
11
|
+
// repeats. The consequence: replay shows how a variant decides on a first
|
|
12
|
+
// look at a recorded market, not how it behaves deep into a running streak.
|
|
13
|
+
//
|
|
14
|
+
// Clock: Date.now() reads the recording's clock plus the real time elapsed in
|
|
15
|
+
// this cycle, so freshness and entry-predicate age checks see the age the data
|
|
16
|
+
// had when recorded (plus the variant's own model latency, as in production).
|
|
17
|
+
import { buildAgentDefinitionSnapshot } from "../definitionSnapshot.js";
|
|
18
|
+
import { clearPmCalibrationCache } from "../observe.js";
|
|
19
|
+
import { runCycle } from "../runner.js";
|
|
20
|
+
import { newState } from "../state.js";
|
|
21
|
+
import { mechanicalBaselineSpec } from "./cassette.js";
|
|
22
|
+
import { FUTURES_SYNTHETIC_FEE_BPS, PM_SYNTHETIC_FEE_RATE_AT_MID, SPOT_SYNTHETIC_FEE_BPS, } from "./costs.js";
|
|
23
|
+
import { scoreAction } from "./labels.js";
|
|
24
|
+
import { BENCH_RUN_ID, SKIP_PROVIDER } from "./recordingClient.js";
|
|
25
|
+
import { ReplayClient } from "./replayClient.js";
|
|
26
|
+
import { buildReport, chronological, } from "./report.js";
|
|
27
|
+
export const BASELINE_SKIP = "baseline:skip";
|
|
28
|
+
export const BASELINE_MARKET = "baseline:market";
|
|
29
|
+
export const BASELINE_BASE_RATE = "baseline:base-rate";
|
|
30
|
+
export const BASELINE_RANDOM = "baseline:random";
|
|
31
|
+
/**
|
|
32
|
+
* Mechanical PM baselines, one deterministic run per cassette each: the
|
|
33
|
+
* market's own probability, an uninformative 50, and a seeded random
|
|
34
|
+
* forecast (mechanical.ts). They need the recorded PM observation, so a
|
|
35
|
+
* futures/spot-only corpus has only baseline:skip.
|
|
36
|
+
*/
|
|
37
|
+
export const MECHANICAL_BASELINES = [
|
|
38
|
+
[BASELINE_MARKET, "market-implied"],
|
|
39
|
+
[BASELINE_BASE_RATE, "base-rate"],
|
|
40
|
+
[BASELINE_RANDOM, "random"],
|
|
41
|
+
];
|
|
42
|
+
export const DEFAULT_REPEATS = 3;
|
|
43
|
+
export const MAX_REPEATS = 50;
|
|
44
|
+
export const DEFAULT_SEED = 20261007;
|
|
45
|
+
export const DEFAULT_BOOTSTRAP_RESAMPLES = 2000;
|
|
46
|
+
const VARIANT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$/;
|
|
47
|
+
/**
|
|
48
|
+
* Run `fn` with Date.now() reading `clockMs` plus the real time elapsed since
|
|
49
|
+
* the call. Restored afterwards even when `fn` throws. `new Date()` with no
|
|
50
|
+
* argument is NOT shifted (it does not call Date.now); in the runner it only
|
|
51
|
+
* stamps the whale-context fetchedAt and fallback asOf values.
|
|
52
|
+
*/
|
|
53
|
+
export async function withReplayClock(clockMs, fn) {
|
|
54
|
+
const realNow = Date.now;
|
|
55
|
+
const offset = clockMs - realNow();
|
|
56
|
+
Date.now = () => realNow() + offset;
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
Date.now = realNow;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The identity of a move WITHOUT its size, for action-set overlap and repeat
|
|
66
|
+
* consistency: did the variant do the same thing, not the same amount.
|
|
67
|
+
*/
|
|
68
|
+
export function actionKey(action) {
|
|
69
|
+
const lower = (s) => (s ?? "").toLowerCase();
|
|
70
|
+
const upper = (s) => (s ?? "").toUpperCase();
|
|
71
|
+
switch (action.type) {
|
|
72
|
+
case "pm_open":
|
|
73
|
+
return `pm_open:${lower(action.source)}/${lower(action.slug)}/${action.outcomeExternalMarketId ?? action.ref ?? ""}`;
|
|
74
|
+
case "futures_open":
|
|
75
|
+
return `futures_open:${upper(action.symbol)}:${action.side}`;
|
|
76
|
+
case "futures_close":
|
|
77
|
+
return `futures_close:${action.positionId}`;
|
|
78
|
+
case "futures_set_sltp":
|
|
79
|
+
return `futures_set_sltp:${action.positionId}`;
|
|
80
|
+
case "spot_order":
|
|
81
|
+
return `spot_order:${upper(action.symbol)}:${action.side}:${action.orderType}`;
|
|
82
|
+
case "spot_cancel":
|
|
83
|
+
return `spot_cancel:${action.orderId}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function sizeOf(action) {
|
|
87
|
+
if (action.type === "pm_open")
|
|
88
|
+
return action.stakeMusd;
|
|
89
|
+
if (action.type === "futures_open")
|
|
90
|
+
return action.marginMusd;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function toRow(cassette, variant, repeat, result, runtimeError, client, labels) {
|
|
94
|
+
const actions = (result?.planned ?? []).map((p) => {
|
|
95
|
+
const forecast = p.action
|
|
96
|
+
.forecastProbability;
|
|
97
|
+
return {
|
|
98
|
+
key: actionKey(p.action),
|
|
99
|
+
type: p.action.type,
|
|
100
|
+
accepted: p.accepted,
|
|
101
|
+
...(p.accepted ? {} : { code: p.code ?? "unknown" }),
|
|
102
|
+
...(p.quote ? { quote: "synthesized" } : {}),
|
|
103
|
+
...(typeof forecast === "number"
|
|
104
|
+
? { forecastProbability: forecast }
|
|
105
|
+
: {}),
|
|
106
|
+
...(p.quote?.entryProbability !== undefined
|
|
107
|
+
? { entryProbability: p.quote.entryProbability }
|
|
108
|
+
: {}),
|
|
109
|
+
...(sizeOf(p.action) !== undefined ? { sizeMusd: sizeOf(p.action) } : {}),
|
|
110
|
+
...(p.accepted
|
|
111
|
+
? {
|
|
112
|
+
score: scoreAction({
|
|
113
|
+
action: p.action,
|
|
114
|
+
quote: p.quote,
|
|
115
|
+
asOf: cassette.asOf,
|
|
116
|
+
labels,
|
|
117
|
+
}),
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
const decisionType = runtimeError
|
|
123
|
+
? "runtime_error"
|
|
124
|
+
: (result?.decisionType ?? (result?.decision === "act" ? "act" : "skip"));
|
|
125
|
+
return {
|
|
126
|
+
cassetteId: cassette.id,
|
|
127
|
+
asOf: cassette.asOf,
|
|
128
|
+
variant,
|
|
129
|
+
repeat,
|
|
130
|
+
decision: result?.decision ?? "skip",
|
|
131
|
+
decisionType,
|
|
132
|
+
modelFailed: result?.modelFailed === true,
|
|
133
|
+
llmCallMade: result?.llmCallMade === true,
|
|
134
|
+
...(result?.routeAttempts
|
|
135
|
+
? {
|
|
136
|
+
routeAttempts: result.routeAttempts.slice(0, 2).map((attempt) => ({
|
|
137
|
+
provider: attempt.provider,
|
|
138
|
+
model: attempt.model,
|
|
139
|
+
outcome: attempt.outcome,
|
|
140
|
+
...(attempt.failureClass
|
|
141
|
+
? { failureClass: attempt.failureClass }
|
|
142
|
+
: {}),
|
|
143
|
+
...(attempt.status !== undefined ? { status: attempt.status } : {}),
|
|
144
|
+
...(attempt.responseSource
|
|
145
|
+
? { responseSource: attempt.responseSource }
|
|
146
|
+
: {}),
|
|
147
|
+
...(attempt.actionsStringDiagnostic
|
|
148
|
+
? { actionsStringDiagnostic: attempt.actionsStringDiagnostic }
|
|
149
|
+
: {}),
|
|
150
|
+
...(attempt.admissionReasons
|
|
151
|
+
? { admissionReasons: [...attempt.admissionReasons] }
|
|
152
|
+
: {}),
|
|
153
|
+
})),
|
|
154
|
+
...(result.routeAttempts.length > 2
|
|
155
|
+
? { routeAttemptsTruncated: true }
|
|
156
|
+
: {}),
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
159
|
+
...(result?.triggerCodes ? { triggerCodes: [...result.triggerCodes] } : {}),
|
|
160
|
+
...(result?.skipReason
|
|
161
|
+
? { skipReason: result.skipReason.slice(0, 200) }
|
|
162
|
+
: {}),
|
|
163
|
+
...(runtimeError ? { runtimeError: runtimeError.slice(0, 200) } : {}),
|
|
164
|
+
actions,
|
|
165
|
+
missingInputs: [...new Set(client.stats.missing)].sort(),
|
|
166
|
+
synthesizedQuotes: client.stats.synthesizedQuotes.length,
|
|
167
|
+
refusedWrites: [...client.stats.refusedWrites],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** Replay one cassette for one variant once, through the real runCycle. */
|
|
171
|
+
export async function replayCycle(cassette, variant, repeat, spec, mergedProse, provider, labels, observeOptions) {
|
|
172
|
+
clearPmCalibrationCache();
|
|
173
|
+
const client = new ReplayClient(cassette);
|
|
174
|
+
let result;
|
|
175
|
+
let runtimeError;
|
|
176
|
+
try {
|
|
177
|
+
result = await withReplayClock(cassette.clockMs, () => runCycle({
|
|
178
|
+
client,
|
|
179
|
+
provider,
|
|
180
|
+
spec,
|
|
181
|
+
mergedProse,
|
|
182
|
+
state: newState(BENCH_RUN_ID),
|
|
183
|
+
live: false,
|
|
184
|
+
...(observeOptions ? { observeOptions } : {}),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
// A crash is a result for this variant, never a reason to drop the cycle.
|
|
189
|
+
runtimeError = error instanceof Error ? error.message : String(error);
|
|
190
|
+
}
|
|
191
|
+
return toRow(cassette, variant, repeat, result, runtimeError, client, labels);
|
|
192
|
+
}
|
|
193
|
+
/** The always-skip baseline: no reads, no model, no action, by definition. */
|
|
194
|
+
function skipBaselineRow(cassette) {
|
|
195
|
+
return {
|
|
196
|
+
cassetteId: cassette.id,
|
|
197
|
+
asOf: cassette.asOf,
|
|
198
|
+
variant: BASELINE_SKIP,
|
|
199
|
+
repeat: 0,
|
|
200
|
+
decision: "skip",
|
|
201
|
+
decisionType: "skip",
|
|
202
|
+
modelFailed: false,
|
|
203
|
+
llmCallMade: false,
|
|
204
|
+
skipReason: "baseline: always skip",
|
|
205
|
+
actions: [],
|
|
206
|
+
missingInputs: [],
|
|
207
|
+
synthesizedQuotes: 0,
|
|
208
|
+
refusedWrites: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Every modelling assumption behind the numbers, written into each report so
|
|
213
|
+
* a reader never has to guess what a "stop" or a "cost" means here.
|
|
214
|
+
*/
|
|
215
|
+
export function benchAssumptions() {
|
|
216
|
+
return {
|
|
217
|
+
quotes: "every quote is synthesized from recorded evidence (PM: the discover probability of the chosen outcome; futures/spot: the recorded market price); none comes from the paper engine",
|
|
218
|
+
entry: "entry at the recorded (synthesized) quote with the paper fee model below; entry latency is not modelled",
|
|
219
|
+
pmFee: {
|
|
220
|
+
constant: "PM_SYNTHETIC_FEE_RATE_AT_MID",
|
|
221
|
+
value: PM_SYNTHETIC_FEE_RATE_AT_MID,
|
|
222
|
+
model: "fee = rate x 4p(1-p) of the price paid (openapi Paper Execution Realism v1 shape: ~1.8% near 50%, ~0 at the extremes); ask spread and size slippage are not modelled, so PM cost is optimistic by an unmeasured amount",
|
|
223
|
+
},
|
|
224
|
+
futuresFee: {
|
|
225
|
+
constant: "FUTURES_SYNTHETIC_FEE_BPS",
|
|
226
|
+
value: FUTURES_SYNTHETIC_FEE_BPS,
|
|
227
|
+
model: "flat taker fee on notional at entry and at exit (the runner's conservative pre-quote estimate); spread and slippage not modelled",
|
|
228
|
+
},
|
|
229
|
+
spotFee: {
|
|
230
|
+
constant: "SPOT_SYNTHETIC_FEE_BPS",
|
|
231
|
+
value: SPOT_SYNTHETIC_FEE_BPS,
|
|
232
|
+
},
|
|
233
|
+
pmEntryFloor: "the synthesized quote does not apply the server floor; the runner validator rejects the same action as pm_entry_below_floor (production: quote_ineligible)",
|
|
234
|
+
pmOutcome: "settled 0/1 from the label file; cost = stake / net shares (fee-inclusive); Brier uses the forecast the agent sent",
|
|
235
|
+
futuresOutcome: "an OHLC walk MODEL over label bars that open after asOf; it is not exact fill or stop behaviour",
|
|
236
|
+
sameBarStopAndTarget: "a bar touching both stop and take-profit is resolved as the STOP (conservative) and counted in labelled.futuresSameBarStopFirst",
|
|
237
|
+
missingBars: "a missing bar before the exit (or before the horizon end) gives outcome unlabelled_gap: counted, never filled",
|
|
238
|
+
funding: "included only when the label file carries funding events for the symbol (labelled.futuresFundingIncluded); otherwise excluded",
|
|
239
|
+
liquidation: "approximated at entry x (1 -/+ 1/leverage); forfeits the margin with no extra fill cost",
|
|
240
|
+
state: "fresh run state per cycle: no journal, theses, debounce or daily counters carried between cassettes or repeats",
|
|
241
|
+
clock: "Date.now = recording clock + real elapsed time, so data ages as recorded plus model latency",
|
|
242
|
+
outcomeCutoff: "asOf is the end of all recording reads; it is not the earlier trades sync cursor",
|
|
243
|
+
pnlComparisons: "zero means a complete no-action cycle; cycles with missing inputs, runtime errors, or any unscored accepted action are excluded from paired PnL, with exclusion counts shown; all repeats of a cassette must qualify",
|
|
244
|
+
inference: "per-cassette bootstrap assumes independent observations; overlapping market windows can violate that assumption; repeated A/A calibration does not establish independence or future performance",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export async function runBench(opts) {
|
|
248
|
+
const repeats = opts.repeats ?? DEFAULT_REPEATS;
|
|
249
|
+
if (!Number.isInteger(repeats) || repeats < 1 || repeats > MAX_REPEATS)
|
|
250
|
+
throw new Error(`repeats must be an integer from 1 to ${MAX_REPEATS}`);
|
|
251
|
+
if (opts.variants.length === 0)
|
|
252
|
+
throw new Error("at least one variant is required");
|
|
253
|
+
const names = new Set();
|
|
254
|
+
for (const v of opts.variants) {
|
|
255
|
+
if (!VARIANT_NAME_RE.test(v.name))
|
|
256
|
+
throw new Error(`variant name "${v.name}" must be 1-40 letters, digits, "_", "." or "-"`);
|
|
257
|
+
if (names.has(v.name))
|
|
258
|
+
throw new Error(`duplicate variant "${v.name}"`);
|
|
259
|
+
names.add(v.name);
|
|
260
|
+
}
|
|
261
|
+
if (opts.cassettes.length === 0)
|
|
262
|
+
throw new Error("the corpus has no cassettes");
|
|
263
|
+
const seed = (opts.seed ?? DEFAULT_SEED) >>> 0;
|
|
264
|
+
const resamples = opts.bootstrapResamples ?? DEFAULT_BOOTSTRAP_RESAMPLES;
|
|
265
|
+
const baselines = opts.baselines !== false;
|
|
266
|
+
const log = opts.log ?? (() => { });
|
|
267
|
+
const cassettes = chronological(opts.cassettes);
|
|
268
|
+
const rows = [];
|
|
269
|
+
for (const cassette of cassettes) {
|
|
270
|
+
const labels = opts.labels?.[cassette.id];
|
|
271
|
+
for (const v of opts.variants) {
|
|
272
|
+
for (let r = 0; r < repeats; r++) {
|
|
273
|
+
const row = await replayCycle(cassette, v.name, r, v.spec, v.mergedProse, v.provider, labels);
|
|
274
|
+
rows.push(row);
|
|
275
|
+
log(`${cassette.id} ${v.name}#${r + 1}: ${row.decisionType}, ${row.actions.filter((a) => a.accepted).length}/${row.actions.length} accepted${row.missingInputs.length ? `, ${row.missingInputs.length} missing input(s)` : ""}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (baselines) {
|
|
279
|
+
rows.push(skipBaselineRow(cassette));
|
|
280
|
+
// Deterministic, so one run per cassette each. Use the recorded agent's
|
|
281
|
+
// board policy, not the replacement mechanical provider's policy.
|
|
282
|
+
// Originally mechanical recordings keep their uncurated board.
|
|
283
|
+
if (cassette.marketBaselineRecorded)
|
|
284
|
+
for (const [name, strategy] of MECHANICAL_BASELINES)
|
|
285
|
+
rows.push(await replayCycle(cassette, name, 0, mechanicalBaselineSpec(cassette.spec, strategy), "", SKIP_PROVIDER, labels, {
|
|
286
|
+
curatedPmBoard: cassette.spec.model?.provider !== "mechanical",
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const variants = opts.variants.map((v) => ({
|
|
291
|
+
name: v.name,
|
|
292
|
+
kind: "variant",
|
|
293
|
+
definitionHash: buildAgentDefinitionSnapshot(v.spec, v.mergedProse)
|
|
294
|
+
.definitionHash,
|
|
295
|
+
}));
|
|
296
|
+
if (baselines) {
|
|
297
|
+
variants.push({ name: BASELINE_SKIP, kind: "baseline" });
|
|
298
|
+
if (cassettes.some((c) => c.marketBaselineRecorded))
|
|
299
|
+
for (const [name] of MECHANICAL_BASELINES)
|
|
300
|
+
variants.push({ name, kind: "baseline" });
|
|
301
|
+
}
|
|
302
|
+
const specHashes = [...new Set(cassettes.map((c) => c.agentSpecHash))].sort();
|
|
303
|
+
return buildReport({
|
|
304
|
+
cassettes,
|
|
305
|
+
rows,
|
|
306
|
+
variants,
|
|
307
|
+
repeats,
|
|
308
|
+
seed,
|
|
309
|
+
bootstrapResamples: resamples,
|
|
310
|
+
assumptions: benchAssumptions(),
|
|
311
|
+
fidelity: {
|
|
312
|
+
recordedAgentDefinitions: specHashes,
|
|
313
|
+
synthesizedQuotes: rows.reduce((s, r) => s + r.synthesizedQuotes, 0),
|
|
314
|
+
cyclesWithMissingInputs: rows.filter((r) => r.missingInputs.length > 0)
|
|
315
|
+
.length,
|
|
316
|
+
refusedWrites: rows.reduce((s, r) => s + r.refusedWrites.length, 0),
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AgentSpec } from "../types.js";
|
|
2
|
+
import type { BenchmarkStrategy } from "../mechanical.js";
|
|
3
|
+
import { LabelFile } from "./labels.js";
|
|
4
|
+
export declare const CASSETTE_SCHEMA = "coinrithm.bench.cassette.v1";
|
|
5
|
+
export interface RecordedResponse {
|
|
6
|
+
/** Canonical request key: METHOD + path + sorted query. */
|
|
7
|
+
key: string;
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
query: Record<string, string>;
|
|
11
|
+
status: number;
|
|
12
|
+
ok: boolean;
|
|
13
|
+
/** The body as the client parses it: JSON when it parses, else the text. */
|
|
14
|
+
data: unknown;
|
|
15
|
+
/** The recorded request failed in transport (no HTTP response at all). */
|
|
16
|
+
transportError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface Cassette {
|
|
19
|
+
schema: typeof CASSETTE_SCHEMA;
|
|
20
|
+
id: string;
|
|
21
|
+
recordedAt: string;
|
|
22
|
+
/** Epoch ms the replay clock starts from (Date.now during replay). */
|
|
23
|
+
clockMs: number;
|
|
24
|
+
/** Cutoff after ALL recording reads, including baseline-only inputs. */
|
|
25
|
+
asOf: string;
|
|
26
|
+
/** definitionHash of the recorded spec + prose (see definitionSnapshot). */
|
|
27
|
+
agentSpecHash: string;
|
|
28
|
+
spec: AgentSpec;
|
|
29
|
+
/** PM observation available for baselines. Legacy recordings used this
|
|
30
|
+
* only to mark an attempted extra mechanical pass; replay still reports
|
|
31
|
+
* missing inputs instead of treating the marker as a coverage guarantee. */
|
|
32
|
+
marketBaselineRecorded: boolean;
|
|
33
|
+
recordCycle: {
|
|
34
|
+
decision: string;
|
|
35
|
+
decisionType?: string;
|
|
36
|
+
skipReason?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Non-GET requests the recorder refused to forward (never sent). */
|
|
39
|
+
refusedRequests: string[];
|
|
40
|
+
responses: RecordedResponse[];
|
|
41
|
+
}
|
|
42
|
+
export interface CanonicalRequest {
|
|
43
|
+
key: string;
|
|
44
|
+
method: string;
|
|
45
|
+
path: string;
|
|
46
|
+
query: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Canonical request identity used for both recording and replay: upper-cased
|
|
50
|
+
* method, the path relative to the API base path, and the query parameters
|
|
51
|
+
* sorted by name. The host is deliberately not part of the key, so a cassette
|
|
52
|
+
* recorded against one base URL replays against the bench's fake one.
|
|
53
|
+
*/
|
|
54
|
+
export declare function canonicalRequest(method: string, url: string, basePath?: string): CanonicalRequest;
|
|
55
|
+
/** The path prefix of an API base URL ("" for a host-root base). */
|
|
56
|
+
export declare function basePathOf(baseUrl: string): string;
|
|
57
|
+
export declare function sha256Hex(text: string): string;
|
|
58
|
+
/** Sorted-key JSON, the same canonicalization the scorecard hash relies on. */
|
|
59
|
+
export declare function canonicalJson(value: unknown): string;
|
|
60
|
+
/**
|
|
61
|
+
* The spec a recording pass runs with: the agent's own spec with its gate
|
|
62
|
+
* forced to fire, so the post-gate reads (whale context) are always recorded
|
|
63
|
+
* and a variant whose gate fires on replay finds them in the cassette.
|
|
64
|
+
*/
|
|
65
|
+
export declare function recordingSpec(spec: AgentSpec): AgentSpec;
|
|
66
|
+
/**
|
|
67
|
+
* The market-implied baseline: the recorded agent's spec and caps with the
|
|
68
|
+
* deterministic mechanical "market-implied" strategy as its brain (mechanical
|
|
69
|
+
* .ts). runBench separately preserves the recorded agent's observation
|
|
70
|
+
* policy, so replacing the provider never changes the PM opportunity set.
|
|
71
|
+
*/
|
|
72
|
+
export declare function marketBaselineSpec(spec: AgentSpec): AgentSpec;
|
|
73
|
+
/**
|
|
74
|
+
* The same recorded spec with one of the mechanical benchmark strategies as
|
|
75
|
+
* its brain. base-rate and random pick the SAME market from the SAME
|
|
76
|
+
* observation as market-implied (mechanical.ts); all use the original
|
|
77
|
+
* recording's PM board and never fetch a separate baseline universe.
|
|
78
|
+
*/
|
|
79
|
+
export declare function mechanicalBaselineSpec(spec: AgentSpec, strategy: BenchmarkStrategy): AgentSpec;
|
|
80
|
+
/** Filesystem-safe, content-addressed cassette id: asOf stamp + body hash. */
|
|
81
|
+
export declare function cassetteId(asOf: string, contentHash: string): string;
|
|
82
|
+
export declare function writeCassette(dir: string, cassette: Cassette): string;
|
|
83
|
+
/** Validate a parsed cassette. Throws on a malformed one (fail closed). */
|
|
84
|
+
export declare function parseCassette(value: unknown, source: string): Cassette;
|
|
85
|
+
export interface Corpus {
|
|
86
|
+
cassettes: Cassette[];
|
|
87
|
+
labels: Record<string, LabelFile>;
|
|
88
|
+
/** Top-level .json files that are not cassettes (e.g. a saved report). */
|
|
89
|
+
ignored: string[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read every cassette at the top level of `dir` plus the optional
|
|
93
|
+
* `labels/<cassetteId>.json` files. A JSON file with the cassette schema that
|
|
94
|
+
* fails validation, or a malformed label file, stops the bench: a silently
|
|
95
|
+
* skipped input would bias the comparison.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readCorpus(dir: string): Corpus;
|