66 min readIntelligence & insights

Implementation Guide: Identify tax-loss harvesting opportunities across client accounts seasonally

Step-by-step implementation guide for deploying AI to identify tax-loss harvesting opportunities across client accounts seasonally for Financial Advisory clients.

Hardware Procurement

Advisor Workstation

Advisor Workstation

DellOptiPlex 7020 Micro (i5-14500T, 16GB RAM, 512GB SSD)Qty: 5

$1,100 per unit MSP cost / $1,450 suggested resale

Primary advisor workstation for accessing TLH dashboards, portfolio management platforms, and CRM. 16GB RAM required for multi-tab SaaS workflows across Orion, Redtail, Holistiplan, and custodial platforms simultaneously.

Dual Monitor Setup

Dell U2724D 27-inch QHD USB-C Monitor

DellDell U2724D 27-inch QHD USB-C MonitorQty: 10

$380 per unit MSP cost / $480 suggested resale

Dual-screen setup per advisor is critical for side-by-side portfolio review and TLH opportunity dashboard viewing. QHD resolution ensures dense financial data tables are readable. USB-C daisy-chaining simplifies cable management.

Next-Generation Firewall

Next-Generation Firewall

FortinetFortiGate 40F (FG-40F)Qty: 1

$500 MSP cost (hardware) + $350/yr FortiGuard UTM Bundle / $1,200 suggested resale + $600/yr managed firewall service

Financial-grade perimeter security with IDS/IPS, web filtering, application control, and SSL inspection. Required for SEC Regulation S-P compliance and to protect custodial API traffic. Supports up to 25 users with full UTM inspection.

Managed Network Switch

Managed Network Switch

UbiquitiUniFi Switch USW-24-PoEQty: 1

$400 MSP cost / $600 suggested resale

Managed PoE switch for office network segmentation. Enables VLAN separation between advisor workstations and guest/IoT networks, supporting compliance requirements for data isolation.

Wireless Access Point

Wireless Access Point

UbiquitiUniFi U7 Pro (Wi-Fi 7)Qty: 2

$190 per unit MSP cost / $280 suggested resale

Enterprise-grade wireless for advisor laptops and mobile devices. Supports WPA3 Enterprise authentication for compliance. Two APs provide redundancy and coverage for typical advisory office layout.

UPS Battery Backup

APC Smart-UPS SMT1500C (1500VA LCD)

APCSMT1500CQty: 1

$550 MSP cost / $750 suggested resale

Power protection for firewall and network switch to maintain connectivity during brief outages. Critical for trade execution continuity when TLH orders are being submitted to custodians.

Software Procurement

Orion Portfolio Solutions (Portfolio Management + Trading + Custom Indexing)

Orion Advisor SolutionsSaaS - AUM-based platform fee

Platform fee: 10–40 bps on AUM under Custom Indexing; base portfolio management starts at ~$1,200–$2,500/mo for firms under $300M AUM. Quote-based.

Core TLH engine. Orion Custom Indexing provides year-round automated tax-loss harvesting with direct indexing capability. Includes daily portfolio scanning, automated substitute security selection, wash-sale rule monitoring, and batch trade execution. Also serves as the primary portfolio management and reporting platform. Ranked #1 in market share for Portfolio Management/Reporting and Trading/Rebalancing in the 2025 T3 Advisor Software Survey.

Redtail CRM

Redtail TechnologyPer-seat SaaSQty: 5-advisor firm

$39/user/month (Launch, billed annually) or $59/user/month (Growth, billed annually). Estimated $195–$295/month for 5-advisor firm.

#1 market share CRM for financial advisors. Provides household-level data structures essential for multi-account TLH coordination, activity logging for compliance audit trails, and deep integrations with Orion, Schwab, and Fidelity. Redtail Imaging add-on provides document management for trade confirmations and TLH reports. Redtail Speak provides compliant text messaging archival.

Holistiplan

HolistiplanSaaS - monthly subscription with household tiers

Starting at $160/month; scales with household count. Typical firm: $160–$350/month.

AI-powered tax return analysis tool. OCR engine ingests client 1040s in 45 seconds, automatically identifying marginal tax rates, capital gain exposure, and specific TLH opportunities. Critical for determining WHICH clients benefit most from harvesting and for sizing the tax impact of proposed harvesting trades. Used in Q4 pre-year-end planning and during spring tax return review season.

BlackRock Tax Evaluator

BlackRockTax Evaluator

Free - no licensing cost

Complementary screening tool used by 7,000+ advisors. Monitors capital gains estimates across 7,000+ mutual funds and ETFs. Advisors use this to identify funds in client portfolios likely to distribute large capital gains, creating urgency for proactive TLH before distribution dates.

CrowdStrike Falcon Go

CrowdStrikePer-endpoint SaaS

$5–$8/endpoint/month MSP cost / $12–$18/endpoint/month managed service resale. ~$60–$90/month for 10 endpoints.

Next-generation endpoint detection and response (EDR) for all advisor workstations. Required for SEC Regulation S-P compliance—written incident-response program must include endpoint monitoring. Falcon's single lightweight agent provides antivirus, EDR, and threat intelligence without performance degradation on advisor workstations.

Smarsh Enterprise Archive

SmarshPer-user SaaSQty: 5 users

$15–$30/user/month depending on channels archived. ~$75–$150/month for 5 users.

SEC/FINRA-mandated email and electronic communications archival. Captures, indexes, and retains all advisor email, text messages, and social media communications for 7+ years per SEC Rule 204-2. Essential for documenting TLH-related client communications and trade rationale.

Duo Security MFA

Cisco (Duo)Per-user SaaS

Duo Essentials: $3/user/month. Duo Advantage: $6/user/month. ~$30/month for 5-user firm on Advantage tier.

Multi-factor authentication for all platforms. All wealthtech vendors (Orion, Schwab, Fidelity) mandate MFA. Duo provides a unified MFA layer across custodial platforms, Orion, Redtail, Holistiplan, and Microsoft 365, with policy-based adaptive authentication and device trust.

Datto SIRIS (Backup & DR)

Kaseya / DattoAppliance + SaaS subscription

$300–$600/month depending on data volume and appliance tier. Includes cloud retention.

Hybrid cloud backup and disaster recovery with AES-256 encryption and 7-year retention capability per SEC recordkeeping rules. Protects local data, Microsoft 365 data, and configuration backups. Enables rapid recovery if ransomware or other incident impacts the advisory firm.

Proofpoint Essentials

ProofpointPer-user SaaSQty: Per user

$3–$5/user/month MSP cost / $8–$12/user/month managed resale. ~$40–$60/month for 5 users.

Advanced email threat protection. Phishing is the #1 attack vector targeting financial advisory firms. Proofpoint filters malicious emails before they reach advisor inboxes, with targeted attack protection (TAP) that identifies spear-phishing attempts using financial services-specific threat intelligence.

Prerequisites

  • Active custodial relationship with at least one major custodian (Charles Schwab Advisor Services, Fidelity Institutional, or Pershing/BNY Mellon) with API or data-feed access enabled and custodial paperwork completed for third-party platform access.
  • Existing client base with taxable brokerage accounts (TLH is only applicable to taxable accounts, NOT tax-deferred IRAs/401(k)s). Minimum viable: 50+ taxable accounts across the client base to justify platform investment.
  • Firm must have a current Form ADV Part 2A (brochure) on file with SEC/state regulators. The ADV will need to be amended to disclose the use of automated TLH tools and any associated fees.
  • Designated Chief Compliance Officer (CCO) or outsourced compliance firm (e.g., RIA in a Box, XY Compliance) available to review and approve supervisory procedures for automated trading.
  • Investment Policy Statements (IPS) documented for all client accounts that will participate in TLH. The IPS must permit tax-aware trading and define acceptable substitute securities.
  • Microsoft 365 Business Premium or equivalent email/productivity suite deployed with MFA already enforced at the tenant level.
  • Minimum 50 Mbps symmetrical internet connection at primary office location. Dual ISP strongly recommended for trade execution continuity. Connection must support sub-100ms latency to major cloud providers.
  • All advisor workstations running Windows 11 Pro (23H2+) or macOS Sonoma 14.x+ with current browser versions (Chrome 120+, Edge 120+, Safari 17+).
  • Domain Name System (DNS) filtering configured (e.g., Cisco Umbrella, DNSFilter) to block malicious domains—standard MSP security baseline.
  • Written Information Security Policy (WISP) documented per SEC Regulation S-P requirements, or commitment to creating one during this implementation. Smaller RIAs must comply by June 3, 2026.
  • Client tax returns (Form 1040) available in digital format (PDF) for at least the most recent tax year for Holistiplan ingestion.
  • Firm principal has approved budget allocation of approximately $2,500–$6,000/month for software licensing and managed services (excluding any AUM-based platform fees).

Installation Steps

...

Step 1: Conduct Technology Environment Assessment & Gap Analysis

Before any software procurement, the MSP technician must perform a thorough assessment of the advisory firm's current technology stack, data flows, and compliance posture. This involves inventorying all existing software (custodial platforms, CRM, portfolio management, financial planning tools), documenting current data integration points, identifying gaps in security infrastructure, and cataloging the client base composition (number of taxable vs. tax-deferred accounts, AUM distribution, existing TLH activity). This assessment determines the exact configuration path and identifies any prerequisite gaps.

1
Document existing software stack inventory using RMM tool (e.g., Datto RMM, ConnectWise Automate) to audit installed applications
2
Verify internet speeds at all advisor locations by running a speed test from each workstation and documenting results
3
Check current firewall model and firmware version
4
Verify browser versions meet minimums
Audit installed software, verify internet speeds, and check browser versions
powershell
Get-WmiObject -Class Win32_Product | Select-Object Name, Version | Export-Csv C:\Audit\InstalledSoftware.csv
Invoke-WebRequest -Uri 'https://speed.cloudflare.com' -UseBasicParsing | Out-Null
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe').'(default)' | ForEach-Object { & $_ --version }
Note

Schedule this as a 2-4 hour on-site visit with the firm principal AND the CCO (or outsourced compliance contact). Capture not just technical data but also the firm's TLH philosophy: Do they currently harvest manually? What thresholds do they use? What substitute securities do they prefer? This context is critical for platform configuration in later steps.

Step 2: Procure and Deploy Security Infrastructure

Before connecting any financial data platforms, the MSP must establish a compliance-grade security foundation. This step involves deploying the FortiGate 40F firewall, configuring network segmentation, installing CrowdStrike Falcon on all endpoints, enforcing Duo MFA across all platforms, deploying Proofpoint email security, and configuring Smarsh archival. This infrastructure is not optional—SEC Regulation S-P amendments require a written incident-response program, and all wealthtech vendors mandate MFA.

1
FortiGate 40F Initial Setup - connect to mgmt port at 192.168.1.99
2
Access via https://192.168.1.99 with default credentials admin/(blank)
3
Configure WAN interfaces for dual ISP if applicable
Configure WAN interface on FortiGate 40F
shell
config system interface
  edit port1
    set mode dhcp
    set allowaccess ping https ssh
    set alias WAN1
  next
end
1
Create VLAN for advisor workstations
Create VLAN 10 for advisor workstation network segment
shell
config system interface
  edit vlan10
    set vdom root
    set ip 10.10.10.1 255.255.255.0
    set allowaccess ping https
    set interface port3
    set vlanid 10
    set alias ADVISOR_LAN
  next
end
1
Enable FortiGuard UTM subscription services
Enable FortiGuard UTM subscription services
shell
config system fortiguard
  set port 443
  set protocol https
end
1
Configure IPS (Intrusion Prevention)
Configure financial-grade IPS sensor policy
shell
config ips sensor
  edit financial-grade
    config entries
      edit 1
        set status enable
        set log-packet enable
        set severity critical high medium
      next
    end
  next
