Raxx · internal docs

internal · gated

Customer Tax-Strategy Features — Research Brief

Date (UTC): 2026-05-20 Scope: US equities + equity options (v1). All times UTC. Status: Research complete. Reference implementations produced. Ready for feature-developer handoff. Branch: docs/customer-tax-strategy-features-2026-05-20


Purpose

This brief documents the data-science research, algorithm definitions, and reference implementations for six tax-aware algorithmic features. These are analytical tools that surface what happened on a user's own data. They are deterministic, rule-based, and fully retrospective — consistent with the Raxx execution model and product positioning.

None of these features execute trades, advise on trades, or project future outcomes.


Statutory references (consolidated)

All URLs must be rendered in code blocks per project style.


Feature 1 — §1091 Wash-Sale Detector

What it does

Given a user's trade history (buy/sell lots), identify all loss sales where a substantially identical security was purchased within 30 calendar days before or after the sale date. Output: a list of violations with the disallowed-loss dollar amount for each.

Math

The wash-sale window is [sale_date - 30 days, sale_date + 30 days] inclusive on both ends.

Disallowed loss calculation: - If replacement quantity >= loss-sale quantity: the entire loss is disallowed. - If replacement quantity < loss-sale quantity: disallowed = loss_amount × (replacement_qty / loss_qty). - The remaining portion of the loss IS deductible.

Basis adjustment for replacement shares: adjusted_basis_of_replacement = cost_basis_of_replacement + disallowed_loss_amount

Substantially identical definition (v1 scope)

  1. Same CUSIP (preferred when available).
  2. Same ticker symbol (fallback).
  3. Equity options on the same underlying ticker (Rev. Rul. 2008-5).
  4. Not covered in v1: economically similar but legally distinct instruments (inverse ETF pairs, swaps, convertible bonds). Flag for BLR team if users request this.

Edge cases documented

Case Treatment Source
§1256 contracts (SPX, /ES) Exempt — §1091 does not apply IRC §1091(e)
IRA/Roth replacement Loss permanently lost (cannot add to IRA basis) Rev. Rul. 2008-5
Spouse account aggregation Pass both spouses' trades as one set; detector is account-agnostic IRS Pub 550
Multiple replacement lots Prorated by quantity IRS Pub 550
Ticker change (same CUSIP) CUSIP matching takes priority Treasury Reg. §1.1012-1

Reference implementation

docs/data-science/reference/tax/wash_sale_detector.py - Input: list[TradeLot] (with account_type for IRA detection) - Output: WashSaleReport (violations + total_disallowed_loss) - 12 test fixtures covering all edge cases

Antlers / Raptor integration shape


Feature 2 — §1256 Contract Identifier

What it does

Given a trade's symbol (root or full OSI), determine whether it qualifies for §1256 treatment (60% long-term / 40% short-term) or is a regular equity option/stock. Used by the gain/loss calculator and backtest tax-awareness feature.

Classification algorithm

1. If symbol starts with "/" → Regulated Futures Contract → §1256 qualified
2. If root in QUALIFIED_INDEX_OPTION_ROOTS (SPX, NDX, RUT, VIX, etc.) → §1256 qualified
3. If root in NOT_QUALIFIED_ETF_ROOTS (SPY, QQQ, IWM, etc.) → NOT §1256 (equity ETF option)
4. If all-alpha ≤ 5 chars (heuristic for equity ticker) → NOT §1256
5. Otherwise → UNKNOWN; flag for manual review via Form 1099-B box 11

Critical distinction: SPX (index option) = §1256. SPY (ETF option) = NOT §1256. Source: Rev. Rul. 2009-39: https://www.irs.gov/pub/irs-drop/rr-09-39.pdf

Tax implications

Type LT% ST% Wash-Sale Mark-to-Market Loss Carryback
§1256 qualified 60% 40% No Yes (Dec 31) 3 years (§1212(c))
Regular equity option 0-100% based on hold 0-100% based on hold Yes No No

Reference implementation

