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.
- IRS Publication 550 (2023) — Investment Income and Expenses:
https://www.irs.gov/publications/p550 - IRS Publication 544 (2023) — Sales and Other Dispositions of Assets:
https://www.irs.gov/publications/p544 - IRC §1091 — Wash Sale:
https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section1091 - IRC §1222 — Capital Gains and Losses (holding period definitions):
https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section1222 - IRC §1256 — Section 1256 contracts:
https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section1256 - IRC §1(h) — Maximum capital gains rates:
https://uscode.house.gov/view.xhtml?req=granuleid:USC-prelim-title26-section1 - IRS Rev. Rul. 2008-5 (options on same underlying = substantially identical):
https://www.irs.gov/pub/irs-drop/rr-08-05.pdf - IRS Rev. Rul. 2009-39 (ETF options ≠ index options for §1256):
https://www.irs.gov/pub/irs-drop/rr-09-39.pdf - IRS Form 6781 Instructions:
https://www.irs.gov/pub/irs-pdf/i6781.pdf - CBOE Regulatory designation table:
https://www.cboe.com/us/options/market_statistics/regulatory/ - Treasury Reg. §1.1012-1(c) — Adequate identification of securities:
https://www.law.cornell.edu/cfr/text/26/1.1012-1 - IRS Tax Topic 409:
https://www.irs.gov/taxtopics/tc409
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)
- Same CUSIP (preferred when available).
- Same ticker symbol (fallback).
- Equity options on the same underlying ticker (Rev. Rul. 2008-5).
- 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
- Raptor endpoint:
POST /api/v1/tax/wash-sale-check - Input: trade history array from user's portfolio
- Output:
WashSaleReportJSON - Antlers display: badge on each loss trade in trade history ("Wash Sale — $X disallowed"); aggregate in tax summary panel
- Data source: user's 1099-B feed via broker connection (Alpaca or BYOB)
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
- Raptor service: called internally by the gain/loss calculation pipeline; also exposed as a lightweight
GET /api/v1/tax/contract-type?symbol=SPX - Antlers display: indicator on trade detail view ("§1256 — 60/40 Treatment"); affects tax summary breakdown
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
- Raptor endpoint:
GET /api/v1/tax/holding-periods?as_of=YYYY-MM-DD - Output: per-lot holding status + days_to_long_term
- Antlers display: "Turns long-term in 23 days" badge on position card; optional 30-day "upcoming LT events" panel in tax summary
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
- Raptor endpoint:
GET /api/v1/tax/tlh-candidates?as_of=YYYY-MM-DD - Antlers display: "Tax Loss Opportunities" section in tax summary panel; each row shows symbol, unrealized loss, risk flag, and days to LT. No buy/sell action buttons — display only.
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
- Raptor: called internally when a user closes a position; computes realized G/L per the user's elected method
- User setting: accounting method preference stored in user profile (
FIFOdefault; user can switch toLIFOorSLID) - Antlers display: trade detail shows lot-level breakdown: which lots were consumed, gain/loss per lot, LT vs ST classification
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
- Raptor endpoint:
POST /api/v1/tax/after-tax-pl - Input: realized gains array + tax rate config
- Output:
AfterTaxReportJSON - Antlers display: tax summary panel shows gross P&L and estimated after-tax P&L side by side. Rates are entered in user settings. Prominent disclaimer text: "Estimated based on your entered rates. Not tax advice. Consult a qualified tax professional."
- Settings:
TaxRateConfigstored in user profile with fields for federal ordinary, federal LT, NIIT toggle, state ordinary, state LT
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) |
Top 3 features recommended for v1
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."