Skip to content

Resting limit orders, sortable drill-down table, and LLM-tuned Dark Horse limits - #1

Merged
KhaledElzarw merged 4 commits into
mainfrom
feat/resting-orders-and-dashboard
Jul 21, 2026
Merged

Resting limit orders, sortable drill-down table, and LLM-tuned Dark Horse limits#1
KhaledElzarw merged 4 commits into
mainfrom
feat/resting-orders-and-dashboard

Conversation

@KhaledElzarw

Copy link
Copy Markdown
Owner

Bundles this session's dashboard + strategy work (squash-merging into one commit).

Wallet drill-down & table

  • Click any wallet row → detail panel: strategy description, performance insights, order history (placed/filled timestamps, side, type, qty, price, notional, fee, realized P&L, status, reason), and open orders.
  • Sortable column headers (asc/desc, numeric-aware, aria-sort).
  • New columns: Unrealized P&L, Fees, Open orders, Completed orders.

Resting limit orders (open orders are now real)

  • New RestingBook: a per-wallet ladder of LIMIT orders that carries across candles, fills when a candle trades through, expires on a TTL, and fills at most once per wallet per candle (respects the audited A01 one-fill rule). ExecutionService is untouched.
  • Strategies rest limits where natural (builtins stay a fixed benchmark): mean-reversion entries rest bids below and take-profit exits rest asks above; breakout/trend entries and all stops stay MARKET.
  • The devserver replay expires/fills/rests each tick and surfaces the resting set into the column + drill-down.

Dark Horse

  • Dark Horse (4h committee) stays MARKET — a conviction accumulator acts now.
  • Darkhorse - Daily gains guardrailed entry_limit_bps / exit_limit_bps tunables.

LLM daily re-tune loop (opt-in --llm-adapt, off by default)

  • harness_adaptation.py wires the existing lesson + adaptation pipeline into the replay: per simulated day it builds DailyFacts, generates lessons via an LLM analyst, and adapts Darkhorse - Daily's params (incl. limit offsets) via an LLM proposer, applying the guardrailed result before the next day. Degrades to a no-op when the model is down; deterministic when off.

Also included

  • Synthetic candles anchored to real recent time; devserver port-in-use guard.
  • Strategies clock elapsed candles via a window-independent bar ordinal (fixes trades freezing after the window filled).

Tests

Full suite green: 899 passing (order-book, strategy-limit-emission, harness-adaptation, degrade-path, sorting/columns, and prior fixes).

🤖 Generated with Claude Code

KhaledElzarw and others added 4 commits July 20, 2026 16:04
…nce insights

Click any wallet row to open a detail panel showing:
- a short description of the wallet's current strategy (from the strategy
  module docstring; permanent wallets get bespoke blurbs),
- a performance snapshot (equity, realized/unrealized P&L, fees, holdings,
  avg cost, trade/buy/sell counts, and sell win rate),
- open orders (empty in the paper harness, which fills or rejects every
  intent on its candle, with an explanatory note), and
- full order history: placed/filled timestamps, side, type, requested vs
  filled qty, price, notional, fee, realized P&L, status, and reason.

The dev harness now records every execution result per wallet (filled and
rejected) with candle-close timestamps, and the read model exposes them via
the existing /orders and /fills endpoints plus insights + description on the
wallet detail. Rows are keyboard-accessible buttons; Escape / backdrop close
the panel. All rendering uses the existing safe DOM primitives (no innerHTML).
…vserver port

Two dev-harness follow-ups:

