06/03 Optimize codes, fix Schwab Account
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,9 +9,13 @@
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Refresh Prices
|
||||
</button>
|
||||
</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">
|
||||
<i class="bi bi-bank2 me-1"></i>Sync Schwab
|
||||
</a>
|
||||
<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
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
||||
@@ -64,9 +68,12 @@
|
||||
<a href="{{ url_for('investments.new') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Holding
|
||||
</a>
|
||||
<a href="{{ url_for('schwab.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-bank2 me-1"></i>Sync from Schwab
|
||||
</a>
|
||||
<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
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user