diff --git a/README.md b/README.md index 1487ece..014db6b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ result.plot() signal values are target weights. they are auto-normalised to sum to 1. rows in signals trigger a rebalance on that date; dates without a row hold the existing positions. +Current fill semantics are same-bar close fills: if a row appears in `signals` +for a given date, crossengine rebalances on that date using the close from the +same row in `prices`. That makes the engine easy to reason about for close-based +portfolio schedules, but it should not be interpreted as next-bar execution. + ### hold without rebalancing (STAY) STAY freezes the share count, not the weight. price movement causes the weight to drift naturally. no trades are generated for STAY assets. diff --git a/docs/api.md b/docs/api.md index 310b5e8..f3213d3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -35,6 +35,11 @@ result = backtest( **returns:** `BacktestResult` +Current execution semantics are same-bar close fills: when a date is present in +`signals`, the rebalance is applied on that same date using the corresponding +close from `prices`. This is a close-fill model, not an implicit next-bar-open +execution model. + ### `STAY` sentinel value for "freeze share count, let weight drift." use in signals DataFrame: diff --git a/src/crossengine/engine.py b/src/crossengine/engine.py index e651f4d..a052e23 100644 --- a/src/crossengine/engine.py +++ b/src/crossengine/engine.py @@ -125,6 +125,10 @@ def run(self) -> tuple[list[dict], list[dict]]: self._process_pending(bar, date, trade_log) if date in self.signals.index: + # Current semantics: signal rows rebalance on the same bar using + # that bar's close price. This is intentional today, but users + # should treat it as a close-fill model rather than next-bar + # execution until execution_delay lands. self._rebalance(bar, date, trade_log) chronicle.append(self.portfolio.snapshot(bar["prices"], date))