end
1
CrowdStrike Falcon deployment via RMM — download sensor installer from Falcon console > Host Setup > Sensor Downloads
2
Deploy via RMM tool push or GPO using the command below
Silent CrowdStrike Falcon sensor deployment via RMM or GPO
shell
msiexec /i CrowdStrike-Falcon-Sensor.msi /quiet CID=<YOUR_CUSTOMER_ID> ProvNoWait=1
1
Duo MFA — Configure via Duo Admin Panel by creating application integrations for each protected platform: Microsoft 365 (Azure AD conditional access), Orion Advisor (SAML SSO integration), Redtail CRM, Schwab Advisor Center, Fidelity Wealthscape
1
Smarsh connector for Microsoft 365 — In Exchange Admin Center > Mail Flow > Rules, create a journal rule forwarding to the Smarsh capture address, BCC-ing all sent/received mail to the address below
Smarsh capture address for Microsoft 365 journal rule
text
capture@<firmname>.smarsh.cloud
Note

Complete this step BEFORE any wealthtech platform deployment. Document all configurations in the firm's WISP (Written Information Security Policy). Take screenshots of every firewall rule and security policy for compliance audit evidence. The FortiGate 40F supports up to 5 Gbps firewall throughput which is more than adequate for a small advisory office. If the firm has more than 25 users, step up to the FortiGate 60F.

Step 3: Deploy and Configure Orion Portfolio Platform with Custom Indexing

This is the core platform deployment. Work with Orion's onboarding team to establish the firm's Orion instance, connect custodial data feeds, configure the Custom Indexing module for automated TLH, and set up model portfolios with designated substitute securities. Orion's onboarding typically takes 4-6 weeks and includes dedicated implementation support. The MSP's role is to facilitate data connectivity, configure SSO/MFA, manage user provisioning, and ensure proper integration with other systems.

1
Initiate Orion onboarding — contact Orion sales/implementation. Request: Orion Portfolio + Orion Trading + Orion Custom Indexing bundle. Provide: Firm CRD number, custodian(s), estimated AUM, user count.
2
Custodial data feed activation — For Schwab: Submit Schwab OpenView Gateway API authorization. Log into Schwab Advisor Center > Technology Access. Authorize Orion as a third-party technology provider. Provide Orion's Schwab integration credentials.
3
Custodial data feed activation — For Fidelity: Submit Fidelity Wealthscape Integration Request. Contact Fidelity Integration Team via advisor.fidelity.com. Complete third-party authorization form for Orion. Data feed typically activates within 5-10 business days.
4
Configure SSO with Azure AD / Entra ID for Orion. In Azure AD Admin > Enterprise Applications > New Application. Search 'Orion Advisor' or configure SAML manually.
5
Configure Duo MFA policy for Orion SSO. In Duo Admin Panel > Applications > Add Application > Generic SAML. Configure Duo policy: Require MFA on every login. Set enrollment policy: Require Duo Mobile + biometric.
6
User provisioning in Orion — Create user accounts for each advisor with role-based permissions.
Orion SAML SSO endpoint and claim mapping reference
text
# Orion SAML SSO Configuration
SAML SSO URL: https://login.orion.com/sso/saml
Entity ID: Provided by Orion during onboarding
Claim mappings: email, first_name, last_name, advisor_id
  • Senior Advisor: Full portfolio access + trade approval
  • Junior Advisor: View-only on TLH recommendations, no trade execution
  • Operations/Admin: Account maintenance, reporting, no trading
  • CCO: Read-only access to all accounts + audit log access
Note

The Orion onboarding team will handle most of the platform configuration including model portfolio setup and Custom Indexing configuration. The MSP's primary value-add is ensuring the infrastructure layer (SSO, MFA, network access, data feed connectivity) is bulletproof. Schedule weekly check-in calls between MSP, firm principal, and Orion onboarding specialist. Expect 4-8 weeks for full data feed reconciliation—initial feeds often have cost-basis discrepancies that must be resolved with the custodian.

Step 4: Configure TLH Engine Parameters in Orion Custom Indexing

Once custodial data feeds are flowing into Orion, configure the tax-loss harvesting engine parameters. This includes setting minimum loss thresholds, defining substitute security mapping tables, configuring wash-sale rule monitoring across all household accounts (including IRAs and 401(k)s), setting seasonal scanning schedules, and defining approval workflows. This step requires close collaboration with the firm's lead advisor and CCO to ensure parameters align with the firm's investment philosophy and compliance requirements.

1. Minimum Loss Threshold

1
Navigate: Orion > Custom Indexing > Settings > Tax Management
2
Set minimum loss for harvesting trigger: typically $100–$500 per lot
3
Set minimum holding period before harvesting: 31 days (wash-sale safe)
4
Set maximum short-term gain tolerance: $0 (avoid creating ST gains)

2. Substitute Security Mapping Table

For each primary holding, define 1–2 acceptable substitutes. These are firm-specific and require advisor approval.

Example substitute security mappings (firm-specific, requires advisor approval)
text
SPY (S&P 500 ETF)      -> IVV or VOO
QQQ (Nasdaq 100)       -> QQQM or VGT
VTI (Total Market)     -> ITOT or SCHB
VXUS (Intl Developed)  -> IXUS or SPDW
BND (Total Bond)       -> AGG or SCHZ
VNQ (Real Estate)      -> SCHH or IYR

3. Wash-Sale Rule Configuration

1
Navigate: Settings > Wash Sale > Monitoring Scope
2
Enable cross-account monitoring within household — must include ALL accounts: taxable, IRA, Roth IRA, 401(k)
3
Enable spouse/partner account monitoring
4
Set lookback window: 30 days before AND after sale
5
Set alert: Flag any potential wash-sale violation BEFORE execution

4. Harvesting Schedule

  • Configure daily background scan: runs at 6:00 AM ET before market open
  • Q4 Year-End Push: Oct 15 – Dec 15 (enhanced daily scanning)
  • Post-Distribution: Late December (harvest after fund distributions)
  • Q1 New Year: January (harvest January effect volatility)
  • Market Corrections: Auto-trigger when S&P 500 drops >5% in 30 days

5. Approval Workflow

1
Set approval mode: 'Advisor Approval Required' (not fully automatic)
2
TLH engine generates recommendations → advisor reviews → approves batch
3
CCO receives weekly summary of all executed TLH trades
Note

NEVER set the system to fully automatic execution without advisor review during the initial deployment. Start with 'recommendation + approval' mode for the first 90 days. After the firm is comfortable with the recommendations quality, they can optionally move to auto-execute with post-trade review. Document every parameter setting in a formal 'TLH Policy & Procedures' document that the CCO must sign off on. This document is the firm's primary defense in any SEC examination related to automated trading.

Step 5: Deploy and Configure Redtail CRM with Orion Integration

Deploy Redtail CRM as the firm's client relationship management hub with deep integration to Orion. Redtail's household data structures are essential for mapping multi-account relationships (e.g., John's taxable account + Jane's IRA + Joint account = one household for wash-sale monitoring). Configure bi-directional sync so that TLH activity in Orion automatically creates activity records in Redtail for compliance documentation.

Step 5a: Provision Redtail Accounts

1
Navigate to admin.redtailtechnology.com
2
Create firm account with Growth plan ($59/user/month for full integrations)
3
Provision user accounts for each advisor and operations staff
4
Configure role-based permissions: Advisor: Full client access, activity logging | Admin/Ops: Full access except sensitive compliance notes | CCO: Full read access across all advisors' clients

Step 5b: Enable Orion ↔ Redtail Integration

1
In Redtail: navigate to Settings > Integrations > Portfolio Management > Orion
2
Enter Orion API credentials (provided by Orion onboarding)
3
Enable bi-directional sync: Contacts/Households: Redtail -> Orion (CRM is source of truth) | Portfolio data: Orion -> Redtail (Orion is source of truth) | Activities: Orion -> Redtail (TLH trades create activity records)
4
Set sync frequency: Every 15 minutes

Step 5c: Configure Household Structures

1
Create Household record in Redtail
2
Link all individual contacts (spouses, dependents)
3
Link ALL accounts: taxable, IRA, Roth IRA, 401(k), 529
4
Tag household with 'TLH Eligible' category if taxable accounts exist
5
Record client's marginal tax rate (from Holistiplan analysis)
6
Record client's capital gain carryforward if applicable

Step 5d: Create TLH-Specific Workflow in Redtail

1
Navigate to Settings > Workflows > Create New
2
Set workflow name: 'Quarterly TLH Review Cycle'
3
Add workflow step 1: Generate TLH opportunity report from Orion (Assign: Ops)
4
Add workflow step 2: Review opportunities with advisor (Assign: Lead Advisor)
5
Add workflow step 3: Client communication if losses exceed $X threshold (Assign: Advisor)
6
Add workflow step 4: Execute approved TLH trades (Assign: Trader/Ops)
7
Add workflow step 5: Document trade rationale and compliance notes (Assign: Ops)
8
Add workflow step 6: CCO review of batch trades (Assign: CCO)
9
Add workflow step 7: File trade confirmations in Redtail Imaging (Assign: Ops)
10
Set quarterly triggers: March 15, June 15, September 15, November 1
Note

Household structure accuracy is THE most critical data quality factor in this entire project. If households are incorrectly mapped, wash-sale violations become likely. Dedicate an entire day to household audit and cleanup in Redtail before activating Orion TLH scanning. Use Redtail's 'Household Report' to verify every linked account. Cross-reference with custodial account listings.

Step 6: Deploy Holistiplan for Tax Return Intelligence

Install and configure Holistiplan to analyze client tax returns and extract marginal tax rates, capital gains/loss carryforwards, AMT exposure, and other data points that inform TLH decision-making. Holistiplan's OCR engine processes a 1040 in approximately 45 seconds, creating a structured tax analysis that feeds into the TLH opportunity sizing process.

Step 6a: Provision Holistiplan Subscription

1
Navigate to holistiplan.com > Sign Up
2
Select plan based on household count
3
Provision user accounts for each advisor
4
Enable MFA via Duo integration (SAML/OAuth if available, otherwise Holistiplan native MFA)

Step 6b: Bulk Upload Client Tax Returns

1
Prepare: Collect most recent Form 1040 (+ schedules) for all households
2
Supported format: PDF (scanned or digital)
3
Navigate: Holistiplan dashboard > Upload Tax Return
4
Batch upload up to 50 returns at a time
5
OCR processing: ~45 seconds per return
6
Review AI-generated analysis for each client: Marginal ordinary income tax rate (federal + state), Long-term capital gains tax rate, Short-term capital gains tax rate, Net capital loss carryforward from prior years, AMT exposure flag, State-specific considerations

Step 6c: Export Tax Intelligence for TLH Decision Support

1
Navigate to Holistiplan > Reports > Tax Summary Export
2
Export CSV with columns: Client Name, SSN-last4, Marginal Rate, LTCG Rate, STCG Rate, Capital Loss Carryforward, AMT Flag
3
Use this data to populate client records in Redtail with custom fields: 'Marginal Tax Rate' from Holistiplan, 'LTCG Tax Rate' from Holistiplan, 'Capital Loss Carryforward' from Holistiplan, 'TLH Priority Score' calculated (see AI components)

Step 6d: Configure Seasonal Workflow Triggers

1
Holistiplan should be re-run annually when new tax returns are available
2
Typical timeline: April–May (after tax filing deadline)
3
Set Redtail workflow reminder: 'Annual Holistiplan Tax Return Update'
4
Set trigger date: April 20 each year
Note