docs/data-science/reference/tax/section1256_identifier.py - Input: symbol string (root or full OSI) - Output: InstrumentClassification (status, category, tax_split, wash_sale_applies, confidence) - 17 test scenarios covering SPX/SPY/NDX/QQQ/equity options/futures/unknown

Antlers / Raptor integration shape


Feature 3 — Holding Period Awareness

What it does

For each open lot in a user's portfolio, compute: 1. The date on which the lot qualifies for long-term capital gains treatment. 2. Days remaining until long-term qualification. 3. Whether the lot is currently short-term or long-term as of a given date. 4. Upcoming "turn long-term" events within N days.

Math

Per IRS Publication 550: - Start counting from the day AFTER the trade date (acquisition date is excluded). - "More than one year" = > 365 calendar days from the excluded start. - Long-term qualification date = acquisition_date + 1 day (skip) + 365 days.

Wash-sale tacking: when a replacement lot inherits a predecessor's holding period, subtract the predecessor's held days from the LT date: lt_date = (effective_acquisition_date + 365) - tacked_days.

Special acquisition methods

Method Holding Period Start Source
Purchase Day after trade date IRS Pub 550
DRIP reinvestment Day after reinvestment date (new clock) IRS Pub 550
Stock dividend Ex-dividend date IRS Pub 550
Stock split Original pre-split acquisition date (carries forward) IRS Pub 550
Wash-sale replacement Replacement acquisition date PLUS tacked predecessor days IRS Pub 550
Options exercise Day after exercise date (not option purchase date) IRS Pub 550
RSU/ESPP vest Vesting date (flag for HR/plan doc review) IRS Pub 525

Reference implementation

docs/data-science/reference/tax/holding_period.py - Input: list[TaxLot] + as_of_date - Output: LotHoldingReport per symbol; next_lt_events() for upcoming qualifications - 10 test scenarios including leap year, wash-sale tacking, splits, DRIP

Antlers / Raptor integration shape


Feature 4 — Tax-Loss Harvesting Candidate Scorer

What it does

At a point in time, identify open positions with unrealized losses and rank them as candidates for tax-loss harvesting, with wash-sale risk flags. This is a candidate identifier, not a recommendation engine. Raxx surfaces the data; the user decides.

Scoring algorithm

For each open lot: 1. Compute unrealized_loss = (current_price - cost_basis_per_share) * quantity. 2. If unrealized_loss >= 0: skip. 3. Assess wash-sale risk: - RECENT_PURCHASE: lot was bought within last 30 days. Selling it and re-buying within 30 days of the sale triggers a wash sale. - EXISTING_REPLACEMENT: another lot of the same security was bought in the last 30 days. Selling now would likely disallow the loss. - IRA_EXPOSURE: an IRA/Roth account holds the same security within the 30-day window — user must verify independently. 4. score = abs(unrealized_loss), with 10% soft penalty if wash-sale risk exists (still visible in list, lower rank). 5. Tiebreak 1: prefer lots with higher days_held (closer to or past LT). 6. Tiebreak 2: prefer larger quantity lots.

Output schema

{
  "symbol": "AAPL",
  "lot_id": "lot-123",
  "account_id": "acct-main",
  "unrealized_loss_usd": 2340.00,
  "unrealized_loss_pct": -12.5,
  "wash_sale_risk": "none",
  "wash_sale_risk_detail": "No wash-sale risk flags detected for this lot.",
  "days_held": 187,
  "days_to_long_term": 178,
  "score": 2340.00
}

Reference implementation

docs/data-science/reference/tax/tlh_candidate_scorer.py - Input: list[OpenPosition] + optional list[TradeLot] (recent trades for wash-sale lookback) - Output: TLHReport (ranked candidates + aggregate totals) - 8 test scenarios

Antlers / Raptor integration shape


Feature 5 — Tax-Lot Accounting Methods (FIFO / LIFO / SLID)

What it does

When a user closes a position (full or partial), compute the realized gain/loss per lot using their elected accounting method.

Methods

