0040. Portfolio Temporal Ownership And General Rebalance Execution
Status
Accepted — implemented
The execution/valuation/analytics separation was implemented in version 1.0.6.
The general rebalance-strategy amendment dated 2026-09-07 is implemented in
the Unreleased line. ImmediateSignal, CalendarEventSignal, TimeWeighted,
VolumeParticipation, and LiquidityConstrained now share one declared-input,
stateful execution contract.
This ADR amends:
- ADR 0030, by
superseding the decision that
PortfoliosDataNodeowns rebalance-index generation, executed-weight production, and local alignment to one portfolio grid; - ADR 0031, by preserving the generic valuation-source contract while separating execution from portfolio valuation.
It provides the architectural direction for GitHub issue #4. At initial adoption, issue #2 and issue #3 were separate implementation defects. Version 1.0.6 subsequently fixed those defects together with the temporal-separation baseline; the general strategy amendment does not reopen them.
Context
Portfolio workflows contain several different kinds of time:
signal observation time
when investment intent becomes observable
rebalance decision time
when a strategy selects target weights
execution time
when portfolio weights actually change
valuation time
when the held portfolio is valued
analysis time
when a consumer groups or samples canonical values
job execution time
when computation happens operationally
These times may coincide for a simple daily close strategy, but they do not have the same meaning. A weekly-rebalanced portfolio may publish daily values. A signal observed after a market close may be eligible only at the next session. A volume-participation strategy may execute across many intraday bars. An analytics consumer may request month-end observations without changing any economic event.
The pre-1.0.6 implementation collapsed these distinctions into a synthetic portfolio index:
PortfoliosDataNode
-> derives start and end from portfolio output progress and current time
-> creates new_index from portfolio_prices_frequency
daily: persisted calendar market_close timestamps
intraday: pd.date_range(...)
-> forward-fills signal observations onto new_index
-> forward-fills valuation observations onto new_index
-> passes already aligned frames to the rebalance strategy
-> calculates executed weights and portfolio values on that index
-> resamples the result again to portfolio_prices_frequency
This makes portfolio_prices_frequency simultaneously act as a rebalance
cadence, valuation cadence, and reporting cadence. It also reverses the stated
configuration contract: BacktestingWeightsConfig says that the rebalance
strategy controls how and when weights become executed weights, but the
portfolio node selects timestamps before invoking the strategy.
The inconsistency was visible in the pre-1.0.6 strategies:
RebalanceStrategyBase.calculate_rebalance_dates(..., "daily")selectsmarket_open;- the active daily portfolio path independently selects
market_close; ImmediateSignalreceives signal weights already interpolated to those closes, so it is not immediate at the original signal timestamp;TimeWeightedandVolumeParticipationraiseNotImplementedErrorbefore their remaining execution code.
Finally, daily resampling can replace a real market-close timestamp with a
midnight bucket label. That violates the Main Sequence rule that time_index
is the observation point represented by the row, not a generic date label.
Version 1.0.6 corrected that temporal ownership for immediate and persisted
calendar-event execution. It did not, however, establish a general execution
strategy contract. Its PortfolioWeights path still:
- declares exactly two dependencies: signal weights and one execution valuation source;
- asks the strategy for timestamps before loading strategy-specific market inputs;
- branches on
timing_modein the updater; - aligns one configured value column plus an optional column named
volume; - passes those fixed frames to
apply_rebalance_logic(...); and - persists only the last executed weights, with no explicit active intent, remaining target, or partial-execution status.
That interface can represent immediate and calendar-triggered full rebalances.
It cannot truthfully represent a strategy driven by observed bars, elapsed
execution windows, traded volume, quotes, order-book depth, available
liquidity, or external fills. The unfinished TimeWeighted and
VolumeParticipation classes correctly remain outside the public strategy
surface, but excluding them does not make the current abstraction general.
Problem
The pre-1.0.6 design had no single truthful owner for economic timestamps.
It causes the following contract failures:
- A portfolio calculator manufactures timestamps that did not come from a signal, execution event, calendar event, or valuation observation.
- A rebalance strategy cannot fully express its timing semantics because its inputs have already been resampled.
- Forward-fill silently decides signal cutoff, stale-price, holiday, and missing-observation behavior.
- Executed weights and portfolio values are forced onto one timeline even when their natural cadences differ.
- Reporting transformations can overwrite economic timestamps in canonical storage.
- Incomplete strategies appear to participate in a complete portfolio workflow even though their execution contracts are not implemented.
- A rerun can reconstruct an already processed event with a differently labelled timestamp instead of returning a deterministic no-op.
The solution is not to add more date-generation logic to
PortfoliosDataNode. Date generation is valid only in a component whose
declared product is an event schedule or analytical series.
The 2026-09-07 amendment addresses a second problem: moving date selection
from PortfoliosDataNode into a method named execution_timestamps(...) is not
enough. A general rebalance strategy must own its input contract, target
activation, event selection, partial transitions, and unfinished state. The
portfolio updater must not know that a particular strategy uses a calendar,
bar volume, quote size, order-book depth, or time slices.
Decision
Separate portfolio target intent, rebalance execution state, executed-weight projection, portfolio valuation, and portfolio analytics into distinct owners.
The amended target graph is:
SignalWeights (target intent) -------------------------+
|
strategy-declared observed dependencies ----------------+--> PortfolioRebalance
persisted calendar events generic stateful updater
execution bars / observed volume |
quotes / order book / available liquidity |
externally published schedule or fills v
PortfolioRebalanceStateStorage
active intent, partial progress,
remaining target, provenance
|
v
PortfolioWeights
executed-weight projection
|
v
PortfolioWeightsStorage
|
ValuationSource -----------------------------------------------------+--> PortfoliosDataNode
valuation-only updater
|
v
PortfoliosStorage
|
v
PortfolioAnalytics
optional derived updater
The portfolio workflow uses composition, not class inheritance.
PortfoliosDataNode does not subclass a rebalance strategy. It inherits the
economic state and timestamps of the rebalance process by consuming the
canonical executed-weight dependency.
Concrete strategies are implementations below RebalanceStrategyBase, not
architectural nodes above it. Immediate, calendar-triggered, time-weighted,
volume-participation, and liquidity-constrained execution all use the same
state-machine boundary.
1. PortfolioRebalance Owns Generic Strategy Orchestration
PortfolioRebalance is the canonical stateful rebalance-execution
TimeIndexTableUpdater. It is generic orchestration: it does not contain
calendar-, volume-, time-, or liquidity-specific branches.
It owns:
- the signal-weight dependency that publishes target intent;
- a serialized
RebalanceStrategyBaseimplementation; - the deterministic union of dependencies declared by that strategy;
- validation of each declared dependency against the strategy's required input columns and grain;
- loading the previous persisted rebalance state;
- feeding ordered, traceable source events to the strategy state machine; and
- production of
PortfolioRebalanceStateStoragerows.
dependencies() must merge the reserved signal_weights dependency with the
strategy's declared TimeIndexTableUpdater or TimeIndexTableRef
dependencies. Dependency construction happens before update() and remains
deterministic. A strategy must not perform hidden table reads, construct an
upstream node inside its event loop, or obtain market inputs from global
process state.
The generic updater may supply execution bounds and persisted prior state. It
must not select strategy events, interpolate signals, assume a column named
volume, or branch on a timing_mode string.
2. RebalanceStrategyBase Is A Dependency-Declaring State Machine
Every active strategy must provide one typed contract with these semantics:
declared_dependencies
named, typed, hash-bearing market/event sources required by the strategy
required_input_contract
expected grain and required fields for each declared dependency
select_events
ordered eligible events selected only from declared source observations
prepare_execution_context
optional run-scoped precomputation for facts reused by many event transitions
activate_target
target-intent cutoff and succession policy
apply_event
deterministic transition from prior state plus one observed event
result
zero or more auditable state transitions plus the next unfinished state
The implemented Python methods are declared_dependencies(),
required_input_contract(), select_events(), target_selection_mode(), and
apply_event(). prepare_execution_context() is the optional optimization
hook for bounded rolling statistics or indexed lookups that must be computed
once rather than once per event. Those responsibilities may not move back into
PortfolioRebalance, PortfolioWeights, or PortfoliosDataNode.
All values that change output participate in serialization and hashing, including dependency identities, required source columns, calendar/session or window rules, cutoff, target succession, participation limits, liquidity limits, price convention, fees, completion tolerance, and cancellation or expiry behavior.
timing_mode remains open-ended descriptive metadata, but it is not the
dispatch interface. Adding a new strategy does not require editing a central
conditional in the generic updater.
3. Unfinished Execution Is Canonical State
A strategy may take many source events to reach one target. The workflow must persist enough state to resume without pretending that a partially filled target is complete or reconstructing progress from job time.
PortfolioRebalanceStateStorage has this conceptual grain:
(time_index, portfolio_identifier, rebalance_intent_id, asset_identifier)
where time_index is the explicit signal, calendar, schedule, bar, quote,
book, liquidity, or fill event at which strategy state changed. The storage
contract must include, at minimum:
rebalance_intent_id deterministic identity of the activated target
target_signal_time_index source signal observation selected for the target
event_source stable declared-dependency key
execution_status pending | partial | complete | superseded |
cancelled | rejected
target_weight activated target allocation
weight_before executed allocation before this transition
weight_after executed allocation after this transition
executed_weight_delta allocation change produced by this event
remaining_weight_delta unfinished allocation after this event
Price, executed quantity/notional, observed volume, and observed available liquidity are optional execution facts when the strategy uses them. A schema-versioned deterministic strategy-state payload may carry additional state that cannot be represented by the common columns; it must not contain live objects, credentials, process-local identifiers, or untraceable current time.
The target-succession policy must explicitly decide what a later signal does to unfinished work: replace, queue, ignore until completion, or another named policy. A target is complete only when the configured completion rule is met. Missing future bars or liquidity leave it unfinished and produce no invented completion timestamp.
When several declared inputs have the same timestamp, the strategy must define
deterministic precedence or consolidation and emit at most one final state row
for each (time_index, portfolio_identifier, rebalance_intent_id,
asset_identifier) coordinate. Venue-level fills that require multiple records
at that coordinate remain in their own fill ledger and may be consumed as one
declared source.
This table is a portfolio rebalance-state ledger, not a broker order/fill ledger. Live order routing and venue-native fill identity remain in the execution domain; a portfolio strategy may consume an explicit fill source.
4. PortfolioWeights Is An Executed-Weight Projection
PortfolioWeights consumes canonical rebalance state and writes the current
executed allocation whenever weight_after changes. It contains no strategy
selection, date generation, signal interpolation, participation logic, or
unfinished-order policy.
It owns:
- the
PortfolioRebalancedependency; - deterministic projection of executed state transitions;
- production of
PortfolioWeightsStoragerows.
Its output grain remains:
(time_index, portfolio_identifier, asset_identifier)
time_index means the UTC timestamp at which the stored weight became
executed/current. It is not a reporting bucket and not the time the job ran.
Its timestamps are copied from rebalance-state transitions that changed executed weights. The upstream strategy may select events from:
- original signal observations for a truly immediate strategy;
- persisted
CalendarSession.opens_atorcloses_atvalues for a calendar-relative strategy; - actual execution/valuation bar timestamps for time-weighted or volume-participation strategies;
- an explicit reusable event-schedule dependency when a schedule is a separately published data product.
It must not use an untyped pd.date_range(...) as a substitute for one of
those sources. A calendar offset is permitted only as a hash-bearing,
traceable transformation of a persisted calendar event. For bar-, volume-,
liquidity-, or fill-driven strategies, the executed transition timestamp is
the actual selected source observation, not the earlier decision event.
5. Concrete Strategies Specialize The General Contract
Every active rebalance strategy must serialize the economic choices that change its output. Calendar-relative strategies must express, at minimum:
calendar_identifier
session_label
rebalance_event market_open | market_close
event_offset
rebalance_cadence
signal_selection
Those fields participate in configuration hashing. Two strategies that differ in event, cutoff, cadence, or offset are different update processes and produce economically different portfolio identities. A strategy that needs an execution price also declares the relevant observed source and price column; that dependency identity and convention participate in hashing.
An immediate strategy has two valid modes, but they must not be conflated:
signal_time
execute at the signal observation when the instrument is executable
next_eligible_event
map the signal observation to an explicitly configured future calendar event
A strategy that applies the latest signal at every market close is a close-event strategy. It must not describe itself as immediate at the original signal timestamp.
Calendar-relative strategies require a persisted calendar identifier and an
explicit PortfolioCalendarEvents or compatible published-event dependency.
That schedule producer reads CalendarSession rows, requires the canonical
Calendar.unique_identifier, and fails when the calendar is missing, an alias
is supplied, or the governed lookup fails.
CalendarEventSignal only filters the published observations; it performs no
hidden calendar read. Neither component silently falls back to
pandas_market_calendars, a process-local always-open calendar, or another
date generator.
Additional strategies use the same contract:
time-weighted
observes bars or a separately published execution schedule inside the
configured window and advances a partial target at each source event
volume-participation
declares price-and-volume bars and limits each transition by the observed
bar volume and configured participation rate
trailing-average-daily-volume-participation
declares completed daily VWAP-and-volume bars plus observable intraday
execution bars; historical VWAP times volume estimates a daily notional cap,
while the intraday price and volume determine actual execution
liquidity-constrained
declares quotes, order-book depth, or another available-liquidity source and
limits each transition by that observed capacity
fill-driven
declares a venue or simulated-fill source and advances only from persisted
fill observations
These strategies may use a calendar event to activate a target or open an eligibility window. That decision timestamp is not automatically the executed weight timestamp. Execution occurs only when the strategy's declared event contract produces a state transition.
PortfolioTable.calendar_uid remains the portfolio's required reference
calendar. It is not an implicit rebalance clock. A calendar-aware strategy
must declare the exact persisted calendar source it consumes and must validate
any required relationship to the portfolio reference calendar. A strategy
that is driven only by bars, volume, liquidity, quotes, order books, schedules,
or fills does not gain calendar events merely because the portfolio row has a
calendar.
Historical VWAP is ex-post and therefore cannot be an assumed execution price.
For trailing daily-liquidity participation, only completed daily bars whose
right-edge availability timestamp is strictly before the configured session
execution window may enter the rolling capacity estimate. The actual
execution_price must come from the separately declared intraday execution
source at the transition timestamp. The per-asset daily capacity consumed must
survive restarts and target supersession within the same session.
The generic runner groups declared observations by timestamp and materializes
signal targets once per run. Strategies with rolling inputs use
prepare_execution_context() to build bounded lookup state once. They must not
scan full trailing history independently for every asset/event transition.
After any required input sorting, the reference trailing strategy performs
linear preparation in daily-history and execution-bar rows, plus the state
transitions that the output contract necessarily emits.
6. PortfoliosDataNode Owns Valuation Only
PortfoliosDataNode consumes:
- canonical executed weights;
- an explicit asset-indexed valuation source;
- the configured numeric valuation column;
- an explicit valuation alignment and staleness policy.
It owns:
- as-of selection of the latest executed holdings at each eligible valuation observation;
- valuation-source coverage checks for required held assets;
- portfolio-period return calculation;
- cumulative portfolio value calculation;
- production of
PortfoliosStoragerows.
Its output grain remains:
(time_index, portfolio_identifier)
time_index means the UTC timestamp of the portfolio valuation observation.
It does not have to equal the latest executed-weight timestamp.
The valuation timeline must come from explicit observations or events:
- by default, eligible timestamps from the valuation source itself;
- optionally, an explicit valuation-event source such as persisted calendar closes;
- never an internal generic date range derived only from an output-frequency string.
For sparse asset-indexed sources, an eligibility policy decides when all required holdings have sufficiently fresh values. Per-asset as-of alignment may select observations at or before an eligible valuation timestamp, but it does not invent a new timestamp and must expose staleness or missing coverage.
7. PortfolioAnalytics Owns Resampling
Frequency conversion for display, comparison, charting, or performance
analysis belongs to an optional derived PortfolioAnalytics
TimeIndexTableUpdater or an equivalent consumer-owned analysis surface.
It may produce daily, weekly, monthly, or other analytical observations, but:
- its output has a separate storage identity from canonical portfolio values;
- its configuration declares frequency, label, closed-side, timezone, calendar, and aggregation semantics;
- it does not overwrite
PortfoliosStoragerows; - it preserves source-observation lineage;
- a bucket label is not presented as an economic execution timestamp.
If an analysis output uses time_index, that timestamp must truthfully be the
observation point of the analytical row. Period names or bucket boundaries
that are not observation points belong in explicit payload columns.
8. Job Time Is Operational Only
A Main Sequence Job schedule controls when computation starts. It does not define signal, rebalance, execution, valuation, or analysis time.
Runs after the latest eligible event return an empty incremental update until another upstream event or observation exists. Holidays, early closes, and DST changes resolve through persisted calendar rows, not through the wall-clock time of the job.
Configuration Boundary
The public configuration is split conceptually into target, rebalance, executed-weight, valuation, and analytics concerns.
Target ownership:
Portfolio rebalance configuration
signal_weights_instance
rebalance_strategy_instance
strategy-declared dependency instances or table references
strategy event, cutoff, succession, completion, and state policy
execution fee/participation/liquidity assumptions
Portfolio executed-weight configuration
portfolio_rebalance_instance
Portfolio valuation configuration
portfolio_weights_instance
valuation_source_instance
valuation_column
valuation alignment/staleness policy
Portfolio analytics configuration
portfolio_values_instance
analysis frequency
aggregation and labelling policy
The implementation realizes these boundaries with PortfolioRebalance,
PortfolioRebalanceStateStorage, PortfolioWeights, and
PortfoliosDataNode. PortfolioCalendarEvents is an optional explicit
schedule producer for calendar-aware strategies.
The portfolio configuration hash includes the canonical serialized rebalance
strategy and every declared dependency identity. This is how the portfolio
inherits the rebalance contract through composition. PortfolioTable does not
copy strategy fields, manufacture dates, or subclass the strategy.
The existing BacktestingWeightsConfig, PortfolioExecutionConfiguration,
and PortfolioWeights-as-executor surface is a migration source, not the final
general contract. No compatibility adapter may hide the new
PortfolioRebalance state dependency or map arbitrary strategies back to one
valuation-plus-optional-volume frame.
portfolio_prices_frequency must no longer drive rebalance event creation or
canonical portfolio resampling. It will be removed from the combined core
contract rather than retained with ambiguous meaning. Users needing a stable
valuation schedule configure a valuation-event source. Users needing sampled
output configure PortfolioAnalytics.
No compatibility adapter may silently recreate the old synthetic grid. If a temporary migration adapter is provided, it must be explicit, deprecated, and produce a warning that names the legacy timing assumptions.
Temporal Invariants
The implementation must enforce these invariants:
- Every
PortfolioRebalanceStateStorage.time_indexis traceable to an explicit signal, persisted calendar, published schedule, bar, quote, order-book, liquidity, or fill event selected by the serialized strategy. - Every
PortfolioWeightsStorage.time_indexequals a rebalance-state event at which executed weight changed; the projection creates no timestamp. - Every
PortfoliosStorage.time_indexis traceable to an explicit valuation observation or valuation event. - A signal observed after an event cutoff is never applied retroactively to that event.
- Executed weights are selected as-of a valuation timestamp; valuation rows are not forced to occur only when weights change.
- A portfolio may rebalance weekly and be valued daily without synthesizing daily rebalance rows.
- Forward-fill or as-of selection is per asset, bounded by an explicit staleness policy, and observable in diagnostics.
- Resampling cannot change canonical rebalance, execution, or valuation timestamps.
- Configuration hashing distinguishes every dependency and policy choice that changes economic output.
- The generic updater contains no strategy-type switch and no fixed assumptions about calendar, volume, liquidity, quote, or order-book columns.
- Partial execution persists its active intent, executed state, remaining state, and status; absence of future input never forces completion.
- A later signal is handled by an explicit hash-bearing target-succession policy.
- Re-running before the next eligible event or observation produces no new rows.
- Operational job timestamps never enter the economic data contract.
Strategy Completion Policy
TimeWeighted, VolumeParticipation, and LiquidityConstrained are valid
active strategies because they declare and validate their observed inputs,
select actual source timestamps, persist partial or pending state, and resume
from that state. No supported strategy contains an unconditional
NotImplementedError or depends on the retired fixed valuation-plus-volume
frame API.
A future strategy stays outside the supported public surface until its declared-input, event-provenance, transition, restart, succession, and no-forced-completion tests pass.
Incremental Update Semantics
Each updater advances from its own canonical output and dependencies:
PortfolioRebalance
progress = latest persisted state transition for portfolio_identifier,
including every unfinished rebalance_intent_id
end = latest eligible event available from declared dependencies
PortfolioWeights
progress = latest projected weight transition for portfolio_identifier
end = latest rebalance-state transition that changes executed weight
PortfoliosDataNode
progress = latest portfolio-valuation timestamp for portfolio_identifier
end = latest eligible valuation observation with complete/policy-valid
held-asset coverage
PortfolioAnalytics
progress = latest analytical observation for its own output identity
end = latest canonical portfolio value eligible for aggregation
Each updater may overlap source reads as required for per-asset seeds and
same-timestamp deterministic replay, but it emits only transitions not already
represented by its canonical state. No component advances another component
by manufacturing timestamps up to now.
Relationship To Existing Storage
The general strategy implementation adds one canonical state table and preserves the existing executed-weight and portfolio-value grains:
PortfolioRebalanceStateStorage
(time_index, portfolio_identifier, rebalance_intent_id, asset_identifier)
PortfolioWeightsStorage
(time_index, portfolio_identifier, asset_identifier)
PortfoliosStorage
(time_index, portfolio_identifier)
PortfolioRebalanceStateStorage.portfolio_identifier references
PortfolioTable.unique_identifier, and asset_identifier references
AssetTable.unique_identifier. The common state columns described above are
ordinary typed columns. Any additional deterministic strategy-state payload
must be schema-versioned and bounded; it is not a substitute for the common
queryable status and remaining-target fields.
The existing PortfolioTable.portfolio_weights_data_node_uid remains the
direct pointer used by valuation and read services. The dependency graph from
that weights projection to PortfolioRebalance provides execution-state
lineage, so this amendment does not require another nullable pointer on
PortfolioTable.
This ADR does not require an immediate physical rename of
PortfolioWeightsStorage.price_current, price_before, or
PortfoliosStorage.close. Those names remain candidates for a separate schema
ADR and migration.
PortfoliosStorage.close_time must not be used to compensate for an incorrect
time_index. During implementation it should either equal the canonical
valuation observation timestamp or be removed/repurposed through an explicit
schema decision.
Consequences
Positive consequences:
- Temporal ownership matches financial meaning.
- Rebalance strategies genuinely control when and how weights execute.
- Weekly execution and daily valuation become natural rather than exceptional.
- Signal cutoff, valuation staleness, early closes, and DST are testable.
- Canonical storage keeps real economic timestamps.
- Analytics can evolve without changing portfolio identity or execution.
- Incomplete strategies cannot borrow apparent completeness from portfolio forward-fill and resampling.
- Time-, volume-, liquidity-, quote-, book-, and fill-driven strategies can declare different inputs without changing generic orchestration.
- Partial execution and later resumption become explicit and auditable.
Tradeoffs:
- The implemented amendment adds
PortfolioRebalance, a new storage contract, and a migration fromPortfolioWeights-as-executor to a weights projection. - Existing configuration payloads using
portfolio_prices_frequencyneed a breaking migration or explicit deprecated adapter. - The dependency graph gains a first-class rebalance-state producer and may gain explicit schedule, bar, liquidity, fill, or analytics producers.
- Execution and valuation may need separate source dependencies even when both happen to use the same market-data table.
- Tests and examples must distinguish signal, execution, valuation, analysis, and job timestamps.
- The additional state table is deliberate complexity: it keeps unfinished execution out of canonical weights and keeps strategy-specific logic out of portfolio valuation.
Rejected Alternatives
Keep One Synthetic Portfolio Grid
Rejected because one grid cannot truthfully represent signal, execution,
valuation, and analysis time. Adding more flags to
portfolio_prices_frequency would preserve the wrong owner.
Put A Complete Timing Policy On PortfoliosDataNode
Rejected because it would make timing explicit but still leave the portfolio valuation calculator responsible for rebalance execution.
Generalize PortfolioWeights With timing_mode Branches
Rejected because a central switch still requires the portfolio updater to know which strategies use signal events, calendars, bars, volume, or liquidity. Optional fixed frames merely move the coupling into a wider method signature.
Store Unfinished Work Only In PortfolioWeightsStorage
Rejected because an active target can change state without changing executed weight, and several target intents may transition at one timestamp. A table whose timestamp means "weight became current" cannot also truthfully represent pending, superseded, rejected, or cancelled intent state.
Make Each Strategy A Different Portfolio Updater
Rejected because it duplicates updater identity, storage lifecycle, incremental
orchestration, and normalization in every strategy. Strategies are composed
state machines under one PortfolioRebalance updater.
Make PortfoliosDataNode Subclass RebalanceStrategyBase
Rejected because object-oriented inheritance would mix DataNode identity, storage lifecycle, strategy configuration, and valuation behavior. The needed relationship is composition through canonical executed weights.
Force Portfolio Values Onto Rebalance Events
Rejected because portfolio valuation may be more frequent than rebalancing. It would also omit economically meaningful mark-to-market movement between weight changes.
Use Job Scheduling As Rebalance Timing
Rejected because operational start time is not a reproducible economic event and cannot correctly represent backfills, holidays, early closes, or delayed data availability.
Resample Canonical Portfolio Values In Place
Rejected because it replaces observation timestamps with analytical bucket labels and makes downstream consumers unable to recover the canonical series.
Migration Plan
Baseline Stages 1-5: Temporal Separation (Implemented In 1.0.6)
Stage 1: Contract Tests
- Add failing tests for signal time, market open/close, early close, DST, before/after cutoff, weekly rebalance with daily valuation, and rerun no-op.
- Add invariants proving output timestamps originate from declared sources.
- Preserve focused regressions for per-asset valuation seed lookup and daily market-close timestamp preservation.
Stage 2: Execution Updater
- Promote
PortfolioWeightsto the execution/rebalance updater. - Move strategy application and previous-weight lookup out of
PortfoliosDataNode. - Give active strategies typed, hash-bearing timing policies.
- Implement
ImmediateSignalwith truthful signal-time and/or next-eligible-event semantics. - Remove unsupported strategies from the active public surface until complete.
Stage 3: Valuation Updater
- Make
PortfoliosDataNodedepend on canonical executed weights. - Drive valuation timestamps from explicit valuation observations/events.
- Implement set-based per-asset as-of seed lookup with bounded staleness.
- Remove rebalance-index generation and final in-place resampling.
Stage 4: Analytics Updater
- Introduce the optional derived portfolio analytics/resampling surface.
- Give analytical outputs separate identity and explicit aggregation policy.
- Preserve canonical source timestamps and lineage.
Stage 5: Public Migration
- [x] Remove
portfolio_prices_frequencyfrom the core contract. - [x] Update portfolio configuration serialization and hashing.
- [x] Update examples, tutorials, portfolio knowledge docs, skills, and changelog.
- [x] Document the breaking migration in the Unreleased changelog.
- [x] Require calendar-event strategies to resolve persisted calendars without a local fallback.
- [x] Provide a dry-run-first migration path for legacy midnight-indexed portfolio values.
Amendment Stage 6: General Contract And State Storage (Implemented)
- [x] Add contract tests for declared dependencies, required input schemas, strategy-free orchestration, target succession, partial execution, restart, and absence of forced completion.
- [x] Add and migrate
PortfolioRebalanceStateStoragewith the state grain and common columns defined by this ADR. - [x] Introduce
PortfolioRebalanceas the generic stateful updater. - [x] Replace
execution_timestamps(...)plusapply_rebalance_logic(...)with the dependency-declaring state-machine contract.
Amendment Stage 7: Executed-Weight Projection (Implemented)
- [x] Convert
PortfolioWeightsinto a pure projection of state transitions that changed executed weight. - [x] Preserve
PortfolioWeightsStoragegrain and its use byPortfoliosDataNodeand read services. - [x] Prove that the projection copies source event timestamps and creates none.
Amendment Stage 8: Strategy Migration (Implemented)
- [x] Migrate immediate and calendar-event behavior onto the general contract without changing their economic results.
- [x] Implement time-weighted and volume-participation strategies after their declared-input, partial-state, and restart tests pass.
- [x] Add a liquidity-constrained reference strategy using observed quote, order-book, or available-liquidity timestamps.
- [x] Keep any strategy with an unfinished state transition or undeclared input outside the public exports.
Amendment Stage 9: Public Configuration Migration (Implemented)
- [x] Replace the combined execution path with explicit rebalance-state, executed-weight, and valuation composition.
- [x] Include strategy-declared dependency identities and all economic policies in canonical hashing.
- [x] Update docs, examples, tutorials, changelog, and migration guidance.
- [x] Treat the public contract change as a versioned breaking change; no adapter may silently recreate the fixed valuation-plus-optional-volume interface.
Amendment Stage 10: Trailing Daily Liquidity (Implemented)
- [x] Add a public trailing daily-volume participation strategy with separate completed-daily capacity and observable intraday execution dependencies.
- [x] Exclude current-session and future daily VWAP observations from capacity
and never use historical VWAP as
execution_price. - [x] Persist per-asset session consumption across restart and target supersession, then reset it only when a new session begins.
- [x] Precompute rolling history, signal targets, and event observation lookups once per run instead of performing repeated full-frame scans.
Legacy Midnight-Indexed Values
Existing rows cannot be corrected by updating time_index in place. They are
owned by PortfoliosDataNode, and changing an indexed coordinate would bypass
the updater's storage lifecycle and make later incremental progress ambiguous.
The supported migration is therefore a portfolio-scoped rollback and replay:
- Inspect the requested legacy window and the latest persisted row.
- Resolve every candidate row through the portfolio's
calendar_uidand persistedCalendarSessionclose. The oldclose_timemust corroborate the mapping. - Refuse missing, ambiguous, conflicting, or only partially inspected tails.
- Delete the inclusive tail with
TimeIndexMetaTable.delete_after_date(..., dimension_filters={"portfolio_identifier": [...]}). - Rerun the migrated portfolio graph so
PortfoliosDataNodedeterministically rebuilds values at canonical valuation-source timestamps.
Apply one portfolio at a time with scheduled writers paused. The result checks the deleted row count against the plan so a concurrent tail change is visible. The migration never reconstructs a close from a fixed UTC hour, never uses raw SQL, and never presents deletion alone as a completed repair. A second dry run after replay must return no rollback.
Acceptance Criteria
Temporal-Separation Baseline (Implemented In 1.0.6)
The original temporal-separation decision is implemented because:
- In the 1.0.6 baseline,
PortfolioWeightsbecame the explicit execution/rebalance producer andPortfoliosDataNodeconsumed its canonical output. The amendment subsequently insertedPortfolioRebalanceand madePortfolioWeightsa projection. PortfoliosDataNodecontains no rebalance-date generation.- canonical portfolio valuation contains no in-place reporting resample.
- no core portfolio path uses a generic
pd.date_range(...)to invent execution or valuation timestamps. - immediate, calendar-relative, and bar-participation timing semantics are distinguishable in serialized configuration and hashing.
- signals immediately before and after a cutoff behave non-retroactively.
- normal sessions, holidays, early closes, and DST resolve from persisted calendar data.
- weekly rebalance with daily valuation produces sparse executed-weight rows and daily canonical value rows.
- per-asset valuation as-of alignment uses the last eligible observation for each required asset and enforces configured staleness.
- reruns before a new eligible event return no new rows.
- legacy midnight-indexed rows have a persisted-calendar-validated, scoped rollback-and-replay path that is idempotent after replay.
- analytical resampling writes a separate derived data product.
- unsupported unfinished strategies are not presented as executable.
- documentation and examples distinguish all temporal concepts and no longer
describe
portfolio_prices_frequencyas a combined execution/valuation clock.
General Strategy Amendment (Implemented)
The amended ADR is implemented because:
PortfolioRebalancemerges signal weights with arbitrary deterministic, typed dependencies declared by the serialized strategy.- the generic updater contains no
timing_modeor concrete-strategy branch and no fixed assumption about valuation, volume, quote, book, or liquidity columns. - every strategy validates the grain and required fields of each declared dependency before applying events.
- event selection occurs after declared inputs are available and every emitted state timestamp is traceable to an input event or a permitted persisted calendar transformation.
- active intent, target signal, before/after executed state, remaining target, and nonterminal/terminal status survive an incremental restart.
- a lack of future bars, volume, liquidity, quotes, book updates, or fills leaves work unfinished and emits no synthetic completion.
- target succession is explicit, hash-bearing, and tested with a later signal arriving during partial execution.
PortfolioWeightsis a pure projection from rebalance state and creates no dates or strategy decisions.- immediate and calendar-event strategies preserve their supported results on the new contract.
- time-weighted, volume-participation, and liquidity-constrained reference strategies pass event-provenance, partial-execution, no-liquidity, and rerun idempotency tests before export.
- trailing daily-volume participation uses only completed history for its daily cap, executes at the current declared intraday price, carries same-session consumption across restart/supersession, and performs bounded linear preparation before transition emission.
- the storage migration, public configuration migration, examples, tutorial, knowledge docs, and changelog are complete.