from __future__ import annotations from datetime import UTC, datetime, timedelta import httpx from arbitrade.api.app import create_app from arbitrade.config.settings import Settings def _seed_metrics_data(app) -> None: store = app.state.store started = datetime.now(UTC) finished = started + timedelta(seconds=20) with store.connect() as conn: conn.execute( """ INSERT INTO trades ( trade_ref, started_at, finished_at, status, realized_pnl, estimated_pnl, capital_used, cycle, leg_count ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ "trade-1", started, finished, "filled", 15.0, 10.0, 100.0, "USD->BTC->ETH->USD", 3, ], ) conn.execute( """ INSERT INTO opportunities ( detected_at, cycle, gross_pct, net_pct, est_profit, executed ) VALUES (?, ?, ?, ?, ?, ?) """, [started, "USD->BTC->ETH->USD", 4.0, 3.0, 0.03, True], ) conn.execute( """ INSERT INTO orders ( trade_ref, order_ref, leg_index, pair, side, volume, user_ref, status, filled_volume, avg_price, raw_response, recorded_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ "trade-1", "order-1", 0, "BTC/USD", "buy", 2.0, 100, "closed", 2.0, 100.0, "{}", started, ], ) conn.execute( """ INSERT INTO portfolio_snapshots ( snapshot_at, balances, total_value_usd ) VALUES (?, ?, ?) """, [started, '{"USD": 1000.0, "BTC": 0.25}', 1250.0], ) conn.execute( """ INSERT INTO trades ( trade_ref, started_at, finished_at, status, realized_pnl, estimated_pnl, capital_used, cycle, leg_count ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ "trade-open", started, None, "open", None, 5.0, 50.0, "USD->BTC->ETH->USD", 3, ], ) async def test_dashboard_page_and_fragment_and_sse(tmp_path) -> None: app = create_app(Settings(DUCKDB_PATH=tmp_path / "dash.duckdb")) _seed_metrics_data(app) transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: page = await client.get("/") fragment = await client.get("/dashboard/fragment/metrics") stream = await client.get("/dashboard/stream/metrics") overview = await client.get("/dashboard/fragment/overview") overview_stream = await client.get("/dashboard/stream/overview") controls = await client.get("/dashboard/fragment/controls") assert page.status_code == 200 assert "EventSource" in page.text assert 'hx-get="/dashboard/fragment/metrics"' in page.text assert 'hx-get="/dashboard/fragment/controls"' in page.text assert fragment.status_code == 200 assert "Realized P&L" in fragment.text assert "15.00 USD" in fragment.text assert "100.0%" in fragment.text assert stream.status_code == 200 assert stream.headers["content-type"].startswith("text/event-stream") assert "event: metrics" in stream.text assert "Realized P&L" in stream.text assert overview.status_code == 200 assert "live" in overview.text assert "Balances Snapshot" in overview.text assert "Open Trades" in overview.text assert "Opportunity Feed" in overview.text assert "1250.00 USD" in overview.text assert "trade-open" in overview.text assert overview_stream.status_code == 200 assert overview_stream.headers["content-type"].startswith( "text/event-stream") assert "event: overview" in overview_stream.text assert "trade-open" in overview_stream.text assert controls.status_code == 200 assert "Runtime Status" in controls.text assert ">running<" in controls.text assert "Paper trading mode" in controls.text assert "Trade capital USD" in controls.text async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> None: app = create_app(Settings(DUCKDB_PATH=tmp_path / "controls.duckdb")) transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: stop_response = await client.post("/dashboard/control/stop") start_response = await client.post("/dashboard/control/start") kill_response = await client.post( "/dashboard/control/kill-switch", data={"reason": "manual"}, ) config_response = await client.post( "/dashboard/control/config", data={ "trade_capital_usd": "250.50", "max_trade_capital_usd": "300.00", "max_concurrent_trades": "4", "paper_trading_mode": "on", }, ) assert stop_response.status_code == 200 assert ">stopped<" in stop_response.text assert start_response.status_code == 200 assert ">running<" in start_response.text assert kill_response.status_code == 200 assert ">active<" in kill_response.text assert "manual" in kill_response.text assert config_response.status_code == 200 assert "250.50 USD" in config_response.text assert "300.00 USD" in config_response.text assert "4" in config_response.text assert app.state.settings.trade_capital_usd == 250.5 assert app.state.settings.max_trade_capital_usd == 300.0 assert app.state.settings.max_concurrent_trades == 4 assert app.state.settings.paper_trading_mode is True