06/03 Optimize codes, fix Schwab Account
This commit is contained in:
@@ -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():
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user