Holistiplan offers discounts for members of NAPFA, ACP, Garrett Planning Network, and Dynasty Financial Partners. Ask the advisory firm about any affiliations before purchasing. The tax data extracted here is the intelligence layer that transforms basic TLH from a blunt instrument into a precision tool—a client in the 37% marginal bracket benefits far more from TLH than one in the 12% bracket. This prioritization is key to advisor efficiency.

Step 7: Configure BlackRock Tax Evaluator Integration

Set up BlackRock Tax Evaluator as a complementary free screening tool for capital gains distribution monitoring. This tool monitors 7,000+ mutual funds and ETFs for upcoming capital gains distributions, enabling proactive TLH before distribution dates create unexpected tax events for clients.

Step 7a: Access BlackRock Tax Evaluator

1
Navigate to: https://www.blackrock.com/us/financial-professionals/tools/tax-evaluator
2
No account required for basic access; recommend creating a BlackRock Advisor Center account for personalized watchlist functionality

Step 7b: Create Client Portfolio Watchlists

1
For each client household with mutual fund holdings, enter fund tickers held in client portfolios
2
Tax Evaluator shows estimated capital gain distribution amounts and dates
3
Flag funds with large expected distributions (>3% of NAV)

Step 7c: Set Calendar Reminders for Key Distribution Seasons

Most mutual fund capital gains distributions occur in November–December. Create a recurring Redtail workflow task: 'BlackRock Tax Evaluator Review' — triggered October 1 annually.

1
Pull current mutual fund holdings report from Orion
2
Screen all funds through Tax Evaluator
3
Flag clients with significant expected distributions
4
Prioritize TLH harvesting to offset anticipated distributions
5
Document findings in Redtail client notes

Step 7d: Bookmark and Train Advisors

1
Add Tax Evaluator to browser bookmark bar on all advisor workstations
2
Create quick-reference guide: 'How to Screen for Capital Gains Distributions'
Note

BlackRock Tax Evaluator is a free tool with no licensing cost—it is essentially a marketing tool that BlackRock provides to drive advisor engagement. However, it provides genuinely valuable data that is not easily obtained elsewhere. The key insight: if a client holds a mutual fund that is about to distribute a 10% capital gain, it may be better to sell BEFORE the distribution (crystallizing any gain at that point) rather than receiving the distribution and having a larger taxable event. This is a nuanced decision that the advisor must make, but the MSP's role is ensuring the screening workflow is in place.

Step 8: Build Custom TLH Opportunity Dashboard and Alerting System

Create a custom dashboard and alerting pipeline that aggregates data from Orion, Holistiplan, and BlackRock Tax Evaluator into a unified TLH opportunity view. This leverages Orion's API and custom reporting capabilities combined with automated email/Slack alerting to ensure advisors never miss a harvesting window. See the custom_ai_components section for detailed implementation specifications.

Step 8a: Enable Orion API Access for Custom Reporting

1
In Orion Admin > API Management > Create API Key
2
Set scope: Read-only access to Portfolios, Holdings, Tax Lots, Gains/Losses
3
Store API key securely in MSP password manager (e.g., IT Glue, Hudu)

Step 8b: Deploy Custom TLH Scoring and Alerting Script

1
See custom_ai_components section for full implementation
2
Choose deployment target: Azure Functions (serverless) or scheduled task on MSP RMM

Option A: Azure Functions Deployment

Provision Azure resource group, storage account, and Function App for TLH scanner
bash
az login
az group create --name rg-tlh-advisor --location eastus
az storage account create --name stgtlhadvisor --resource-group rg-tlh-advisor --sku Standard_LRS
az functionapp create --name func-tlh-scanner --resource-group rg-tlh-advisor --storage-account stgtlhadvisor --consumption-plan-location eastus --runtime python --runtime-version 3.11 --functions-version 4
Deploy TLH scanner function code to Azure Functions
bash
cd tlh-scanner-function
func azure functionapp publish func-tlh-scanner

Option B: PowerShell Scheduled Task via RMM

1
Deploy Python script to MSP management server
2
Schedule via Windows Task Scheduler or RMM tool with the following triggers: Daily scan at 6:30 AM ET (before market open), Weekly summary on Monday at 7:00 AM ET, Market correction trigger: Check S&P 500 daily at 4:30 PM ET

Step 8c: Configure Email Alerting

  • Use SendGrid or Microsoft Graph API for email delivery
  • Alert recipients: Lead advisor + operations
  • Alert type — Daily: 'New TLH Opportunities Found' (only sent when opportunities exist)
  • Alert type — Weekly: 'TLH Weekly Summary' (always sent Monday AM)
  • Alert type — Urgent: 'Market Correction TLH Alert' (triggered by >5% decline)
  • Alert type — Seasonal: 'Q4 Year-End TLH Push' (sent daily October 15 – December 15)
Note

The custom dashboard is where the MSP adds differentiated value beyond what any single vendor provides out-of-the-box. By aggregating data from multiple sources and applying the TLH Priority Scoring algorithm (defined in custom AI components), the MSP creates a unified intelligence layer that would cost the advisory firm significantly more to build internally. This is also the MSP's most defensible recurring revenue—the firm becomes dependent on this integrated view.

Step 9: Implement Compliance Documentation and Audit Trail Framework

Configure the complete compliance documentation framework required for automated TLH. This includes trade documentation templates, supervisory review procedures, wash-sale violation alerting, and SEC examination readiness materials. This step is absolutely essential—the 2024 SEC enforcement action against a robo-advisor for TLH wash-sale failures demonstrates that automated TLH without proper controls is a significant regulatory risk.

Step 9a: Create TLH Trade Documentation Template in Redtail

1
Navigate: Redtail > Settings > Note Templates > Create New
2
Template name: 'TLH Trade Documentation'
3
Add template fields: Date of harvest; Account number(s) affected; Security sold (ticker, CUSIP, quantity, cost basis, sale price); Loss harvested (short-term vs. long-term, dollar amount); Replacement security purchased (ticker, quantity, purchase price); Wash-sale check result: PASS / FLAG (with explanation); Rationale: Why this harvest benefits the client; IPS alignment: Confirm replacement security fits client's IPS; Advisor approval: Name and timestamp; CCO review: Name, timestamp, and notes

Step 9b: Configure Orion Audit Logging

1
Navigate to Orion Admin > Compliance > Audit Trail Settings
2
Enable: Log all trade proposals, approvals, modifications, and executions
3
Set retention period to 7 years (exceeds SEC 5-year minimum)
4
Set export format to CSV for archival in Smarsh/document management

Step 9c: Create CCO Review Dashboard in Orion

1
Navigate: Orion > Reporting > Custom Reports > Create
2
Name the report: 'Weekly TLH Activity Summary for CCO'
3
Include in report: All TLH trades executed in trailing 7 days; Any wash-sale flags triggered (resolved or unresolved); Total losses harvested by client/household; Substitute securities used (verify diversification maintained); Any overrides of system recommendations (with advisor notes)
4
Schedule: Auto-generate and email to CCO every Monday at 8:00 AM

Step 9d: Create SEC Examination Readiness Binder

1
Store in: Redtail Imaging or firm's document management system (DMS)
2
TLH Policy & Procedures (signed by CCO)
3
Substitute Security Mapping Table (dated, version-controlled)
4
Wash-Sale Monitoring Configuration Documentation
5
Sample TLH trade documentation (10+ examples)
6
CCO Weekly Review Reports (rolling 24 months)
7
Orion Custom Indexing system configuration screenshots
8
Vendor due diligence documentation (Orion SOC 2 report)
9
Client disclosure language (ADV Part 2A excerpt)
10
Incident log: any wash-sale violations and remediation steps
11
Annual TLH performance report by client household
Note

This step is non-negotiable and should not be skipped or deferred. The SEC enforcement action cited in the research involved a firm that was fined specifically because wash sales occurred in 31% of accounts despite marketing claims of automated avoidance. The documentation framework created here is the firm's primary defense. The MSP should position this compliance infrastructure as a key deliverable and ongoing managed service. Review all documentation with the firm's compliance consultant before going live.

Step 10: User Acceptance Testing and Phased Go-Live

Conduct thorough testing of the entire TLH pipeline using a controlled subset of client accounts before rolling out to the full client base. Testing must validate data feed accuracy, TLH opportunity identification, wash-sale rule enforcement, trade execution, compliance documentation, and reporting. Plan for a 3-phase rollout: pilot (10 accounts), limited (50 accounts), full production.

Phase 1: Pilot Testing (10 accounts, 2 weeks)

Select 10 client accounts representing diverse scenarios:

  • 2 accounts with obvious TLH opportunities (large unrealized losses)
  • 2 accounts with NO TLH opportunities (test for false positives)
  • 2 accounts with potential wash-sale risk (IRA + taxable same household)
  • 2 accounts with mutual funds approaching distribution dates
  • 2 accounts with complex cost basis (multiple lots, gifts, inheritances)

Phase 2: Limited Rollout (50 accounts, 2 weeks)

Expand to 50 accounts covering all major account types. Monitor for:

  • Data feed latency or errors
  • TLH recommendation quality (advisor validates each recommendation)
  • System performance under increased load
  • Workflow efficiency (time from recommendation to execution)

Phase 3: Full Production

  • Roll out to all eligible taxable accounts
  • First full production cycle ideally timed for Q4 (October–December) when TLH activity is highest and results are most visible to clients
Note

Do NOT rush through testing to meet a deadline. A single wash-sale violation that goes undetected during testing could result in client harm and regulatory issues. The pilot phase should reveal any data quality issues—cost basis discrepancies are the most common problem, especially for accounts transferred from other custodians where cost basis may have been incorrectly reported. Budget a full week just for cost basis reconciliation if the firm has recently moved custodians.

Custom AI Components

TLH Priority Scoring Engine

Type: agent A scoring algorithm that combines portfolio-level unrealized loss data from Orion with client-level tax intelligence from Holistiplan to generate a prioritized list of TLH opportunities ranked by estimated after-tax value. This ensures advisors focus their limited time on the highest-impact harvesting opportunities rather than reviewing every account equally. The engine runs daily and produces both a ranked opportunity list and automated alerts for high-priority situations.

Implementation

TLH Priority Scoring Engine — main script
python
import requests
import json
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.html import MIMEHTML
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Optional
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('tlh_scorer')

# ============================================================
# CONFIGURATION - Update these values per client deployment
# ============================================================
ORION_API_BASE = 'https://api.orion.com/api/v1'
ORION_API_KEY = 'YOUR_ORION_API_KEY'  # Store in Azure Key Vault or env var
SMTP_SERVER = 'smtp.office365.com'
SMTP_PORT = 587
SMTP_USER = 'alerts@advisorfirm.com'
SMTP_PASS = 'USE_APP_PASSWORD'  # Store in Key Vault
ALERT_RECIPIENTS = ['lead.advisor@firm.com', 'ops@firm.com']
CCO_EMAIL = 'cco@firm.com'

# TLH Policy Parameters (configured per firm)
MIN_LOSS_THRESHOLD = 100  # Minimum $ loss to consider harvesting
MIN_HOLDING_PERIOD_DAYS = 31  # Must hold >30 days (wash-sale safe)
SHORT_TERM_GAIN_TOLERANCE = 0  # Don't create short-term gains
MARKET_CORRECTION_THRESHOLD = -0.05  # 5% S&P decline triggers alert


@dataclass
class TaxProfile:
    household_id: str
    client_name: str
    marginal_rate: float  # e.g., 0.37 for 37%
    ltcg_rate: float      # e.g., 0.20 for 20%
    stcg_rate: float      # e.g., 0.37 (same as marginal)
    capital_loss_carryforward: float  # $ amount from prior years
    amt_flag: bool
    state_rate: float     # State income tax rate


