from __future__ import annotations from nicegui import ui from app.pages.common import dashboard_page from app.models.portfolio import PortfolioConfig, get_portfolio_repository @ui.page("/settings") def settings_page(): """Settings page with persistent portfolio configuration.""" # Load current configuration repo = get_portfolio_repository() config = repo.load() with dashboard_page( "Settings", "Configure portfolio assumptions, preferred market data inputs, alert thresholds, and import/export behavior.", "settings", ): with ui.row().classes("w-full gap-6 max-lg:flex-col"): with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Portfolio Parameters").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") gold_value = ui.number( "Gold collateral value ($)", value=config.gold_value, min=0.01, # Must be positive step=1000 ).classes("w-full") loan_amount = ui.number( "Loan amount ($)", value=config.loan_amount, min=0, step=1000 ).classes("w-full") margin_threshold = ui.number( "Margin call LTV threshold", value=config.margin_threshold, min=0.1, max=0.95, step=0.01 ).classes("w-full") monthly_budget = ui.number( "Monthly hedge budget ($)", value=config.monthly_budget, min=0, step=500 ).classes("w-full") # Show calculated values with ui.row().classes("w-full gap-2 mt-4 p-4 bg-slate-50 dark:bg-slate-800 rounded-lg"): ui.label("Current LTV:").classes("font-medium") ltv_display = ui.label(f"{(config.loan_amount / config.gold_value * 100):.1f}%") ui.label("Margin buffer:").classes("font-medium ml-4") buffer_display = ui.label(f"{((config.margin_threshold - config.loan_amount / config.gold_value) * 100):.1f}%") ui.label("Margin call at:").classes("font-medium ml-4") margin_price_display = ui.label(f"${(config.loan_amount / config.margin_threshold):,.2f}") with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Data Sources").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") primary_source = ui.select( ["yfinance", "ibkr", "alpaca"], value=config.primary_source, label="Primary source", ).classes("w-full") fallback_source = ui.select( ["fallback", "yfinance", "manual"], value=config.fallback_source, label="Fallback source", ).classes("w-full") refresh_interval = ui.number( "Refresh interval (seconds)", value=config.refresh_interval, min=1, step=1 ).classes("w-full") def update_calculations(): """Update calculated displays when values change.""" try: gold = gold_value.value or 1 # Avoid division by zero loan = loan_amount.value or 0 margin = margin_threshold.value or 0.75 ltv = (loan / gold) * 100 buffer = (margin - loan / gold) * 100 margin_price = loan / margin if margin > 0 else 0 ltv_display.set_text(f"{ltv:.1f}%") buffer_display.set_text(f"{buffer:.1f}%") margin_price_display.set_text(f"${margin_price:,.2f}") except Exception: pass # Ignore calculation errors during editing # Connect update function to value changes gold_value.on_value_change(update_calculations) loan_amount.on_value_change(update_calculations) margin_threshold.on_value_change(update_calculations) with ui.row().classes("w-full gap-6 max-lg:flex-col"): with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Alert Thresholds").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") ltv_warning = ui.number( "LTV warning level", value=config.ltv_warning, min=0.1, max=0.95, step=0.01 ).classes("w-full") vol_alert = ui.number( "Volatility spike alert", value=config.volatility_spike, min=0.01, max=2.0, step=0.01 ).classes("w-full") price_alert = ui.number( "Spot drawdown alert (%)", value=config.spot_drawdown, min=0.1, max=50.0, step=0.5 ).classes("w-full") email_alerts = ui.switch( "Email alerts", value=config.email_alerts ) with ui.card().classes( "w-full rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900" ): ui.label("Export / Import").classes("text-lg font-semibold text-slate-900 dark:text-slate-100") export_format = ui.select( ["json", "csv", "yaml"], value="json", label="Export format" ).classes("w-full") ui.switch("Include scenario history", value=True) ui.switch("Include option selections", value=True) ui.button("Import settings", icon="upload").props("outline color=primary") ui.button("Export settings", icon="download").props("outline color=primary") def save_settings(): """Save settings with validation and persistence.""" try: # Create new config from form values new_config = PortfolioConfig( gold_value=float(gold_value.value), loan_amount=float(loan_amount.value), margin_threshold=float(margin_threshold.value), monthly_budget=float(monthly_budget.value), ltv_warning=float(ltv_warning.value), primary_source=str(primary_source.value), fallback_source=str(fallback_source.value), refresh_interval=int(refresh_interval.value), volatility_spike=float(vol_alert.value), spot_drawdown=float(price_alert.value), email_alerts=bool(email_alerts.value), ) # Save to repository repo.save(new_config) status.set_text( f"Saved: gold=${new_config.gold_value:,.0f}, " f"loan=${new_config.loan_amount:,.0f}, " f"LTV={new_config.current_ltv:.1%}, " f"margin={new_config.margin_threshold:.1%}, " f"buffer={new_config.margin_buffer:.1%}" ) ui.notify("Settings saved successfully", color="positive") except ValueError as e: ui.notify(f"Validation error: {e}", color="negative") except Exception as e: ui.notify(f"Failed to save: {e}", color="negative") with ui.row().classes("w-full items-center justify-between gap-4 mt-6"): status = ui.label( f"Current: gold=${config.gold_value:,.0f}, loan=${config.loan_amount:,.0f}, " f"current LTV={config.current_ltv:.1%}" ).classes("text-sm text-slate-500 dark:text-slate-400") ui.button("Save settings", on_click=save_settings).props("color=primary")