Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(`application/log_setup.py`); tick errors now log their traceback
(previously invisible — no handler existed). Interactive CLI output
unchanged. (#226)
- **Per-unit daemon log trail** — every evaluated rebalance logs one
grep-stable summary (`unit=… rebalance asof=… legs/submitted/round_up/
skipped/on_target`, counters partitioning the universe); round-up/skip leg
decisions log their full `LegDecision.reason` (the exact Decimals and the
binding venue minimum) at INFO; order submits (cid, side, qty, px,
venue id) at INFO and refusals at WARN; each applied fill logs
cid/qty/price/fee/fee-asset (`OrderFillSync.unit_name`, additive); unit
start/stop/step-error transitions logged. Anti-spam pinned: an idle tick
(no new bar) adds zero per-unit lines — a unit's inactivity is now
explained by the log. (#227)

### Changed

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

---

### 2026-07-14 Unit event trail: runner-attributed lines, bus and log are two sinks (PR #227) [accepted]
- **Choice**: order-lifecycle log lines live in the **runner** (per-unit, sees
the router's outcome and the unit name) — not the shared router; the new
module-logger INFO/WARN lines are emitted **alongside** the existing
EventBus `LogEvent`s, not instead (the bus feeds the SSE dashboard, the
logger feeds `daemon.log` — two sinks, two audiences). `OrderFillSync`
gains an additive `unit_name` for fill attribution; supervisor step errors
are logged with traceback then **re-raised** (attribution without
swallowing the CLI tick's safety net).
- **Why**: threading unit identity through the shared router would touch its
signature for a logging concern; the runner already owns the name and the
outcome. Replacing the bus emits would have silently unplugged the
dashboard's live log feed.
- **Rejected alternatives**: per-unit `LoggerAdapter` injected into the
router (signature churn); replacing `LogEvent`s with logging (breaks SSE);
logging every idle tick per unit (2 units × 1440 min/day of noise — the
anti-spam rule is pinned in the plan).

### 2026-07-14 Daemon logging spine: root handlers, offset timestamps (PR #226) [accepted]
- **Choice**: the daemon's logging config lives in `application/log_setup.py`
and wires the **root** logger with two owned, tagged handlers (midnight
Expand Down
8 changes: 5 additions & 3 deletions doc/dev/06-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ engine code: **real-key live enablement** (validate Kraken private endpoints +
venue-level idempotency against a real-key sandbox, then flip `live_enabled`) — the one
maintainer step in [`07-roadmap.md`](07-roadmap.md).

- **`daemon-logging` in flight (1/3)**: the logging spine landed (rotated,
- **`daemon-logging` in flight (2/3)**: the logging spine (rotated,
tz-offset-timestamped `logs/daemon.log`, manifest `logging:` section, tick
tracebacks). Next: the per-unit event trail (leaf 02), tick-timing metrics
(leaf 03). Plan: `plans/daemon-logging/`.
tracebacks) and the per-unit event trail (rebalance summaries, skip
reasons, order/fill lines — a unit's inactivity is now explained by the
log) have landed. Next: tick-timing metrics (leaf 03). Plan:
`plans/daemon-logging/`.

## Known gaps / deferred

Expand Down
2 changes: 1 addition & 1 deletion doc/dev/plans/daemon-logging/00-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ What the audit measured (the evidence this epic answers):
## Leaf checklist

- [x] 01 log-setup — `feat/daemon-log-setup` — medium
- [ ] 02 unit-event-trail — `feat/daemon-unit-event-logs` — medium
- [x] 02 unit-event-trail — `feat/daemon-unit-event-logs` — medium
- [ ] 03 tick-timing-metrics — `feat/tick-timing-metrics` — medium

## Dependencies
Expand Down
113 changes: 0 additions & 113 deletions doc/dev/plans/daemon-logging/02-unit-event-trail.md

This file was deleted.

29 changes: 28 additions & 1 deletion trading_bot/application/order_fill_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ class OrderFillSync:
:class:`~trading_bot.domain.order.Order` aggregates to update.
bus : EventBus
The shared bus to listen on and re-emit updated orders onto.
unit_name : str or None, optional
The managed unit this sync belongs to, threaded from
:func:`~trading_bot.application.service_factory.build_engine` for a
single-unit slice. When given, every applied-fill INFO line is prefixed
``unit=<name> ``; when ``None`` (the whole-system / multi-unit path, the
default — keeps the historic signature working) the same line is logged
without the prefix (starting at ``fill cid=…``).

Examples
--------
Expand All @@ -136,9 +143,12 @@ class OrderFillSync:

"""

def __init__(self, router: OrderRouter, bus: EventBus) -> None:
def __init__(
self, router: OrderRouter, bus: EventBus, *, unit_name: str | None = None
) -> None:
self._router = router
self._bus = bus
self._unit_name = unit_name
# Fill-id dedup: every fill this component has consumed — applied, or
# deliberately skipped (untracked/terminal on replay) — so a re-emitted
# event or a replayed history never double-applies. Fills are the PnL
Expand Down Expand Up @@ -302,4 +312,21 @@ def _apply(self, order: Order, fill: Fill) -> bool:
exc,
)
return False
# One INFO per applied fill — the execution the log's fill line reports
# (signed qty, price, fee) is exactly what reached the tracked order, so a
# reader can tie the daemon log to the store's fills table. The fee is
# denominated in ``fee_asset`` when the venue set one, else the quote
# currency. Prefixed ``unit=<name> `` when this sync knows its unit; the
# same line without the prefix otherwise (see the ``unit_name`` param).
fee_ccy = fill.fee_asset or fill.instrument.symbol.quote
prefix = f"unit={self._unit_name} " if self._unit_name is not None else ""
logger.info(
"%sfill cid=%s %s @ %s fee=%s %s",
prefix,
fill.client_order_id,
fill.signed_qty,
fill.price,
fill.fee,
fee_ccy,
)
return True
Loading
Loading