Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
25d8482
feat: per-engine mark cache (bar closes published by the runner)
ArthurBernard Jul 11, 2026
1bd25a6
chore: closeout leaf 01 mark-cache (changelog, plan tick)
ArthurBernard Jul 11, 2026
d56f6f5
Merge pull request #209 from ArthurBernard/feat/mark-cache
ArthurBernard Jul 11, 2026
fcae892
feat: position rows carry mark, value, unrealised and fee currency
ArthurBernard Jul 11, 2026
f9fd626
chore: closeout leaf 02 position-row-marks (changelog, ADR, plan tick)
ArthurBernard Jul 11, 2026
376d364
Merge pull request #211 from ArthurBernard/feat/position-row-marks
ArthurBernard Jul 11, 2026
dc9437f
feat: display currency — server-side converted *_display money fields
ArthurBernard Jul 11, 2026
cafd09e
chore: closeout leaf 03 display-currency (changelog, ADR, plan tick)
ArthurBernard Jul 11, 2026
ba3586d
Merge pull request #212 from ArthurBernard/feat/display-currency
ArthurBernard Jul 11, 2026
e924c2f
feat: GET /api/balances + last_asof surfacing + contract sweep
ArthurBernard Jul 11, 2026
8bf259b
docs: plan dashboard-tables-ux
ArthurBernard Jul 11, 2026
d2f7b4d
Merge pull request #213 from ArthurBernard/docs/plan-dashboard-tables-ux
ArthurBernard Jul 11, 2026
7dd9cbe
chore: closeout leaf 04 + api-completeness epic (changelog, status, r…
ArthurBernard Jul 12, 2026
9fadcab
Merge pull request #214 from ArthurBernard/feat/api-balances
ArthurBernard Jul 12, 2026
d178c4e
feat: asset-first positions tables with expandable detail rows
ArthurBernard Jul 12, 2026
f9efa40
chore: closeout leaf 01 positions-tables (changelog, plan tick)
ArthurBernard Jul 12, 2026
aaf2e06
Merge pull request #215 from ArthurBernard/feat/ui-positions-tables
ArthurBernard Jul 12, 2026
7b0fd64
feat: orders tables with Filled %, Value and expandable fills detail
ArthurBernard Jul 12, 2026
8a1c0f8
chore: closeout leaf 02 orders-fills-tables (changelog, plan tick)
ArthurBernard Jul 12, 2026
9d71c42
Merge pull request #216 from ArthurBernard/feat/ui-orders-fills
ArthurBernard Jul 12, 2026
f417a26
feat: honest bar-timing chip (last bar → next bar from last_asof_ts)
ArthurBernard Jul 12, 2026
04ae255
chore: closeout leaf 03 bar-timing-chip (changelog, plan tick)
ArthurBernard Jul 12, 2026
9679a6b
Merge pull request #217 from ArthurBernard/feat/ui-bar-timing
ArthurBernard Jul 12, 2026
2522585
feat: timezone label + filtered empty states
ArthurBernard Jul 12, 2026
48c6a75
chore: closeout leaf 04 + dashboard-tables-ux epic (changelog, status…
ArthurBernard Jul 12, 2026
cda403c
Merge pull request #218 from ArthurBernard/feat/ui-tz-empty-states
ArthurBernard Jul 12, 2026
beed0d0
fix: serialize reject_reason on /api/orders rows
ArthurBernard Jul 12, 2026
54e9815
Merge pull request #219 from ArthurBernard/fix/order-reject-reason-api
ArthurBernard Jul 12, 2026
03935a0
docs: plan canary-roundtrip
ArthurBernard Jul 12, 2026
79a003a
Merge pull request #220 from ArthurBernard/docs/plan-canary-roundtrip
ArthurBernard Jul 12, 2026
70eddba
feat: canary scenario with exact paper oracle
ArthurBernard Jul 12, 2026
d5c2963
chore: closeout leaf 01 canary-scenario (changelog, ADR, plan tick)
ArthurBernard Jul 12, 2026
8f2ee75
Merge pull request #221 from ArthurBernard/feat/canary-scenario
ArthurBernard Jul 12, 2026
a1573ce
feat: trading-bot canary CLI (paper)
ArthurBernard Jul 12, 2026
c3fb32f
chore: closeout leaf 02 canary-cli (changelog, plan tick)
ArthurBernard Jul 12, 2026
6dea2b4
Merge pull request #222 from ArthurBernard/feat/canary-cli
ArthurBernard Jul 12, 2026
12ded4a
feat: canary venue oracle — testnet wired, live gated
ArthurBernard Jul 12, 2026
2c24701
chore: closeout leaf 03 + canary-roundtrip epic + the 2026-07-11 road…
ArthurBernard Jul 12, 2026
d0a06a0
Merge pull request #223 from ArthurBernard/feat/canary-venue
ArthurBernard Jul 12, 2026
63043bd
chore: release v0.14.0
ArthurBernard Jul 12, 2026
e33bcd1
Merge pull request #224 from ArthurBernard/chore/release-0.14.0
ArthurBernard Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

## [0.14.0] - 2026-07-12

### Added

- **The canary runs against real venues (closes the `canary-roundtrip`
epic).** Venue identity oracle: our store-recorded fills == the
venue-reported fills (exact triples), per-asset balance deltas == the
fills math (fee-denomination-aware — never an assumed zero), position
flat, cost bounded. `--mode testnet` wired (Binance sandbox; Kraken
refused — no testnet); `--mode live` implemented behind the existing
`live_enabled` gate + typed confirmation. `doc/dev/09-go-live.md` names
the live canary as the road-to-1.0 #1 validation vehicle. **Proven on
real Binance testnet: 16/16 checks PASS** (real cancel accepted, venue
dedup on the duplicate client-order-id, cost 0.00511 USDT). (#223)
- **`Fill.fee_asset`** — the venue may charge the fee in a non-quote asset
(Binance charges a market buy's commission in BASE; found by the canary's
identity oracle refusing an unexplained −8E-8 BTC). Additive domain field
(`None` = quote, the historic meaning), threaded through the Binance
adapter, the store (migration) and the oracle's fills math. Also:
Binance HTTP-400 `{code,msg}` rejections now map to the same domain
errors as in-band bodies (they escaped as transport errors);
`BrokerConfig.symbols` threads the per-symbol trade-history scope Binance
requires for `fills()`. (#223)
- **`trading-bot canary`** — the platform self-test as a one-liner (paper by
default): self-contained factory engine funded with `--budget`, quantity
sized to the venue's REAL minimums (public-endpoint resolver + real last
price as the mark), refusal before any order when the exact implied cost
exceeds `--max-cost`, evidence table (PASS/FAIL, expected vs observed)
and exit code. `--mode testnet|live` reserved for the venue leaf. (#222)
- **The canary scenario** (`application/canary.py`) — a deterministic
platform self-test: cancel probe (far-off resting limit → real cancel,
persisted), client-order-id idempotency probe, then a sequential market
round-trip, every step recorded as expected-vs-observed evidence. The
**exact paper oracle** asserts realised PnL == −Σ fees, flat position,
store-persisted terminal orders and balance deltas, Decimal-exact.
Supporting seams: `paper_starting_balances` config (factory-funded
simulators) and a strict-paper marketability rule (a passive-side limit
with an injected mark now RESTS instead of filling at its price —
permissive/markless behaviour unchanged). (#221)
- `/api/orders` rows carry `reject_reason` — the UI's expanded order detail
rendered it defensively since #216; the API now serializes it (additive;
contract sweep updated). (#219)
- **Timezone label + honest empty states (closes the `dashboard-tables-ux`
epic).** Every page footer says which timezone its times render in; the
Orders page's filtered-to-zero tables say "No orders/fills match the
filters." instead of masquerading as an empty book. (#218)
- **Honest bar timing.** "last bar X ago → next in Y" chip on the strategies
roster (replacing the "Next bar" column) and the strategy-detail header —
both derived from `last_asof_ts` + span (server truth), never a fake
countdown (`—` when nothing was evaluated). The epoch-aligned
`nextBarCloseMs` guess is retired. (#217)
- **Orders tables show what executed; fills live under their order.** All
three surfaces gain Filled % (exact fraction on hover), Avg fill and
Value (filled × avg fill, else qty × limit — the tooltip says which);
clicking an order (detail + Orders pages) expands its fills, ids,
limit/stop, Σ fees and reject reason. The strategy detail's standalone
"Recent fills" table is **removed** (its data lives in the expansions);
the Orders page's audit fills view gains Order and Value columns. (#216)
- **Positions tables read asset-first, with a value.** Overview + strategy
detail: Asset | Qty | Avg entry | Price *(as-of)* | Value | Unrealised |
Realised (net) — values prefer the configured display currency, marks
always show their freshness (relative as-of, absolute + source in the
tooltip). Click a row for the native pair, cumulative fees, the
gross-vs-fees realised breakdown and the mark provenance: the shared
**expandable-row helper** lands in `base.html` (keyboard-accessible,
expanded state survives the SSE/poll rebuilds). (#215)
- **`GET /api/balances` + `last_asof_ts` documentation + the epic contract
sweep (closes the `api-completeness` epic).** Per running unit, the
broker's balances as exact Decimal strings (stopped units absent; a broker
error degrades to an `error` row, HTTP 200 — poll-safe); the seam for the
future positions↔balances cross-check and the canary live oracle. One
consolidated additive-only contract test now pins the exact field sets of
all six frozen endpoints before the road-to-1.0 API freeze. (#214)
- **Display currency** — `display_currency` (global default), per-exchange
`display_currency_overrides` and static `conversion_rates` in `AppConfig`;
server-side converted `*_display` money fields (pure
`application/display_ccy.py`) on position rows
(`value_display`/`unrealised_display`), strategy rows
(`total_value_display`/`unrealised_display`) and KPI rows
(`realised_pnl_display`/`fees_paid_display`), each tagged with the resolved
`display_currency`. Missing rate → `null`, never a guessed conversion;
native-quote fields untouched. (#212)
- **Position rows carry mark, value, unrealised and fee currency.**
`/api/positions` rows gain `mark` / `mark_asof_ts` / `mark_source`
(`bar_close` from the mark cache, `last_fill` fallback, never untagged),
`value` (mark × |qty|), `unrealised` (sign-correct for shorts) and
`fee_ccy` — and the strategy aggregate now uses the SAME mark policy, so
the roster total equals the row sum (verified Decimal-exact on both live
books' copies against real dccd frames). All additive; existing fields
contract-regression-tested. (#211)
- **Per-engine mark cache** (`application/mark_cache.py`) — the portfolio
runner publishes each rebalance's last dccd bar closes per symbol
(`Mark(price, asof_ms, source="bar_close")`, exact Decimal) into
`Engine.mark_cache`, so the API layer can serve marks with their as-of
timestamp without any I/O. Single-instrument `StrategyRunner` is a
documented follow-up seam. Verified against the real dccd store: cache ==
independently-read frame closes, exact price and asof. (#209)

## [0.13.0] - 2026-07-11

### Added
Expand Down
82 changes: 82 additions & 0 deletions doc/dev/03-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,88 @@ rejected approaches as tombstones.

---

### 2026-07-12 The venue oracle is an accounting identity; fees carry their asset (PR #223) [accepted]
- **Choice**: on a real venue the canary asserts the **identity** — our
fills == venue-reported fills, per-asset balance deltas == the fills math
— never a precomputed PnL. Supporting discoveries hardened the domain:
`Fill.fee_asset` (Binance charges market-buy commissions in BASE; `None`
keeps the historic quote meaning), Binance HTTP-400 rejections mapped to
domain errors, `BrokerConfig.symbols` for Binance's per-symbol
trade-history scope, and a 15 % venue probe offset (the venue's
`PERCENT_PRICE_BY_SIDE` band rejects 50 %). A settle loop attributes the
venue's async executions to our client-order-ids and re-emits them on the
bus — venue truth flows through the ordinary fill plumbing.
- **Why**: spread and drift make absolute expectations flaky exactly where
correctness matters most; the identity is exact on any venue. The two
intermediate testnet failures WERE the canary working — each became a
domain fix with offline tests before the final green run. PnL folding
still values fees at face value when fee_asset ≠ quote (documented
approximation); the balance identity itself is exact.
- **Rejected alternatives**: absolute-PnL venue oracle (nondeterministic);
ignoring sub-1e-7 balance dust (it was the fee-denomination bug, not
dust); adapter-specific oracle code (the broker port suffices — the
oracle stays venue-agnostic).

### 2026-07-12 The canary: two oracles, probes before money, resting limits in strict paper (PR #221) [accepted]
- **Choice**: the canary runs probes FIRST (cancel, idempotency — both free)
and only then risks the round-trip; paper mode gets an EXACT oracle
(PnL == −Σ fees, flat, store-persisted terminals, balance deltas), venue
modes will get the identity oracle (leaf 03) — never a precomputed
absolute PnL. Sequential legs, never simultaneous (self-trade
prevention). To make the cancel probe implementable, strict paper gains a
marketability rule: a passive-side limit with an injected mark RESTS until
cancelled; permissive/markless behaviour is byte-identical (regression
tests pin it).
- **Why**: probes cost nothing and validate the two scariest paths (real
cancel = the kill-switch; venue-side client-order-id dedup = the
idempotency invariant) before any money moves. Paper is deterministic so
exactness is free; venues are not, so correctness must be an accounting
identity, not a price prediction. The simulator's fill-everything-at-limit
model made "a resting order" inexpressible — a simulator that cannot rest
an order cannot rehearse a cancel.
- **Rejected alternatives**: simultaneous buy+sell (STP nondeterminism);
skipping probes in paper (the paper canary is the per-release regression —
it must cover the same steps the live one will); relaxing the
marketability rule beyond strict+mark (would silently change every
existing permissive test and the daemon's mark-less maker legs).

### 2026-07-11 One display numeraire, converted server-side, never guessed (PR #212) [accepted]
- **Choice**: `convert()` always targets the single global
`display_currency` (exact Decimal, identity when the source quote already
matches, `conversion_rates` lookup otherwise, `None` on a missing rate —
never guessed). The per-exchange override is a display **label** on the
row (`resolve_currency`), numerically truthful only when paired with an
identity-equivalent rate — exactly the stablecoin case it exists for
(binance→USDT with `USDT: 1`).
- **Why**: server-side conversion gives every consumer the same figures (no
per-client drift); a single numeraire keeps cross-exchange aggregation
meaningful; refusing to guess a rate keeps a missing `EUR` declaration
visibly `null` instead of silently wrong — money display errors are worse
than money display gaps.
- **Rejected alternatives**: client-side conversion from an exposed rates
config (N consumers, N roundings, N bugs); per-exchange numeraires
(aggregates across exchanges become meaningless); defaulting unknown
rates to 1 (silently wrong for anything non-stable).

### 2026-07-11 Marks are bar closes with a mandatory as-of, one policy at every altitude (PR #211) [accepted]
- **Choice**: v1 mark = the last dccd bar close the runner saw (published to
a per-engine cache at rebalance time), serialized ONLY together with
`mark_asof_ts` and `mark_source` (`bar_close` | `last_fill` fallback).
Both the per-position rows and the strategy aggregate (`_unrealised_of`)
read the same `_mark_of` helper — one mark policy at every altitude. The
API path does zero I/O; a live ticker is post-1.0.
- **Why**: the bar close is the price the strategy actually evaluated on —
fresher and more honest than the previous last-own-fill mark for
daily-rebalance books (which only moved when the unit itself traded). A
mark without its timestamp invites mistaking a day-old close for a live
price — the UI can only render "as of", never bare. Splitting policies
between rows and aggregate would let the roster disagree with its own
detail rows.
- **Rejected alternatives**: last-own-fill as primary (stale for days on
low-churn books); fetching a ticker on the API path (network I/O per
request, and a freshness the engine's own decisions don't have); marks
without `mark_source` (the fallback would masquerade as fresh data).

### 2026-07-11 Venue minimums shape quantities upstream; the Order keeps the bare Instrument (PR #204) [accepted]
- **Choice**: the round-up-or-skip policy (`order_prep.prepare_leg`) runs at
leg preparation with the resolved venue spec — but the routed `Order`
Expand Down
29 changes: 29 additions & 0 deletions doc/dev/06-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,35 @@ and `CHANGELOG.md` for what shipped.
simulator rejects like the venue. Verified on the live books' copies with
real specs: the audit's whole dust rebalance (14 legs) skips; Kraken
round-ups land exactly on `ordermin`; two-pass convergence proven.
- **`api-completeness` (2026-07-12, PRs #209–#214)**: the API says what the
engine knows — per-engine mark cache (bar closes published at rebalance),
position rows with `mark`/`mark_asof_ts`/`mark_source`/`value`/
`unrealised`/`fee_ccy` under ONE mark policy (aggregate == Σ rows,
Decimal-exact on the real books), server-side display currency
(never-guess-a-rate), `GET /api/balances`, `last_asof_ts` documented, and
an additive-only contract sweep across the six frozen endpoints — ready
for the road-to-1.0 #5 freeze. Known seam: paper broker balances are empty
across restarts (the simulator's internal ledger is not persisted;
`/api/balances` relays it honestly) — the canary/guardrail funding hook.
- **`dashboard-tables-ux` (2026-07-12, PRs #215–#218)**: the dashboard renders
the full 2026-07-11 UX design — asset-first positions with price (as-of) /
value / unrealised, shared expandable rows (SSE-surviving), orders with
Filled % / Avg fill / Value and click-to-expand fills (standalone fills
table retired from the detail page), honest last→next bar chip, timezone
label, filtered-vs-empty states. The `reject_reason` follow-up shipped in
#219.
- **`canary-roundtrip` (2026-07-12, PRs #221–#223)**: `trading-bot canary` —
the deterministic platform self-test. Probes first (real cancel = the
kill-switch path; duplicate client-order-id = the idempotency invariant),
then a sequential round-trip; paper oracle exact (PnL == −Σ fees, flat,
persisted terminals, balance deltas), venue oracle = the accounting
identity (fills/balances == venue-reported, fee-denomination-aware) +
bounded cost. **Proven on real Binance testnet: 16/16 PASS** (cost
0.00511 USDT). Two real findings hardened the domain: `Fill.fee_asset`
(Binance charges market-buy fees in BASE) and Binance HTTP-400 → domain
error mapping. `09-go-live.md` names the live canary as the road-to-1.0
#1 validation vehicle. Cadence: paper per release, testnet at will, live
once per venue at go-live.

## Pending

Expand Down
65 changes: 25 additions & 40 deletions doc/dev/07-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,48 +97,33 @@ order — none started yet, each is a `/pick-task` candidate:
200-row tail + server timestamps on snapshot endpoints, then the public
contract is declared stable — 0.x-style breaking removals end at 1.0.

## UI consistency & accounting integrity (2026-07-11)
## UI consistency & accounting integrity (2026-07-11) — SHIPPED

A dashboard-driven audit (position ≠ Σ orders on `alloc1-binance`) uncovered two
engine bugs and a set of API/UX gaps. Shipped 2026-07-11 (plan trees archived):
the `paper-integrity` epic (durable paper ids, order↔fill lifecycle sync,
persisted orphan-closes — PRs #190–#194, v0.12.0; restart the daemon to heal
the live books), the `accounting-guardrail` epic (pure invariant checker,
startup + 60 s-TTL wiring with SSE alerts, per-strategy health surface incl.
the kill-switch fold — the *status* half of road-to-1.0 #3 — and the dashboard
health pill; PRs #197–#201), and the `venue-minimums` epic (keyless cached
spec resolver, round-up-or-skip order prep with the spot-sell cap, strict
factory paper — PRs #203–#206; sub-minimum orders are structurally
impossible on the portfolio path). In dependency order:

4. [ ] **`api-completeness` — API exposes what the app layer knows.** Per-
position mark price (v1 = last dccd bar close, always with its `asof` ts;
live ticker is post-1.0), value + unrealised PnL per position; display
currency (global default + per-exchange override, static conversion rates);
`/api/balances`; `fee_ccy`; `last_asof_ts` on every surface. Must land
before the API contract freeze (road-to-1.0 #5).
5. [ ] **`dashboard-tables-ux` — tables redesign.** Positions keyed by asset
(not pair) with value/mark/unrealised as primary columns; expandable rows
for secondary detail (fills under their order, ids, fee breakdown —
expanded state survives the SSE re-render); fills demoted to order detail
(flat audit view stays on the Orders page); "last bar → next bar" timing
chip; timezone affordance.
6. [ ] **`canary-roundtrip` — deterministic self-test strategy.** A minimal
round-trip canary run as its own strategy unit with its own tiny ledger
(~10 USDT): **sequential** market buy *x* then sell *x* (never simultaneous
— self-trade prevention would make it non-deterministic), plus two free
probes: a far-off-limit **cancel** (the real kill-switch path) and a
client-order-id **idempotency** re-submit. Two oracles: **paper** = exact
Decimal equality against the precomputed expectation (PnL = −2·fees, flat
position, terminal orders — CI-runnable, per release); **live** = exact
*accounting identity* (our fills/balances == venue-reported) + **bounded**
total cost (≤ 1–2 EUR) — never a precomputed absolute PnL (spread/drift are
not deterministic). Cadence: paper per release; testnet at will; live once
per venue at go-live + after any broker-adapter change. Depends on #1
(order lifecycle) + #2 (health surface for the pass/fail report) + #3
(venue minimums; Binance BNB-fee discount must be off or modeled). The live
canary is the validation vehicle for **Road to v1.0.0 #1** (real-key
enablement: venue idempotency, real cancel, balance reconciliation).
engine bugs and a set of API/UX gaps. **All six epics shipped 2026-07-11/12**
(plan trees archived; details in `06-status.md`, decisions in `03-decisions.md`):

1. `paper-integrity` — durable paper ids, order↔fill lifecycle sync, persisted
orphan-closes (PRs #190–#194, **v0.12.0**).
2. `accounting-guardrail` — pure invariant checker, startup + 60 s-TTL wiring
with SSE alerts, per-strategy health surface incl. the kill-switch fold
(the *status* half of road-to-1.0 #3), dashboard health pill
(PRs #197–#201, **v0.13.0**).
3. `venue-minimums` — keyless cached spec resolver, round-up-or-skip order
prep with the spot-sell cap, strict factory paper (PRs #203–#206,
**v0.13.0**).
4. `api-completeness` — mark cache + bar-close marks with mandatory as-of,
value/unrealised/`fee_ccy` per position, server-side display currency,
`GET /api/balances`, additive-only contract sweep — ready for the
road-to-1.0 #5 freeze (PRs #209–#214).
5. `dashboard-tables-ux` — asset-first positions, shared expandable rows,
orders with Filled %/Value + fills as expanded detail, honest bar-timing
chip, timezone + filtered empty states (PRs #215–#219).
6. `canary-roundtrip` — deterministic self-test (`trading-bot canary`):
probes + round-trip, exact paper oracle, venue identity oracle **proven
16/16 on real Binance testnet**; the live canary is the named validation
vehicle for road-to-1.0 #1 (PRs #221–#223; found `Fill.fee_asset` and the
Binance HTTP-400 mapping along the way).

## Not gating 1.0 (post-1.0 candidates)

Expand Down
Loading
Loading