
Implementation Guide: Analyze portfolio drift vs. target allocation and flag rebalancing opportunities
Step-by-step implementation guide for deploying AI to analyze portfolio drift vs. target allocation and flag rebalancing opportunities for Financial Advisory clients.
Hardware Procurement
Next-Gen Firewall (Primary Recommendation)
Next-Gen Firewall (Primary Recommendation)
$586 MSP cost / $800–$850 suggested resale
Perimeter security for the advisory office network. Provides SSL inspection, IPS, application control, DNS filtering, and VPN for remote advisors accessing rebalancing platforms. Required for SEC Reg S-P compliance. The 40F supports up to 10 users; upgrade to FG-70F for larger firms.
Next-Gen Firewall (Larger Firms)
$750 MSP cost / $1,000–$1,200 suggested resale
Higher-throughput firewall for firms with 10+ users or multiple VLANs. Same security feature set as 40F but with greater concurrent session capacity and throughput for simultaneous rebalancing, CRM, and custodian portal access.
Business Workstation
$900 MSP cost per unit / $1,100–$1,300 suggested resale per unit
Advisor desktops for accessing rebalancing dashboards, CRM, and custodian portals. 16GB RAM and SSD ensure responsive multi-tab browser sessions with Orion/Tamarac/iRebal. Windows 11 Pro provides BitLocker encryption and domain join capability.
Dual Monitor Set
$200 MSP cost per monitor / $260 suggested resale per monitor
Dual-monitor configuration per advisor workstation. Rebalancing workflows require simultaneous view of drift reports, model targets, trade blotter, and CRM — dual screens significantly improve efficiency and reduce errors during trade review.
Network Attached Storage
Network Attached Storage
$800 MSP cost (NAS + drives) / $1,100–$1,300 suggested resale
Local encrypted backup and compliance archive. Stores exported trade records, rebalancing decision logs, client IPS documents, and compliance audit trails. SEC Rule 204-2 requires 5+ year record retention. RAID 1 mirror for redundancy.
UPS Battery Backup
$220 MSP cost per unit / $300 suggested resale per unit
Power protection for firewall, NAS, and primary workstation. Prevents data corruption during trade execution windows. One unit for network closet (firewall + NAS), one for lead advisor workstation. Provides 10–15 minutes runtime for graceful shutdown.
Enterprise Wireless Access Point
$350 MSP cost / $475 suggested resale
WPA3 enterprise-grade wireless for the advisory office. Managed by FortiGate for unified security policy enforcement. Supports VLAN segmentation to isolate guest Wi-Fi from production network carrying financial data.
Software Procurement
Orion Advisor Solutions — Trading (Eclipse)
Custom quote; typically bundled in Orion Essentials ($) or Advantage ($$) stacks. Standalone trading module available. Contact Orion sales for RIA-specific pricing based on AUM.
Primary rebalancing and drift detection engine. Provides model portfolio management, multi-account rebalancing, drift tolerance monitoring, tax-loss harvesting automation, asset location optimization, and FIX-based trade execution to custodians. This is the core intelligence platform for the project.
Orion Risk Intelligence
$275/user/month (annual billing discount available)
Complementary risk assessment and drift visualization overlay. Provides stress testing, 3D risk profiling, portfolio construction tools, and proposal generation. Helps advisors contextualize drift alerts within client risk tolerance and IPS parameters.
Redtail CRM
$39/user/month (Launch, billed annually) to $59/user/month (Growth)
Advisor CRM for managing client relationships, logging rebalancing activities, and maintaining compliance communication archives. Integrates natively with Orion for workflow automation — rebalancing alerts can trigger CRM tasks and notes.
Nitrogen (formerly Riskalyze) — Autopilot Module
~$500/month for Autopilot module (user-reported); base Risk Number tool priced separately
Alternative/complementary drift monitoring and risk tolerance alignment tool. The Risk Number quiz quantifies client risk tolerance, and Autopilot monitors portfolios against targets and enables model-based rebalancing and block trading. Useful if client prefers Nitrogen over Orion Risk Intelligence.
Microsoft 365 Business Premium
$22/user/month (direct); MSP can resell at $25–$30/user/month
Core productivity and identity platform. Provides Exchange Online (compliant email), SharePoint (document management), Teams (collaboration), and critically Entra ID with Conditional Access and MFA enforcement. MFA on all financial platforms is an SEC expectation.
SentinelOne Singularity (via MSP program)
$4–$6/endpoint/month MSP cost; resell at $8–$15/endpoint/month
AI-powered endpoint detection and response (EDR). Protects advisor workstations against ransomware, credential theft, and zero-day exploits. Required for SEC Reg S-P compliance. SentinelOne's autonomous response capability is critical when advisors may not have dedicated IT staff on-site.
Datto SIRIS (Backup & Disaster Recovery)
$200–$500/month depending on data volume; includes cloud replication
Cloud and local backup of NAS compliance archives, workstation images, and Microsoft 365 data. Meets SEC 5-year record retention requirement with immutable cloud backups. Enables rapid recovery if ransomware encrypts local compliance archives.
Keeper Business (Password Manager)
$3.75/user/month (billed annually); MSP program pricing may be lower
Enforce strong, unique passwords across all financial platforms (custodian portals, rebalancing tools, CRM). Provides secure sharing of service account credentials among advisors. Audit trail of credential access for compliance documentation.
KnowBe4 Security Awareness Training
$15–$25/user/month through MSP program
Mandatory security awareness training for all advisory staff. Phishing simulations and training modules address the human element of cybersecurity — critical given that financial advisory firms are high-value targets. Supports SEC Reg S-P compliance documentation.
Prerequisites
- Active custodial relationship with at least one supported custodian (Charles Schwab, Fidelity Institutional, BNY Pershing, or Altruist) with API/data feed access enabled
- Documented Investment Policy Statements (IPS) for all client accounts or household tiers, specifying target asset allocations and acceptable drift tolerance ranges
- Model portfolio definitions: complete list of model portfolios used by the firm with target allocation percentages for each asset class (e.g., US Large Cap 30%, International Developed 20%, Fixed Income 40%, Cash 10%)
- Chief Compliance Officer (CCO) engagement: the firm's CCO must be available for 10–20 hours across the project to review and approve automated rebalancing rules, supervisory procedures, and compliance documentation
- Business-class internet service: minimum 50 Mbps symmetrical (100+ Mbps recommended) with static IP if VPN is required
- Microsoft 365 Business Premium (or equivalent) deployed with Entra ID and MFA enforced on all user accounts
- Current inventory of all advisory technology: existing CRM, financial planning software, reporting tools, and any current portfolio management platforms
- List of all custodial account numbers and household groupings to be onboarded to the rebalancing platform
- Firm ADV Part 2A disclosure review: CCO should confirm whether the use of automated rebalancing tools requires disclosure updates in the firm's ADV brochure
- Tax sensitivity parameters: firm-wide or per-client rules for short-term vs. long-term capital gains treatment, wash-sale avoidance windows, and tax-loss harvesting thresholds
- Network audit of existing infrastructure: document current firewall, switches, wireless, and endpoint configurations before beginning hardening
Installation Steps
...
Step 1: Conduct Discovery & Environment Audit
Meet with the advisory firm's principals, lead advisor, operations manager, and CCO. Document the current technology stack including custodial platforms, CRM, financial planning software, and any existing portfolio management tools. Inventory all endpoints, network equipment, and internet service. Collect model portfolio definitions, IPS templates, and compliance policies. Identify the primary custodian(s) and confirm API/data feed eligibility.
Use a standardized discovery questionnaire. Critical outputs: custodian list with account counts, model portfolio spreadsheets, current network topology diagram, and a compliance requirements checklist signed by the CCO. This phase takes 2–3 weeks and determines which rebalancing platform to recommend.
Step 2: Deploy and Configure Network Security Infrastructure
Install and configure the Fortinet FortiGate 40F (or 70F for larger firms) as the perimeter firewall. Configure WAN/LAN interfaces, DHCP, and DNS. Enable Unified Threat Protection (UTP) with IPS, application control, web filtering, antivirus, and SSL deep inspection. Create firewall policies allowing HTTPS traffic to custodian portals, rebalancing platforms, CRM, and Microsoft 365. Block all unnecessary outbound traffic. Configure VLAN segmentation: production VLAN for advisor workstations, management VLAN for network devices, and guest VLAN for visitor Wi-Fi. Deploy FortiAP 231F wireless access point managed by the FortiGate.
# interfaces, UTP security profiles, VLAN segmentation, DNS filtering, and
# SSL VPN
# FortiGate initial setup via CLI (after console connection)
config system interface
edit port1
set ip 192.168.1.1 255.255.255.0
set allowaccess ping https ssh
set alias 'LAN'
next
end
# Enable UTP Security Profiles
config antivirus profile
edit 'av-default'
set scan-mode full
next
end
config ips sensor
edit 'ips-protect'
config entries
edit 1
set severity high critical
set status enable
set action block
next
end
next
end
# Create VLAN for production advisor network
config system interface
edit 'VLAN10-Advisors'
set vdom root
set ip 10.10.10.1 255.255.255.0
set allowaccess ping https
set interface port1
set vlanid 10
next
end
# Enable DNS filtering (block malicious + uncategorized)
config dnsfilter profile
edit 'dns-advisory'
config ftgd-dns
config filters
edit 1
set category 2
set action block
next
end
end
next
end
# Configure SSL VPN for remote advisors
config vpn ssl settings
set servercert 'Fortinet_Factory'
set tunnel-ip-pools 'SSLVPN_TUNNEL_ADDR1'
set port 10443
set source-interface 'wan1'
set source-address 'all'
endReplace IP ranges with client-specific addressing scheme. Ensure the FortiGate firmware is updated to the latest stable release before configuration. SSL deep inspection requires deploying the FortiGate CA certificate to all managed endpoints. Document all firewall rules in a change management log for compliance. Consider FortiGate 70F if the firm has more than 10 concurrent users or plans to grow.
Step 3: Deploy Endpoint Security and Identity Management
Roll out SentinelOne Singularity agent to all advisor workstations via the MSP's RMM tool. Configure detection policies to alert-and-quarantine mode. Enforce Microsoft Entra ID Conditional Access policies requiring MFA for all access to Microsoft 365, custodian portals, and rebalancing platforms. Deploy Keeper Business password manager to all users. Configure BitLocker full-disk encryption on all Windows 11 Pro endpoints. Enroll all endpoints in Microsoft Intune for device compliance policies.
# Deploy SentinelOne via PowerShell (RMM push)
# Download installer from SentinelOne management console first
Start-Process -FilePath '.\SentinelOneInstaller.exe' -ArgumentList '/SITE_TOKEN=YOUR_SITE_TOKEN_HERE /quiet' -Wait
# Verify SentinelOne is running
Get-Service -Name SentinelAgent | Select-Object Status, StartType
# Enable BitLocker on C: drive
Enable-BitLocker -MountPoint 'C:' -EncryptionMethod XtsAes256 -UsedSpaceOnly -RecoveryPasswordProtector
# Backup BitLocker recovery key to Azure AD
BackupToAAD-BitLockerKeyProtector -MountPoint 'C:' -KeyProtectorId (Get-BitLockerVolume -MountPoint 'C:').KeyProtector[0].KeyProtectorId
# Configure Windows Firewall to allow only necessary outbound
Set-NetFirewallProfile -Profile Domain,Public,Private -DefaultInboundAction Block -DefaultOutboundAction Allow -NotifyOnListen True
# Install Keeper via MSI (silent)
msiexec /i KeeperSetup.msi /quiet /norestartStore all BitLocker recovery keys in both Entra ID and MSP's documentation system. SentinelOne site token is unique per client — retrieve from the MSP's SentinelOne multi-tenant console. Conditional Access policies should require compliant device AND MFA for access to any financial application. Test MFA enrollment with 1–2 users before firm-wide rollout.
Step 4: Select and Procure Rebalancing Platform
Based on the discovery phase findings, select the primary rebalancing platform. Decision tree: (1) If the firm custodies primarily with Schwab → start with iRebal (included free). (2) If multi-custodial or wanting an integrated stack → Orion Eclipse Trading. (3) If the firm already uses Envestnet → Envestnet Tamarac Trading. (4) If startup/small RIA wanting simplicity → Altruist (custody + rebalancing integrated). Execute vendor agreements, request sandbox/demo environments, and assign platform admin credentials secured with MFA via Keeper.
Schwab iRebal is the zero-cost starting point for Schwab-custodied firms and should be the default recommendation unless the firm has specific multi-custodial or advanced tax-optimization needs. Orion Eclipse is the premium choice — negotiate pricing based on AUM and whether the firm will adopt other Orion modules (Risk Intelligence, Redtail CRM, Planning). Vendor onboarding typically takes 1–2 weeks for account provisioning and data feed activation. Ensure the vendor agreement includes professional services hours for initial configuration.
Step 5: Configure Model Portfolios and Drift Tolerance Parameters
Working directly with the firm's lead advisor and CCO, configure all model portfolios in the rebalancing platform. For each model, define: (a) Target asset class allocations with exact percentages, (b) Drift tolerance bands (typically ±3% for equities, ±2% for fixed income, ±1% for cash), (c) Rebalancing trigger thresholds (when any single asset class exceeds tolerance OR when total portfolio drift exceeds aggregate threshold), (d) Tax-loss harvesting rules including minimum loss thresholds, wash-sale lookback windows (31 days), and short-term/long-term gain preferences, (e) Cash reserve minimums and maximums, (f) Restricted securities lists (per client or firm-wide), (g) Account-level overrides for clients with unique IPS requirements.
- Example: Orion Eclipse model configuration (via platform UI, documented here as reference)
- Model: Moderate Growth 60/40
- Target Allocations: US Large Cap Equity: 30% (tolerance: ±3%), US Small/Mid Cap Equity: 10% (tolerance: ±3%), International Developed Equity: 12% (tolerance: ±3%), Emerging Markets Equity: 8% (tolerance: ±3%), US Aggregate Bond: 25% (tolerance: ±2%), International Bond: 7% (tolerance: ±2%), Short-Term Treasury: 3% (tolerance: ±1%), Cash: 5% (tolerance: ±1%, minimum 2%)
- Tax-Loss Harvesting Parameters: Minimum unrealized loss: $1,000, Wash-sale lookback: 31 days, Harvest only long-term losses unless short-term loss exceeds $5,000, Replacement security mapping: define tax-loss partners for each holding
- Rebalancing Trigger: ANY single asset class exceeds tolerance band, OR total portfolio drift score exceeds 5%, OR cash position drops below 2% minimum
This is the most critical configuration step — errors in model definitions or tolerance bands directly impact client portfolios and fiduciary compliance. The CCO MUST review and sign off on all model configurations and drift parameters before any live accounts are assigned. Document all model definitions in a compliance-approved spreadsheet that serves as the authoritative reference. Each model configuration should map directly to the firm's ADV Part 2A disclosures about investment strategy.
Step 6: Establish Custodian Data Feeds and Integration
Connect the rebalancing platform to the firm's custodian(s) for real-time or daily position, pricing, and cash data feeds. For Schwab: activate Schwab Advisor Center API access and configure FIX connectivity for trade execution. For Fidelity: establish Wealthscape data feeds. For Pershing: configure NetX360 integration. Verify that all accounts are mapped correctly, positions reconcile against custodian statements, and pricing data is accurate.
# Schwab API integration (conceptual — exact process varies by rebalancing platform)
# 1. Log into Schwab Advisor Center → Technology Solutions → API Access
# 2. Generate API credentials (Client ID + Secret)
# 3. In Orion Eclipse: Settings → Custodian Connections → Add Schwab
# Enter Client ID, Client Secret, and firm's Schwab master account number
# 4. Authorize OAuth2 connection (redirect to Schwab login for consent)
# 5. Configure data sync schedule: positions at 6:00 AM ET, prices real-time during market hours
# Verify reconciliation (Orion typically reconciles before market open)
# Check: Orion positions vs. Schwab Advisor Center positions
# Tolerance: $0.01 per position (should be exact match)
# Review: Cash balances, pending settlements, accrued income
# Test FIX trade routing (paper/test mode first)
# Generate a test rebalancing trade order in the platform
# Verify order appears in Schwab's staging queue
# Confirm order details: security, quantity, side (buy/sell), account numberCustodian integration is the most common point of delay — Schwab and Fidelity may require 2–4 weeks to provision API access. Start this process in parallel with model portfolio configuration. CRITICAL: Run a full position reconciliation across ALL accounts before enabling automated trade generation. Any discrepancy must be resolved with the custodian before go-live. Keep the custodian's technology support contact information readily available during this phase.
Step 7: Configure CRM Integration and Alert Workflows
Connect the rebalancing platform to the firm's CRM (Redtail or Wealthbox) to enable automated workflow triggers. Configure the following alert-to-action workflows: (1) Drift Alert → CRM Task: when a portfolio exceeds drift tolerance, automatically create a task in the CRM assigned to the responsible advisor with client name, account number, drift summary, and recommended rebalancing trades. (2) Rebalancing Completed → CRM Note: after rebalancing trades execute, log a note in the client's CRM record documenting the action taken. (3) Tax-Loss Harvesting Opportunity → CRM Alert: flag accounts with harvestable losses above the minimum threshold. (4) Cash Threshold Breach → CRM Task: alert when cash falls below minimum or exceeds maximum.
- Workflow: Drift Alert → Redtail Task | Trigger: Any account drift exceeds tolerance band | Action: Create Redtail activity | Type: Task | Category: Portfolio Management | Subject: 'REBALANCE NEEDED: [Client Name] - [Account#]' | Description: 'Portfolio drift detected. [Asset Class] at [Current%] vs [Target%] target (±[Tolerance%] band). Recommended action: [Buy/Sell summary]. Review and approve in [Rebalancing Platform].' | Assigned To: [Primary Advisor] | Due Date: [Today + 2 business days] | Priority: High
- Workflow: Tax-Loss Harvest Opportunity → Redtail Task | Trigger: Unrealized loss on any position exceeds $1,000 AND no wash-sale conflict | Action: Create Redtail activity | Subject: 'TAX-LOSS HARVEST: [Client Name] - [Security] - $[Loss Amount]' | Due Date: [Today + 5 business days] | Priority: Medium
Test all workflow automations in a sandbox/staging environment before connecting to the production CRM. Verify that CRM tasks are created accurately with correct client mapping and advisor assignment. The CCO should review sample alerts to confirm they contain appropriate information for supervisory review. Over-alerting is a real risk — tune drift tolerances to avoid generating excessive tasks that cause alert fatigue.
Step 8: Configure Compliance Monitoring and Audit Trail
Set up the compliance and supervisory framework required by SEC Rule 206(4)-7 and FINRA Rule 3110. Configure the rebalancing platform's built-in compliance features: (1) Pre-trade compliance checks — ensure all proposed rebalancing trades comply with client restrictions, concentration limits, and regulatory requirements before execution. (2) Supervisory review queue — configure a workflow where the CCO or designated supervisor must approve rebalancing trades above a threshold (e.g., trades over $50,000 or trades in restricted securities). (3) Audit trail logging — verify that ALL drift detections, alerts, rebalancing proposals, trade executions, and approvals are logged with timestamps and user attribution. (4) Export/archive schedule — configure weekly export of trade logs and monthly export of drift reports to the Synology NAS and cloud backup.
# Configure automated compliance export to Synology NAS
# Create shared folder on Synology for compliance archives
# On Synology DSM:
# Control Panel → Shared Folder → Create
# Name: ComplianceArchive
# Enable data checksum: Yes
# Enable encryption: Yes (AES-256)
# Permissions: Read/Write for compliance service account only
# Create scheduled task to pull exports (PowerShell on admin workstation)
# Run weekly via Task Scheduler
$exportDate = Get-Date -Format 'yyyy-MM-dd'
$nasPath = '\\SYNOLOGY-NAS\ComplianceArchive\TradeReports'
$exportFile = "$nasPath\rebalancing-report-$exportDate.csv"
# Note: Actual export mechanism depends on platform
# Orion: Reports → Scheduled Reports → Export to CSV → save to mapped NAS drive
# Configure Orion to email reports to a compliance mailbox AND save to NAS
# Verify NAS backup to Datto
# Datto SIRIS agent on NAS should capture ComplianceArchive folder
# Confirm in Datto portal: Protect → Synology NAS → Volumes → ComplianceArchive
# Retention: 5 years (SEC Rule 204-2 requirement)SEC Rule 204-2 mandates retention of all records related to investment advisory activities for at least 5 years (first 2 years in easily accessible location). The audit trail MUST capture who initiated the rebalance, what drift condition triggered it, the proposed trades, any modifications by the advisor, supervisory approval, and final execution confirmation. Have the CCO document the supervisory review procedures in the firm's compliance manual before go-live. This documentation will be essential during SEC examination.
Step 9: Conduct Paper-Trade Testing and Validation
Before enabling live trade execution, run the system in 'paper trade' or 'propose-only' mode for 2–4 weeks. The rebalancing platform will detect drift and generate trade proposals, but NO actual trades will be sent to custodians. During this period: (1) Verify drift calculations are accurate by manually spot-checking 10+ accounts against target allocations. (2) Confirm tolerance band triggers fire correctly — both for individual asset class drift and aggregate portfolio drift. (3) Review tax-loss harvesting proposals for accuracy — verify wash-sale conflict detection works. (4) Test CRM workflow integration — confirm tasks are created with correct client/advisor mapping. (5) Have the CCO review 20+ sample rebalancing proposals for compliance with client IPS and firm policies. (6) Document any parameter adjustments needed based on testing.
# Python spot-check script for MSP validation
# Manual drift calculation verification (Excel/Python spot-check)
# For each test account, calculate expected drift:
# Drift = |Current_Allocation% - Target_Allocation%|
# Example: US Large Cap target 30%, current 34% → Drift = 4%
# If tolerance is ±3%, this should trigger an alert
# Python spot-check script (optional, for MSP validation)
import pandas as pd
# Sample account data (export from rebalancing platform)
holdings = {
'US Large Cap': {'target': 0.30, 'current': 0.34},
'US Small Cap': {'target': 0.10, 'current': 0.08},
'Intl Developed': {'target': 0.12, 'current': 0.11},
'Emerging Markets': {'target': 0.08, 'current': 0.09},
'US Bonds': {'target': 0.25, 'current': 0.24},
'Intl Bonds': {'target': 0.07, 'current': 0.06},
'Short Treasury': {'target': 0.03, 'current': 0.03},
'Cash': {'target': 0.05, 'current': 0.05}
}
tolerance = {'equity': 0.03, 'bond': 0.02, 'cash': 0.01}
asset_type = {
'US Large Cap': 'equity', 'US Small Cap': 'equity',
'Intl Developed': 'equity', 'Emerging Markets': 'equity',
'US Bonds': 'bond', 'Intl Bonds': 'bond',
'Short Treasury': 'bond', 'Cash': 'cash'
}
for asset, alloc in holdings.items():
drift = abs(alloc['current'] - alloc['target'])
tol = tolerance[asset_type[asset]]
status = 'ALERT' if drift > tol else 'OK'
print(f"{asset}: Target={alloc['target']:.0%} Current={alloc['current']:.0%} Drift={drift:.1%} Tolerance=±{tol:.0%} → {status}")Paper-trade testing is NON-NEGOTIABLE — never go live with automated rebalancing without a validation period. The CCO must provide written approval that the system's drift detection and trade proposals meet the firm's fiduciary standards. Document all discrepancies found during testing and their resolution. Keep the testing documentation as part of the permanent compliance record. If more than 5% of proposals require manual correction, revisit model/tolerance configuration before proceeding.
Step 10: Go-Live: Enable Live Rebalancing with Phased Rollout
After successful paper-trade testing and CCO sign-off, enable live trade execution in a phased approach. Phase A (Week 1): Enable live rebalancing for 10–20% of accounts — select a representative sample across model portfolios and custodians. Monitor daily for 5 business days. Phase B (Week 2–3): Expand to 50% of accounts if no issues detected. Phase C (Week 4+): Enable remaining accounts. During the first 30 days, maintain heightened monitoring: daily reconciliation checks, daily review of CRM alerts, and weekly CCO review of all executed rebalancing trades.
The lead advisor should be the primary point of contact during the first 30 days — they know their clients and can quickly identify if any rebalancing action seems inappropriate. Set up a dedicated Slack/Teams channel or shared email for the MSP, lead advisor, and CCO to communicate issues during the rollout period. If ANY unexpected trade executes, immediately pause automated trading and investigate. Have the custodian's trade desk phone number on speed dial during the first week.
Step 11: Deliver Training and Documentation to Advisory Staff
Conduct structured training sessions for all advisory staff who will interact with the rebalancing system. Training should cover: (1) For Advisors: How to read drift reports, review and approve rebalancing proposals, handle client-specific overrides, understand tax-loss harvesting alerts, and use the CRM task workflow. (2) For Operations Staff: Daily reconciliation procedures, troubleshooting common data feed issues, managing cash flow events (contributions, distributions), and handling corporate actions. (3) For CCO: Supervisory review procedures, audit trail access, compliance report generation, and regulatory filing implications. Provide written runbooks for each role.
Record all training sessions for future reference and new employee onboarding. Create a one-page 'Quick Reference Card' for advisors showing the daily workflow: check CRM tasks → review drift alerts → approve/modify proposals → execute → document. Include screenshots specific to the firm's configured platform. Schedule a 30-day follow-up training session to address questions that arise from real-world usage.
Step 12: Configure Ongoing Monitoring and Alerting for MSP
Set up the MSP's internal monitoring to ensure the system continues operating correctly. Configure alerts in the MSP's RMM/PSA platform for: (1) Custodian data feed failures — if position data stops flowing, drift detection breaks. (2) Rebalancing platform availability — monitor the SaaS platform's status page and API endpoints. (3) Firewall health — FortiGate system events, UTM license expiration, firmware updates. (4) NAS health — Synology drive health, storage capacity, backup success/failure. (5) Endpoint compliance — BitLocker status, SentinelOne agent health, OS patch status. (6) Certificate/credential expiration — API keys, OAuth tokens, SSL certificates.
# Add FortiGate to MSP's monitoring (SNMP example)
# On FortiGate:
config system snmp sysinfo
set status enable
set contact-info 'MSP NOC - support@mspname.com'
end
config system snmp community
edit 1
set name 'MSP-Monitor-String'
config hosts
edit 1
set ip 10.10.10.250 255.255.255.255
next
end
set events cpu-high mem-low log-full intf-ip vpn-tun-up vpn-tun-down ha-switch av-virus ips-signature
next
end
# Synology SNMP monitoring
# DSM → Control Panel → Terminal & SNMP → SNMP → Enable SNMP
# Add community string matching MSP's monitoring platform
# PowerShell: Scheduled health check script (run daily via RMM)
$checks = @()
# Check SentinelOne agent status
$s1 = Get-Service -Name SentinelAgent -ErrorAction SilentlyContinue
$checks += [PSCustomObject]@{Check='SentinelOne'; Status=if($s1.Status -eq 'Running'){'OK'}else{'FAIL'}}
# Check BitLocker status
$bl = Get-BitLockerVolume -MountPoint 'C:' -ErrorAction SilentlyContinue
$checks += [PSCustomObject]@{Check='BitLocker'; Status=if($bl.ProtectionStatus -eq 'On'){'OK'}else{'FAIL'}}
# Output results
$checks | Format-Table -AutoSize
$failures = $checks | Where-Object {$_.Status -ne 'OK'}
if($failures){
# Send alert to MSP PSA/ticketing
Write-Warning 'Health check failures detected - create ticket'
}Set up a dedicated monitoring dashboard for all RIA clients in the MSP's NOC. Financial advisory systems have low tolerance for downtime — especially during market hours (9:30 AM – 4:00 PM ET). Define SLA: critical issues (data feed failure, security breach) = 15-minute response during market hours; standard issues (workstation problems, non-critical alerts) = 2-hour response. Schedule monthly security review calls with the advisory firm to review alerts, compliance status, and system health.
Custom AI Components
Portfolio Drift Calculator and Alert Engine
Type: workflow A lightweight Python-based drift monitoring service that can supplement or validate the primary rebalancing platform's drift detection. This component pulls portfolio data from custodian APIs or CSV exports, calculates drift against target model allocations, and generates structured alerts via email or webhook to the CRM. This is particularly useful for MSPs whose RIA clients use iRebal or other platforms without granular alert customization, or as an independent validation layer for compliance purposes.
Implementation
# supplementary drift detection and alerting service
#!/usr/bin/env python3
"""
Portfolio Drift Monitor - Supplementary drift detection and alerting service.
Designed to run as a scheduled job (daily after market close or on-demand).
Pulls portfolio data, calculates drift vs. model targets, and sends alerts.
Requirements:
pip install pandas requests jinja2 smtplib-ssl
Configuration: Set environment variables or update config dict below.
"""
import os
import json
import logging
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import pandas as pd
import requests
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('drift_monitor')
# ============================================================
# CONFIGURATION
# ============================================================
CONFIG = {
'firm_name': os.getenv('FIRM_NAME', 'Acme Wealth Advisors'),
'smtp_host': os.getenv('SMTP_HOST', 'smtp.office365.com'),
'smtp_port': int(os.getenv('SMTP_PORT', '587')),
'smtp_user': os.getenv('SMTP_USER', 'alerts@acmewealth.com'),
'smtp_password': os.getenv('SMTP_PASSWORD', ''),
'alert_recipients': os.getenv('ALERT_RECIPIENTS', 'advisor@acmewealth.com,cco@acmewealth.com').split(','),
'crm_webhook_url': os.getenv('CRM_WEBHOOK_URL', ''), # Optional: Redtail/Wealthbox webhook
'data_source': os.getenv('DATA_SOURCE', 'csv'), # 'csv' or 'api'
'portfolio_csv_path': os.getenv('PORTFOLIO_CSV', './data/current_holdings.csv'),
'models_json_path': os.getenv('MODELS_JSON', './config/model_portfolios.json'),
'output_dir': os.getenv('OUTPUT_DIR', './reports'),
'tax_loss_min_threshold': float(os.getenv('TLH_MIN', '1000')),
'wash_sale_lookback_days': int(os.getenv('WASH_SALE_DAYS', '31')),
}
# ============================================================
# MODEL PORTFOLIO DEFINITIONS
# ============================================================
# Example model_portfolios.json structure:
# {
# "Moderate Growth 60/40": {
# "allocations": {
# "US Large Cap": {"target": 0.30, "tolerance": 0.03, "asset_type": "equity"},
# "US Small Mid Cap": {"target": 0.10, "tolerance": 0.03, "asset_type": "equity"},
# "Intl Developed": {"target": 0.12, "tolerance": 0.03, "asset_type": "equity"},
# "Emerging Markets": {"target": 0.08, "tolerance": 0.03, "asset_type": "equity"},
# "US Aggregate Bond": {"target": 0.25, "tolerance": 0.02, "asset_type": "fixed_income"},
# "Intl Bond": {"target": 0.07, "tolerance": 0.02, "asset_type": "fixed_income"},
# "Short Term Treasury": {"target": 0.03, "tolerance": 0.01, "asset_type": "fixed_income"},
# "Cash": {"target": 0.05, "tolerance": 0.01, "asset_type": "cash"}
# },
# "aggregate_drift_threshold": 0.05,
# "cash_minimum": 0.02
# }
# }
def load_model_portfolios(path: str) -> dict:
"""Load model portfolio definitions from JSON file."""
with open(path, 'r') as f:
models = json.load(f)
logger.info(f"Loaded {len(models)} model portfolios")
return models
def load_portfolio_data_csv(path: str) -> pd.DataFrame:
"""
Load current portfolio holdings from CSV export.
Expected columns: account_id, client_name, advisor, model_name,
asset_class, current_value, total_account_value,
unrealized_gain_loss, purchase_date, security_name, ticker
"""
df = pd.read_csv(path)
required_cols = ['account_id', 'client_name', 'advisor', 'model_name',
'asset_class', 'current_value', 'total_account_value']
missing = [c for c in required_cols if c not in df.columns]
if missing:
raise ValueError(f"Missing required columns: {missing}")
logger.info(f"Loaded {len(df)} holdings across {df['account_id'].nunique()} accounts")
return df
def calculate_drift(account_holdings: pd.DataFrame, model: dict) -> List[dict]:
"""
Calculate drift for a single account against its assigned model.
Returns list of drift results per asset class.
"""
total_value = account_holdings['total_account_value'].iloc[0]
allocations = model['allocations']
results = []
# Aggregate current allocation by asset class
current_alloc = account_holdings.groupby('asset_class')['current_value'].sum()
current_pct = current_alloc / total_value if total_value > 0 else current_alloc * 0
for asset_class, params in allocations.items():
target = params['target']
tolerance = params['tolerance']
current = current_pct.get(asset_class, 0.0)
drift = current - target
abs_drift = abs(drift)
breached = abs_drift > tolerance
# Calculate rebalancing trade needed
trade_amount = -drift * total_value # Positive = buy, negative = sell
results.append({
'asset_class': asset_class,
'target_pct': target,
'current_pct': current,
'drift_pct': drift,
'abs_drift_pct': abs_drift,
'tolerance_pct': tolerance,
'breached': breached,
'trade_amount': trade_amount,
'asset_type': params.get('asset_type', 'unknown'),
})
return results
def calculate_aggregate_drift(drift_results: List[dict]) -> float:
"""Calculate aggregate portfolio drift score (sum of absolute drifts / 2)."""
return sum(r['abs_drift_pct'] for r in drift_results) / 2
def check_cash_minimum(drift_results: List[dict], model: dict) -> Optional[dict]:
"""Check if cash allocation is below the model's minimum."""
cash_min = model.get('cash_minimum', 0.02)
for r in drift_results:
if r['asset_class'].lower() == 'cash' and r['current_pct'] < cash_min:
return {
'alert_type': 'CASH_BELOW_MINIMUM',
'current_cash_pct': r['current_pct'],
'minimum_cash_pct': cash_min,
'shortfall_pct': cash_min - r['current_pct'],
}
return None
def detect_tax_loss_opportunities(account_holdings: pd.DataFrame) -> List[dict]:
"""
Identify holdings with unrealized losses exceeding the minimum threshold
and no wash-sale conflict (based on purchase date).
"""
opportunities = []
if 'unrealized_gain_loss' not in account_holdings.columns:
return opportunities
threshold = CONFIG['tax_loss_min_threshold']
washsale_cutoff = datetime.now() - timedelta(days=CONFIG['wash_sale_lookback_days'])
for _, row in account_holdings.iterrows():
ugl = row.get('unrealized_gain_loss', 0)
if ugl < -threshold: # Unrealized loss exceeds threshold
purchase_date = pd.to_datetime(row.get('purchase_date', None), errors='coerce')
wash_sale_risk = False
if purchase_date and purchase_date > washsale_cutoff:
wash_sale_risk = True
opportunities.append({
'security': row.get('security_name', 'Unknown'),
'ticker': row.get('ticker', 'N/A'),
'unrealized_loss': ugl,
'purchase_date': str(purchase_date.date()) if purchase_date else 'Unknown',
'wash_sale_risk': wash_sale_risk,
'asset_class': row.get('asset_class', 'Unknown'),
})
# Filter out wash-sale risks
clean_opportunities = [o for o in opportunities if not o['wash_sale_risk']]
return clean_opportunities
def analyze_all_accounts(portfolio_df: pd.DataFrame, models: dict) -> List[dict]:
"""Run drift analysis across all accounts. Returns list of account-level results."""
results = []
for account_id, account_data in portfolio_df.groupby('account_id'):
model_name = account_data['model_name'].iloc[0]
client_name = account_data['client_name'].iloc[0]
advisor = account_data['advisor'].iloc[0]
total_value = account_data['total_account_value'].iloc[0]
if model_name not in models:
logger.warning(f"Account {account_id}: Model '{model_name}' not found in definitions")
continue
model = models[model_name]
drift_results = calculate_drift(account_data, model)
agg_drift = calculate_aggregate_drift(drift_results)
agg_threshold = model.get('aggregate_drift_threshold', 0.05)
breached_classes = [r for r in drift_results if r['breached']]
cash_alert = check_cash_minimum(drift_results, model)
tlh_opportunities = detect_tax_loss_opportunities(account_data)
needs_rebalancing = len(breached_classes) > 0 or agg_drift > agg_threshold
account_result = {
'account_id': account_id,
'client_name': client_name,
'advisor': advisor,
'model_name': model_name,
'total_value': total_value,
'aggregate_drift': agg_drift,
'aggregate_threshold': agg_threshold,
'aggregate_breached': agg_drift > agg_threshold,
'needs_rebalancing': needs_rebalancing,
'drift_details': drift_results,
'breached_classes': breached_classes,
'cash_alert': cash_alert,
'tlh_opportunities': tlh_opportunities,
'analysis_timestamp': datetime.now().isoformat(),
}
results.append(account_result)
rebal_count = sum(1 for r in results if r['needs_rebalancing'])
logger.info(f"Analyzed {len(results)} accounts. {rebal_count} need rebalancing.")
return results
def generate_alert_email(flagged_accounts: List[dict]) -> str:
"""Generate HTML email body for drift alerts."""
html = f"""
<html><body>
<h2>Portfolio Drift Alert — {CONFIG['firm_name']}</h2>
<p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M ET')}</p>
<p><strong>{len(flagged_accounts)} account(s) require rebalancing review.</strong></p>
<table border='1' cellpadding='5' cellspacing='0' style='border-collapse:collapse;font-family:Arial,sans-serif;font-size:12px;'>
<tr style='background:#003366;color:white;'>
<th>Client</th><th>Account</th><th>Model</th><th>Value</th>
<th>Agg Drift</th><th>Breached Classes</th><th>TLH Opportunities</th><th>Cash Alert</th>
</tr>
"""
for acct in flagged_accounts:
breached_str = ', '.join([f"{b['asset_class']} ({b['drift_pct']:+.1%})" for b in acct['breached_classes']])
tlh_str = ', '.join([f"{t['ticker']} (${t['unrealized_loss']:,.0f})" for t in acct['tlh_opportunities']]) or 'None'
cash_str = f"Cash at {acct['cash_alert']['current_cash_pct']:.1%} (min {acct['cash_alert']['minimum_cash_pct']:.1%})" if acct['cash_alert'] else 'OK'
row_color = '#FFF3CD' if acct['aggregate_breached'] else '#FFFFFF'
html += f"""
<tr style='background:{row_color};'>
<td>{acct['client_name']}</td>
<td>{acct['account_id']}</td>
<td>{acct['model_name']}</td>
<td>${acct['total_value']:,.0f}</td>
<td>{acct['aggregate_drift']:.1%}</td>
<td>{breached_str}</td>
<td>{tlh_str}</td>
<td>{cash_str}</td>
</tr>
"""
html += """</table>
<p style='font-size:11px;color:#666;'>This is an automated alert from the Portfolio Drift Monitor.
Please review flagged accounts in your rebalancing platform and take appropriate action.
All actions are logged for compliance review.</p>
</body></html>"""
return html
def send_email_alert(html_body: str, subject: str):
"""Send alert email via SMTP (Microsoft 365)."""
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = CONFIG['smtp_user']
msg['To'] = ', '.join(CONFIG['alert_recipients'])
msg.attach(MIMEText(html_body, 'html'))
try:
with smtplib.SMTP(CONFIG['smtp_host'], CONFIG['smtp_port']) as server:
server.starttls()
server.login(CONFIG['smtp_user'], CONFIG['smtp_password'])
server.send_message(msg)
logger.info(f"Alert email sent to {CONFIG['alert_recipients']}")
except Exception as e:
logger.error(f"Failed to send email: {e}")
raise
def send_crm_webhook(flagged_accounts: List[dict]):
"""Send alerts to CRM via webhook (for Redtail/Wealthbox/Zapier integration)."""
if not CONFIG['crm_webhook_url']:
logger.info("No CRM webhook configured, skipping.")
return
for acct in flagged_accounts:
payload = {
'event': 'portfolio_drift_alert',
'timestamp': datetime.now().isoformat(),
'account_id': acct['account_id'],
'client_name': acct['client_name'],
'advisor': acct['advisor'],
'model': acct['model_name'],
'aggregate_drift': round(acct['aggregate_drift'], 4),
'breached_classes': [b['asset_class'] for b in acct['breached_classes']],
'needs_rebalancing': acct['needs_rebalancing'],
'tlh_count': len(acct['tlh_opportunities']),
'cash_alert': acct['cash_alert'] is not None,
'suggested_action': 'Review and approve rebalancing trades in platform',
}
try:
resp = requests.post(CONFIG['crm_webhook_url'], json=payload, timeout=10)
resp.raise_for_status()
logger.info(f"CRM webhook sent for account {acct['account_id']}")
except Exception as e:
logger.error(f"CRM webhook failed for {acct['account_id']}: {e}")
def save_report(all_results: List[dict], flagged: List[dict]):
"""Save detailed drift report as JSON and CSV for compliance archive."""
os.makedirs(CONFIG['output_dir'], exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M')
# JSON full report (for compliance archive)
json_path = os.path.join(CONFIG['output_dir'], f'drift_report_{timestamp}.json')
with open(json_path, 'w') as f:
json.dump(all_results, f, indent=2, default=str)
logger.info(f"Full report saved: {json_path}")
# CSV summary (for easy review)
summary_rows = []
for r in all_results:
summary_rows.append({
'account_id': r['account_id'],
'client_name': r['client_name'],
'advisor': r['advisor'],
'model': r['model_name'],
'total_value': r['total_value'],
'aggregate_drift': r['aggregate_drift'],
'needs_rebalancing': r['needs_rebalancing'],
'breached_count': len(r['breached_classes']),
'tlh_opportunities': len(r['tlh_opportunities']),
'cash_alert': r['cash_alert'] is not None,
'timestamp': r['analysis_timestamp'],
})
csv_path = os.path.join(CONFIG['output_dir'], f'drift_summary_{timestamp}.csv')
pd.DataFrame(summary_rows).to_csv(csv_path, index=False)
logger.info(f"Summary CSV saved: {csv_path}")
def main():
"""Main execution: load data, analyze drift, send alerts, save reports."""
logger.info("=== Portfolio Drift Monitor Starting ===")
# Load model definitions
models = load_model_portfolios(CONFIG['models_json_path'])
# Load current portfolio data
if CONFIG['data_source'] == 'csv':
portfolio_df = load_portfolio_data_csv(CONFIG['portfolio_csv_path'])
else:
raise NotImplementedError("API data source not yet implemented. Use CSV exports.")
# Analyze all accounts
all_results = analyze_all_accounts(portfolio_df, models)
# Filter accounts needing rebalancing
flagged = [r for r in all_results if r['needs_rebalancing']]
tlh_flagged = [r for r in all_results if len(r['tlh_opportunities']) > 0]
cash_flagged = [r for r in all_results if r['cash_alert'] is not None]
# Save compliance report (always, even if no alerts)
save_report(all_results, flagged)
# Send alerts if accounts need attention
if flagged:
subject = f"[DRIFT ALERT] {len(flagged)} accounts need rebalancing — {CONFIG['firm_name']}"
html = generate_alert_email(flagged)
send_email_alert(html, subject)
send_crm_webhook(flagged)
else:
logger.info("No accounts need rebalancing. No alerts sent.")
# Summary
logger.info(f"=== Analysis Complete ===")
logger.info(f"Total accounts analyzed: {len(all_results)}")
logger.info(f"Accounts needing rebalancing: {len(flagged)}")
logger.info(f"Accounts with TLH opportunities: {len(tlh_flagged)}")
logger.info(f"Accounts with cash alerts: {len(cash_flagged)}")
if __name__ == '__main__':
main()Deployment
- Save as
drift_monitor.pyon the admin workstation or a lightweight Azure/AWS VM - Install dependencies:
pip install pandas requests jinja2 - Create
config/model_portfolios.jsonwith the firm's model definitions - Schedule via Windows Task Scheduler or cron to run daily at 5:30 PM ET (after market close)
- Store credentials in environment variables, not in code
- Output reports are saved to the
reports/directory — map this to the Synology NAS for compliance archiving
Drift Tolerance Configuration Template
Type: prompt A structured JSON configuration template that defines all model portfolios, target allocations, drift tolerance bands, and rebalancing rules for the advisory firm. This template is consumed by both the rebalancing platform configuration and the custom drift calculator component. It serves as the single source of truth for investment models. Implementation:
# It must be reviewed and signed off by the CCO before deployment. Any
# changes to this file require a change management process with CCO
# approval. Store the versioned history in the compliance archive on the
# Synology NAS.
{
"firm_name": "Acme Wealth Advisors",
"last_updated": "2025-01-15",
"approved_by": "Jane Smith, CCO",
"models": {
"Conservative Income 30/70": {
"description": "Capital preservation with income focus. Suitable for retirees and low-risk-tolerance clients.",
"risk_score_range": [1, 35],
"allocations": {
"US Large Cap Equity": {"target": 0.15, "tolerance": 0.02, "asset_type": "equity", "benchmark_ticker": "VTI"},
"International Developed Equity": {"target": 0.08, "tolerance": 0.02, "asset_type": "equity", "benchmark_ticker": "VXUS"},
"Emerging Markets Equity": {"target": 0.02, "tolerance": 0.02, "asset_type": "equity", "benchmark_ticker": "VWO"},
"US Small Mid Cap Equity": {"target": 0.05, "tolerance": 0.02, "asset_type": "equity", "benchmark_ticker": "VXF"},
"US Aggregate Bond": {"target": 0.35, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "BND"},
"US Treasury Short-Term": {"target": 0.15, "tolerance": 0.01, "asset_type": "fixed_income", "benchmark_ticker": "VGSH"},
"TIPS": {"target": 0.10, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "VTIP"},
"International Bond": {"target": 0.05, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "BNDX"},
"Cash": {"target": 0.05, "tolerance": 0.01, "asset_type": "cash", "benchmark_ticker": null}
},
"aggregate_drift_threshold": 0.04,
"cash_minimum": 0.03,
"cash_maximum": 0.10,
"rebalancing_frequency": "quarterly_or_drift",
"tax_loss_harvest": true,
"tlh_minimum_loss": 1000,
"wash_sale_lookback_days": 31
},
"Moderate Growth 60/40": {
"description": "Balanced growth and income. Core model for most accumulation-phase clients.",
"risk_score_range": [36, 65],
"allocations": {
"US Large Cap Equity": {"target": 0.30, "tolerance": 0.03, "asset_type": "equity", "benchmark_ticker": "VTI"},
"US Small Mid Cap Equity": {"target": 0.10, "tolerance": 0.03, "asset_type": "equity", "benchmark_ticker": "VXF"},
"International Developed Equity": {"target": 0.12, "tolerance": 0.03, "asset_type": "equity", "benchmark_ticker": "VXUS"},
"Emerging Markets Equity": {"target": 0.08, "tolerance": 0.03, "asset_type": "equity", "benchmark_ticker": "VWO"},
"US Aggregate Bond": {"target": 0.25, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "BND"},
"International Bond": {"target": 0.07, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "BNDX"},
"US Treasury Short-Term": {"target": 0.03, "tolerance": 0.01, "asset_type": "fixed_income", "benchmark_ticker": "VGSH"},
"Cash": {"target": 0.05, "tolerance": 0.01, "asset_type": "cash", "benchmark_ticker": null}
},
"aggregate_drift_threshold": 0.05,
"cash_minimum": 0.02,
"cash_maximum": 0.10,
"rebalancing_frequency": "quarterly_or_drift",
"tax_loss_harvest": true,
"tlh_minimum_loss": 1000,
"wash_sale_lookback_days": 31
},
"Aggressive Growth 80/20": {
"description": "Growth-focused for long time horizon clients with high risk tolerance.",
"risk_score_range": [66, 85],
"allocations": {
"US Large Cap Equity": {"target": 0.35, "tolerance": 0.04, "asset_type": "equity", "benchmark_ticker": "VTI"},
"US Small Mid Cap Equity": {"target": 0.15, "tolerance": 0.04, "asset_type": "equity", "benchmark_ticker": "VXF"},
"International Developed Equity": {"target": 0.18, "tolerance": 0.04, "asset_type": "equity", "benchmark_ticker": "VXUS"},
"Emerging Markets Equity": {"target": 0.12, "tolerance": 0.04, "asset_type": "equity", "benchmark_ticker": "VWO"},
"US Aggregate Bond": {"target": 0.12, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "BND"},
"International Bond": {"target": 0.05, "tolerance": 0.02, "asset_type": "fixed_income", "benchmark_ticker": "BNDX"},
"Cash": {"target": 0.03, "tolerance": 0.01, "asset_type": "cash", "benchmark_ticker": null}
},
"aggregate_drift_threshold": 0.06,
"cash_minimum": 0.02,
"cash_maximum": 0.08,
"rebalancing_frequency": "quarterly_or_drift",
"tax_loss_harvest": true,
"tlh_minimum_loss": 1500,
"wash_sale_lookback_days": 31
},
"All Equity 100/0": {
"description": "Maximum growth for very long time horizons and highest risk tolerance.",
"risk_score_range": [86, 99],
"allocations": {
"US Large Cap Equity": {"target": 0.40, "tolerance": 0.05, "asset_type": "equity", "benchmark_ticker": "VTI"},
"US Small Mid Cap Equity": {"target": 0.15, "tolerance": 0.05, "asset_type": "equity", "benchmark_ticker": "VXF"},
"International Developed Equity": {"target": 0.25, "tolerance": 0.05, "asset_type": "equity", "benchmark_ticker": "VXUS"},
"Emerging Markets Equity": {"target": 0.15, "tolerance": 0.05, "asset_type": "equity", "benchmark_ticker": "VWO"},
"REITs": {"target": 0.03, "tolerance": 0.03, "asset_type": "equity", "benchmark_ticker": "VNQ"},
"Cash": {"target": 0.02, "tolerance": 0.01, "asset_type": "cash", "benchmark_ticker": null}
},
"aggregate_drift_threshold": 0.07,
"cash_minimum": 0.01,
"cash_maximum": 0.05,
"rebalancing_frequency": "quarterly_or_drift",
"tax_loss_harvest": true,
"tlh_minimum_loss": 2000,
"wash_sale_lookback_days": 31
}
},
"global_settings": {
"rebalancing_blackout_periods": ["tax_year_end_dec_15_to_jan_15"],
"minimum_trade_amount": 100,
"maximum_single_trade_pct": 0.25,
"supervisory_approval_threshold": 50000,
"restricted_securities": [],
"default_tax_lot_method": "specific_identification",
"short_term_gain_avoidance": true,
"short_term_threshold_days": 365
}
}CRM Task Automation Workflow (Zapier/Power Automate)
Type: integration An automation workflow that bridges the drift monitoring alerts to the CRM system when native integrations are insufficient. Uses Microsoft Power Automate (included with M365 Business Premium) or Zapier to listen for drift alert webhooks and create structured tasks in Redtail or Wealthbox CRM with all relevant rebalancing information.
Implementation:
# Drift Alert to CRM Task
# Microsoft Power Automate Flow Definition
# Flow Name: Drift Alert to CRM Task
# Trigger: When an HTTP request is received (webhook)
# Actions: Parse JSON → Condition → Create CRM Task
flow_name: "Portfolio Drift Alert → CRM Task Creator"
trigger:
type: http_webhook
method: POST
schema:
type: object
properties:
event: { type: string }
timestamp: { type: string }
account_id: { type: string }
client_name: { type: string }
advisor: { type: string }
model: { type: string }
aggregate_drift: { type: number }
breached_classes: { type: array, items: { type: string } }
needs_rebalancing: { type: boolean }
tlh_count: { type: integer }
cash_alert: { type: boolean }
suggested_action: { type: string }
steps:
- step: 1
action: parse_json
input: trigger_body
schema: (as above)
- step: 2
action: condition
if: "@equals(body('parse_json')?['needs_rebalancing'], true)"
then:
- step: 2a
action: compose
inputs:
task_subject: "REBALANCE NEEDED: @{body('parse_json')?['client_name']} - @{body('parse_json')?['account_id']}"
task_body: |
Portfolio drift detected for @{body('parse_json')?['client_name']}.
Account: @{body('parse_json')?['account_id']}
Model: @{body('parse_json')?['model']}
Aggregate Drift: @{formatNumber(body('parse_json')?['aggregate_drift'], 'P1')}
Breached Asset Classes: @{join(body('parse_json')?['breached_classes'], ', ')}
Tax-Loss Harvest Opportunities: @{body('parse_json')?['tlh_count']}
Cash Alert: @{if(body('parse_json')?['cash_alert'], 'YES - Cash below minimum', 'No')}
ACTION REQUIRED: Review and approve rebalancing trades in your trading platform.
Timestamp: @{body('parse_json')?['timestamp']}
task_due: "@{addDays(utcNow(), 2)}"
task_priority: high
# Option A: Redtail CRM via API
- step: 2b_redtail
action: http_request
method: POST
uri: "https://smf.crm3.redtailtechnology.com/api/public/v1/activities"
headers:
Authorization: "Userkeyauth @{variables('redtail_api_key')}"
Content-Type: application/json
body:
subject: "@{outputs('compose')?['task_subject']}"
category: 100 # Portfolio Management category ID
type_id: 1 # Task
priority: 1 # High
start_date: "@{utcNow()}"
end_date: "@{outputs('compose')?['task_due']}"
description: "@{outputs('compose')?['task_body']}"
status_id: 0 # Open
# Option B: Wealthbox CRM via API
- step: 2b_wealthbox
action: http_request
method: POST
uri: "https://api.crmworkspace.com/v1/tasks"
headers:
Authorization: "Bearer @{variables('wealthbox_api_token')}"
Content-Type: application/json
body:
name: "@{outputs('compose')?['task_subject']}"
description: "@{outputs('compose')?['task_body']}"
due_date: "@{outputs('compose')?['task_due']}"
priority: 2 # High
category: "Rebalancing"
assigned_to: "@{body('parse_json')?['advisor']}"
- step: 3
action: condition
if: "@greater(body('parse_json')?['tlh_count'], 0)"
then:
- step: 3a
action: send_email_v2
to: "@{variables('cco_email')}"
subject: "TAX-LOSS HARVEST: @{body('parse_json')?['client_name']} - @{body('parse_json')?['tlh_count']} opportunities"
body: "Tax-loss harvesting opportunities detected. Please review in trading platform."
variables:
redtail_api_key: "(stored in Power Automate connection or Azure Key Vault)"
wealthbox_api_token: "(stored in Power Automate connection or Azure Key Vault)"
cco_email: "cco@acmewealth.com"
notes: |
- Deploy this Power Automate flow in the firm's Microsoft 365 tenant
- The HTTP webhook URL generated by the trigger should be configured as the CRM_WEBHOOK_URL
in the Portfolio Drift Calculator component
- Use Azure Key Vault or Power Automate's secure connection store for API credentials
- Test with sample payloads before connecting to live drift monitor
- Monitor flow run history weekly for failuresCompliance Audit Report Generator
Type: skill A scheduled report generator that compiles weekly and monthly compliance-ready summaries of all drift detection events, rebalancing actions taken, tax-loss harvesting executions, and supervisory approvals. Outputs both PDF and CSV formats suitable for SEC examination preparation and internal compliance reviews.
Implementation:
# Python script for weekly/monthly compliance report generation
#!/usr/bin/env python3
"""
Compliance Audit Report Generator
Generates weekly/monthly compliance reports from drift monitoring data.
Outputs: PDF summary report + CSV detailed log
Schedule: Weekly (Fridays 6 PM ET) and Monthly (1st business day)
Requirements:
pip install pandas jinja2 weasyprint
(weasyprint requires system deps: apt-get install libpango1.0-dev libcairo2-dev on Linux
or use pre-built wheels on Windows)
"""
import os
import json
import glob
from datetime import datetime, timedelta
from typing import List
import pandas as pd
CONFIG = {
'reports_input_dir': os.getenv('DRIFT_REPORTS_DIR', './reports'),
'compliance_output_dir': os.getenv('COMPLIANCE_OUTPUT_DIR', './compliance_reports'),
'firm_name': os.getenv('FIRM_NAME', 'Acme Wealth Advisors'),
'cco_name': os.getenv('CCO_NAME', 'Jane Smith'),
'report_period': os.getenv('REPORT_PERIOD', 'weekly'), # 'weekly' or 'monthly'
}
def load_drift_reports(input_dir: str, start_date: datetime, end_date: datetime) -> List[dict]:
"""Load all drift report JSON files within the date range."""
all_results = []
pattern = os.path.join(input_dir, 'drift_report_*.json')
for filepath in sorted(glob.glob(pattern)):
filename = os.path.basename(filepath)
# Extract date from filename: drift_report_YYYYMMDD_HHMM.json
try:
date_str = filename.replace('drift_report_', '').replace('.json', '')
file_date = datetime.strptime(date_str, '%Y%m%d_%H%M')
except ValueError:
continue
if start_date <= file_date <= end_date:
with open(filepath, 'r') as f:
data = json.load(f)
for record in data:
record['report_date'] = file_date.isoformat()
all_results.extend(data)
return all_results
def generate_compliance_summary(results: List[dict], period_label: str) -> dict:
"""Generate summary statistics for the compliance report."""
if not results:
return {'total_analyses': 0, 'period': period_label}
df = pd.DataFrame(results)
total_accounts = df['account_id'].nunique()
total_analyses = len(results)
rebal_needed = df[df['needs_rebalancing'] == True]
unique_rebal_accounts = rebal_needed['account_id'].nunique() if len(rebal_needed) > 0 else 0
# Aggregate breach statistics
all_breaches = []
all_tlh = []
cash_alerts = 0
for r in results:
all_breaches.extend(r.get('breached_classes', []))
all_tlh.extend(r.get('tlh_opportunities', []))
if r.get('cash_alert'):
cash_alerts += 1
# Most commonly breached asset classes
breach_counts = {}
for b in all_breaches:
ac = b['asset_class']
breach_counts[ac] = breach_counts.get(ac, 0) + 1
summary = {
'period': period_label,
'firm_name': CONFIG['firm_name'],
'cco_name': CONFIG['cco_name'],
'generated_at': datetime.now().isoformat(),
'total_accounts_monitored': total_accounts,
'total_drift_analyses': total_analyses,
'accounts_needing_rebalancing': unique_rebal_accounts,
'rebalancing_rate_pct': round(unique_rebal_accounts / total_accounts * 100, 1) if total_accounts > 0 else 0,
'total_tolerance_breaches': len(all_breaches),
'breach_by_asset_class': dict(sorted(breach_counts.items(), key=lambda x: -x[1])),
'total_tlh_opportunities': len(all_tlh),
'total_tlh_value': sum(abs(t.get('unrealized_loss', 0)) for t in all_tlh),
'cash_alerts': cash_alerts,
'average_aggregate_drift': round(df['aggregate_drift'].mean(), 4) if 'aggregate_drift' in df.columns else 0,
'max_aggregate_drift': round(df['aggregate_drift'].max(), 4) if 'aggregate_drift' in df.columns else 0,
}
return summary
def generate_html_report(summary: dict, detailed_df: pd.DataFrame) -> str:
"""Generate HTML compliance report."""
breach_table = ''
for ac, count in summary.get('breach_by_asset_class', {}).items():
breach_table += f'<tr><td>{ac}</td><td>{count}</td></tr>\n'
html = f"""
<!DOCTYPE html>
<html>
<head><style>
body {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
h1 {{ color: #003366; border-bottom: 2px solid #003366; padding-bottom: 10px; }}
h2 {{ color: #004488; margin-top: 30px; }}
table {{ border-collapse: collapse; width: 100%; margin: 15px 0; }}
th {{ background: #003366; color: white; padding: 10px; text-align: left; }}
td {{ border: 1px solid #ddd; padding: 8px; }}
tr:nth-child(even) {{ background: #f9f9f9; }}
.metric {{ display: inline-block; background: #f0f4f8; border-radius: 8px; padding: 15px 25px; margin: 5px; text-align: center; }}
.metric-value {{ font-size: 28px; font-weight: bold; color: #003366; }}
.metric-label {{ font-size: 12px; color: #666; }}
.footer {{ margin-top: 40px; font-size: 11px; color: #999; border-top: 1px solid #ddd; padding-top: 10px; }}
.alert {{ background: #FFF3CD; border-left: 4px solid #FFC107; padding: 10px; margin: 10px 0; }}
</style></head>
<body>
<h1>Portfolio Drift Compliance Report</h1>
<p><strong>Firm:</strong> {summary['firm_name']}<br>
<strong>Period:</strong> {summary['period']}<br>
<strong>Generated:</strong> {summary['generated_at']}<br>
<strong>Prepared for:</strong> {summary['cco_name']}, Chief Compliance Officer</p>
<h2>Executive Summary</h2>
<div>
<div class='metric'><div class='metric-value'>{summary['total_accounts_monitored']}</div><div class='metric-label'>Accounts Monitored</div></div>
<div class='metric'><div class='metric-value'>{summary['accounts_needing_rebalancing']}</div><div class='metric-label'>Needed Rebalancing</div></div>
<div class='metric'><div class='metric-value'>{summary['rebalancing_rate_pct']}%</div><div class='metric-label'>Rebalancing Rate</div></div>
<div class='metric'><div class='metric-value'>{summary['total_tlh_opportunities']}</div><div class='metric-label'>TLH Opportunities</div></div>
<div class='metric'><div class='metric-value'>${summary['total_tlh_value']:,.0f}</div><div class='metric-label'>Potential TLH Value</div></div>
</div>
<h2>Drift Statistics</h2>
<table>
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>Total Drift Analyses Performed</td><td>{summary['total_drift_analyses']}</td></tr>
<tr><td>Average Aggregate Drift</td><td>{summary['average_aggregate_drift']:.2%}</td></tr>
<tr><td>Maximum Aggregate Drift</td><td>{summary['max_aggregate_drift']:.2%}</td></tr>
<tr><td>Total Tolerance Breaches</td><td>{summary['total_tolerance_breaches']}</td></tr>
<tr><td>Cash Threshold Alerts</td><td>{summary['cash_alerts']}</td></tr>
</table>
<h2>Breaches by Asset Class</h2>
<table>
<tr><th>Asset Class</th><th>Breach Count</th></tr>
{breach_table}
</table>
<h2>Supervisory Attestation</h2>
<div class='alert'>
<p>I, the undersigned Chief Compliance Officer, have reviewed this automated drift monitoring report
and confirm that all flagged accounts have been reviewed for compliance with their respective
Investment Policy Statements and the firm's fiduciary obligations.</p>
<p><br>Signature: _________________________ Date: _____________</p>
<p>Name: {summary['cco_name']}, CCO</p>
</div>
<div class='footer'>
<p>This report is generated automatically by the Portfolio Drift Monitoring System.
It is intended for internal compliance use only and should be retained for a minimum of 5 years
per SEC Rule 204-2. Contact your MSP for technical support.</p>
</div>
</body></html>
"""
return html
def main():
"""Generate compliance report for the configured period."""
now = datetime.now()
if CONFIG['report_period'] == 'weekly':
start_date = now - timedelta(days=7)
period_label = f"Week of {start_date.strftime('%B %d')} — {now.strftime('%B %d, %Y')}"
else: # monthly
start_date = now.replace(day=1) - timedelta(days=1)
start_date = start_date.replace(day=1)
period_label = f"{start_date.strftime('%B %Y')}"
results = load_drift_reports(CONFIG['reports_input_dir'], start_date, now)
summary = generate_compliance_summary(results, period_label)
if results:
df = pd.DataFrame(results)
else:
df = pd.DataFrame()
# Generate HTML report
html = generate_html_report(summary, df)
# Save outputs
os.makedirs(CONFIG['compliance_output_dir'], exist_ok=True)
timestamp = now.strftime('%Y%m%d')
prefix = 'weekly' if CONFIG['report_period'] == 'weekly' else 'monthly'
html_path = os.path.join(CONFIG['compliance_output_dir'], f'{prefix}_compliance_{timestamp}.html')
with open(html_path, 'w') as f:
f.write(html)
print(f"HTML report saved: {html_path}")
# Save CSV detail
if not df.empty:
csv_path = os.path.join(CONFIG['compliance_output_dir'], f'{prefix}_detail_{timestamp}.csv')
df.to_csv(csv_path, index=False)
print(f"CSV detail saved: {csv_path}")
# Save summary JSON
json_path = os.path.join(CONFIG['compliance_output_dir'], f'{prefix}_summary_{timestamp}.json')
with open(json_path, 'w') as f:
json.dump(summary, f, indent=2)
print(f"Summary JSON saved: {json_path}")
print(f"\nCompliance report generated for: {period_label}")
print(f"Accounts monitored: {summary['total_accounts_monitored']}")
print(f"Accounts needing rebalancing: {summary['accounts_needing_rebalancing']}")
if __name__ == '__main__':
main()Deployment:
- Schedule weekly run (Friday 6 PM ET) and monthly run (1st business day) via Task Scheduler or cron
- Output HTML report can be converted to PDF using weasyprint or printed to PDF from browser
- Store all outputs on Synology NAS ComplianceArchive share
- CCO should review and sign the attestation section of each report
- Retain for minimum 5 years per SEC Rule 204-2
Testing & Validation
- NETWORK SECURITY TEST: Run a vulnerability scan (Nessus or Qualys) against the FortiGate external interface and all VLAN segments. Verify zero critical or high vulnerabilities. Confirm SSL deep inspection is functioning by visiting an HTTPS site and verifying the FortiGate CA certificate appears in the browser chain.
- MFA ENFORCEMENT TEST: Attempt to log into Microsoft 365, the rebalancing platform (Orion/iRebal/Tamarac), and the CRM (Redtail/Wealthbox) without completing MFA. Verify that access is blocked in all cases. Test with each user account, not just the admin account.
- ENDPOINT SECURITY TEST: Verify SentinelOne agent is active and communicating on all workstations by checking the SentinelOne management console. Run an EICAR test file download and confirm it is detected and quarantined within 60 seconds. Verify BitLocker encryption is enabled on all drives.
- CUSTODIAN DATA FEED TEST: Compare 20 randomly selected account positions in the rebalancing platform against the custodian portal (Schwab Advisor Center, Fidelity Wealthscape, etc.). All positions, quantities, and market values must match within $0.01. Run this test for 5 consecutive business days to confirm data feed reliability.
- DRIFT CALCULATION ACCURACY TEST: For 10 accounts, manually calculate current allocation percentages in Excel using custodian data. Compare against the rebalancing platform's drift report. All values must match within 0.1%. Document any discrepancies and root causes.
- TOLERANCE BAND TRIGGER TEST: Identify or create (in sandbox) an account where at least one asset class has drifted beyond its configured tolerance band. Verify the platform generates a drift alert within the expected monitoring cycle. Verify the alert contains correct asset class, current allocation, target allocation, and drift amount.
- AGGREGATE DRIFT THRESHOLD TEST: Verify that an account with multiple small drifts that collectively exceed the aggregate drift threshold (e.g., 5%) triggers an alert even if no single asset class has breached its individual tolerance band.
- CASH MINIMUM ALERT TEST: Simulate or identify an account where cash allocation has dropped below the configured minimum (e.g., 2%). Verify a cash threshold breach alert is generated in both the platform dashboard and CRM task queue.
- TAX-LOSS HARVESTING DETECTION TEST: Identify an account with an unrealized loss exceeding the configured minimum ($1,000). Verify the platform flags it as a TLH opportunity. Then identify a position with a loss but a purchase date within the 31-day wash-sale window and verify it is NOT flagged (wash-sale conflict detected).
- CRM TASK CREATION TEST: Trigger a drift alert (via the rebalancing platform or custom webhook) and verify a corresponding task is created in Redtail or Wealthbox CRM with: correct client name, account number, advisor assignment, drift details in the description, due date within 2 business days, and high priority.
- TRADE PROPOSAL VALIDATION TEST: In paper-trade mode, review 20 rebalancing trade proposals generated by the platform. Verify each proposed trade would move the account closer to its target allocation. Verify no proposals violate client restrictions (restricted securities, concentration limits). Have the lead advisor and CCO sign off on the sample.
- COMPLIANCE AUDIT TRAIL TEST: Generate a rebalancing proposal, approve it, and (in paper-trade mode) execute it. Then locate the complete audit trail in the platform: the drift event that triggered the proposal, the proposed trades, who approved them, timestamps for each action, and the simulated execution record. Export this audit trail and verify it can be saved to the Synology NAS.
- BACKUP AND RECOVERY TEST: Verify Datto SIRIS is successfully backing up the Synology NAS ComplianceArchive folder. Perform a test restore of a compliance report file from 7 days ago. Verify the file is intact and readable.
- SUPERVISORY REVIEW QUEUE TEST: Generate a trade proposal exceeding the supervisory approval threshold (e.g., >$50,000). Verify the platform routes it to the CCO's approval queue and does NOT allow execution without CCO approval.
- END-TO-END WORKFLOW TEST: Starting from a portfolio with intentional drift, verify the complete workflow: (1) Platform detects drift, (2) Alert appears in advisor dashboard, (3) CRM task is created, (4) Advisor reviews and approves rebalancing trades, (5) CCO approves trades above threshold, (6) Paper trades execute, (7) Compliance report captures the event, (8) Report exports successfully to NAS. Document timing of each step.
Client Handoff
The client handoff should be conducted as a structured meeting (2–3 hours) with the firm's principals, lead advisor, operations manager, and CCO present. Cover the following topics:
Documentation to Leave Behind
Success Criteria Review
Maintenance
Ongoing MSP Responsibilities:
Daily (Automated + Spot-Check):
- Monitor custodian data feed health via platform dashboard or custom alerts — any feed failure during market hours is P1
- Verify SentinelOne agent health across all endpoints via MSP console
- Check FortiGate system events for security alerts, IPS triggers, or connectivity issues
- Confirm Datto backup completion for NAS and cloud data
Weekly:
- Review FortiGate firewall logs for anomalous traffic patterns or blocked intrusion attempts
- Verify the weekly compliance drift report was generated and archived to NAS
- Check rebalancing platform for any failed or stuck trade orders from the prior week
- Review CRM task queue to ensure no orphaned or duplicate alerts
- Confirm the custom drift monitor script (if deployed) ran successfully each day
Monthly:
- Patch all endpoints (Windows updates, application updates) during a scheduled maintenance window — avoid patching during market hours (9:30 AM – 4:00 PM ET)
- Update FortiGate firmware if a new stable release is available
- Review and rotate API keys/tokens for custodian and CRM integrations (if expiration-based)
- Conduct monthly security review call with the advisory firm — review security alerts, compliance report summary, and any system issues
- Verify NAS storage capacity — alert if below 20% free space
- Review SentinelOne threat reports and KnowBe4 phishing simulation results with the firm
Quarterly:
- Full security assessment: re-run vulnerability scan, review firewall rules, verify MFA compliance, check for deprovisioned user accounts that should be removed
- Review drift tolerance parameters with the lead advisor — market conditions may warrant adjustment of tolerance bands
- CCO compliance review meeting — present quarterly summary of drift events, rebalancing activity, and any system issues
- Test disaster recovery: perform a full restore test from Datto backup
- Review vendor contracts and renewal dates — Orion, Redtail, SentinelOne, FortiGate UTP
Annually:
- Full compliance audit preparation: compile all drift reports, rebalancing records, trade logs, and supervisory approvals for the year
- Review and update the firm's cybersecurity policies and incident response plan (SEC Reg S-P requirement)
- Reassess the technology stack — evaluate if the rebalancing platform still meets the firm's needs as AUM grows
- Conduct a tabletop cybersecurity exercise with firm staff
- Review and renew all vendor agreements, hardware warranties, and insurance policies
Model/Parameter Update Triggers:
- Any change to the firm's model portfolios or investment strategy requires immediate reconfiguration of the rebalancing platform and update to the model_portfolios.json configuration file
- Significant market events (e.g., major index moves >10%) may warrant temporary adjustment of drift tolerance bands — coordinate with lead advisor and CCO
- Regulatory changes from the SEC or FINRA affecting automated trading or rebalancing require compliance policy review within 30 days
- New custodial relationship requires integration configuration and 2-week parallel testing period
SLA Framework:
- P1 (Critical — data feed failure during market hours, security breach, platform outage): 15-minute response, 1-hour resolution target during market hours (9:30 AM – 4:00 PM ET)
- P2 (High — failed trades, reconciliation discrepancies, compliance report failures): 1-hour response, 4-hour resolution during business hours
- P3 (Medium — workstation issues, non-critical alerts, user support): 2-hour response, next-business-day resolution
- P4 (Low — feature requests, documentation updates, non-urgent questions): 1-business-day response, scheduled resolution
Escalation Path:
Alternatives
Schwab iRebal (Zero-Cost Entry Point)
For RIAs that custody exclusively with Charles Schwab, iRebal is included at no additional cost with the custody relationship. It provides real-time positions, drift monitoring, model-based rebalancing, and direct trade execution through Schwab. This eliminates the primary software cost entirely, with the MSP focusing solely on infrastructure, security, and compliance.
Altruist All-in-One Platform
Altruist operates as an integrated custodian + technology platform, combining custody, trading, rebalancing, reporting, and billing in a single interface. The RIA moves their custody to Altruist and gets rebalancing and portfolio management tools included. This eliminates the need for separate rebalancing software entirely.
Envestnet Tamarac Trading
Envestnet Tamarac is a leading portfolio management, reporting, trading, and rebalancing platform widely used by mid-size RIAs. It offers deep CRM integration, automated model construction, and direct broker execution. Pricing is AUM-based and negotiated per firm.
SS&C Black Diamond with Rebalancing
Black Diamond provides portfolio management, reporting, and rebalancing with an emphasis on sophisticated N-tier model architecture and tax-efficient trading. Pricing is approximately 1 basis point of AUM annually.
Custom Python Drift Analytics (Self-Built)
Build a fully custom drift detection and rebalancing recommendation engine using Python with libraries like pyportfolioopt, riskfolio-lib, and quantlib. Pulls data from custodian APIs, runs drift calculations with custom logic, and generates alerts via email or webhook.
Nitrogen Autopilot as Primary Rebalancer
Use Nitrogen (formerly Riskalyze) Autopilot as the primary rebalancing engine. Nitrogen is widely known for its Risk Number questionnaire and Autopilot adds model-based rebalancing and block trading capabilities on top of the risk assessment platform.
Want early access to the full toolkit?