#!/usr/bin/env python3 """ Schwab daily auto-sync — balance, positions, and transactions. Add to crontab (crontab -e): # Sync Schwab every day at 7 AM 0 7 * * * /home/pfm/venv/bin/python /home/pfm/web/scripts/sync_schwab.py >> /home/pfm/web/logs/sync_schwab.log 2>&1 Or run manually: python scripts/sync_schwab.py """ import sys import os import logging from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import create_app from app.extensions import db logging.basicConfig( level=logging.INFO, format='%(asctime)s|%(levelname)s|%(message)s', datefmt='%Y-%m-%d %H:%M:%S', ) log = logging.getLogger(__name__) app = create_app() with app.app_context(): from app.models.schwab_connection import SchwabConnection, SchwabAccount from app.services.schwab_service import ( sync_account_snapshot, sync_preview, import_transactions, ) connection = SchwabConnection.query.filter_by(is_active=True).first() if not connection: log.warning('[sync_schwab] No active Schwab connection — skipping.') sys.exit(0) # Warn if refresh token is close to expiry if connection.refresh_token_expires_at: days_left = (connection.refresh_token_expires_at - datetime.utcnow()).days if days_left <= 2: log.warning('[sync_schwab] Schwab refresh token expires in %d day(s)! ' 'Log in and reconnect at /schwab/connect.', days_left) accounts = SchwabAccount.query.filter( SchwabAccount.pfm_account_id != None, SchwabAccount.is_active == True, ).all() if not accounts: log.warning('[sync_schwab] No mapped Schwab accounts found.') sys.exit(0) log.info('[sync_schwab] Syncing %d account(s)…', len(accounts)) for sa in accounts: sa.connection = connection # always use active connection # ── 1. Balance + positions ──────────────────────────────────────────── try: bal_updated, pos_synced = sync_account_snapshot(sa) log.info('[sync_schwab] %s — balance updated=%s positions=%d', sa.account_name, bal_updated, pos_synced) except Exception as e: log.error('[sync_schwab] snapshot failed for %s: %s', sa.account_name, e, exc_info=True) # ── 2. New transactions ─────────────────────────────────────────────── try: preview = sync_preview(sa) if preview: imported, skipped = import_transactions(preview, sa) log.info('[sync_schwab] %s — transactions: imported=%d skipped=%d', sa.account_name, imported, skipped) else: log.info('[sync_schwab] %s — no new transactions.', sa.account_name) except Exception as e: log.error('[sync_schwab] transaction sync failed for %s: %s', sa.account_name, e, exc_info=True) log.info('[sync_schwab] Done.')