@dataclass
class TLHOpportunity:
    household_id: str
    client_name: str
    account_id: str
    account_name: str
    security_ticker: str
    security_name: str
    quantity: float
    cost_basis: float
    current_value: float
    unrealized_loss: float
    loss_type: str  # 'short_term' or 'long_term'
    holding_period_days: int
    substitute_ticker: str
    substitute_name: str
    wash_sale_risk: bool
    wash_sale_detail: Optional[str]
    tax_benefit_estimate: float
    priority_score: float


# ============================================================
# SUBSTITUTE SECURITY MAPPING TABLE
# Firm-specific: must be approved by lead advisor and CCO
# ============================================================
SUBSTITUTE_MAP = {
    'SPY':  {'sub': 'IVV',  'name': 'iShares Core S&P 500 ETF'},
    'IVV':  {'sub': 'VOO',  'name': 'Vanguard S&P 500 ETF'},
    'VOO':  {'sub': 'SPY',  'name': 'SPDR S&P 500 ETF Trust'},
    'QQQ':  {'sub': 'QQQM', 'name': 'Invesco NASDAQ 100 ETF'},
    'QQQM': {'sub': 'QQQ',  'name': 'Invesco QQQ Trust'},
    'VTI':  {'sub': 'ITOT', 'name': 'iShares Core S&P Total US Stock Market ETF'},
    'ITOT': {'sub': 'SCHB', 'name': 'Schwab US Broad Market ETF'},
    'SCHB': {'sub': 'VTI',  'name': 'Vanguard Total Stock Market ETF'},
    'VXUS': {'sub': 'IXUS', 'name': 'iShares Core MSCI Total Intl Stock ETF'},
    'IXUS': {'sub': 'SPDW', 'name': 'SPDR Portfolio Developed World ex-US ETF'},
    'VWO':  {'sub': 'IEMG', 'name': 'iShares Core MSCI Emerging Markets ETF'},
    'BND':  {'sub': 'AGG',  'name': 'iShares Core US Aggregate Bond ETF'},
    'AGG':  {'sub': 'SCHZ', 'name': 'Schwab US Aggregate Bond ETF'},
    'VNQ':  {'sub': 'SCHH', 'name': 'Schwab US REIT ETF'},
    'SCHH': {'sub': 'IYR',  'name': 'iShares US Real Estate ETF'},
    'VEA':  {'sub': 'SPDW', 'name': 'SPDR Portfolio Developed World ex-US ETF'},
    'EFA':  {'sub': 'VEA',  'name': 'Vanguard FTSE Developed Markets ETF'},
    'VIG':  {'sub': 'DGRO', 'name': 'iShares Core Dividend Growth ETF'},
    'SCHD': {'sub': 'VIG',  'name': 'Vanguard Dividend Appreciation ETF'},
}


