openoutcry 0.1.0__tar.gz
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.
- openoutcry-0.1.0/PKG-INFO +42 -0
- openoutcry-0.1.0/README.md +23 -0
- openoutcry-0.1.0/openoutcry/Cargo.toml +25 -0
- openoutcry-0.1.0/openoutcry/GOVERNANCE.md +93 -0
- openoutcry-0.1.0/openoutcry/README.md +84 -0
- openoutcry-0.1.0/openoutcry/contract/conformance/empty-portfolio-start.json +12 -0
- openoutcry-0.1.0/openoutcry/contract/conformance/legacy-decision-shape.json +18 -0
- openoutcry-0.1.0/openoutcry/contract/conformance/normal-multi-symbol.json +31 -0
- openoutcry-0.1.0/openoutcry/contract/conformance/signed-weight-short.json +23 -0
- openoutcry-0.1.0/openoutcry/contract/conformance/single-symbol.json +17 -0
- openoutcry-0.1.0/openoutcry/contract/decision.schema.json +46 -0
- openoutcry-0.1.0/openoutcry/contract/observation.schema.json +64 -0
- openoutcry-0.1.0/openoutcry/examples/reference-agent.py +41 -0
- openoutcry-0.1.0/openoutcry/examples/reference-agent.ts +36 -0
- openoutcry-0.1.0/openoutcry/examples/score-a-trajectory.rs +48 -0
- openoutcry-0.1.0/openoutcry/src/contract.rs +20 -0
- openoutcry-0.1.0/openoutcry/src/lib.rs +74 -0
- openoutcry-0.1.0/openoutcry/tests/conformance.rs +102 -0
- openoutcry-0.1.0/openoutcry/tests/smoke.rs +47 -0
- openoutcry-0.1.0/openoutcry-py/.gitignore +7 -0
- openoutcry-0.1.0/openoutcry-py/Cargo.lock +235 -0
- openoutcry-0.1.0/openoutcry-py/Cargo.toml +24 -0
- openoutcry-0.1.0/openoutcry-py/README.md +23 -0
- openoutcry-0.1.0/openoutcry-py/src/lib.rs +211 -0
- openoutcry-0.1.0/openoutcry-py/tests/test_gym.py +127 -0
- openoutcry-0.1.0/openoutcry-py/tests/test_verifiers.py +48 -0
- openoutcry-0.1.0/pyproject.toml +34 -0
- openoutcry-0.1.0/python/openoutcry/__init__.py +17 -0
- openoutcry-0.1.0/python/openoutcry/gym.py +162 -0
- openoutcry-0.1.0/python/openoutcry/verifiers_env.py +134 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openoutcry
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
7
|
+
Classifier: Topic :: Scientific/Engineering
|
|
8
|
+
Requires-Dist: numpy>=1.21
|
|
9
|
+
Requires-Dist: gymnasium>=1.0
|
|
10
|
+
Requires-Dist: verifiers ; extra == 'verifiers'
|
|
11
|
+
Provides-Extra: verifiers
|
|
12
|
+
Summary: OpenOutcry — a leak-free, point-in-time Gym for trading agents (Python distribution).
|
|
13
|
+
Keywords: trading,agent,environment,backtest,reinforcement-learning,gymnasium
|
|
14
|
+
Author: General Liquidity, Inc.
|
|
15
|
+
License: MIT OR Apache-2.0
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
18
|
+
|
|
19
|
+
# openoutcry (Python)
|
|
20
|
+
|
|
21
|
+
Python distribution of **OpenOutcry** — a leak-free, point-in-time *Gym for trading
|
|
22
|
+
agents*. A pyo3 binding over the Rust environment plus a `gymnasium`-compatible
|
|
23
|
+
wrapper and a PrimeIntellect `verifiers` environment.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from openoutcry import OpenOutcryEnv
|
|
27
|
+
|
|
28
|
+
env = OpenOutcryEnv(n_symbols=4, n_days=120, seed=7)
|
|
29
|
+
obs, info = env.reset()
|
|
30
|
+
done = False
|
|
31
|
+
while not done:
|
|
32
|
+
action = env.action_space.sample() # target-weight vector
|
|
33
|
+
obs, reward, terminated, truncated, info = env.step(action)
|
|
34
|
+
done = terminated or truncated
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The native binding (`openoutcry.openoutcry_py.TradingEnv`) exchanges the
|
|
38
|
+
language-agnostic wire JSON at its boundary: `reset() -> str` and
|
|
39
|
+
`step(decision_json) -> (obs_json, reward, done, info_json)`.
|
|
40
|
+
|
|
41
|
+
Build from source with [maturin](https://www.maturin.rs): `python -m maturin develop`.
|
|
42
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# openoutcry (Python)
|
|
2
|
+
|
|
3
|
+
Python distribution of **OpenOutcry** — a leak-free, point-in-time *Gym for trading
|
|
4
|
+
agents*. A pyo3 binding over the Rust environment plus a `gymnasium`-compatible
|
|
5
|
+
wrapper and a PrimeIntellect `verifiers` environment.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from openoutcry import OpenOutcryEnv
|
|
9
|
+
|
|
10
|
+
env = OpenOutcryEnv(n_symbols=4, n_days=120, seed=7)
|
|
11
|
+
obs, info = env.reset()
|
|
12
|
+
done = False
|
|
13
|
+
while not done:
|
|
14
|
+
action = env.action_space.sample() # target-weight vector
|
|
15
|
+
obs, reward, terminated, truncated, info = env.step(action)
|
|
16
|
+
done = terminated or truncated
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The native binding (`openoutcry.openoutcry_py.TradingEnv`) exchanges the
|
|
20
|
+
language-agnostic wire JSON at its boundary: `reset() -> str` and
|
|
21
|
+
`step(decision_json) -> (obs_json, reward, done, info_json)`.
|
|
22
|
+
|
|
23
|
+
Build from source with [maturin](https://www.maturin.rs): `python -m maturin develop`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "openoutcry"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
license = "MIT OR Apache-2.0"
|
|
6
|
+
description = "OpenOutcry — leak-free point-in-time trading-agent environment (a Gym for trading agents)."
|
|
7
|
+
repository = "https://github.com/general-liquidity/openoutcry"
|
|
8
|
+
keywords = ["trading", "agent", "environment", "backtest", "simulation"]
|
|
9
|
+
categories = ["science", "finance", "simulation"]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
|
|
12
|
+
[dependencies]
|
|
13
|
+
sharpebench-sim = "0.0.7"
|
|
14
|
+
sharpebench-protocol = "0.0.7"
|
|
15
|
+
sharpebench-core = "0.0.7"
|
|
16
|
+
|
|
17
|
+
[dev-dependencies]
|
|
18
|
+
serde_json = "1"
|
|
19
|
+
|
|
20
|
+
# Lints enforced by the manifest (not just CI's `-D warnings`), so a plain
|
|
21
|
+
# `cargo clippy` fails the same way CI does. Crates opt in via `[lints] workspace = true`.
|
|
22
|
+
[lints.clippy]
|
|
23
|
+
all = { level = "deny", priority = -1 }
|
|
24
|
+
todo = "deny"
|
|
25
|
+
dbg_macro = "deny"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# OpenOutcry Agent Interface — Governance
|
|
2
|
+
|
|
3
|
+
The contract is the product. Agents are **external** programs in any language that
|
|
4
|
+
read a `MarketObservation` (JSON) and reply with a `Decision` (JSON). The whole
|
|
5
|
+
adoption story is that this surface stays tiny and stable, so any vendor can build
|
|
6
|
+
against it once and not have it move under them. This document is the promise that
|
|
7
|
+
backs that.
|
|
8
|
+
|
|
9
|
+
The authoritative artifacts are:
|
|
10
|
+
|
|
11
|
+
- `contract/observation.schema.json` and `contract/decision.schema.json` — JSON
|
|
12
|
+
Schema (draft 2020-12) that non-Rust implementers validate against.
|
|
13
|
+
- `src/contract.rs` — `CONTRACT_VERSION`, the single source of truth for the wire
|
|
14
|
+
version.
|
|
15
|
+
- `contract/conformance/*.json` + `tests/conformance.rs` — the conformance kit.
|
|
16
|
+
|
|
17
|
+
## 1. Additive-only evolution
|
|
18
|
+
|
|
19
|
+
The contract evolves **additively**. The only backwards-compatible change is adding
|
|
20
|
+
a **new field that is optional with a default** — every field a producer may omit
|
|
21
|
+
must deserialize on the consumer to a sensible default, so an agent written against
|
|
22
|
+
an older version keeps parsing newer observations and the harness keeps parsing
|
|
23
|
+
older decisions.
|
|
24
|
+
|
|
25
|
+
Concretely, in Rust this means the field carries `#[serde(default)]` (or
|
|
26
|
+
`#[serde(default = "…")]`). Today's optional-with-default fields are `fundamentals`
|
|
27
|
+
and `news` on `SymbolSnapshot`, `confidence` and `rationale` on `Order`, and
|
|
28
|
+
`reasoning` on `Decision`. They are **not** in the schemas' `required` lists, by
|
|
29
|
+
construction.
|
|
30
|
+
|
|
31
|
+
The following are **breaking** and are forbidden on the v1 surface:
|
|
32
|
+
|
|
33
|
+
- removing a field,
|
|
34
|
+
- renaming a field,
|
|
35
|
+
- retyping a field (e.g. `number` → `string`, widening an enum's existing variant),
|
|
36
|
+
- making a previously optional field required, or
|
|
37
|
+
- removing or renaming an `action` enum value.
|
|
38
|
+
|
|
39
|
+
A breaking change does not mutate `MarketObservation` / `Decision` in place. It ships
|
|
40
|
+
a **parallel namespace** — `ObservationV2` / `DecisionV2` with their own schemas and
|
|
41
|
+
their own `CONTRACT_VERSION` major — and the two run side by side through the
|
|
42
|
+
deprecation window below. Adding a *new* `action` variant is itself breaking for
|
|
43
|
+
consumers that exhaustively match, so it also goes through V2, not an additive bump.
|
|
44
|
+
|
|
45
|
+
## 2. `CONTRACT_VERSION` semantics
|
|
46
|
+
|
|
47
|
+
`CONTRACT_VERSION` (currently `"1.0"`) versions the **wire shape only**. It is
|
|
48
|
+
deliberately decoupled from the `openoutcry` crate version and the npm / PyPI package
|
|
49
|
+
versions, which move on their own release cadence.
|
|
50
|
+
|
|
51
|
+
- **Major** (`1.0` → `2.0`): a breaking change shipped as a parallel `…V2` namespace.
|
|
52
|
+
- **Minor** (`1.0` → `1.1`): reserved for a *batch* of additive fields significant
|
|
53
|
+
enough to advertise. A single additive field needs no bump at all — existing agents
|
|
54
|
+
are unaffected by definition, and the conformance badge stays valid.
|
|
55
|
+
|
|
56
|
+
A package release never, on its own, bumps `CONTRACT_VERSION`. Bug fixes, new baseline
|
|
57
|
+
agents, docs, and extra re-exports change the package version and leave the contract
|
|
58
|
+
version frozen.
|
|
59
|
+
|
|
60
|
+
## 3. Deprecation window
|
|
61
|
+
|
|
62
|
+
When a major (`V2`) lands, the previous major is **supported in parallel for at least
|
|
63
|
+
two minor package releases (no less than 90 days)**, whichever is longer. During the
|
|
64
|
+
window:
|
|
65
|
+
|
|
66
|
+
1. both namespaces deserialize and run; the harness accepts either,
|
|
67
|
+
2. the superseded version is marked `#[deprecated]` in Rust and flagged as deprecated
|
|
68
|
+
in the schema `description`, and
|
|
69
|
+
3. the changelog states the removal release up front.
|
|
70
|
+
|
|
71
|
+
Only after the window closes may the old namespace be removed — and that removal is
|
|
72
|
+
itself a major package release.
|
|
73
|
+
|
|
74
|
+
## 4. Conformance badge
|
|
75
|
+
|
|
76
|
+
An implementation that passes the conformance kit
|
|
77
|
+
(`contract/conformance/*.json` exercised by `tests/conformance.rs`, or the equivalent
|
|
78
|
+
validation against the published JSON Schemas) may state:
|
|
79
|
+
|
|
80
|
+
> **conforms to the OpenOutcry Agent Interface v1.0**
|
|
81
|
+
|
|
82
|
+
To earn it, an agent must, for every observation in the kit:
|
|
83
|
+
|
|
84
|
+
1. parse the `MarketObservation` against `observation.schema.json`,
|
|
85
|
+
2. emit a `Decision` that validates against `decision.schema.json` — every `action`
|
|
86
|
+
in the enum, every `target_weight` finite, and every order `symbol` a subset of
|
|
87
|
+
the observed symbols, and
|
|
88
|
+
3. round-trip the legacy decision shape (no `confidence` / `rationale` / `reasoning`)
|
|
89
|
+
without error, proving the additive-only discipline holds.
|
|
90
|
+
|
|
91
|
+
The badge names the **contract** version it was earned against (`v1.0`), not the
|
|
92
|
+
package version. It remains valid across additive (non-bumping) changes and must be
|
|
93
|
+
re-earned against a new major.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# OpenOutcry
|
|
2
|
+
|
|
3
|
+
**A leak-free, point-in-time environment for trading agents — and the language-agnostic contract they speak.**
|
|
4
|
+
|
|
5
|
+
OpenOutcry is the open-outcry trading floor for agents: the harness hands the agent a point-in-time
|
|
6
|
+
`Observation`, the agent returns a `Decision`, repeat. Look-ahead is *structurally impossible* (the
|
|
7
|
+
environment owns the time cursor and never hands out a future bar), and trajectories are
|
|
8
|
+
recompute-from-raw-decisions, so an agent cannot lie about its returns.
|
|
9
|
+
|
|
10
|
+
The strategic bet is **interface ownership**: if every trading agent in the open ecosystem conforms to
|
|
11
|
+
the OpenOutcry `Observation`/`Decision` contract, then [SharpeBench](https://crates.io/crates/sharpebench-core)
|
|
12
|
+
is the natural scorer and the whole funnel — env → trajectory → score → leaderboard — runs on one
|
|
13
|
+
standard. The interface *is* the product; the simulator is the credibility behind it.
|
|
14
|
+
|
|
15
|
+
## The agent contract (the standard)
|
|
16
|
+
|
|
17
|
+
An agent is just a program that reads an `Observation` and writes a `Decision` — in any language, over
|
|
18
|
+
stdio (newline-JSON) or HTTP (`POST /decide`):
|
|
19
|
+
|
|
20
|
+
```jsonc
|
|
21
|
+
// Observation (harness → agent)
|
|
22
|
+
{ "date": "2025-01-02", "cash": 1.0,
|
|
23
|
+
"symbols": [{ "symbol": "AAPL", "close_history": [187.2, 188.0, 190.4] }],
|
|
24
|
+
"portfolio": [] }
|
|
25
|
+
|
|
26
|
+
// Decision (agent → harness)
|
|
27
|
+
{ "orders": [{ "symbol": "AAPL", "action": "buy", "target_weight": 0.5 }] }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The wire shape is versioned (`CONTRACT_VERSION`), evolves **additively only** (new fields are optional
|
|
31
|
+
with defaults), and is pinned by published JSON Schemas + a conformance kit. See
|
|
32
|
+
[`GOVERNANCE.md`](./GOVERNANCE.md) and [`contract/`](./contract/).
|
|
33
|
+
|
|
34
|
+
## The Gym lifecycle
|
|
35
|
+
|
|
36
|
+
The same engine SharpeBench runs *closed* (`run_backtest`), OpenOutcry exposes *open* — the caller drives it:
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
use openoutcry::{TradingEnv, Dataset, CostModel, Window, BuyAndHold, Agent};
|
|
40
|
+
|
|
41
|
+
let data = Dataset::synthetic(4, 120, 1);
|
|
42
|
+
let mut env = TradingEnv::new(data, Window { start: 20, end: 120 }, CostModel::default(), 7);
|
|
43
|
+
let mut agent = BuyAndHold;
|
|
44
|
+
let mut obs = env.reset();
|
|
45
|
+
loop {
|
|
46
|
+
let decision = agent.decide(&obs);
|
|
47
|
+
let step = env.step(decision); // -> { observation, reward, done, info }
|
|
48
|
+
obs = step.observation;
|
|
49
|
+
if step.done { break; }
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both stepping surfaces call one shared `step_once` body, so a trajectory the env produces is
|
|
54
|
+
**byte-identical** to the equivalent `run_backtest` (enforced by `env_step_matches_run_backtest`).
|
|
55
|
+
|
|
56
|
+
## env → SharpeBench score
|
|
57
|
+
|
|
58
|
+
Run with capture, hand the trajectory to a *separate verifier* that recomputes the submission from the
|
|
59
|
+
raw decisions + frozen data alone, then score:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
cargo run -p openoutcry --example score-a-trajectory
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
(see [`examples/score-a-trajectory.rs`](./examples/score-a-trajectory.rs)). Tamper with the trajectory
|
|
66
|
+
and the honest replay recomputes to different returns — this is the trust hinge of the whole ecosystem.
|
|
67
|
+
|
|
68
|
+
## Distribution
|
|
69
|
+
|
|
70
|
+
OpenOutcry ships from one Rust engine to every surface, with a language-agnostic wire contract on top so
|
|
71
|
+
agents can be written in anything:
|
|
72
|
+
|
|
73
|
+
- **Rust** — `openoutcry` (this crate).
|
|
74
|
+
- **TypeScript / npm** — `@general-liquidity/openoutcry` (the engine compiled to WASM).
|
|
75
|
+
- **Python / PyPI** — `openoutcry`, with a `gymnasium.Env` adapter and a PrimeIntellect `verifiers`
|
|
76
|
+
environment so it plugs into the RL-training stacks directly.
|
|
77
|
+
|
|
78
|
+
Reference agents in Rust, TypeScript, and Python double as the conformance smoke tests
|
|
79
|
+
([`examples/`](./examples/)).
|
|
80
|
+
|
|
81
|
+
## Status
|
|
82
|
+
|
|
83
|
+
Incubating inside the SharpeBench workspace (it depends on the published `sharpebench-sim` engine).
|
|
84
|
+
It graduates to its own repository at distribution time, consuming the engine as a versioned crate.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cold start: full cash, no holdings, snapshots omit optional fundamentals/news",
|
|
3
|
+
"observation": {
|
|
4
|
+
"date": "2025-01-02",
|
|
5
|
+
"cash": 1.0,
|
|
6
|
+
"symbols": [
|
|
7
|
+
{ "symbol": "SPY", "close_history": [468.1, 470.0, 472.5] },
|
|
8
|
+
{ "symbol": "QQQ", "close_history": [402.3, 404.9, 401.0] }
|
|
9
|
+
],
|
|
10
|
+
"portfolio": []
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "legacy decision shape: orders omit confidence/rationale and the decision omits reasoning",
|
|
3
|
+
"observation": {
|
|
4
|
+
"date": "2025-02-10",
|
|
5
|
+
"cash": 1.0,
|
|
6
|
+
"symbols": [
|
|
7
|
+
{ "symbol": "GLD", "close_history": [188.0, 189.5, 190.1] },
|
|
8
|
+
{ "symbol": "SLV", "close_history": [22.1, 22.4, 22.0] }
|
|
9
|
+
],
|
|
10
|
+
"portfolio": []
|
|
11
|
+
},
|
|
12
|
+
"legacy_decision": {
|
|
13
|
+
"orders": [
|
|
14
|
+
{ "symbol": "GLD", "action": "buy", "target_weight": 0.5 },
|
|
15
|
+
{ "symbol": "SLV", "action": "close", "target_weight": 0.0 }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "normal multi-symbol observation with fundamentals, news and held positions",
|
|
3
|
+
"observation": {
|
|
4
|
+
"date": "2025-03-14",
|
|
5
|
+
"cash": 1.0,
|
|
6
|
+
"symbols": [
|
|
7
|
+
{
|
|
8
|
+
"symbol": "AAPL",
|
|
9
|
+
"close_history": [186.4, 187.2, 188.0, 190.4, 191.1],
|
|
10
|
+
"fundamentals": { "pe": 28.4, "revenue_yoy": 0.06 },
|
|
11
|
+
"news": ["Apple unveils new chip", "Services revenue at record"]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"symbol": "MSFT",
|
|
15
|
+
"close_history": [402.1, 405.6, 409.0, 411.3, 408.7],
|
|
16
|
+
"fundamentals": { "pe": 35.1, "revenue_yoy": 0.12 },
|
|
17
|
+
"news": ["Azure growth accelerates"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"symbol": "NVDA",
|
|
21
|
+
"close_history": [870.0, 905.4, 889.2, 921.0, 950.5],
|
|
22
|
+
"fundamentals": { "pe": 61.0, "revenue_yoy": 1.22 },
|
|
23
|
+
"news": []
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"portfolio": [
|
|
27
|
+
{ "symbol": "AAPL", "shares": 1.5, "avg_price": 180.0 },
|
|
28
|
+
{ "symbol": "MSFT", "shares": 0.8, "avg_price": 395.0 }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exercises the full decision surface: sell/hold actions and a signed (short) target weight",
|
|
3
|
+
"observation": {
|
|
4
|
+
"date": "2025-05-05",
|
|
5
|
+
"cash": 1.0,
|
|
6
|
+
"symbols": [
|
|
7
|
+
{ "symbol": "TSLA", "close_history": [240.0, 235.5, 230.1] },
|
|
8
|
+
{ "symbol": "F", "close_history": [12.1, 12.0, 11.8] },
|
|
9
|
+
{ "symbol": "GM", "close_history": [44.0, 44.6, 45.2] }
|
|
10
|
+
],
|
|
11
|
+
"portfolio": [
|
|
12
|
+
{ "symbol": "TSLA", "shares": 0.5, "avg_price": 250.0 }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"legacy_decision": {
|
|
16
|
+
"orders": [
|
|
17
|
+
{ "symbol": "TSLA", "action": "sell", "target_weight": -0.3, "confidence": 0.7, "rationale": "downtrend" },
|
|
18
|
+
{ "symbol": "F", "action": "hold", "target_weight": 0.0 },
|
|
19
|
+
{ "symbol": "GM", "action": "buy", "target_weight": 0.5 }
|
|
20
|
+
],
|
|
21
|
+
"reasoning": "tilt short autos ex-GM"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "single-symbol universe with one open position",
|
|
3
|
+
"observation": {
|
|
4
|
+
"date": "2025-06-20",
|
|
5
|
+
"cash": 0.25,
|
|
6
|
+
"symbols": [
|
|
7
|
+
{
|
|
8
|
+
"symbol": "BTC",
|
|
9
|
+
"close_history": [61000.0, 62450.0, 60900.5, 63100.0],
|
|
10
|
+
"news": ["ETF inflows continue"]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"portfolio": [
|
|
14
|
+
{ "symbol": "BTC", "shares": 0.012, "avg_price": 59000.0 }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openoutcry.dev/contract/v1/decision.schema.json",
|
|
4
|
+
"title": "Decision",
|
|
5
|
+
"description": "What an agent returns at one decision step: per-instrument orders plus an optional free-text rationale. OpenOutcry Agent Interface v1.0.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["orders"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"orders": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"description": "Per-instrument instructions. May be empty (a hold).",
|
|
13
|
+
"items": { "$ref": "#/$defs/Order" }
|
|
14
|
+
},
|
|
15
|
+
"reasoning": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Free-text rationale captured into the trajectory. Optional; defaults to empty."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"$defs": {
|
|
21
|
+
"Order": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["symbol", "action", "target_weight"],
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"symbol": { "type": "string" },
|
|
27
|
+
"action": {
|
|
28
|
+
"description": "Discrete action label (sizing is carried by target_weight).",
|
|
29
|
+
"enum": ["buy", "sell", "hold", "close"]
|
|
30
|
+
},
|
|
31
|
+
"target_weight": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Target portfolio weight for this symbol, signed for shorts."
|
|
34
|
+
},
|
|
35
|
+
"confidence": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "Stated conviction in [0, 1], scored for calibration. Optional; defaults to 0.5."
|
|
38
|
+
},
|
|
39
|
+
"rationale": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Optional one-line rationale for this order. Defaults to empty."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openoutcry.dev/contract/v1/observation.schema.json",
|
|
4
|
+
"title": "MarketObservation",
|
|
5
|
+
"description": "What an agent sees at one point-in-time decision step. All data is point-in-time: close_history, fundamentals and news only ever hold information available at or before `date`. OpenOutcry Agent Interface v1.0.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["date", "cash", "symbols", "portfolio"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"date": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "ISO-8601 date of the decision point."
|
|
13
|
+
},
|
|
14
|
+
"cash": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"description": "Cash available to the agent."
|
|
17
|
+
},
|
|
18
|
+
"symbols": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"description": "Point-in-time snapshot per tradable instrument.",
|
|
21
|
+
"items": { "$ref": "#/$defs/SymbolSnapshot" }
|
|
22
|
+
},
|
|
23
|
+
"portfolio": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"description": "The agent's current holdings.",
|
|
26
|
+
"items": { "$ref": "#/$defs/PositionState" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"$defs": {
|
|
30
|
+
"SymbolSnapshot": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": ["symbol", "close_history"],
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"properties": {
|
|
35
|
+
"symbol": { "type": "string" },
|
|
36
|
+
"close_history": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"description": "Trailing closes up to and including `date` (oldest first).",
|
|
39
|
+
"items": { "type": "number" }
|
|
40
|
+
},
|
|
41
|
+
"fundamentals": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"description": "Named fundamental fields (e.g. `pe`, `revenue_yoy`). Optional; defaults to empty.",
|
|
44
|
+
"additionalProperties": { "type": "number" }
|
|
45
|
+
},
|
|
46
|
+
"news": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"description": "Headlines published on or before `date`. Optional; defaults to empty.",
|
|
49
|
+
"items": { "type": "string" }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"PositionState": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"required": ["symbol", "shares", "avg_price"],
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"properties": {
|
|
58
|
+
"symbol": { "type": "string" },
|
|
59
|
+
"shares": { "type": "number" },
|
|
60
|
+
"avg_price": { "type": "number" }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reference OpenOutcry agent (Python) — the simplest thing that honors the contract.
|
|
3
|
+
|
|
4
|
+
Transport: stdio. Reads one MarketObservation (JSON) per line on stdin and writes one
|
|
5
|
+
Decision (JSON) per line on stdout. Strategy: equal-weight buy-and-hold — the baseline
|
|
6
|
+
every real agent must beat. Fork it, replace ``decide``.
|
|
7
|
+
|
|
8
|
+
python examples/reference-agent.py # then feed it MarketObservation JSON lines
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def decide(obs):
|
|
15
|
+
"""MarketObservation -> Decision. Replace this body with your strategy."""
|
|
16
|
+
symbols = obs["symbols"]
|
|
17
|
+
weight = 1.0 / max(len(symbols), 1)
|
|
18
|
+
orders = [
|
|
19
|
+
{"symbol": s["symbol"], "action": "buy", "target_weight": weight,
|
|
20
|
+
"confidence": 0.5, "rationale": "equal-weight hold"}
|
|
21
|
+
for s in symbols
|
|
22
|
+
]
|
|
23
|
+
return {"orders": orders, "reasoning": "equal-weight buy-and-hold"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
for raw in sys.stdin:
|
|
28
|
+
line = raw.strip()
|
|
29
|
+
if not line:
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
decision = decide(json.loads(line))
|
|
33
|
+
except Exception:
|
|
34
|
+
# Any bad input degrades to an empty-orders hold — never crashes the harness.
|
|
35
|
+
decision = {"orders": [], "reasoning": "parse error -> hold"}
|
|
36
|
+
sys.stdout.write(json.dumps(decision) + "\n")
|
|
37
|
+
sys.stdout.flush()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == "__main__":
|
|
41
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reference OpenOutcry agent (TypeScript) — the simplest thing that honors the contract.
|
|
3
|
+
//
|
|
4
|
+
// Transport: stdio. Reads one MarketObservation (JSON) per line on stdin and writes
|
|
5
|
+
// one Decision (JSON) per line on stdout. Strategy: equal-weight buy-and-hold — the
|
|
6
|
+
// baseline every real agent must beat. Fork it, replace `decide`.
|
|
7
|
+
//
|
|
8
|
+
// node examples/reference-agent.ts # then feed it MarketObservation JSON lines
|
|
9
|
+
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
|
|
12
|
+
interface Order { symbol: string; action: string; target_weight: number; confidence: number; rationale: string; }
|
|
13
|
+
interface Decision { orders: Order[]; reasoning: string; }
|
|
14
|
+
|
|
15
|
+
/** MarketObservation -> Decision. Replace this body with your strategy. */
|
|
16
|
+
function decide(obs: { symbols: { symbol: string }[] }): Decision {
|
|
17
|
+
const weight = 1.0 / Math.max(obs.symbols.length, 1);
|
|
18
|
+
const orders = obs.symbols.map((s) => ({
|
|
19
|
+
symbol: s.symbol, action: "buy", target_weight: weight, confidence: 0.5, rationale: "equal-weight hold",
|
|
20
|
+
}));
|
|
21
|
+
return { orders, reasoning: "equal-weight buy-and-hold" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rl = createInterface({ input: process.stdin });
|
|
25
|
+
rl.on("line", (raw) => {
|
|
26
|
+
const line = raw.trim();
|
|
27
|
+
if (line === "") return;
|
|
28
|
+
let decision: Decision;
|
|
29
|
+
try {
|
|
30
|
+
decision = decide(JSON.parse(line));
|
|
31
|
+
} catch {
|
|
32
|
+
// Any bad input degrades to an empty-orders hold — never crashes the harness.
|
|
33
|
+
decision = { orders: [], reasoning: "parse error -> hold" };
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write(JSON.stringify(decision) + "\n");
|
|
36
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//! The end-to-end ecosystem path: **env → trajectory → SharpeBench score.**
|
|
2
|
+
//!
|
|
3
|
+
//! Run an agent in OpenOutcry while capturing only its raw decisions, then hand the
|
|
4
|
+
//! trajectory to a *separate verifier* that recomputes the submission from those
|
|
5
|
+
//! decisions + the frozen data alone (the agent cannot lie about its returns), and
|
|
6
|
+
//! finally score it with SharpeBench's `CompositeScore`.
|
|
7
|
+
//!
|
|
8
|
+
//! `cargo run -p openoutcry --example score-a-trajectory`
|
|
9
|
+
|
|
10
|
+
use openoutcry::{
|
|
11
|
+
replay_submission, run_backtest_capture, AgentTrajectory, BuyAndHold, CostModel, Dataset,
|
|
12
|
+
Window, CONTRACT_VERSION,
|
|
13
|
+
};
|
|
14
|
+
use sharpebench_core::{score_agent, ScoreConfig};
|
|
15
|
+
|
|
16
|
+
fn main() {
|
|
17
|
+
let data = Dataset::synthetic(4, 160, 7);
|
|
18
|
+
let window = Window {
|
|
19
|
+
start: 20,
|
|
20
|
+
end: 160,
|
|
21
|
+
};
|
|
22
|
+
let costs = CostModel::default();
|
|
23
|
+
|
|
24
|
+
// 1) env → trajectory: run the agent with capture — the artifact holds ONLY the
|
|
25
|
+
// raw per-step decisions + the window/seed coordinates, no self-reported metric.
|
|
26
|
+
let (_run, run_traj) = run_backtest_capture(&data, &mut BuyAndHold, window, 1, costs);
|
|
27
|
+
let trajectory = AgentTrajectory {
|
|
28
|
+
agent_id: "buy-and-hold".to_string(),
|
|
29
|
+
runs: vec![run_traj],
|
|
30
|
+
in_sample_trials: 0,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 2) verify: recompute the scoreable submission from the decisions + frozen data
|
|
34
|
+
// alone. Tamper with the trajectory and this recomputes to different returns.
|
|
35
|
+
let submission = replay_submission(&data, &trajectory, costs);
|
|
36
|
+
|
|
37
|
+
// 3) score: deflated Sharpe / pass^k / process gate — the SharpeBench verdict.
|
|
38
|
+
let score = score_agent(&submission, &ScoreConfig::default());
|
|
39
|
+
|
|
40
|
+
println!(
|
|
41
|
+
"OpenOutcry contract v{CONTRACT_VERSION} — scored '{}':",
|
|
42
|
+
submission.agent_id
|
|
43
|
+
);
|
|
44
|
+
println!(
|
|
45
|
+
"{}",
|
|
46
|
+
serde_json::to_string_pretty(&score).expect("score serializes")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//! The frozen wire contract version.
|
|
2
|
+
|
|
3
|
+
/// The version of the **OpenOutcry Agent Interface** — the JSON wire shape an
|
|
4
|
+
/// agent and the harness exchange ([`MarketObservation`](crate::MarketObservation)
|
|
5
|
+
/// in, [`Decision`](crate::Decision) out).
|
|
6
|
+
///
|
|
7
|
+
/// This is **not** the crate / npm / PyPI package version. It tracks only the
|
|
8
|
+
/// *shape* of the contract and moves independently: shipping bug fixes, new
|
|
9
|
+
/// baselines, or extra re-exports bumps the package version but leaves
|
|
10
|
+
/// `CONTRACT_VERSION` untouched.
|
|
11
|
+
///
|
|
12
|
+
/// It bumps **only** on a breaking wire change. Additive changes — a new field
|
|
13
|
+
/// that is optional-with-default, so every existing agent keeps parsing — do
|
|
14
|
+
/// **not** bump it (that is the whole additive-only discipline; see
|
|
15
|
+
/// `GOVERNANCE.md`). Removing or retyping a field is a major bump and requires a
|
|
16
|
+
/// parallel `…V2` namespace rather than mutating this surface in place.
|
|
17
|
+
///
|
|
18
|
+
/// Implementers in any language target this version: passing the conformance kit
|
|
19
|
+
/// earns "conforms to the OpenOutcry Agent Interface v1.0".
|
|
20
|
+
pub const CONTRACT_VERSION: &str = "1.0";
|