Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ec5d68f
fix(exit): stop strangling winners — trailing floor, executable marks…
nahimrgz Aug 30, 2026
3f50ca8
fix(execution): venue-rule sizing, dust stays open, calibration groun…
nahimrgz Aug 30, 2026
c6005fd
feat(security): API token auth, versioned migrations, snapshot retention
nahimrgz Aug 30, 2026
b80c8df
chore: frontend tests, CSRF origin guard, runtime docs, monitor base
nahimrgz Aug 30, 2026
b52f115
fix: apply max code-review findings, with wiring regression tests
nahimrgz Aug 31, 2026
cc53f54
docs: correct AGENTS.md CI gates, document migration and auth rules
nahimrgz Aug 31, 2026
4b79874
fix(execution): settle every answered order, cancel what did not fill
nahimrgz Sep 6, 2026
cdbfb1a
docs: runtime notes catch up with the code
nahimrgz Sep 6, 2026
547ad28
chore: pin python 3.13, ignore skills cache and coverage data
nahimrgz Sep 6, 2026
edb048e
ci: make ruff format a blocking gate
nahimrgz Sep 6, 2026
45cf94a
ci: add dependabot for actions, uv and npm
nahimrgz Sep 6, 2026
dd507be
fix(types): clear the mypy errors and make it a blocking gate
nahimrgz Sep 6, 2026
84c4c6a
docs: describe the peak rebuild as the code actually does it
nahimrgz Sep 6, 2026
6bb99fa
ci: keep the trading SDK and the gate tools out of grouped bumps
nahimrgz Sep 6, 2026
d202128
fix(execution): never lose a fill to a malformed response
nahimrgz Sep 6, 2026
86495f2
fix(execution): one rule for what a fill was, instead of five
nahimrgz Sep 6, 2026
0b516ff
fix(execution): symmetric price band, shared with paper
nahimrgz Sep 6, 2026
866ff74
chore: pin every gate to the python version we actually run
nahimrgz Sep 6, 2026
0a71860
fix(markets): drop out-of-domain order-book levels at ingest
nahimrgz Sep 6, 2026
534ebfc
fix(execution): tell a rejection from a lost response
nahimrgz Sep 6, 2026
7e8698c
fix(execution): don't retry a permanent write failure, don't hide it …
nahimrgz Sep 6, 2026
db94701
fix(execution): read the settle signals for what they actually say
nahimrgz Sep 6, 2026
b41a5ba
test(execution): stop leaking the fake clock into the real process
nahimrgz Sep 6, 2026
e86f6a9
test(exit-monitor): stop racing a heartbeat count against CPU load
nahimrgz Sep 6, 2026
fc1547c
fix(execution): a 5xx is not a rejection, a 404 is not purged, no ord…
nahimrgz Sep 6, 2026
d468151
merge: audit follow-up — fill accounting, price band, settle signals
nahimrgz Sep 7, 2026
17b9187
test(news-ws-client): drive reconnect-after-drop through a fake trans…
nahimrgz Sep 14, 2026
d87a96e
test(news-ws-client): migrate reconnect_attempt to fake transport, co…
nahimrgz Sep 14, 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
42 changes: 41 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,47 @@ OPENPOLY_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com

# --- News source (M3+) -----------------------------------------------------
# TradingNews WS auth token. Resolved via env: ref in canvas / secret store.
OPENPOLY_TRADINGNEWS_API_KEY=
OPENPOLY_TRADINGNEWS_KEY=

# --- API access control (Phase 3) ------------------------------------------
# OPENPOLY_API_TOKEN — shared secret required in the `X-OpenPoly-Token` header
# on every mutating route (POST / PUT / DELETE / PATCH). Reads are never
# gated. Leave it unset for loopback development: the backend still works,
# logs one WARNING at startup, and REFUSES to switch to live mode
# (`POST /api/system/mode` returns 403 `api_token_required`).
#
# The value may be the literal token, or a `*_ref` in the same indirection
# every other secret uses, so the token need not sit in this file:
# OPENPOLY_API_TOKEN=env:OPENPOLY_API_TOKEN_VALUE
# OPENPOLY_API_TOKEN=local:api-token
# A ref that does not resolve fails CLOSED — every mutating route 401s.
# Generate one with: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
#
# ASCII ONLY. An HTTP header value cannot carry a code point above U+00FF and
# the browser's fetch() throws on one, so a non-ASCII token breaks every
# mutating call from the web UI (token_urlsafe above is already ASCII).
#
# For the web UI: paste the same value into Keys -> API token. It is kept in
# that browser's localStorage (openpoly_api_token) and attached as
# X-OpenPoly-Token to mutating requests only.
OPENPOLY_API_TOKEN=

