06/03 Optimize codes, fix Schwab Account

This commit is contained in:
2026-06-03 14:04:39 -04:00
parent b75f18d6c8
commit 5c8854a65a
3 changed files with 67 additions and 9 deletions
+40
View File
@@ -93,6 +93,46 @@ def _recalc_holding(investment):
db.session.commit() 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('/') @investments_bp.route('/')
@login_required @login_required
def index(): def index():
+14 -3
View File
@@ -239,15 +239,26 @@ def sync_account_snapshot(schwab_account):
# ── 2. Positions ────────────────────────────────────────────────────────── # ── 2. Positions ──────────────────────────────────────────────────────────
positions_synced = 0 positions_synced = 0
for pos in sec.get('positions', []): raw_positions = sec.get('positions') or [] # guard: API may send null
instrument = pos.get('instrument', {}) 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', '') asset_key = instrument.get('assetType', '')
symbol = (instrument.get('symbol') or '').upper().strip() symbol = (instrument.get('symbol') or '').upper().strip()
long_qty = float(pos.get('longQuantity') or 0) long_qty = float(pos.get('longQuantity') or 0)
pfm_type = ASSET_TYPE_MAP.get(asset_key) 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 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) avg_price = float(pos.get('averagePrice') or pos.get('averageLongPrice') or 0)
market_value = float(pos.get('marketValue') or 0) market_value = float(pos.get('marketValue') or 0)
+11 -4
View File
@@ -9,9 +9,13 @@
<i class="bi bi-arrow-clockwise me-1"></i>Refresh Prices <i class="bi bi-arrow-clockwise me-1"></i>Refresh Prices
</button> </button>
</form> </form>
<a href="{{ url_for('schwab.index') }}" class="btn btn-sm btn-outline-secondary me-1" style="font-size:12px;" title="Sync positions from Schwab"> <form method="POST" action="{{ url_for('investments.sync_schwab') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-secondary me-1" style="font-size:12px;"
title="Pull latest holdings and balances from Schwab">
<i class="bi bi-bank2 me-1"></i>Sync Schwab <i class="bi bi-bank2 me-1"></i>Sync Schwab
</a> </button>
</form>
<a href="{{ url_for('investments.new') }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Add Holding</a> <a href="{{ url_for('investments.new') }}" class="btn btn-sm btn-primary" style="font-size:12px;"><i class="bi bi-plus-lg me-1"></i>Add Holding</a>
{% endblock %} {% endblock %}
@@ -64,9 +68,12 @@
<a href="{{ url_for('investments.new') }}" class="btn btn-primary btn-sm"> <a href="{{ url_for('investments.new') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i>Add Holding <i class="bi bi-plus-lg me-1"></i>Add Holding
</a> </a>
<a href="{{ url_for('schwab.index') }}" class="btn btn-outline-secondary btn-sm"> <form method="POST" action="{{ url_for('investments.sync_schwab') }}" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-bank2 me-1"></i>Sync from Schwab <i class="bi bi-bank2 me-1"></i>Sync from Schwab
</a> </button>
</form>
</div> </div>
</div> </div>
{% else %} {% else %}