# ============================================================
# TAX PROFILE STORE
# Populated from Holistiplan export + Redtail custom fields
# In production, this reads from a database or API
# ============================================================
def load_tax_profiles() -> dict:
    """Load tax profiles from Redtail custom fields or local CSV.
    Returns dict keyed by household_id."""
    # Example: read from CSV exported from Holistiplan/Redtail
    import csv
    profiles = {}
    try:
        with open('/data/tax_profiles.csv', 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                profiles[row['household_id']] = TaxProfile(
                    household_id=row['household_id'],
                    client_name=row['client_name'],
                    marginal_rate=float(row['marginal_rate']),
                    ltcg_rate=float(row['ltcg_rate']),
                    stcg_rate=float(row['stcg_rate']),
                    capital_loss_carryforward=float(row.get('loss_carryforward', 0)),
                    amt_flag=row.get('amt_flag', 'false').lower() == 'true',
                    state_rate=float(row.get('state_rate', 0))
                )
    except FileNotFoundError:
        logger.warning('Tax profiles CSV not found. Using default rates.')
    return profiles


def get_orion_holdings(api_base: str, api_key: str) -> list:
    """Fetch all holdings with unrealized gain/loss data from Orion API."""
    headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
    holdings = []
    page = 1
    while True:
        resp = requests.get(
            f'{api_base}/portfolio/holdings',
            headers=headers,
            params={
                'page': page,
                'pageSize': 500,
                'includeUnrealizedGL': True,
                'includeTaxLots': True,
                'accountTypes': 'taxable'  # Only taxable accounts
            }
        )
        resp.raise_for_status()
        data = resp.json()
        holdings.extend(data.get('results', []))
        if page >= data.get('totalPages', 1):
            break
        page += 1
    return holdings


def check_wash_sale_risk(household_id: str, ticker: str, api_base: str, api_key: str) -> tuple:
    """Check if selling this ticker creates wash-sale risk due to recent
    purchases of the same or substantially identical security in any
    account within the household (including IRAs/401k).
    Returns (is_risky: bool, detail: str)"""
    headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
    
    # Get all transactions for this household in the wash-sale window
    window_start = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
    window_end = (datetime.now() + timedelta(days=30)).strftime('%Y-%m-%d')
    
    resp = requests.get(
        f'{api_base}/portfolio/transactions',
        headers=headers,
        params={
            'householdId': household_id,
            'ticker': ticker,
            'transactionType': 'buy',
            'startDate': window_start,
            'endDate': window_end,
            'includeAllAccountTypes': True  # Include IRAs, 401(k)s
        }
    )
    resp.raise_for_status()
    transactions = resp.json().get('results', [])
    
    if transactions:
        acct_types = set(t.get('accountType', 'unknown') for t in transactions)
        detail = (f'WASH SALE RISK: {ticker} purchased within 30-day window '
                  f'in account types: {", ".join(acct_types)}. '
                  f'{len(transactions)} conflicting transaction(s) found.')
        return True, detail
    
    # Also check substitute security (substantially identical concern)
    sub = SUBSTITUTE_MAP.get(ticker, {}).get('sub')
    if sub:
        resp2 = requests.get(
            f'{api_base}/portfolio/transactions',
            headers=headers,
            params={
                'householdId': household_id,
                'ticker': sub,
                'transactionType': 'buy',
                'startDate': window_start,
                'endDate': window_end,
                'includeAllAccountTypes': True
            }
        )
        resp2.raise_for_status()
        sub_transactions = resp2.json().get('results', [])
        if sub_transactions:
            detail = (f'WASH SALE RISK: Substitute {sub} purchased within '
                      f'30-day window. Cannot use this substitute.')
            return True, detail
    
    return False, 'No wash-sale risk detected.'


def calculate_priority_score(loss_amount: float, tax_profile: TaxProfile,
                              loss_type: str, wash_sale_risk: bool) -> tuple:
    """Calculate TLH priority score (0-100) and estimated tax benefit.
    
    Scoring factors:
    1. Tax benefit magnitude (40% weight) - higher marginal rate = higher benefit
    2. Loss amount (30% weight) - larger losses = higher priority
    3. Loss type (15% weight) - short-term losses offset higher-taxed ST gains
    4. Risk/complexity (15% weight) - penalize wash-sale risk
    """
    # Calculate estimated tax benefit
    combined_rate = tax_profile.marginal_rate + tax_profile.state_rate
    if loss_type == 'short_term':
        applicable_rate = tax_profile.stcg_rate + tax_profile.state_rate
    else:
        applicable_rate = tax_profile.ltcg_rate + tax_profile.state_rate
    
    tax_benefit = abs(loss_amount) * applicable_rate
    
    # If client has existing carryforward, reduce priority slightly
    # (they already have losses to use)
    carryforward_penalty = min(tax_profile.capital_loss_carryforward / 50000, 0.2)
    
    # Score component 1: Tax benefit magnitude (0-40 points)
    # Scale: $0 = 0 pts, $5000+ = 40 pts
    benefit_score = min(tax_benefit / 5000, 1.0) * 40
    
    # Score component 2: Loss amount (0-30 points)
    # Scale: $0 = 0 pts, $25000+ = 30 pts
    loss_score = min(abs(loss_amount) / 25000, 1.0) * 30
    
    # Score component 3: Loss type (0-15 points)
    type_score = 15 if loss_type == 'short_term' else 8
    
    # Score component 4: Risk penalty (0-15 points)
    risk_score = 0 if wash_sale_risk else 15
    
    raw_score = benefit_score + loss_score + type_score + risk_score
    adjusted_score = raw_score * (1 - carryforward_penalty)
    
    return round(adjusted_score, 1), round(tax_benefit, 2)


def scan_for_opportunities(holdings: list, tax_profiles: dict,
                           api_base: str, api_key: str) -> List[TLHOpportunity]:
    """Main scanning engine: evaluate all holdings for TLH opportunities."""
    opportunities = []
    
    for holding in holdings:
        ticker = holding.get('ticker', '')
        unrealized_gl = holding.get('unrealizedGainLoss', 0)
        
        # Skip if no loss or loss below threshold
        if unrealized_gl >= -MIN_LOSS_THRESHOLD:
            continue
        
        # Skip if no substitute available
        if ticker not in SUBSTITUTE_MAP:
            logger.debug(f'No substitute mapped for {ticker}, skipping')
            continue
        
        # Skip if holding period too short
        purchase_date = datetime.strptime(
            holding.get('purchaseDate', '2020-01-01'), '%Y-%m-%d')
        holding_days = (datetime.now() - purchase_date).days
        if holding_days < MIN_HOLDING_PERIOD_DAYS:
            continue
        
        # Determine loss type
        loss_type = 'short_term' if holding_days <= 365 else 'long_term'
        
        # Check wash-sale risk
        household_id = holding.get('householdId', '')
        wash_risk, wash_detail = check_wash_sale_risk(
            household_id, ticker, api_base, api_key)
        
        # Get tax profile (use defaults if not found)
        tax_profile = tax_profiles.get(household_id, TaxProfile(
            household_id=household_id,
            client_name=holding.get('clientName', 'Unknown'),
            marginal_rate=0.24, ltcg_rate=0.15, stcg_rate=0.24,
            capital_loss_carryforward=0, amt_flag=False, state_rate=0.05
        ))
        
        # Calculate priority score
        priority_score, tax_benefit = calculate_priority_score(
            unrealized_gl, tax_profile, loss_type, wash_risk)
        
        sub = SUBSTITUTE_MAP[ticker]
        opportunities.append(TLHOpportunity(
            household_id=household_id,
            client_name=tax_profile.client_name,
            account_id=holding.get('accountId', ''),
            account_name=holding.get('accountName', ''),
            security_ticker=ticker,
            security_name=holding.get('securityName', ''),
            quantity=holding.get('quantity', 0),
            cost_basis=holding.get('costBasis', 0),
            current_value=holding.get('marketValue', 0),
            unrealized_loss=unrealized_gl,
            loss_type=loss_type,
            holding_period_days=holding_days,
            substitute_ticker=sub['sub'],
            substitute_name=sub['name'],
            wash_sale_risk=wash_risk,
            wash_sale_detail=wash_detail if wash_risk else None,
            tax_benefit_estimate=tax_benefit,
            priority_score=priority_score
        ))
    
    # Sort by priority score descending
    opportunities.sort(key=lambda x: x.priority_score, reverse=True)
    return opportunities


def generate_html_report(opportunities: List[TLHOpportunity],
                         report_type: str = 'daily') -> str:
    """Generate HTML email report of TLH opportunities."""
    total_losses = sum(abs(o.unrealized_loss) for o in opportunities)
    total_benefit = sum(o.tax_benefit_estimate for o in opportunities)
    wash_sale_count = sum(1 for o in opportunities if o.wash_sale_risk)
    
    html = f'''
    <html><body style="font-family: Arial, sans-serif; max-width: 900px;">
    <h2>Tax-Loss Harvesting Opportunity Report - {report_type.title()}</h2>
    <p>Generated: {datetime.now().strftime('%B %d, %Y at %I:%M %p ET')}</p>
    
    <div style="background: #f0f7ff; padding: 15px; border-radius: 8px; margin: 15px 0;">
        <h3 style="margin-top:0;">Summary</h3>
        <table>
            <tr><td><strong>Total Opportunities:</strong></td><td>{len(opportunities)}</td></tr>
            <tr><td><strong>Total Harvestable Losses:</strong></td><td>${total_losses:,.2f}</td></tr>
            <tr><td><strong>Estimated Tax Benefit:</strong></td><td>${total_benefit:,.2f}</td></tr>
            <tr><td><strong>Wash-Sale Flags:</strong></td>
                <td style="color: {'red' if wash_sale_count > 0 else 'green'};">
                    {wash_sale_count} {'⚠️' if wash_sale_count > 0 else '✅'}</td></tr>
        </table>
    </div>
    
    <table style="border-collapse: collapse; width: 100%; font-size: 12px;">
    <tr style="background: #2c3e50; color: white;">
        <th style="padding:8px;">Priority</th>
        <th style="padding:8px;">Client</th>
        <th style="padding:8px;">Account</th>
        <th style="padding:8px;">Security</th>
        <th style="padding:8px;">Loss</th>
        <th style="padding:8px;">Type</th>
        <th style="padding:8px;">Tax Benefit</th>
        <th style="padding:8px;">Substitute</th>
        <th style="padding:8px;">Wash Sale</th>
    </tr>
    '''
    
    for i, opp in enumerate(opportunities[:50]):  # Top 50
        bg = '#fff3f3' if opp.wash_sale_risk else ('#f9f9f9' if i % 2 else '#ffffff')
        ws_icon = '⚠️ RISK' if opp.wash_sale_risk else '✅ Clear'
        html += f'''
        <tr style="background: {bg};">
            <td style="padding:6px; text-align:center; font-weight:bold;">{opp.priority_score}</td>
            <td style="padding:6px;">{opp.client_name}</td>
            <td style="padding:6px;">{opp.account_name}</td>
            <td style="padding:6px;"><strong>{opp.security_ticker}</strong></td>
            <td style="padding:6px; color:red;">${abs(opp.unrealized_loss):,.2f}</td>
            <td style="padding:6px;">{opp.loss_type.replace('_', ' ').title()}</td>
            <td style="padding:6px; color:green;">${opp.tax_benefit_estimate:,.2f}</td>
            <td style="padding:6px;">{opp.substitute_ticker}</td>
            <td style="padding:6px;">{ws_icon}</td>
        </tr>
        '''
    
    html += '''</table>
    <p style="font-size:11px; color:#666; margin-top:20px;">
    This report is generated by automated TLH scanning software for advisor review only.
    All harvesting decisions require advisor approval. Wash-sale flagged items must NOT 
    be executed without manual review. This is not investment advice.
    </p>
    </body></html>'''
    
    return html


def send_alert(html_content: str, subject: str, recipients: list,
               include_cco: bool = False):
    """Send email alert via SMTP."""
    msg = MIMEMultipart('alternative')
    msg['Subject'] = subject
    msg['From'] = SMTP_USER
    all_recipients = recipients.copy()
    if include_cco:
        all_recipients.append(CCO_EMAIL)
    msg['To'] = ', '.join(all_recipients)
    msg.attach(MIMEHTML(html_content, 'html'))
    
    with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
        server.starttls()
        server.login(SMTP_USER, SMTP_PASS)
        server.sendmail(SMTP_USER, all_recipients, msg.as_string())
    logger.info(f'Alert sent to {len(all_recipients)} recipients: {subject}')


def check_market_correction() -> bool:
    """Check if S&P 500 has declined more than threshold in trailing 30 days.
    Uses a simple API call to check market conditions."""
    try:
        # Using Yahoo Finance or similar free endpoint
        resp = requests.get(
            'https://query1.finance.yahoo.com/v8/finance/chart/SPY',
            params={'range': '1mo', 'interval': '1d'},
            headers={'User-Agent': 'TLH-Scanner/1.0'}
        )
        data = resp.json()
        prices = data['chart']['result'][0]['indicators']['quote'][0]['close']
        prices = [p for p in prices if p is not None]
        if len(prices) >= 2:
            change = (prices[-1] - prices[0]) / prices[0]
            return change <= MARKET_CORRECTION_THRESHOLD
    except Exception as e:
        logger.warning(f'Market check failed: {e}')
    return False


def main():
    """Main execution: daily TLH scan and alerting."""
    logger.info('Starting TLH opportunity scan...')
    
    # Load tax profiles
    tax_profiles = load_tax_profiles()
    logger.info(f'Loaded {len(tax_profiles)} tax profiles')
    
    # Fetch holdings from Orion
    holdings = get_orion_holdings(ORION_API_BASE, ORION_API_KEY)
    logger.info(f'Fetched {len(holdings)} holdings from Orion')
    
    # Scan for opportunities
    opportunities = scan_for_opportunities(
        holdings, tax_profiles, ORION_API_BASE, ORION_API_KEY)
    logger.info(f'Found {len(opportunities)} TLH opportunities')
    
    # Determine report type and urgency
    today = datetime.now()
    is_q4_push = today.month >= 10 and today.month <= 12
    is_market_correction = check_market_correction()
    is_monday = today.weekday() == 0
    
    if is_market_correction:
        subject = '🚨 URGENT: Market Correction TLH Opportunities'
        report_type = 'market_correction'
        include_cco = True
    elif is_q4_push:
        subject = f'📊 Q4 Year-End TLH Opportunities - {today.strftime("%b %d")}'
        report_type = 'q4_push'
        include_cco = False
    elif is_monday:
        subject = f'📋 Weekly TLH Summary - Week of {today.strftime("%b %d")}'
        report_type = 'weekly'
        include_cco = True
    else:
        subject = f'📊 Daily TLH Scan - {today.strftime("%b %d")}'
        report_type = 'daily'
        include_cco = False
    
    if opportunities:
        html = generate_html_report(opportunities, report_type)
        send_alert(html, subject, ALERT_RECIPIENTS, include_cco)
    elif is_monday:  # Always send Monday summary even if no opportunities
        html = generate_html_report([], report_type)
        send_alert(html, subject + ' (No Opportunities Found)',
                   ALERT_RECIPIENTS, include_cco)
    
    logger.info('TLH scan complete.')


if __name__ == '__main__':
    main()
Azure Functions timer trigger binding
json
# function.json. Schedule: 6:30 AM ET (10:30 UTC) Monday–Friday.

{
  "bindings": [
    {
      "name": "timer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 30 10 * * 1-5"
    }
  ]
}
Python dependencies — requirements.txt
text
requests>=2.31.0
python-dateutil>=2.8.2

Seasonal TLH Campaign Orchestrator

Type: workflow

A workflow automation that orchestrates the four key seasonal TLH campaigns throughout the year: Q1 January Effect (January), Spring Tax Return Review (April-May), Q3 Mid-Year Rebalance Check (July), and Q4 Year-End Push (October-December). Each campaign triggers a specific sequence of actions including data refresh from Holistiplan, enhanced scanning frequency, targeted client communications, and compliance documentation cycles. Implemented as a series of Redtail workflow templates with integrated calendar triggers.

Implementation

seasonal_tlh_campaigns.yaml
yaml
# Configuration file for the 4 seasonal TLH campaigns. Deploy as Redtail
# workflows + calendar-triggered automation.

campaigns:
  q1_january_effect:
    name: "Q1 January Effect TLH Sweep"
    trigger_date: "January 2"
    end_date: "January 31"
    description: >
      Capitalize on January selling pressure and year-start volatility.
      Many securities experience price dips in early January as investors
      sell winners from prior year for cash flow or rebalancing.
    scan_frequency: "daily"
    actions:
      - action: "enhanced_scan"
        description: "Run TLH Priority Scorer daily with lowered thresholds"
        min_loss_override: 50  # Lower threshold during high-opportunity window
      - action: "redtail_workflow"
        workflow_name: "Q1 January TLH Review"
        steps:
          - assignee: "operations"
            task: "Generate TLH opportunity report from Orion Custom Indexing"
            due: "January 3"
          - assignee: "lead_advisor"
            task: "Review top 20 priority opportunities and approve/reject"
            due: "January 5"
          - assignee: "operations"
            task: "Execute approved TLH trades in Orion"
            due: "January 6"
          - assignee: "operations"
            task: "Document all trades in Redtail using TLH Trade Documentation template"
            due: "January 7"
          - assignee: "cco"
            task: "Review January TLH batch for compliance"
            due: "January 10"
      - action: "client_communication"
        template: "q1_tlh_proactive"
        subject: "Starting the Year Smart: Tax-Efficient Portfolio Review"
        send_to: "households_with_executed_tlh"
        timing: "After trades execute"

  spring_tax_return_review:
    name: "Spring Tax Return TLH Intelligence Refresh"
    trigger_date: "April 20"
    end_date: "May 31"
    description: >
      Annual refresh of client tax intelligence using newly filed tax returns.
      Update marginal rates, capital loss carryforwards, and AMT exposure
      in Holistiplan. Recalculate TLH priority scores with fresh data.
    scan_frequency: "weekly"
    actions:
      - action: "holistiplan_refresh"
        description: "Upload all new client 1040s to Holistiplan"
        steps:
          - "Collect digital copies of all client tax returns from filing"
          - "Batch upload to Holistiplan (up to 50 at a time)"
          - "Export updated tax intelligence CSV"
          - "Update Redtail custom fields: marginal rate, LTCG rate, loss carryforward"
          - "Update tax_profiles.csv for TLH Priority Scoring Engine"
      - action: "redtail_workflow"
        workflow_name: "Spring Tax Intelligence Update"
        steps:
          - assignee: "operations"
            task: "Collect all 2024 tax returns from clients/CPAs"
            due: "April 25"
          - assignee: "operations"
            task: "Upload returns to Holistiplan and export data"
            due: "May 1"
          - assignee: "operations"
            task: "Update Redtail custom fields for all households"
            due: "May 5"
          - assignee: "lead_advisor"
            task: "Review clients with significant rate changes or new carryforwards"
            due: "May 10"
          - assignee: "lead_advisor"
            task: "Schedule tax planning meetings for high-priority households"
            due: "May 15"

  q3_midyear_check:
    name: "Q3 Mid-Year TLH and Rebalance Check"
    trigger_date: "July 1"
    end_date: "July 31"
    description: >
      Mid-year review combining rebalancing with TLH opportunity identification.
      Summer is often overlooked for TLH but can yield significant opportunities
      especially in volatile sectors or individual positions.
    scan_frequency: "weekly"
    actions:
      - action: "enhanced_scan"
        description: "Run combined rebalance + TLH analysis"
      - action: "redtail_workflow"
        workflow_name: "Q3 Mid-Year TLH Review"
        steps:
          - assignee: "operations"
            task: "Generate mid-year TLH report and YTD harvested losses summary"
            due: "July 5"
          - assignee: "lead_advisor"
            task: "Review mid-year opportunities; prioritize accounts under-harvested YTD"
            due: "July 10"
          - assignee: "lead_advisor"
            task: "Review BlackRock Tax Evaluator for early capital gains distribution estimates"
            due: "July 15"
          - assignee: "operations"
            task: "Execute approved mid-year TLH trades"
            due: "July 18"
          - assignee: "cco"
            task: "Mid-year compliance review of all TLH activity YTD"
            due: "July 25"

  q4_yearend_push:
    name: "Q4 Year-End TLH Push"
    trigger_date: "October 1"
    end_date: "December 20"
    description: >
      The most critical TLH period. Enhanced daily scanning, proactive client
      outreach, capital gains distribution monitoring, and final harvesting
      before year-end. Last trade date typically December 29-30.
    scan_frequency: "daily"
    priority: "highest"
    actions:
      - action: "blackrock_screen"
        description: "Screen all client mutual fund holdings through BlackRock Tax Evaluator"
        timing: "October 1-15"
        steps:
          - "Export all mutual fund/ETF holdings from Orion"
          - "Screen each fund through BlackRock Tax Evaluator"
          - "Flag funds with estimated distributions >3% of NAV"
          - "Create proactive TLH plan for affected accounts"
      - action: "enhanced_scan"
        description: "Daily TLH scanning with Q4 urgency alerts"
        min_loss_override: 50
        alert_escalation: true
      - action: "redtail_workflow"
        workflow_name: "Q4 Year-End TLH Campaign"
        steps:
          - assignee: "operations"
            task: "BlackRock Tax Evaluator screening of all fund holdings"
            due: "October 10"
          - assignee: "lead_advisor"
            task: "Review capital gains distribution exposure report"
            due: "October 15"
          - assignee: "advisor_team"
            task: "Client outreach: year-end tax planning discussions"
            due: "October 31"
          - assignee: "operations"
            task: "Execute first wave of Q4 TLH trades"
            due: "November 15"
          - assignee: "lead_advisor"
            task: "Final review: last chance TLH opportunities"
            due: "December 10"
          - assignee: "operations"
            task: "Execute final TLH trades (last trading day minus 3)"
            due: "December 20"
          - assignee: "operations"
            task: "Generate annual TLH performance report by household"
            due: "December 31"
          - assignee: "cco"
            task: "Q4 and annual TLH compliance review"
            due: "January 10 (next year)"
      - action: "client_communication"
        template: "q4_yearend_results"
        subject: "Your 2024 Tax-Smart Portfolio Summary"
        send_to: "all_tlh_eligible_households"
        timing: "January 5-10 (next year)"

# Email Templates
templates:
  q1_tlh_proactive:
    subject: "Starting {year} Smart: Tax-Efficient Portfolio Review"
    body: >
      Dear {client_name},
      
      As part of our ongoing commitment to maximizing your after-tax returns,
      we've completed an early-year review of your portfolio and identified
      opportunities to improve your tax position.
      
      We recently executed tax-loss harvesting trades in your account(s) that
      are expected to generate approximately ${estimated_benefit} in tax
      savings for {year}. These trades maintain your target allocation while
      capturing available tax losses.
      
      We'll continue monitoring your portfolio throughout the year for
      additional opportunities. Please don't hesitate to reach out if you
      have questions.
      
      Best regards,
      {advisor_name}

  q4_yearend_results:
    subject: "Your {year} Tax-Smart Portfolio Summary"
    body: >
      Dear {client_name},
      
      As we close out {year}, I'm pleased to share a summary of the
      tax-efficiency efforts we've undertaken on your behalf this year:
      
      • Total tax losses harvested: ${total_losses_harvested}
      • Estimated tax benefit: ${estimated_tax_benefit}
      • Number of harvesting events: {harvest_count}
      • Portfolio tracking error: maintained within target
      
      These losses can offset capital gains realized this year and, if
      unused, up to $3,000 of ordinary income, with the remainder carrying
      forward to future years.
      
      I'd like to schedule a brief call to discuss your year-end tax
      position and planning for {next_year}. Please let me know a
      convenient time.
      
      Best regards,
      {advisor_name}

Deploy these workflows in Redtail by creating each campaign as a Workflow Template. Set Redtail calendar reminders on the trigger dates to initiate each campaign. The email templates should be loaded into the firm's email platform or CRM mail-merge system.

Wash-Sale Compliance Guardian

Type: integration A dedicated compliance monitoring integration that continuously validates all proposed TLH trades against the IRS wash-sale rule across all household accounts, including tax-deferred accounts (IRAs, 401(k)s, Roth IRAs). This is the most critical compliance safeguard in the system, designed to prevent the type of wash-sale violations that resulted in SEC enforcement actions against automated TLH platforms. It operates as a pre-trade validation gate that must return PASS before any TLH trade can be executed.

Implementation

Wash-Sale Compliance Guardian — full implementation
python
import requests
import json
import logging
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Tuple, Optional
from enum import Enum

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('wash_sale_guardian')


class WashSaleVerdict(Enum):
    PASS = 'PASS'
    FAIL_SAME_SECURITY = 'FAIL_SAME_SECURITY'
    FAIL_SUBSTANTIALLY_IDENTICAL = 'FAIL_SUBSTANTIALLY_IDENTICAL'
    FAIL_SPOUSE_ACCOUNT = 'FAIL_SPOUSE_ACCOUNT'
    FAIL_IRA_PURCHASE = 'FAIL_IRA_PURCHASE'
    WARNING_PENDING_ORDER = 'WARNING_PENDING_ORDER'
    ERROR_DATA_UNAVAILABLE = 'ERROR_DATA_UNAVAILABLE'


@dataclass
class WashSaleCheckResult:
    verdict: WashSaleVerdict
    ticker_checked: str
    household_id: str
    conflicting_transactions: list
    conflicting_accounts: list
    explanation: str
    check_timestamp: str
    window_start: str
    window_end: str
    recommendation: str


# Substantially identical security groups
# Securities within the same group are considered substantially identical
# for wash-sale purposes. This is a CONSERVATIVE interpretation.
SUBSTANTIALLY_IDENTICAL_GROUPS = {
    'sp500': ['SPY', 'IVV', 'VOO', 'SPLG', 'BBUS'],
    'total_us': ['VTI', 'ITOT', 'SCHB', 'SPTM'],
    'nasdaq100': ['QQQ', 'QQQM'],
    'intl_dev': ['VXUS', 'IXUS', 'VEA', 'EFA', 'SPDW', 'IEFA'],
    'intl_em': ['VWO', 'IEMG', 'EEM', 'SCHE'],
    'total_bond': ['BND', 'AGG', 'SCHZ', 'FBND'],
    'us_reit': ['VNQ', 'SCHH', 'IYR', 'XLRE'],
    'us_dividend': ['VIG', 'DGRO', 'SCHD', 'DVY'],
    'us_growth': ['VUG', 'IWF', 'SCHG', 'SPYG'],
    'us_value': ['VTV', 'IWD', 'SCHV', 'SPYV'],
    'us_smallcap': ['VB', 'IJR', 'SCHA', 'IWM'],
    'tips': ['TIP', 'SCHP', 'VTIP'],
    'short_term_bond': ['BSV', 'SHY', 'SCHO', 'VGSH'],
}


def get_identical_group(ticker: str) -> Optional[set]:
    """Find all substantially identical securities for a given ticker."""
    for group_name, members in SUBSTANTIALLY_IDENTICAL_GROUPS.items():
        if ticker.upper() in [m.upper() for m in members]:
            return set(m.upper() for m in members)
    return None


def check_wash_sale(
    ticker: str,
    household_id: str,
    proposed_sale_date: datetime,
    orion_api_base: str,
    orion_api_key: str
) -> WashSaleCheckResult:
    """Comprehensive wash-sale rule check.
    
    Checks:
    1. Same security purchases within 30 days before/after in ALL accounts
    2. Substantially identical security purchases in ALL accounts
    3. Spouse/partner accounts within the household
    4. Tax-deferred accounts (IRA, Roth IRA, 401(k))
    5. Pending/open orders for the same or substantially identical securities
    
    Returns WashSaleCheckResult with verdict and full audit trail.
    """
    headers = {
        'Authorization': f'Bearer {orion_api_key}',
        'Content-Type': 'application/json'
    }
    
    check_timestamp = datetime.now().isoformat()
    window_start = (proposed_sale_date - timedelta(days=30)).strftime('%Y-%m-%d')
    window_end = (proposed_sale_date + timedelta(days=30)).strftime('%Y-%m-%d')
    
    # Build set of tickers to check (same + substantially identical)
    tickers_to_check = {ticker.upper()}
    identical_group = get_identical_group(ticker)
    if identical_group:
        tickers_to_check = tickers_to_check.union(identical_group)
    
    all_conflicts = []
    conflict_accounts = []
    
    try:
        # Get all accounts in household (including spouse, IRAs, 401(k)s)
        acct_resp = requests.get(
            f'{orion_api_base}/household/{household_id}/accounts',
            headers=headers,
            params={'includeAllTypes': True}
        )
        acct_resp.raise_for_status()
        household_accounts = acct_resp.json().get('results', [])
        
        for account in household_accounts:
            account_id = account.get('accountId')
            account_name = account.get('accountName', 'Unknown')
            account_type = account.get('registrationType', 'Unknown')
            owner = account.get('ownerName', 'Unknown')
            
            for check_ticker in tickers_to_check:
                # Check completed transactions
                txn_resp = requests.get(
                    f'{orion_api_base}/account/{account_id}/transactions',
                    headers=headers,
                    params={
                        'ticker': check_ticker,
                        'transactionType': 'buy',
                        'startDate': window_start,
                        'endDate': window_end
                    }
                )
                txn_resp.raise_for_status()
                transactions = txn_resp.json().get('results', [])
                
                for txn in transactions:
                    conflict = {
                        'type': 'completed_transaction',
                        'ticker': check_ticker,
                        'account_id': account_id,
                        'account_name': account_name,
                        'account_type': account_type,
                        'owner': owner,
                        'transaction_date': txn.get('transactionDate'),
                        'quantity': txn.get('quantity'),
                        'amount': txn.get('amount'),
                        'is_same_security': check_ticker.upper() == ticker.upper(),
                        'is_tax_deferred': account_type in [
                            'IRA', 'Roth IRA', 'SEP IRA', 'SIMPLE IRA',
                            '401(k)', '403(b)', '457(b)'
                        ]
                    }
                    all_conflicts.append(conflict)
                    if account_id not in conflict_accounts:
                        conflict_accounts.append({
                            'account_id': account_id,
                            'account_name': account_name,
                            'account_type': account_type,
                            'owner': owner
                        })
                
                # Check pending/open orders
                order_resp = requests.get(
                    f'{orion_api_base}/account/{account_id}/orders',
                    headers=headers,
                    params={
                        'ticker': check_ticker,
                        'status': 'open,pending',
                        'side': 'buy'
                    }
                )
                if order_resp.status_code == 200:
                    open_orders = order_resp.json().get('results', [])
                    for order in open_orders:
                        conflict = {
                            'type': 'pending_order',
                            'ticker': check_ticker,
                            'account_id': account_id,
                            'account_name': account_name,
                            'account_type': account_type,
                            'owner': owner,
                            'order_date': order.get('orderDate'),
                            'quantity': order.get('quantity'),
                            'is_same_security': check_ticker.upper() == ticker.upper()
                        }
                        all_conflicts.append(conflict)
    
    except requests.RequestException as e:
        logger.error(f'API error during wash-sale check: {e}')
        return WashSaleCheckResult(
            verdict=WashSaleVerdict.ERROR_DATA_UNAVAILABLE,
            ticker_checked=ticker,
            household_id=household_id,
            conflicting_transactions=[],
            conflicting_accounts=[],
            explanation=f'Unable to complete wash-sale check due to API error: {str(e)}',
            check_timestamp=check_timestamp,
            window_start=window_start,
            window_end=window_end,
            recommendation='DO NOT EXECUTE. Manual review required. API connectivity issue.'
        )
    
    # Determine verdict
    if not all_conflicts:
        return WashSaleCheckResult(
            verdict=WashSaleVerdict.PASS,
            ticker_checked=ticker,
            household_id=household_id,
            conflicting_transactions=[],
            conflicting_accounts=[],
            explanation=(
                f'No purchases of {ticker} or substantially identical securities '
                f'found in any household account within the 61-day wash-sale window '
                f'({window_start} to {window_end}). '
                f'Checked {len(tickers_to_check)} tickers across '
                f'{len(household_accounts)} accounts.'
            ),
            check_timestamp=check_timestamp,
            window_start=window_start,
            window_end=window_end,
            recommendation='SAFE TO EXECUTE. No wash-sale risk detected.'
        )
    
    # Categorize the failure
    has_ira_conflict = any(c.get('is_tax_deferred') for c in all_conflicts)
    has_same_security = any(c.get('is_same_security') for c in all_conflicts)
    has_pending = any(c.get('type') == 'pending_order' for c in all_conflicts)
    
    if has_pending and not any(c.get('type') == 'completed_transaction' for c in all_conflicts):
        verdict = WashSaleVerdict.WARNING_PENDING_ORDER
        recommendation = (
            'CANCEL PENDING BUY ORDERS before executing TLH sale, or '
            'wait until pending orders are resolved. Manual review required.'
        )
    elif has_ira_conflict:
        verdict = WashSaleVerdict.FAIL_IRA_PURCHASE
        recommendation = (
            'DO NOT EXECUTE. Wash-sale rule applies even when the replacement '
            'purchase is in a tax-deferred account. The loss will be permanently '
            'disallowed (cannot be added to IRA cost basis). Wait 31 days after '
            'the most recent purchase date.'
        )
    elif has_same_security:
        verdict = WashSaleVerdict.FAIL_SAME_SECURITY
        recommendation = (
            'DO NOT EXECUTE. Same security purchased within wash-sale window. '
            f'Wait until after {window_end} or use a different substitute security '
            'that is not in the substantially identical group.'
        )
    else:
        verdict = WashSaleVerdict.FAIL_SUBSTANTIALLY_IDENTICAL
        recommendation = (
            'DO NOT EXECUTE. Substantially identical security purchased within '
            'wash-sale window. Consider using a substitute from a different '
            'asset class or strategy, or wait until after the wash-sale window closes.'
        )
    
    explanation_parts = [f'Found {len(all_conflicts)} conflicting transaction(s):']
    for c in all_conflicts:
        explanation_parts.append(
            f"  - {c['type']}: {c['ticker']} in {c['account_name']} "
            f"({c['account_type']}, owner: {c['owner']}) "
            f"on {c.get('transaction_date', c.get('order_date', 'N/A'))}"
        )
    
    return WashSaleCheckResult(
        verdict=verdict,
        ticker_checked=ticker,
        household_id=household_id,
        conflicting_transactions=all_conflicts,
        conflicting_accounts=conflict_accounts,
        explanation='\n'.join(explanation_parts),
        check_timestamp=check_timestamp,
        window_start=window_start,
        window_end=window_end,
        recommendation=recommendation
    )


def validate_tlh_batch(
    proposed_trades: list,
    orion_api_base: str,
    orion_api_key: str
) -> dict:
    """Validate an entire batch of proposed TLH trades.
    Returns a summary with per-trade verdicts.
    
    proposed_trades format:
    [
        {'ticker': 'SPY', 'household_id': 'HH001', 'sale_date': '2025-01-15'},
        ...
    ]
    """
    results = {
        'batch_timestamp': datetime.now().isoformat(),
        'total_trades': len(proposed_trades),
        'passed': 0,
        'failed': 0,
        'warnings': 0,
        'errors': 0,
        'trade_results': []
    }
    
    for trade in proposed_trades:
        result = check_wash_sale(
            ticker=trade['ticker'],
            household_id=trade['household_id'],
            proposed_sale_date=datetime.strptime(trade['sale_date'], '%Y-%m-%d'),
            orion_api_base=orion_api_base,
            orion_api_key=orion_api_key
        )
        
        trade_result = {
            'ticker': trade['ticker'],
            'household_id': trade['household_id'],
            'verdict': result.verdict.value,
            'explanation': result.explanation,
            'recommendation': result.recommendation
        }
        results['trade_results'].append(trade_result)
        
        if result.verdict == WashSaleVerdict.PASS:
            results['passed'] += 1
        elif result.verdict.value.startswith('WARNING'):
            results['warnings'] += 1
        elif result.verdict.value.startswith('ERROR'):
            results['errors'] += 1
        else:
            results['failed'] += 1
    
    # Safety gate: if ANY trade fails, flag entire batch for manual review
    results['batch_verdict'] = 'APPROVED' if results['failed'] == 0 and results['errors'] == 0 else 'REQUIRES_MANUAL_REVIEW'
    
    logger.info(
        f"Batch validation complete: {results['passed']} passed, "
        f"{results['failed']} failed, {results['warnings']} warnings, "
        f"{results['errors']} errors. Batch verdict: {results['batch_verdict']}"
    )
    
    return results

This component is called by the TLH Priority Scoring Engine before any trade recommendation is surfaced to the advisor. If the verdict is anything other than PASS, the opportunity is flagged in the dashboard with red highlighting and the specific wash-sale conflict details. The full check result is logged for compliance audit trail purposes.

TLH Performance Attribution Report Generator

Type: prompt An AI prompt template used with GPT-4 or Claude to generate personalized, client-facing TLH performance reports that quantify the after-tax value delivered by the harvesting program. These reports are used in quarterly and annual client review meetings to demonstrate the tangible value of tax-aware portfolio management, supporting advisor-client retention and fee justification.

Implementation

System Prompt

You are a senior financial advisor's report writing assistant. Your task is to generate a personalized, professional tax-loss harvesting (TLH) performance report for a specific client household.

  • Written in warm, professional language appropriate for high-net-worth clients
  • Quantitative: include specific dollar amounts and percentages
  • Educational: briefly explain WHY tax-loss harvesting matters
  • Forward-looking: mention ongoing monitoring and next steps
  • Compliant: include appropriate disclaimers
  • Formatted in clean HTML suitable for email or PDF generation
Note

Do NOT provide investment advice. Focus on reporting what was accomplished.

User Prompt Template

Generate a personalized Tax-Loss Harvesting Performance Report for the following client household. Use the data provided to create a professional, quantitative narrative report.

User prompt template with variable placeholders for client data injection
markdown
### Client Information
- **Household Name:** {household_name}
- **Primary Contact:** {primary_contact_name}
- **Advisor:** {advisor_name}
- **Reporting Period:** {period_start} to {period_end}
- **Household AUM:** ${household_aum}
- **Number of Taxable Accounts:** {taxable_account_count}
- **Marginal Federal Tax Rate:** {marginal_rate}%
- **State Tax Rate:** {state_rate}%
- **Long-Term Capital Gains Rate:** {ltcg_rate}%

### TLH Activity Summary
- **Total Harvesting Events:** {harvest_count}
- **Total Short-Term Losses Harvested:** ${st_losses}
- **Total Long-Term Losses Harvested:** ${lt_losses}
- **Total Losses Harvested:** ${total_losses}
- **Estimated Federal Tax Savings:** ${federal_tax_savings}
- **Estimated State Tax Savings:** ${state_tax_savings}
- **Total Estimated Tax Savings:** ${total_tax_savings}
- **Capital Loss Carryforward (if applicable):** ${carryforward_amount}
- **Wash-Sale Violations:** {wash_sale_violations} (should be 0)

### Harvesting Events Detail
{harvesting_events_table}
<!-- JSON array of: date, security_sold, loss_amount, loss_type, substitute_purchased -->

### Portfolio Impact
- **Portfolio Tracking Error vs. Benchmark:** {tracking_error}%
- **Portfolio Total Return (pre-tax):** {total_return}%
- **Benchmark Return:** {benchmark_return}%
- **Estimated After-Tax Alpha from TLH:** {after_tax_alpha} bps

Report Requirements

1
Open with a personalized greeting using the client's name
2
Summarize the period's TLH results in 2-3 sentences
3
Provide a "TLH Value Delivered" section with key metrics
4
Include a brief educational section (2-3 sentences) explaining how TLH works
5
Mention the carryforward benefit if applicable
6
Close with forward-looking language about continued monitoring
7
Include disclaimer at the bottom in small text: "This report is provided for informational purposes only and does not constitute tax advice. Please consult your tax professional regarding the application of tax-loss harvesting to your specific situation. Past tax savings do not guarantee future results. Tax laws are subject to change."
Note

Format the output as clean HTML with inline styles suitable for email delivery. Use a professional color scheme (navy headers, dark gray body text).

Integration Script (Python)

Python integration script
python
# loads prompt template, injects client data variables, and calls the OpenAI
# Chat Completions API

import openai
import json

def generate_tlh_report(client_data: dict, api_key: str) -> str:
    """Generate client-facing TLH report using LLM."""
    client = openai.OpenAI(api_key=api_key)
    
    # Load prompt template and fill in variables
    with open('tlh_report_prompt.md', 'r') as f:
        prompt_template = f.read()
    
    # Replace template variables
    prompt = prompt_template
    for key, value in client_data.items():
        prompt = prompt.replace(f'{{{key}}}', str(value))
    
    response = client.chat.completions.create(
        model='gpt-5.4',
        messages=[
            {'role': 'system', 'content': prompt.split('## User Prompt Template')[0]},
            {'role': 'user', 'content': prompt.split('## User Prompt Template')[1]}
        ],
        temperature=0.3,
        max_tokens=3000
    )
    
    return response.choices[0].message.content

Cost: ~$0.01–0.03 per report generation via GPT-5.4 API. For a firm with 200 households generating quarterly reports, annual LLM API cost is approximately $8–24.

Testing & Validation

  • CUSTODIAL DATA FEED TEST: After activating Schwab/Fidelity data feeds in Orion, select 10 client accounts and manually compare position quantities, tickers, and cost basis in Orion against the custodial platform login. All positions must match within 1 business day's reconciliation. Cost basis must match to the penny for tax lots purchased after the firm's custodial onboarding date. Document any discrepancies and open cases with Orion support for resolution before proceeding.
  • HOUSEHOLD MAPPING TEST: Select 5 households that have both taxable AND tax-deferred accounts. Verify in Orion that all accounts for each household are properly linked under the correct household ID. Cross-reference against Redtail CRM household records. Verify that spouse/partner accounts are included. A single missed account linkage could cause a wash-sale violation.
  • TLH OPPORTUNITY IDENTIFICATION TEST: Using the 10 pilot accounts, manually calculate unrealized gains and losses per holding using custodial cost basis data and current market prices. Compare manual calculations against Orion's TLH opportunity scan results. All opportunities identified by Orion should match manual calculations within $5 tolerance. Any opportunities found manually but missed by Orion must be investigated and resolved.
  • WASH-SALE DETECTION TEST: Create a deliberate test scenario: In a pilot household with both a taxable account and an IRA, purchase a small amount of a security (e.g., 1 share of SPY) in the IRA. Then attempt to harvest a loss on SPY in the taxable account. The system MUST flag this as a wash-sale violation and block the trade recommendation. If it does not, STOP deployment and escalate to Orion support immediately. This is the most critical safety test.
  • SUBSTANTIALLY IDENTICAL SECURITY TEST: In a pilot account, simulate harvesting a loss on VOO (Vanguard S&P 500 ETF) while the system proposes IVV (iShares S&P 500 ETF) as a substitute. Then verify the system checks whether IVV was purchased in any household account within the 30-day window. While the IRS has not definitively ruled that different S&P 500 ETFs are 'substantially identical,' the system should flag this as a risk per the firm's conservative compliance policy.
  • SUBSTITUTE SECURITY MAPPING TEST: Review every entry in the substitute security mapping table with the lead advisor. For each primary->substitute pair, verify: (1) the substitute tracks a different index or has a materially different composition, (2) the substitute is available for trading at the firm's custodian, (3) the substitute has adequate liquidity (average daily volume >100K shares), and (4) the advisor agrees the substitute is appropriate for client portfolios. Document advisor approval with signature and date.
  • TRADE EXECUTION TEST: Execute 2-3 actual TLH trades in pilot accounts (with advisor and client approval). Verify: trade executes at the custodian within expected timeframe, replacement security is purchased same-day, trade confirmation appears in Orion, Redtail activity record is auto-created with TLH Trade Documentation template populated, and the CCO weekly report includes the trades. Check that realized loss amounts match pre-trade estimates.
  • COMPLIANCE DOCUMENTATION TEST: After executing test trades, pull the full audit trail: Orion trade log, Redtail activity note, Smarsh email archive of any related communications. Verify the documentation would be sufficient to explain the trade rationale, wash-sale check result, client suitability, and advisor approval to an SEC examiner. Have the CCO or compliance consultant review and sign off.
  • HOLISTIPLAN TAX INTELLIGENCE TEST: Upload 5 client tax returns to Holistiplan. Verify the OCR correctly extracts: filing status, adjusted gross income, total tax liability, marginal rate, and capital gains/loss carryforward from Schedule D. Compare against the actual tax return values. Any OCR errors must be corrected manually and flagged to Holistiplan support.
  • TLH PRIORITY SCORING ENGINE TEST: Run the custom scoring engine against the pilot accounts. Verify that clients with higher marginal tax rates and larger unrealized losses receive higher priority scores. Verify that households with existing large capital loss carryforwards receive lower priority scores (they already have losses to use). Verify that opportunities with wash-sale risk receive significantly reduced scores. Review the top-10 ranked opportunities with the lead advisor for reasonableness.
  • EMAIL ALERTING TEST: Trigger a test alert from the TLH Priority Scoring Engine. Verify the email is delivered to all configured recipients (lead advisor, operations), contains the correct HTML report format, displays opportunity data accurately, and is captured by Smarsh archival. Test the market correction alert by temporarily lowering the threshold to trigger on current market conditions.
  • PERFORMANCE REPORT GENERATION TEST: Generate a TLH Performance Attribution Report for 3 pilot accounts using the LLM-based report generator. Review each report for: factual accuracy (dollar amounts match Orion data), professional tone, appropriate disclaimers included, and clean HTML formatting. Have the lead advisor review and provide feedback before deploying to production client communications.

Client Handoff

The client handoff should be structured as a series of training sessions over 2-3 weeks, with comprehensive documentation left behind:

Training Session 1 (2 hours - Lead Advisor + Operations Staff)

  • Walkthrough of Orion Custom Indexing TLH dashboard: how to read the opportunity screen, interpret priority scores, review wash-sale flags, and approve/reject trade recommendations
  • Demonstration of the daily TLH alert email: what each field means, when to act, when to wait
  • Practice exercise: review 5 current TLH opportunities end-to-end from identification through trade approval
  • Substitute security mapping review: ensure the advisor understands and approves every pairing

Training Session 2 (1.5 hours - Lead Advisor + CCO)

  • Compliance framework review: TLH Policy & Procedures document walkthrough
  • Wash-sale rule refresher with practical examples specific to the firm's client base
  • CCO weekly review dashboard demonstration: what the CCO needs to check every Monday
  • SEC examination readiness binder review: where everything is stored, how to access it
  • Supervisory procedures sign-off (CCO must formally sign the procedures document)

Training Session 3 (1 hour - All Advisors)

  • Holistiplan demonstration: how to upload a new client tax return, interpret the analysis, and use the data for TLH prioritization
  • BlackRock Tax Evaluator walkthrough: how to screen for capital gains distributions during Q4
  • Client communication templates review: when and how to communicate TLH value to clients
  • Seasonal campaign calendar review: what happens each quarter and who is responsible

Documentation Package to Leave Behind

1
TLH System User Guide (step-by-step screenshots for all daily/weekly/quarterly tasks)
2
TLH Policy & Procedures Document (signed by CCO)
3
Substitute Security Mapping Table (dated, version-controlled)
4
Seasonal Campaign Calendar (wall-printable and digital)
5
Troubleshooting Guide (common issues, error messages, who to call)
6
Emergency Procedures (what to do if data feeds fail, wash-sale alert fires, system is down)
7
MSP Contact Information and SLA summary
8
Vendor support contacts (Orion, Redtail, Holistiplan, custodians)

Success Criteria to Review Together

Maintenance

Weekly Maintenance (MSP Technician - 1-2 hours/week)

  • Monitor custodial data feed health: verify daily reconciliation completes without errors in Orion. Check for stale data (positions not updated >24 hours on business days). Investigate and resolve any feed failures within 4 hours during market hours.
  • Review TLH Priority Scoring Engine logs: verify daily scans complete successfully, check for API errors or timeout issues, validate email alerts are being delivered.
  • Verify Smarsh archival is capturing all TLH-related communications. Spot-check 2-3 recent emails per week.
  • Review CrowdStrike Falcon dashboard for any endpoint security alerts on advisor workstations.

Monthly Maintenance (MSP Technician - 2-3 hours/month)

  • Patch management: Apply Windows/macOS updates to all advisor workstations during approved maintenance window (typically Saturday morning). Test Orion/Redtail/Holistiplan browser compatibility after any major browser update.
  • FortiGate firmware review: Check for available firmware updates. Apply during maintenance window if critical security patches are included. Back up firewall configuration before any update.
  • Orion platform updates: Review Orion release notes for any changes to Custom Indexing or TLH features. Test in sandbox environment if available. Coordinate with firm if UI changes require advisor notification.
  • Review and rotate API keys for custom integrations (TLH Scoring Engine, Wash-Sale Guardian) per security policy.
  • Generate monthly TLH activity summary for firm principal: total opportunities identified, trades executed, losses harvested, wash-sale flags.

Quarterly Maintenance (MSP Technician + Advisor - 3-4 hours/quarter)

  • Seasonal TLH Campaign kickoff support: assist operations staff with initiating the quarterly Redtail workflow (Q1 January, Spring Tax Return, Q3 Mid-Year, Q4 Year-End).
  • Holistiplan tax profile refresh: assist with bulk upload of updated tax returns (especially critical in Q2 after spring tax filing).
  • Substitute security mapping table review: check with lead advisor if any pairs need updating due to fund closures, new ETF launches, or changes in the firm's investment models.
  • Compliance technology audit: verify Smarsh retention policies, backup integrity (test restore of 1 random backup), MFA enforcement across all platforms, and user access reviews (remove departed employees, add new hires).
  • Generate quarterly TLH Performance Attribution Reports using the LLM-based report generator for all active households.

Annual Maintenance (MSP Technician + Advisor + CCO - Full day)

  • Annual TLH program review: compile full-year harvesting results across all households. Calculate total tax savings delivered. Compare against prior year.
  • Compliance annual review: CCO reviews all TLH procedures, wash-sale logs, and trade documentation. Update TLH Policy & Procedures document if needed. Re-sign.
  • Form ADV update: verify TLH-related disclosures in ADV Part 2A are current and accurate.
  • Vendor contract review: evaluate Orion/Redtail/Holistiplan pricing vs. alternatives. Consider consolidation opportunities.
  • Security annual assessment: penetration test or vulnerability scan of the advisory firm's network. Update Written Information Security Policy (WISP). Test incident response plan.
  • Hardware lifecycle review: evaluate workstation and firewall age. Plan replacements for equipment >4 years old.

Escalation Paths

  • Tier 1 (MSP Help Desk): Password resets, browser issues, basic connectivity problems. SLA: 1-hour response, 4-hour resolution.
  • Tier 2 (MSP Senior Technician): Data feed failures, integration errors, TLH scoring engine issues. SLA: 2-hour response, 8-hour resolution.
  • Tier 3 (MSP + Vendor Support): Platform-level bugs, wash-sale detection failures, compliance-critical issues. SLA: 1-hour response for compliance issues, coordinated vendor escalation immediately.
  • Emergency (Trade Day Issues): If any TLH-related system is down during market hours on a day when trades are pending, MSP must provide immediate response. Establish direct cell phone contact for the MSP lead technician and Orion support case priority.

Model/Configuration Retraining Triggers

  • Tax law changes (new tax brackets, rate changes, wash-sale rule modifications) -> Update tax profiles and scoring parameters within 30 days of effective date.
  • New ETF/fund launches that could serve as better substitute securities -> Review and update substitute mapping table within 60 days.
  • Significant change in client base composition (e.g., firm acquires another RIA's book) -> Full household mapping audit and tax profile refresh.
  • Platform vendor releases major TLH feature update -> Test in sandbox, update documentation, retrain advisors within 2 weeks of release.

Alternatives

Altruist TaxIQ (Lower-Cost Custodial-Native Approach)

For smaller RIA firms under $150M AUM that are already on or willing to move to the Altruist custodial platform, Altruist TaxIQ provides native tax-loss harvesting at the custodial level with no additional platform fees. TaxIQ includes automated drift monitoring, tax loss harvesting, and rebalancing built directly into the custodian's portfolio management tools. The TLH feature is free on all fee-bearing marketplace models and only 10 bps per year on no-fee models and custom portfolios. Direct indexing minimums start at just $2,000 per account.

Note

RECOMMEND WHEN: The firm is a startup RIA, is already on Altruist, has under $150M AUM, and wants the simplest possible deployment with lowest ongoing costs.

Envestnet/Tamarac Enterprise Tax Overlay

For mid-size to large RIA firms ($500M+ AUM) with complex multi-manager, multi-custodian environments, Envestnet's Tamarac Trading platform provides the most sophisticated tax overlay engine in the industry. Tamarac Trading offers multiple 'Saved Search' options to proactively find tax loss opportunities across model-level, account-level, and security-level scans. Backed by $7.0 trillion in platform assets and 25+ years of experience, Envestnet provides enterprise-grade capabilities including UMA (Unified Managed Account) tax management and multi-sleeve portfolio overlays.

Note

RECOMMEND WHEN: The firm has $500M+ AUM, uses multiple sub-advisors or model managers, custodies across multiple platforms, and needs institutional-grade tax management capabilities.

Parametric/Vanguard Direct Indexing TAMP (Outsourced Approach)

Instead of building an in-house TLH capability with Orion, the firm can outsource tax-aware portfolio management to a dedicated direct indexing TAMP (Turnkey Asset Management Platform) such as Parametric Custom Core (Morgan Stanley) or Vanguard Personalized Indexing. The TAMP handles all TLH scanning, trade execution, wash-sale monitoring, and compliance documentation as a managed service. The advisor delegates portfolio management of eligible accounts to the TAMP, which constructs individual stock portfolios tracking a benchmark while continuously harvesting tax losses.

Note

RECOMMEND WHEN: The firm has a concentrated HNW client base with $250K+ taxable accounts, wants to minimize technology complexity, and is willing to pay higher fees for institutional-grade TLH delivered as a managed service. Best used for HNW client segments alongside the Orion-based approach for mass-affluent clients.

Spreadsheet-Based Manual TLH (Minimal Technology Approach)

For very small RIA firms (under $50M AUM, fewer than 50 taxable accounts) that cannot justify the platform costs, a structured manual approach using Excel/Google Sheets with quarterly discipline can deliver meaningful TLH value. The MSP deploys a custom spreadsheet template that pulls position data via custodial CSV exports, calculates unrealized gains/losses, identifies opportunities, and documents the TLH decision process. Combined with Holistiplan for tax intelligence and BlackRock Tax Evaluator (free) for fund screening, this approach costs under $300/month total.

Want early access to the full toolkit?