# OPENPOLY_ALLOWED_HOSTS — comma-separated extra Host header values the backend
# will answer to. `localhost`, `127.0.0.1` and `[::1]` are always allowed, so
# the default loopback + SSH-tunnel setup needs nothing here. Any other Host
# gets 421 Misdirected Request — this is what stops a browser on the
# operator's machine from being talked into driving the loopback API under a
# hostname that resolves to 127.0.0.1 (DNS rebinding). Set it only when the
# backend is reached through a real name (a reverse proxy, say); `*` disables
# the check entirely and should be a deliberate choice.
# OPENPOLY_ALLOWED_HOSTS=openpoly.internal.example.com

# --- Retention -------------------------------------------------------------
# order_book_snapshot is the one table that grows without bound. The database
# section prunes it hourly, keeping the last 7 days by default; the window is
# the `order_book_retention_days` field on the database section's config (0
# disables the prune). Reclaimed rows are reported as
# `retention.pruned_rows` on GET /api/inspect/db-status.

# --- Boot behavior ---------------------------------------------------------
# Set to "0" to skip auto-starting the news + market sources on app
Expand Down
40 changes: 40 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Keeps the pinned GitHub Actions, the uv lockfile and the frontend lockfile
# moving. Minor and patch bumps arrive grouped (one PR per ecosystem per week);
# majors come alone so they can be read on their own.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions-minor-patch:
update-types: ["minor", "patch"]

- package-ecosystem: uv
directory: /
schedule:
interval: weekly
# The trading SDK is never bumped unattended: it is pinned to a
# pre-release, it places real orders, and execution/clob_patch.py
# monkey-patches its HTTP internals, so every move needs a deliberate
# paper run and a live smoke rather than a green CI badge.
ignore:
- dependency-name: py-clob-client-v2
groups:
python-minor-patch:
update-types: ["minor", "patch"]
# ruff and mypy gate CI, and their exact pins are the reason those
# gates cannot change the rules under a contributor. They arrive as
# their own PRs so a reformat or a stricter check is read on its own.
exclude-patterns:
- ruff
- mypy

- package-ecosystem: npm
directory: /frontend
schedule:
interval: weekly
groups:
npm-minor-patch:
update-types: ["minor", "patch"]
18 changes: 13 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ jobs:
- name: Ruff lint
run: uv run ruff check .

# Intentionally NOT a gate: formatting is a local convention, not enforced
# in CI (keeps ruff-version-drift churn from blocking merges). Info only.
- name: Ruff format check (informational)
# Blocking gate — ruff is pinned in pyproject (ruff==0.15.13), so a newer
# ruff cannot change the formatting rules under us; the only way this
# fails is an unformatted file. Run `uv run ruff format .` before pushing.
- name: Ruff format
run: uv run ruff format --check .
continue-on-error: true

# Blocking gate; config in pyproject.toml [tool.mypy].
- name: mypy
run: uv run mypy

frontend:
name: frontend (typecheck)
name: frontend (typecheck + lint + tests)
runs-on: ubuntu-latest
defaults:
run:
Expand All @@ -64,3 +68,7 @@ jobs:
# Blocking gate — ESLint must pass (errors cleared).
- name: Lint
run: yarn lint

# Blocking gate — vitest unit suite (canvas store + template (de)serialization).
- name: Tests
run: yarn test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ CLAUDE.md
# Skills CLI tooling (personal; not part of openPoly)
.agents/
skills-lock.json
.atl/