FIFO (First In, First Out): Default per IRS if no specific identification. Oldest lots consumed first. Favors long-term treatment when oldest lots are held > 1 year.

LIFO (Last In, First Out): Most recently acquired lots consumed first. Legal for securities if elected. May produce more short-term gains depending on recent activity.

SLID (Specific Lot Identification): Taxpayer designates exact lots. Requires "adequate identification" per Treasury Reg. §1.1012-1(c). In practice: broker must confirm the specific lot at the time of sale. Source: https://www.irs.gov/pub/irs-drop/rp-10-34.pdf

Math

For each lot consumed: - net_proceeds_for_lot = (qty_sold_from_lot / total_qty_sold) * (gross_proceeds - commission) - realized_gain_loss = net_proceeds_for_lot - (qty_sold_from_lot * cost_basis_per_share) - is_long_term = (sale_date - acquisition_date).days > 365

Short sales: always short-term (§1233), regardless of days held.

Edge cases

Case Treatment Source
Short sale Always short-term IRC §1233
Put exercise (holder) Proceeds adjusted by option premium; holding period of PUT irrelevant to stock IRS Pub 550
Call assignment (writer) Add call premium received to proceeds IRS Pub 550
Call exercise (holder) Basis = strike + option premium; holding period starts day after exercise IRS Pub 550
Partial lot close Engine handles fractional lot consumption automatically
Insufficient lots Raises ValueError with remaining quantity

Reference implementation

docs/data-science/reference/tax/lot_accounting.py - Input: list[Lot] (open inventory) + SaleInstruction + AccountingMethod - Output: RealizationReport (lines + totals + remaining_lots) - 10 test scenarios including commission proration, boundary LT days, short sales, SLID invalid lot

Antlers / Raptor integration shape


Feature 6 — Net-After-Tax P&L

What it does

Given a list of realized gains/losses (from Feature 5) and a user's self-reported marginal tax rates, compute the estimated net-after-tax P&L. Displays alongside gross P&L in trade history and the Backtesting Lab.

Important boundary: Raxx never tells users what their tax rate is. Users input their own rates. The module applies those rates mechanically. This keeps the tool-not-counsel boundary clean.

Math

Rate framework (parameterized, always verify with current-year IRS Rev. Proc.): - Short-term capital gains: federal_ordinary_rate + niit_if_applicable + state_rate - Long-term capital gains: federal_lt_rate + niit_if_applicable + state_lt_rate - §1256 (60/40 blend): 0.60 × lt_effective_rate + 0.40 × st_effective_rate - Wash-sale disallowed: no current-year tax effect (basis adjustment only)

NIIT: IRC §1411 adds 3.8% on net investment income for high earners. Fixed by statute.

Loss deductibility: - Short-term losses offset short-term gains first; excess offsets long-term gains. - Long-term losses offset long-term gains first; excess offsets short-term gains. - Net remaining capital loss deductible up to $3,000/year against ordinary income. - Excess carries forward (carryforward computation flagged in output notes; not computed).

