diff --git a/app/routes/investments.py b/app/routes/investments.py index e210936..20635a7 100644 --- a/app/routes/investments.py +++ b/app/routes/investments.py @@ -93,6 +93,46 @@ def _recalc_holding(investment): db.session.commit() +@investments_bp.route('/sync-schwab', methods=['POST']) +@login_required +def sync_schwab(): + """Sync balance + positions for every mapped Schwab account, then return here.""" + from app.models.schwab_connection import SchwabConnection, SchwabAccount + from app.services.schwab_service import sync_account_snapshot + import logging + log = logging.getLogger(__name__) + + connection = SchwabConnection.query.filter_by(is_active=True).first() + if not connection: + flash('No active Schwab connection. Connect at the Schwab page first.', 'warning') + return redirect(url_for('investments.index')) + + accounts = SchwabAccount.query.filter( + SchwabAccount.connection_id == connection.id, + SchwabAccount.pfm_account_id != None, + SchwabAccount.is_active == True, + ).all() + + if not accounts: + flash('No Schwab accounts mapped yet. Map them on the Schwab page first.', 'warning') + return redirect(url_for('investments.index')) + + total_pos = 0 + for sa in accounts: + try: + _, pos = sync_account_snapshot(sa) + total_pos += pos + except Exception as e: + log.error('[schwab] sync_schwab investments failed for %s: %s', sa.account_name, e, exc_info=True) + flash(f'Sync failed for {sa.account_name}: {e}', 'danger') + + if total_pos: + flash(f'Synced {total_pos} holding(s) from Schwab.', 'success') + else: + flash('Sync complete — no positions found. Check System Logs for details.', 'info') + return redirect(url_for('investments.index')) + + @investments_bp.route('/') @login_required def index(): diff --git a/app/services/schwab_service.py b/app/services/schwab_service.py index a5ffa9f..cac4778 100644 --- a/app/services/schwab_service.py +++ b/app/services/schwab_service.py @@ -239,15 +239,26 @@ def sync_account_snapshot(schwab_account): # ── 2. Positions ────────────────────────────────────────────────────────── positions_synced = 0 - for pos in sec.get('positions', []): - instrument = pos.get('instrument', {}) + raw_positions = sec.get('positions') or [] # guard: API may send null + log.info('[schwab] %s has %d position(s) in API response', + schwab_account.account_name, len(raw_positions)) + + for pos in raw_positions: + instrument = pos.get('instrument') or {} asset_key = instrument.get('assetType', '') symbol = (instrument.get('symbol') or '').upper().strip() long_qty = float(pos.get('longQuantity') or 0) pfm_type = ASSET_TYPE_MAP.get(asset_key) - if not pfm_type or not symbol or long_qty <= 0: + log.debug('[schwab] position: symbol=%s assetType=%s longQty=%s pfm_type=%s', + symbol, asset_key, long_qty, pfm_type) + + # Skip unknowns, empty symbols, and zero-quantity positions + if not symbol or long_qty <= 0: continue + # Fall back to 'other' if the asset type isn't in our map + if not pfm_type: + pfm_type = 'other' avg_price = float(pos.get('averagePrice') or pos.get('averageLongPrice') or 0) market_value = float(pos.get('marketValue') or 0) diff --git a/app/templates/investments/index.html b/app/templates/investments/index.html index 4f02b11..9ecf832 100644 --- a/app/templates/investments/index.html +++ b/app/templates/investments/index.html @@ -9,9 +9,13 @@ Refresh Prices - - Sync Schwab - +
+ + +
Add Holding {% endblock %} @@ -64,9 +68,12 @@ Add Holding - - Sync from Schwab - +
+ + +
{% else %}