# Personal AI-workflow docs (local-only; kept off the public repo).
# Published design docs live in docs/architecture/ and docs/deploy/.
Expand All @@ -61,3 +62,6 @@ docs/superpowers/

# Live trade source data (real on-chain tx hashes) — kept local, never published
material/

# Coverage data (pytest-cov)
.coverage
32 changes: 32 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Pre-commit hooks — the same two ruff commands CI runs, on the files you are
# about to commit.
#
# Local `uv run` hooks rather than the upstream ruff mirror on purpose: the ruff
# version is already pinned in `pyproject.toml`'s dev group, and a mirror `rev`
# is a second place to bump it — which is exactly how a hook starts disagreeing
# with CI. This way there is one pin.
#
# Install once: uv run pre-commit install
# Run manually: uv run pre-commit run --all-files
#
# Formatting is a fix-in-place hook: it rewrites the file and fails the commit,
# so you re-stage and commit again. CI runs the same check with --check, where
# both lint and format block the merge. mypy is a CI gate too but is not wired
# here: it type-checks the whole package, which is too slow for a per-commit
# hook — run `uv run mypy` before pushing.
repos:
- repo: local
hooks:
- id: ruff-check
name: ruff check
entry: uv run ruff check --force-exclude
language: system
types_or: [python, pyi]
require_serial: true

- id: ruff-format
name: ruff format
entry: uv run ruff format --force-exclude
language: system
types_or: [python, pyi]
require_serial: true
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
23 changes: 18 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ uv run uvicorn openpoly.api.main:app
# Tests
uv run pytest

# Lint / format (line length 100, target py312)
# Lint / format (line length 100, target py313)
uv run ruff check .
uv run ruff format .

Expand All @@ -22,12 +22,16 @@ yarn install
yarn dev # Vite dev server on :5173, proxies /api → 127.0.0.1:8000
yarn typecheck # tsc -b --noEmit
yarn lint # eslint
yarn test # vitest run
```

CI enforces (must pass to merge): **`ruff check` + `pytest`** (backend) and
**`yarn typecheck` + `yarn lint`** (frontend). **`ruff format` is a local
convention, not a CI gate** — run it before pushing, but it is intentionally not
enforced (avoids ruff-version-drift churn blocking merges).
CI enforces (must pass to merge): **`pytest` + `ruff check` + `ruff format
--check` + `mypy`** (backend) and **`yarn typecheck` + `yarn lint` + `yarn
test`** (frontend). Ruff is pinned in `pyproject.toml`, so the format gate
cannot drift; run `uv run ruff format .` before pushing. Run `uv run mypy`
before pushing too (config: `[tool.mypy]` in `pyproject.toml`).
`.pre-commit-config.yaml` runs ruff check + format locally
(`uv run pre-commit install` to enable).

Same-machine (backend + frontend on one box) is the **default and needs no env
vars** — the frontend proxies to `127.0.0.1:8000` out of the box.
Expand Down Expand Up @@ -76,6 +80,15 @@ unless you are deliberately changing the contract.
- **Front/back mirror** — `frontend/src/sections/<type>/` mirrors
`openpoly/sections/<type>/` by name. Add a section type in one, mirror it in
the other.
- **Schema changes need a migration** — editing `openpoly/db/tables.py` only
covers *fresh* databases (`create_all`). For an existing one, append an entry to
`MIGRATIONS` in [`openpoly/db/migrations.py`](./openpoly/db/migrations.py)
(PRAGMA-check before altering, so re-running is a no-op); `LATEST_VERSION`
follows the list automatically.
- **Mutating routes carry the auth dependency** — every `POST` / `PUT` / `DELETE` /
`PATCH` route declares `dependencies=[Depends(require_api_token)]`
([`openpoly/api/security.py`](./openpoly/api/security.py)). A test enumerates
`app.routes` and fails if one is missing.
- **Don't confuse layers** — `openpoly/db/` is the SQLite engine, *not*
`sections/database/` (a swappable section). `openpoly/news/` · `markets/` are
domain logic, *not* `sections/news_source/` · `market_source/` (section impls).
Expand Down
Loading