Backtest integration (Epic #79 sub-card candidate)

def backtest_net_after_tax_return(
    gross_return: Decimal,
    gross_pnl: Decimal,
    realized_items: list[RealizedGainLossItem],
    config: TaxRateConfig,
) -> tuple[Decimal, Decimal]:
    ...

This function is the natural extension point for Backtesting Lab to add net-after-tax return alongside gross return metrics. Recommend creating a sub-card under Epic #79.

Reference implementation

docs/data-science/reference/tax/net_after_tax_pl.py - Input: list[RealizedGainLossItem] + TaxRateConfig (user-supplied rates) - Output: AfterTaxReport (per-line tax computation + aggregate totals + notes) - 9 test scenarios including NIIT, §1256 blended rate, wash-sale zero-effect, large-loss carryforward note

Antlers / Raptor integration shape


Backtest Tax-Awareness (Epic #79 Sub-Card)

The backtest_net_after_tax_return function in net_after_tax_pl.py is a ready-to-use integration point for Backtesting Lab. Suggested sub-card:

Title: "Backtesting Lab: add net-after-tax return metric" Parent epic: #79 (Backtesting Lab) Scope: After a backtest run completes: 1. Classify each trade in the backtest's trade list by gain type (ST/LT/§1256 using the identifier from Feature 2). 2. Apply user's saved TaxRateConfig to compute after_tax_return and after_tax_pnl. 3. Add after_tax_return and after_tax_pnl to the backtest results metrics table alongside gross_return. Data dependency: user must have saved a TaxRateConfig in settings; if not, show prompt to enter rates first.


Test fixtures

All test fixtures are in docs/data-science/reference/tax/fixtures/:

File Coverage
wash_sale_scenarios.json 12 wash-sale scenarios (pre-sale/post-sale/boundary/IRA/partial/CUSIP/§1256 exempt)
section1256_scenarios.json 20 §1256 classification scenarios (SPX/SPY/NDX/QQQ/futures/OSI parsing/unknown)
holding_period_scenarios.json 10 holding period scenarios (leap year/DRIP/splits/tacking/short sale)

Ranked by: smallest implementation surface + highest customer value.

Rank 1 — Feature 3: Holding Period Awareness

Why first: Zero regulatory ambiguity. The rule (day-after acquisition + 365 days) is deterministic and based on unambiguous statute. No user rate input required. Extremely high customer value: "Your AAPL position turns long-term in 23 days" is a clear, actionable signal. Small surface: one endpoint, one display widget. No external data dependency beyond existing lot inventory.

BLR sign-off needed: None. Pure date math on public statutory rule.

Rank 2 — Feature 5: Tax-Lot Accounting (FIFO / LIFO / SLID)

Why second: Every brokerage already does this; Raxx just needs to surface it with transparency. FIFO is the safe default (IRS default if nothing elected). SLID requires broker confirmation documentation note in the UI but adds no compliance risk to Raxx itself. This is infrastructure that all other tax features depend on. Medium implementation surface: accounting engine + user preference setting + lot-level trade detail view.

BLR sign-off needed: Light touch — SLID documentation requirement (adequate identification disclaimer in UI).

Rank 3 — Feature 2: §1256 Contract Identifier

Why third: Raxx already serves options traders. §1256 classification directly affects how their P&L is displayed in the tax summary. The classification logic is deterministic (lookup table + heuristic). Zero user input required. SPX/NDX/RUT traders will immediately notice the correct treatment. Small surface: lookup + classification badge on options trades.

BLR sign-off needed: Light touch — confirm that displaying "§1256 Treatment — 60/40" alongside a trade does not constitute tax advice under state RIA registration thresholds. The display is factual classification, not a recommendation to trade.


Notes for BLR team

The following features carry legal-posture questions that BLR should review before feature-developer begins implementation. None of these are blockers to development; they are disclosure and copy review items.

1. Net-After-Tax P&L display (Feature 6)

Question: Does displaying an "estimated after-tax P&L" figure based on user-entered rates constitute tax advice, financial planning advice, or investment advice under: - Investment Advisers Act §202(a)(11) - State RIA registration thresholds in states where Raxx users reside - IRS Circular 230 (if the product ever offers any tax-position guidance)

Raxx's current boundary: The figure is mechanical application of user-entered rates to user's own historical data. No rates are suggested or inferred. Disclaimer text required in UI.

Recommendation for BLR: Review the proposed disclaimer copy before it ships. Suggest treatment: "Estimated based on rates you entered. This is not tax advice. Consult a qualified tax professional for your actual tax liability."

2. "Substantially identical" securities for wash-sale (Feature 1)

Question: For instruments beyond same-ticker and equity options on same underlying (Rev. Rul. 2008-5), what constitutes substantially identical? Specifically: - Are inverse ETFs (SH vs SPY) substantially identical? - Are leveraged ETFs (UPRO vs SPY) substantially identical? - Are synthetic long positions (long call + short put same strike) substantially identical to the underlying?

Raxx's current position: v1 only matches same-ticker/CUSIP and equity options on same underlying. We are NOT making broader substantially-identical determinations. The UI should note "Raxx checks same security and options on the same underlying. Other similar positions may also be subject to wash-sale rules. Consult a tax professional."

Recommendation for BLR: Confirm that the v1 scope limitation (same-ticker + Rev. Rul. 2008-5 options) is stated clearly enough to avoid liability for false negatives.

3. §1256 classification confidence for exotic / novel instruments (Feature 2)

Question: For instruments with confidence: "medium" or "low" in the classifier output, what disclosure is appropriate?

Raxx's current position: UNKNOWN status triggers a user-visible flag: "We could not determine the tax treatment for this instrument. Check your broker's 1099-B (box 11) or consult a tax professional."

Recommendation for BLR: Confirm that the UNKNOWN-status disclosure language is sufficient and that Raxx is not implicitly classifying by silence.

4. Tax-loss harvesting candidate scorer (Feature 4)

Question: Does surfacing a ranked list of "harvesting candidates" cross the line from data display into investment advice, particularly if users act on the ranked list?

Raxx's current position: The scorer is purely analytical — it shows positions with unrealized losses and wash-sale risk status. No action buttons. No explicit "harvest this" language. Per Raxx product thesis: the user decides, Raxx enforces structure.

Recommendation for BLR: Review the proposed UI copy for the TLH panel. Avoid language like "candidates for harvesting" or "opportunities" if it reads as a recommendation. Prefer "positions with unrealized losses" + "wash-sale status."


Handoff packet — feature-developer

Files produced

File Purpose
docs/data-science/reference/tax/wash_sale_detector.py §1091 wash-sale detection
docs/data-science/reference/tax/section1256_identifier.py §1256 contract classification
docs/data-science/reference/tax/holding_period.py Holding period + LT date calculation
docs/data-science/reference/tax/tlh_candidate_scorer.py TLH candidate ranking
docs/data-science/reference/tax/lot_accounting.py FIFO / LIFO / SLID accounting
docs/data-science/reference/tax/net_after_tax_pl.py Net-after-tax P&L + backtest integration
docs/data-science/reference/tax/fixtures/wash_sale_scenarios.json 12 wash-sale test scenarios
docs/data-science/reference/tax/fixtures/section1256_scenarios.json 20 §1256 scenarios
docs/data-science/reference/tax/fixtures/holding_period_scenarios.json 10 holding period scenarios

Input data requirements (Raptor service)

All features consume data from the user's connected broker (Alpaca / BYOB). The minimum data fields required per lot:

{
  "lot_id": "string (unique)",
  "account_id": "string",
  "account_type": "taxable | ira | roth_ira | hsa | unknown",
  "ticker": "string (root symbol)",
  "cusip": "string | null",
  "asset_type": "equity | equity_option | index_option | futures | other",
  "action": "buy | sell | short | cover",
  "trade_date": "YYYY-MM-DD (UTC date)",
  "quantity": "decimal",
  "proceeds": "decimal | null (sell lots)",
  "cost_basis": "decimal | null (adjusted)"
}

Execution model notes

These algorithms are deterministic, not ML-based. Each function is pure (no side effects; no external API calls). They can be deployed as synchronous Raptor service calls with sub-millisecond latency for typical portfolio sizes (< 1,000 lots). For very large portfolios (> 10,000 lots), the wash-sale detector's O(n²) scan should be replaced with an interval-tree or hash-indexed implementation — but that is outside v1 scope.

Missing data fallbacks

Missing data Behavior
No CUSIP Fall back to ticker matching
No cost_basis Lot is excluded from gain/loss calculations; flagged in output
No account_type Default to unknown; IRA cross-account flags will not fire
No current_price TLH scorer cannot compute unrealized P&L; lot excluded with note

Disclaimer text (required in all UI surfaces)

The following disclaimer text must appear on every tax-feature UI surface. Legal copy to be reviewed by BLR before shipping:

"Tax information is estimated based on your historical trade data and the rates you have entered. This is not tax advice. Raxx is not a tax advisor. Please consult a qualified tax professional for your actual tax obligations."