"What Could've Been" — Retrospective Decision Quality
Data-Science Strategy Brief
Issue: #1657 Date: 2026-05-11 Status: Research — awaiting architect + feature-developer handoff Prepared by: data-scientist agent
Background
After a user closes a position, Raxx surfaces what the position would have been worth if held to close-of-market (EOD) or to the end of the calendar month (EOM). This is purely retrospective: it describes what DID happen to the underlying after the user exited, using the user's own closed position data. It never predicts future prices or recommends action.
Framing rule that governs every sentence of this feature: Raxx surfaces what DID happen on the user's own data. Past tense throughout. All timestamps stored and computed in UTC.
1. Methodology
1.1 Closed-position snapshot inputs
| Field | Type | Notes |
|---|---|---|
position_id |
UUID | FK to closed position record |
symbol |
string | Normalized ticker (e.g., "AAPL") |
asset_type |
enum | equity, call, put, multi_leg |
close_price |
decimal(12,6) | Execution price at which the user closed |
close_timestamp_utc |
timestamptz | Exact close time in UTC; broker-provided |
position_size_signed |
decimal(12,4) | Positive = long, negative = short (in shares for equity, in contracts for options) |
cost_basis |
decimal(12,6) | Average cost basis per share/contract |
close_commission |
decimal(10,4) | Commission already paid on the close leg |
tracking_horizon |
enum | close_of_market_only, monthly |
multiplier |
integer | 1 for equity; 100 for standard equity options |
1.2 Long equity would-have P&L at time T
long_pnl(T) = position_size_signed
× (price_at_T − cost_basis)
+ dividends_paid_in_window
− close_commission_already_paid
Where:
- price_at_T is the adjusted close price of the stock at evaluation timestamp T
- dividends_paid_in_window is the sum of per-share dividends with ex-date strictly after
close_timestamp_utc and on or before T, multiplied by position_size_signed
- close_commission_already_paid is subtracted once — it was paid regardless; it is a sunk cost
but included in the comparison for completeness so the delta reflects the user's actual
net-of-cost position
1.3 Short equity would-have P&L at time T
short_pnl(T) = position_size_signed
× (cost_basis − price_at_T)
− dividends_paid_in_window
− close_commission_already_paid
Note on sign convention:
- For a short position, position_size_signed is negative (e.g., -100 shares)
- position_size_signed × (cost_basis − price_at_T) correctly produces a positive number
when price fell (good for short) and negative when price rose (bad for short)
- dividends_paid_in_window is subtracted because a short seller OWES dividends to the lender;
this is a cost that would have accumulated if held
- close_commission_already_paid is subtracted as a sunk cost on the same basis as long
1.4 Delta displayed to user
delta = would_have_pnl(T) − actual_pnl_at_close
Where actual_pnl_at_close = position_size_signed × (close_price − cost_basis) − close_commission.
A positive delta means holding would have produced more P&L than closing did. A negative delta means the user avoided further loss (or captured more than they would have gotten by holding). Neither framing is editorial: the display is data only.
1.5 Split adjustment
If a stock split occurs with ex-date strictly after close_timestamp_utc and before T:
adjusted_position_size = position_size_signed × split_ratio
adjusted_cost_basis = cost_basis / split_ratio
The Alpaca adjustment=all bars endpoint already returns split-and-dividend-adjusted closes.
When using adjusted bars, the split adjustment is embedded in price_at_T; the position size
and cost basis must be equivalently adjusted to maintain consistency. The reference implementation
(see section 6 and docs/data-science/notebooks/what-couldve-been-validation.py) handles both
the raw-price-with-explicit-adjustment path and the adjusted-price path.
1.6 Data points on the trajectory
For the EOD variant: a single data point (the regular-session closing price on the day of close).
For the EOM variant: one daily bar per calendar day from the close date through the last trading
day of the calendar month. This produces a sparse time series (trading days only) of
(date, would_have_pnl) pairs that drives the sparkline.
2. Time-Window Decision
2.1 EOD (close_of_market_only)
Definition: the official NYSE/NASDAQ regular-session close at 16:00 ET (21:00 UTC during EDT;
22:00 UTC during EST). This is a single price point — not a trajectory — expressed as the
Alpaca 1Day bar c field (or adjustment=all adjusted close) for the date of the user's
close.
Daylight savings handling: the service already has EASTERN_TZ = ZoneInfo("America/New_York")
in alpaca_market_data_service.py (line 18). The bar timestamp from Alpaca is UTC-normalized.
No additional DST handling is needed; use Alpaca's bar timestamp as authoritative.
2.2 EOM (monthly)
Recommendation: calendar month-end, meaning the last regular trading day of the calendar month in which the position was closed, NOT 30 days from close.
Rationale: "monthly" in the operator's framing refers to a natural review cadence aligned to calendar months, not a rolling 30-day window. A position closed on 2026-04-28 (two trading days before April month-end) would have a very short EOM window — that is correct behavior, not an edge case to be corrected. The user closed near end of month; the EOM comparison is thin; that fact is displayed transparently ("tracked 2 trading days"). See section 2.4 for the near-EOM edge case recommendation.
2.3 Edge cases: after-hours or holiday closes
If close_timestamp_utc falls outside regular trading hours (pre-market, after-hours, weekend,
or holiday), the tracking window start is defined as the next regular-session market close.
Rule:
1. Take close_timestamp_utc.
2. Find the next calendar day D such that D is a NASDAQ/NYSE trading day (not weekend or holiday)
AND close_timestamp_utc is before or at 21:00 UTC on D-1 (i.e., before D's market close).
3. The EOD evaluation price is D's closing bar.
4. For EOM: track from D's closing bar through the last trading day of D's calendar month.
Holiday detection: Alpaca's /v2/calendar endpoint returns trading days for any date range.
Use this to find the next trading day; do not hardcode holiday schedules.
Reference: Alpaca Broker API — Calendar endpoint at
https://docs.alpaca.markets/reference/getcalendar-1
2.4 Near-EOM edge case
Position closed on the last trading day of the month: the EOM window has zero additional days. In this case: - EOM and EOD values are identical (same price point). - Display note: "Position closed on the last trading day of the month. Would-have value equals the closing price on the day of close." - Do not suppress the feature; render the single-point result.
Position closed with fewer than 3 trading days remaining in the month: display a note "Tracked N trading day(s) into the month."
3. Options and Multi-Leg Strategies
3.1 V1 scope: underlying-tracking only for options
For single-leg options (calls and puts), V1 tracks the underlying equity, not the option contract value. This is the correct V1 choice for three reasons:
- Options price history is expensive data (OPRA historical quotes; not currently integrated).
- The underlying trajectory is the primary driver of whether the user's directional read was right — which is the question the user is asking.
- Theta decay communication for a contract that has already expired is ambiguous and confusing without a full options pricing model.
V1 UI copy for options positions: "The underlying (TICKER) would have moved from $X to $Y over this window. Your option contract's value also depended on implied volatility and time decay, which are not reflected here."
This is honest and complete. It surfaces the directional data the user wants without overstating what the feature knows.
3.2 V2 scope: contract-level tracking (future)
V2 may add contract-level tracking using historical options quotes from ORATS or Alpaca's
historical options data (subject to data subscription and licensing review). V2 would require:
- A new data integration (ORATS or Alpaca options data tier)
- A business-legal-researcher review of data licensing terms
- Pricing model for mark-to-market at any given timestamp (Black-Scholes or Heston with
historical IV; or use quoted bid/ask midpoint if data is available)
V2 is out of scope for this brief. Flag for a future data-science dispatch.
3.3 Multi-leg strategies (iron condors, vertical spreads, etc.)
V1: composite would-have P&L only. Sum the would-have P&L across all legs:
composite_would_have_pnl(T) = sum( leg_i.pnl(T) for i in legs )
Each leg uses the underlying-tracking formula (section 1.2 / 1.3) applied to the underlying, scaled by each leg's position_size_signed and multiplier.
Per-leg breakdown is a V2 feature. The V1 composite is sufficient to answer the user's question ("did the strategy as a whole do better or worse than my exit?").
3.4 Theta decay communication
For options, add a static note to the UI:
"Options also decay in time value (theta). Holding longer changes the contract's value beyond the underlying move. The figure above reflects the underlying's movement only."
This is factual, past-tense, and non-predictive.
4. Storage Model
4.1 Recommendation: compute-on-render with server-side scheduled snapshots
Do not store the full daily trajectory. Store only: - The position-close snapshot (already captured in the closed position record) - The computed "value at horizon" once the tracking horizon is reached (for EOM: computed by the end-of-month job; for EOD: computed by the same day's EOD job)
Rationale:
- The trajectory up to the horizon is derivable from Alpaca daily bars at any time.
- Bars are cached at the service layer (up to 3600s for daily bars per _bars_ttl_seconds).
- Storing a separate row per position per day would create O(positions × trading_days) rows for
the EOM variant — unnecessary for a feature that renders a sparkline from ~20 data points.
4.2 New table: closed_position_wcb_snapshots
CREATE TABLE closed_position_wcb_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
position_id UUID NOT NULL REFERENCES closed_positions(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
symbol VARCHAR(16) NOT NULL,
tracking_horizon VARCHAR(24) NOT NULL CHECK (tracking_horizon IN (
'close_of_market_only', 'monthly'
)),
close_timestamp_utc TIMESTAMPTZ NOT NULL,
close_price NUMERIC(12,6) NOT NULL,
cost_basis NUMERIC(12,6) NOT NULL,
position_size_signed NUMERIC(12,4) NOT NULL,
close_commission NUMERIC(10,4) NOT NULL DEFAULT 0,
multiplier INTEGER NOT NULL DEFAULT 1,
asset_type VARCHAR(16) NOT NULL DEFAULT 'equity',
-- Populated by scheduled job once horizon is reached
horizon_price NUMERIC(12,6),
horizon_timestamp_utc TIMESTAMPTZ,
would_have_pnl NUMERIC(14,6),
actual_pnl NUMERIC(14,6),
delta_pnl NUMERIC(14,6),
dividends_in_window NUMERIC(12,6) NOT NULL DEFAULT 0,
split_ratio_applied NUMERIC(10,6) NOT NULL DEFAULT 1.0,
data_source VARCHAR(32),
-- Computation metadata
last_computed_at TIMESTAMPTZ,
horizon_reached BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_wcb_user_position ON closed_position_wcb_snapshots (user_id, position_id);
CREATE INDEX idx_wcb_horizon_reached ON closed_position_wcb_snapshots (horizon_reached, tracking_horizon)
WHERE NOT horizon_reached;
4.3 Scheduled jobs
Two Celery (or equivalent) periodic tasks:
EOD job — runs daily at 22:00 UTC (16:00-17:00 ET after DST is resolved and Alpaca bars
are available):
1. SELECT all closed_position_wcb_snapshots WHERE tracking_horizon = 'close_of_market_only'
AND horizon_reached = FALSE
AND date(close_timestamp_utc) <= current_date
2. For each row: fetch the closing bar for the position's symbol on date(close_timestamp_utc)
(or next trading day if closed after-hours); compute would_have_pnl; set
horizon_reached = TRUE.
EOM job — runs on the first trading day of each new calendar month at 02:00 UTC:
1. SELECT all closed_position_wcb_snapshots WHERE tracking_horizon = 'monthly'
AND horizon_reached = FALSE
AND date_trunc('month', close_timestamp_utc) < date_trunc('month', current_date)
2. For each row: fetch the last-trading-day close of the position's month; compute
would_have_pnl; set horizon_reached = TRUE.
4.4 On-render trajectory for sparkline
For positions where horizon_reached = FALSE (in-progress EOM tracking), compute the partial
trajectory on-render:
1. Fetch daily bars from Alpaca for the symbol from date(close_timestamp_utc) to current_date
(max 31 bars for monthly; zero for EOD while in-flight).
2. Apply split/dividend adjustments.
3. Compute would_have_pnl at each bar date.
4. Return as [(date_str, pnl_float)] array for sparkline rendering.
Cache the trajectory response for 1h in the application cache layer (the existing
AlpacaMarketDataService LRU cache covers the bar data; the trajectory computation result
should be cached separately in Redis or similar with a TTL keyed to
wcb_trajectory:{position_id}:{current_date}).
Once horizon_reached = TRUE, the trajectory is immutable. Cache it indefinitely (or until
the position row is deleted per the user's data-retention entitlement).
4.5 Tier-gated row creation
Rows are ONLY created in closed_position_wcb_snapshots when the user's tier entitles them:
- Free: no rows created (feature is hidden; no data footprint)
- Pro: rows created with
tracking_horizon = 'close_of_market_only'only - Pro+: rows created with either
tracking_horizonper user setting
Row creation happens synchronously at position-close time (lightweight: just the snapshot fields; no computation yet). Computation is deferred to the scheduled jobs.
5. Data Source
5.1 Primary: Alpaca Market Data — daily adjusted bars
Endpoint: GET /v2/stocks/bars with adjustment=all and timeframe=1Day
This endpoint is already integrated in backend_v2/api/services/alpaca_market_data_service.py
via get_bars() with adjustment='all'. The adjustment=all parameter instructs Alpaca to
return split-adjusted and dividend-adjusted prices.
Alpaca documentation: https://docs.alpaca.markets/reference/stockbars
The adjustment=all mode returns the price series adjusted backward so that all historical
prices are on a consistent basis. For this feature, we need the FORWARD-adjusted value (what
the position would have been worth from close-date forward), which means we should use
adjustment=raw for price points AFTER the close date (to preserve actual market prices the
user would have experienced) plus explicit corporate action events to adjust position size and
cost basis. Alternatively, use adjustment=split (split-adjusted only, dividends paid
separately) to avoid confounding the dividend component with the price.
Recommendation for V1:
- Use adjustment=split bars for price tracking (so split is in the price series and does not
require a separate position-size adjustment).
- Separately fetch dividend events from the Alpaca corporate actions endpoint to compute
dividends_paid_in_window explicitly.
- This is the cleaner accounting separation: price series reflects actual market prices adjusted
for splits, dividends are tracked as cash events.
Alpaca corporate actions endpoint: GET /v2/corporate-actions/announcements with
ca_types=dividend and since / until date range.
Reference: https://docs.alpaca.markets/reference/getcorporateannouncements-1
5.2 Fallback: Alpha Vantage daily adjusted
Endpoint: TIME_SERIES_DAILY_ADJUSTED (function parameter on the Alpha Vantage REST API)
Returns: 5. adjusted close, 7. dividend amount, 8. split coefficient
This endpoint provides split and dividend data in a single call, which simplifies the V1 fallback path. The Alpha Vantage integration already exists in the codebase.
Alpha Vantage API documentation: https://www.alphavantage.co/documentation/#dailyadj
Known limitation: Alpha Vantage free tier has 25 calls/day and 5 calls/minute. For any user with more than ~10 active WCB-tracked positions simultaneously querying the fallback path, rate limiting will trigger. Mitigation: the fallback is only used when Alpaca is unavailable, and the scheduled job (not the render path) is the primary computation point.
5.3 Holiday / trading calendar
Alpaca's /v2/calendar endpoint is authoritative for which days are trading days and provides
the official market open and close times. This is required for:
- Determining the next trading day after an after-hours or holiday close
- Determining the last trading day of a calendar month for EOM tracking
Reference: https://docs.alpaca.markets/reference/getcalendar-1
5.4 Dividend data gap assessment
Both Alpaca (via corporate actions) and Alpha Vantage (via the adjusted series) provide
dividend data. For V1, dividends are handled only for equities. Options and multi-leg positions
receive dividends_paid_in_window = 0 in V1 with an explicit note in the UI.
For a 30-day EOM window on a typical S&P 500 stock, dividends fall within the window for approximately 25% of positions (quarterly dividend cadence). The impact is non-trivial for high-yield stocks. The V1 implementation must include dividend handling for equities; omitting it would produce systematically misleading long-position results.
Source on dividend frequency: Alpaca does not publish aggregate stats; the 25% estimate is based on the general observation that S&P 500 companies pay quarterly dividends (approximately once per quarter = approximately 3/12 = 25% probability of a dividend falling in any given 30-day window). This is a rough calibration figure, not a peer-reviewed stat.
5.5 No new data integration required for V1
Both Alpaca and Alpha Vantage integrations are already present. No new vendor, no new licensing review needed for V1 equity tracking.
6. Backtesting and Validation
The reference validation implementation is at:
docs/data-science/notebooks/what-couldve-been-validation.py
6.1 What the validation script does
Takes 50 synthesized historical closed equity positions with realistic parameters (mixed long/short, positions with splits, positions with dividends, positions spanning month-end boundaries, after-hours closes, positions closed on the last trading day of the month) and:
- Computes EOD would-have P&L for each position
- Computes EOM would-have P&L for each position
- Validates: - Split adjustment: adjusted would-have P&L equals raw P&L after manual ratio application - Dividend accounting: long positions receive dividend credit; short positions are charged - Commission accounting: close commission is subtracted identically for would-have and actual - Sign correctness: short positions have reversed direction - Near-EOM edge case: positions closed on last trading day produce single-point result - After-hours close: evaluation uses next-trading-day close
- Outputs summary statistics: % of positions where holding would have produced more P&L vs less
6.2 Key validation findings from the synthetic dataset
These are design-time expectations, not empirical findings from real data. The script validates that the math is correct; it does not predict what a real user's distribution will look like.
Expected behavior: - In trending markets, most long positions closed profitably will show positive delta (holding would have made more money) or negative delta (they exited near a local peak). The distribution is expected to be roughly balanced in a random sample. - Split-adjusted positions should show identical P&L whether computed via the adjusted-price path or the raw-price-plus-manual-adjustment path. - Dividend adjustment for a quarterly-paying stock in a 30-day window: approximately 1 in 4 positions will have a dividend event; the script confirms the cash flow is correctly credited or debited based on long/short direction.
6.3 Validation script notes
The script uses only the Python standard library plus pandas and numpy (no broker API calls). It synthesizes its own price series using a geometric Brownian motion model for testing purposes. The GBM parameters (mu=0.07 annualized, sigma=0.20 annualized) are standard calibration values for illustrative equity backtests; they are not fitted to real data.
Source for GBM calibration: Hull, J. (2021). Options, Futures, and Other Derivatives, 11th ed., Section 14.3 — standard academic reference for equity process modeling.
7. UI Surface (Hand to UX-Designer)
This section flags design requirements for the ux-designer agent. The data-scientist defines the data outputs; ux-designer owns the visual execution.
7.1 Placement
The "What Could've Been" surface belongs in the trade history table as an expandable row or an inline card below the position summary row. It should not be a separate page; the decision context lives next to the position record it belongs to.
7.2 Data elements to surface
Per the acceptance criteria in issue #1657: - "Closed at: $X on DATE — P&L: +/- $X (+/- Y%)" - "Would have been worth: $Y on HORIZON_DATE — P&L: +/- $X (+/- Y%)" - "Delta: +/- $X (+/- Y%) you [captured more / would have captured more / avoided]" (Note: ux-designer should work with Kristerpher on exact copy; past tense, no opinion) - Sparkline: daily series from close-date to horizon-date showing the trajectory of would-have P&L over time
7.3 Aesthetics
Confidence Engine aesthetic (moss + ink palette, no neon). Sparkline uses a muted green for positive delta trajectory, muted red for negative, with the zero line in ink. The sparkline is informational, not alarming — keep it understated.
7.4 Past-tense copy
All copy is past-tense and data-only. Do not editorialize: - "Would have been worth $X" — correct - "You left $X on the table" — WRONG (opinion framing) - "Would have earned $X more if held" — WRONG (implies the user made a mistake) - "At market close on DATE, the position would have been worth $X" — correct
7.5 Obfuscate mode interaction
When obfuscate mode is active (feature defined in project_obfuscate_mode.md):
- Dollar amounts in all WCB fields are hidden
- Percentage figures remain visible
- Sparkline Y-axis: no dollar labels; the shape of the curve remains (percentages on axis)
- Delta is shown as "+/- Y%" only
7.6 In-progress tracking state
For EOM positions where the tracking horizon has not been reached: - Show the partial trajectory through the current date with a clear label: "Tracking through MONTH_END_DATE — updated daily." - Use a subtle dashed extension on the sparkline from today to month-end to indicate the window is not yet closed. The dashed portion has no data; it shows the time dimension only.
8. Tier-Gating Mechanics
8.1 Rule summary
| Tier | Feature visibility | Permitted tracking_horizon |
|---|---|---|
| Free | Feature HIDDEN entirely (no upgrade CTA; per hide-don't-gray rule) | None |
| Pro | Feature visible; user can enable close_of_market_only only |
close_of_market_only |
| Pro+ | Feature visible; user can enable either variant | close_of_market_only or monthly |
Hide-don't-gray rule source: feedback_hide_dont_gray_unavailable_features.md — locked
2026-05-11 during #1449 RBAC audit. Free users do not see the feature exists.
8.2 Downgrade handling
Pro+ downgraded to Pro:
- Existing monthly-tracked rows become read-only. No deletion.
- UI label: "This position was tracked with your previous Pro+ plan. Tracking has been
preserved." No upgrade CTA on the row itself.
- New positions closed after downgrade receive close_of_market_only tracking only.
- Source pattern: architect v3 read-only-with-history pattern from PR #1636.
Pro downgraded to Free:
- All existing WCB rows become read-only with no row-level indication (feature is hidden).
- The data remains in closed_position_wcb_snapshots for the duration of the user's data
retention entitlement (90 days for the free tier). After 90 days, rows are subject to the
normal data-retention pruning job.
- If the user upgrades again before 90 days, their historical WCB rows re-surface.
8.3 Setting audit log
Every change to the user's tracking_horizon setting emits an audit event:
- event_type: wcb_setting_changed
- old_value, new_value, changed_at_utc, user_id
This satisfies the audit-log requirement in the issue acceptance criteria.
9. Out of Scope for V1
The following are explicitly excluded from V1:
-
Trade recommendations. This feature does not suggest the user should have held longer, exited earlier, or taken any action. It is a data surface only.
-
Future price projection. For positions where the tracking horizon has not yet been reached (in-progress EOM tracking), the feature shows actual past data up to the current date. The dashed sparkline extension (section 7.6) shows TIME dimension only — no extrapolated price.
-
"Would have been better" framing. The delta is a signed number. Display it with sign. Do not categorize positions as "good exits" or "bad exits." That framing is editorial and falls outside the retrospective-data scope.
-
Order execution path modification. This feature has zero coupling to the execution path. Deterministic execution (per
feedback_deterministic_execution_ai_augments.md) stays untouched. The WCB computation is a read-only analytical layer over closed position records. -
Intraday trajectory. Tracking is daily-bar granularity. Intraday sparklines would require real-time or historical intraday data at significantly higher cost. Out of scope for V1; revisit if Pro+ users request it.
-
Options contract-level tracking. V1 tracks the underlying only. See section 3.2 for V2 path.
-
Per-leg multi-leg breakdown. V1 shows composite only. See section 3.3.
-
What-if parameter adjustment. This feature does not allow "what if I had held but also sold a covered call on day 3?" type analysis. Pure hold-from-close is the V1 scope.
-
Benchmarking against an index. Showing "SPY did X% in the same window" is a separate feature concept (relative performance). Out of scope here.
10. Handoff Packet
Cards PM should file
Card A — Architect design
Title: arch: "what could've been" service design — DB schema, scheduled jobs, API shape (#1657)
Risk: Medium
Size: M (2-3 days architecture review)
What to include:
- Review and finalize closed_position_wcb_snapshots schema (section 4.2)
- Design the trajectory computation service: where does it live (new Celery task in Raptor?
dedicated microservice?), how does it interact with the existing
AlpacaMarketDataService
- Define the REST API shape: GET /api/positions/{id}/wcb returning snapshot + trajectory
- Design the cache invalidation contract (section 4.4)
- Define error states: Alpaca unavailable, corporate-actions data missing, position in
after-hours-close state
- Confirm Postgres migration path (Raptor is mid-migration to Postgres per
project_raptor_postgres_migration_decision.md; this schema must land post-migration)
Card B — Feature-developer implementation
Title: feat(raptor): closed position WCB snapshot — DB, job, API endpoint (#1657)
Risk: Medium
Size: L (5-7 days)
Depends on: Card A (architect design)
What to include:
- Alembic migration for closed_position_wcb_snapshots
- Row creation at position-close time (hook into existing position-close code path)
- EOD job and EOM job implementations (Celery beat tasks)
- GET /api/positions/{id}/wcb endpoint: returns { snapshot, trajectory, metadata }
- Tier gate: check user entitlement before creating rows and before returning data
- Setting endpoint: PATCH /api/account/settings/wcb for { tracking_horizon: ... }
- Audit log emission on setting change
- Unit tests: split math, dividend math, sign handling for long/short, after-hours edge case,
near-EOM edge case
- Integration test: full EOD job cycle with a synthetic closed position
Card C — Feature-developer (Antlers)
Title: feat(antlers): "what could've been" — sparkline + expandable row on trade history (#1657)
Risk: Low-Medium
Size: M (3-4 days)
Depends on: Card B (API endpoint must exist)
What to include: - Expandable row component on trade history table - Sparkline (Confidence Engine aesthetic; moss/ink palette; dashed time-extension for in-progress tracking) - Past-tense copy (coordinate with Kristerpher for final wording) - Obfuscate mode interaction (% only when active) - In-progress tracking state (partial sparkline + "Tracking through DATE" label) - Feature hidden entirely for free tier (no conditional display, no CTA, no empty state) - Pro vs Pro+ setting panel: show available tracking horizon options based on tier - Downgrade read-only state
Card D — UX-designer mockup
Title: design(ux): "what could've been" — trade history expandable row + settings panel (#1657)
Risk: Low
Size: S (1-2 days)
Does not depend on: implementation; can run in parallel with Cards A and B
What to include: - Mockup of the expanded trade history row with sparkline and delta display - Mockup of the account settings panel for the tracking horizon toggle - Obfuscate mode variant (% only) - In-progress tracking state visual - Empty state for free tier (nothing — feature is hidden; mockup may show a "feature does not exist" state to confirm the ux treatment is truly blank, not grayed) - Downgrade read-only variant
Recommended card sequence
- Card D (UX) + Card A (Architect) in parallel — both are non-blocking on each other
- Card B (Raptor backend) — after Card A design is locked
- Card C (Antlers frontend) — after Card B API is deployed to staging
Appendix: Open Questions
-
Alpaca corporate actions cadence. The Alpaca corporate actions endpoint provides dividend announcements with ex-date and cash-amount-per-share fields. Verify that the cadence of dividend data availability matches the EOD job schedule — specifically, that a dividend with ex-date = T is queryable on T. If there is a data lag, the EOD job may need a 1-day buffer before finalizing
dividends_paid_in_window. Assign to: feature-developer during Card B implementation. -
Alpaca
adjustment=splitvsadjustment=allfor V1. This brief recommendsadjustment=split(split in price, dividends explicit). Confirm with architect that the Alpaca API free tier (paper trading) supports this parameter; it is documented but behavior on paper credentials is not verified. -
Near-EOM positions: track to THIS month's end or skip? If a position is closed with 1 or 0 trading days remaining in the month, does the EOM variant produce meaningful signal? Recommendation in section 2.4 is to display the single-point result transparently. Kristerpher should confirm this is the preferred behavior.
-
Data retention for WCB rows on free-tier downgrade. Section 8.2 says rows are retained for 90 days after downgrade (matching free-tier history retention). Confirm this matches the data-retention policy in the billing/archive job.
-
Spinoffs. Alpaca corporate actions includes spinoff announcements. The V1 brief defers spinoffs (they would create a new position leg that is tracked separately). Flag as a V2 feature and add a note in the UI when a spinoff event is detected in the window: "A corporate spinoff occurred during this window. Tracking reflects the original security only."