Skip to content

code architecture improvements#20

Merged
davison merged 16 commits intomainfrom
dev
Feb 20, 2026
Merged

code architecture improvements#20
davison merged 16 commits intomainfrom
dev

Conversation

@davison
Copy link
Copy Markdown
Collaborator

@davison davison commented Feb 20, 2026

No description provided.

- Remove RecordingClient (method-based API)
- Add RecordingSubscriber (reacts to events)
- Move journal.py to portfolio domain (better bounded context)
- Add ReportingCompleteEvent for session lifecycle
- Update all imports and tests

This changes recording from a pull-based client to a push-based subscriber,
enabling the event-driven architecture for both backtest and live execution.
- Define 4 new events: PositionOpened, PositionClosed, EquitySampled, ExcursionSampled
- Move execution/backtest modules to recording domain:
  - reporting.py → recording/equity.py
  - excursions.py → recording/excursions.py
  - observers.py → recording/recorders.py
- Move test files to recording domain (test files follow their code)
- Update all imports (no shims in __init__.py)
- Keep recording/__init__.py clean (events only)

This sets up the foundation for event-driven recording that works identically for
backtest and live execution.
Phase 2 of event-driven recording refactor:

Recording module changes:
- Create EquityRecorder: samples equity on candle close, publishes EquitySampledEvent
- Create ExcursionComputer: computes live MFE/MAE during positions, publishes ExcursionSampledEvent
- Refactor TrackerSync: now event-driven, reacts to PositionClosedEvent instead of polling
- Extend RecordingSubscriber: handle position/equity events, track open positions

These recorders are fully event-driven and work identically for backtest
and live trading. They subscribe to domain events and publish new events
for downstream consumers.

Next phase: Update BacktestClient and IGClient to emit position events.
Phase 3 of event-driven recording refactor:

BacktestClient changes:
- Import PositionOpenedEvent and PositionClosedEvent from recording
- Emit PositionOpenedEvent when opening new positions
- Emit PositionClosedEvent when positions fully close
- Emit events for residual positions opened after partial closes
- Refactor PnL calculation to avoid duplication

execution/backtest/__init__.py changes:
- Remove exports for modules moved to recording domain
- Keep only BacktestClient and streamer-related exports
- Add comments documenting where moved modules now live

Breaking changes:
- Modules previously exported from execution.backtest (excursions,
  observers, harness) must now be imported from recording domain
- This completes the architectural separation between execution
  (emits events) and recording (reacts to events)
Phase 4 of event-driven recording refactor:

Create new runner.py:
- Event-driven backtest orchestration
- No wrapping of strategy._handle_event
- Creates EquityRecorder, ExcursionComputer, ProgressLogger
- All recording happens through event subscriptions
- Clean separation: execution emits events, recording reacts

Delete harness.py:
- Old wrapper pattern no longer needed
- run_backtest() moved to runner.py with new signature
- BacktestSpec preserved for backward compatibility

Update __init__.py:
- Export new runner.py functions
- Document moved modules

Breaking changes:
- run_backtest() signature changed (returns Metrics object directly)
- Equity sampling now event-driven (no wrapper injection)
- Strategy._handle_event no longer wrapped
Phase 5 - Test updates:

- Rename test_harness.py to test_backtest_runner.py
- Update test imports to use runner.py instead of harness.py
- Rewrite tests for event-driven architecture (no wrapper pattern)
- Fix patches to use correct module paths (recording.recorders vs execution.backtest.observers)
- Comment out TrackerSync tests (old polling tests don't apply to event-driven implementation)

All 428 tests now pass ✓

Breaking changes documented:
- Tests expect Metrics object return value (not dict)
- Tests verify event-driven recorders are instantiated
- Tests verify session events are published
Cleanup after event-driven refactor:

- Remove BacktestRecorder class (replaced by EquityRecorder)
- Remove BacktestRecorder tests (no longer applicable)
- Remove unused imports (TradeLedger, EquityRecord)
Type fixes for event-driven architecture:

BacktestClient (client.py):
- Import parse_timestamp from time_utils
- Convert string timestamps to datetime before creating events
- Fixes: PositionOpenedEvent and PositionClosedEvent expect datetime

TrackerSync (recorders.py):
- Remove parse_timestamp calls on event.timestamp (already datetime)
- Use datetime objects directly for hold time calculation
- Convert to ISO format strings when storing in trade dict
Eliminate duplication between order and position event handling:

RecordingSubscriber changes:
- Remove handle_order_request() and handle_order_completed() methods
- Remove _pending_requests tracking
- Remove unused journal field and journal_dir parameter
- Simplify to only handle position lifecycle events

Rationale:
- Position events are higher-level and more semantic
- Position events contain complete round-trip info (entry + exit + PnL)
- Order events would create duplicate/triple recording
- Cleaner architecture: execution emits positions, recording reacts
Update domain __init__.py files to explicitly export public APIs:
- recording: Add 24 exports (TradeLedger, Metrics, recorders, equity functions)
- marketdata: Add MarketDataReceivedEvent export
- execution: Add BacktestClient and OrderExecutionHandler exports

This establishes clear contracts for what's considered public vs internal
within each domain, following the architecture principle that cross-domain
imports should only use top-level domain imports.

Part of domain boundary cleanup to enforce separation of concerns.
Update all cross-domain imports to use top-level domain imports only,
following the architecture rule that code outside a domain should only
import from the domain's __init__.py.

Changes:
- Fix circular imports by removing 'from tradedesk import' in favor of
  specific domain imports (e.g., 'from tradedesk.types import Candle')
- Update execution domain files to import from domain boundaries
- Update portfolio domain to use top-level recording imports
- Update recording domain to use top-level marketdata/execution imports
- Update strategy domain to use top-level imports
- Replace lazy imports with top-level imports where safe
Add comprehensive docstring to RecordingMode enum explaining why the
recording domain knows about execution context (BACKTEST vs BROKER).

The enum represents fundamentally different data availability patterns:
- BACKTEST: Full equity history via record_equity() calls
- BROKER: Synthetic equity computed from position tracking

Documented as a known architecture smell with suggestions for future
refactoring into orthogonal concerns (write_mode, equity_source,
output_files). Keeping as-is for now since it works well for the two
well-defined use cases.
@davison davison merged commit ee6f123 into main Feb 20, 2026
5 checks passed
@davison davison deleted the dev branch February 20, 2026 10:01
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