- build_market now accepts end_ms (the last candle's close time). build_view
  passes 'now' aligned to a 5-minute boundary, so synthetic (offline) order
  history reads with real recent timestamps instead of the 1970 epoch. The
  price walk and balances are unchanged — only the timeline shifts. _candle
  keeps its legacy epoch-relative default so existing tests are unaffected.

- main() now checks the port before the market replay and exits 1 with a clear
  message if it is already bound, instead of losing the bind race to a stale
  process that keeps serving old in-memory code (the trap behind the earlier
  'Dark Horse frozen' report).
The 12 builtin strategies used `len(candles)` as an absolute candle index for
their time-based gates (entry cooldown in base, grid re-centre in s01). That
conflates "how much history was handed to me" with "how much time has elapsed".
When the caller feeds a bounded trailing window — the dev harness passes only
the last 150 candles — that length flatlines once the window fills, so every
`index - last >= N` gate reads "no time has passed" and the strategies stop
entering after ~12 h. Observed as: all wallet trades bunched on the first day
of the live window and then near-silence.

Replace the window length with `bar_ordinal`, a monotonic clock derived from
the newest candle's open time and the candle spacing, so it keeps climbing
across the whole run regardless of how much history is retained. Gates compare
relative differences, so the change is behaviour-preserving for the existing
tests; it only unfreezes the clock under a bounded window.

Live replay trades now spread across the whole window (per-day builtin fills
17->157/19/4/7 before, 64/286/236/266 after) with the harness window unchanged.
…-tuned Dark Horse limits

Wallet table:
- every column header sorts asc/desc (numeric-aware), with aria-sort
- four new columns: Unrealized P&L, Fees, Open orders, Completed orders

Resting limit orders (open orders are now real, not structurally zero):
- new RestingBook: a per-wallet ladder of LIMIT orders that carries across
  candles, fills when a candle trades through the price, expires on a TTL, and
  fills at most once per wallet per candle (respects the audited A01). The
  audited ExecutionService is unchanged — the book only decides what to submit.
- strategies rest limits WHERE NATURAL (builtins stay fixed): mean-reversion
  entries rest bids below and take-profit exits rest asks above; breakout/trend
  entries and all stops stay MARKET. base.py buy/sell helpers gained limit_bps.
- the devserver replay expires/fills/rests each tick and surfaces the resting
  set into open_orders_by_wallet, lighting up the column and the drill-down.

Dark Horse:
- Dark Horse (4h committee) stays MARKET — a conviction accumulator acts now.
- Darkhorse - Daily gains guardrailed entry_limit_bps/exit_limit_bps tunables.

LLM daily re-tune loop (opt-in --llm-adapt, off by default):
- new harness_adaptation.py wires the existing lesson + adaptation pipeline into
  the replay: per simulated day it builds DailyFacts, generates lessons via an
  LLM analyst, and adapts Darkhorse - Daily's params (incl. limit offsets) via
  an LLM proposer, applying the guardrailed result before the next day.
- degrades to a no-op when the local model is down; deterministic when off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@KhaledElzarw
KhaledElzarw merged commit 2d43b99 into main Jul 21, 2026
11 of 15 checks passed
@KhaledElzarw
KhaledElzarw deleted the feat/resting-orders-and-dashboard branch July 21, 2026 01:39

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44012754f5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tradebot/api/devserver.py
Comment on lines +531 to +535
book.rest(RestingOrder(
order_id=iid, wallet_id=wallet.wallet_id,
strategy_version_id=version_id, side=side,
limit_price=limit_price, quantity=qty, reason_code=reason,
placed_open_ms=snapshot.open_time_ms))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent repeated entry signals from overcommitting cash

When an entry predicate remains true across multiple candles, such as a prolonged Bollinger deep-z signal, the wallet remains non-holding until a limit fills, so every tick reaches this call and adds another order without consulting book.has_order() or reserving cash. With the default one-candle cooldown, the ladder can therefore contain several orders each sized from the same original balance; two 40% orders can later fill on separate candles and deploy about 80% instead of the intended entry allocation, while further orders become ledger rejects. Deduplicate or replace pending entries, or reserve their notional when sizing new orders.

Useful? React with 👍 / 👎.

Comment on lines +56 to +57
return [self.sell_all_intent(context, "profit_target",
limit_bps=self.exit_limit_bps)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve strategy state until a limit exit actually fills

When this profit target emits a resting limit, BuiltinStrategy.on_market_snapshot immediately clears entry_price as though the sell had filled. If the ask is not reached and expires after its TTL, this strategy still owns BTC but entry_price remains None, so the profit-target condition can never submit another exit while the EMA trend remains bullish. Limit-order placement therefore needs pending-order state distinct from fill-driven position state, or the entry state must only be cleared after execution.

Useful? React with 👍 / 👎.

Comment thread tradebot/api/devserver.py
Comment on lines +493 to +496
new_params = retuner.end_day(current_day, wallet_by_id,
prev.mark_price, version_by_id,
day_fills)
_apply_daily_params(permanents, new_params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Activate the new strategy version after daily adaptation

When --llm-adapt produces a changed adaptation, DailyReTuner.end_day advances retuner.daily_version_id, but this boundary applies only its parameters. The permanent slot and version_by_id remain on dark-horse-daily-v1, so subsequent committee intents and fills are attributed to the old strategy version and the next day's lesson also reports the old version despite trading with new parameters. Propagate the returned version to the active slot and version map together with the parameters.

Useful? React with 👍 / 👎.

Comment thread tradebot/api/devserver.py
Comment on lines +713 to +717
pr.accumulate_fraction = params["accumulate_fraction"]
pr.reduce_fraction = params["reduce_fraction"]
pr.cadence_seconds = int(params["signal_cadence_hours"] * 3600)
pr.entry_limit_bps = params["entry_limit_bps"]
pr.exit_limit_bps = params["exit_limit_bps"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the tuned confidence thresholds to committee decisions

The proposer is allowed to adjust accumulate_confidence and reduce_confidence, but this application step ignores both values and _permanent_committee_intent continues calling synthesize, which uses the hard-coded ACCUMULATE_CONFIDENCE and REDUCE_CONFIDENCE constants. Consequently, successful adaptations can record new confidence values and versions while producing no behavioral change; wire these thresholds into the committee decision or remove them from the harness's advertised tunable surface.

Useful? React with 👍 / 👎.

Comment thread tradebot/api/devserver.py
Comment on lines +185 to +186
ts = _iso(snapshot.close_time_ms)
placed_at = _iso(placed_ms + FIVE_MIN_MS) if placed_ms is not None else ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive resting-order placement times from the candle interval

In live mode the CLI accepts intervals other than 5m, but a resting order's placement timestamp is reconstructed by adding the fixed FIVE_MIN_MS to its candle open. With --live --interval 1h, for example, every resting order is displayed as placed 55 minutes before the strategy actually evaluated the closed candle; the expired-order record has the same assumption. Store the placement close time directly or derive it from the snapshot's actual close time.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant