from fastapi.testclient import TestClient from app.db import auth_db from app.db.live_trading import ( create_live_order_intents_for_accounts, delete_live_account, get_live_account, list_live_accounts, list_live_order_intents, upsert_live_account, ) from app.db.runtime_config_db import set_config from app.integrations.binance_live import build_binance_client from app.services.live_trading_account import get_live_account_overview from app.services.live_trading_smoke import run_binance_testnet_smoke from app.web import web_server def _login_user(email: str, password: str = "StrongPass123", admin: bool = False) -> str: reg = auth_db.register_user(email, password) auth_db.verify_email(email, reg["verification_code"]) user = auth_db.get_user_by_email(email) auth_db.claim_free_trial(user["id"]) if admin: auth_db.set_user_admin(email, True) return auth_db.login_user(email, password)["token"] def test_live_trading_page_and_api_require_admin(): token = _login_user("normal-live@example.com") client = TestClient(web_server.app) client.cookies.set("altcoin_session", token) page = client.get("/live-trading") summary = client.get("/api/live-trading/summary") assert page.status_code == 403 assert summary.status_code == 403 def test_live_trading_admin_can_access_page_and_seed_account(): token = _login_user("admin-live@example.com", admin=True) client = TestClient(web_server.app) client.cookies.set("altcoin_session", token) page = client.get("/live-trading") created = client.post("/api/live-trading/accounts", json={ "account_code": "binance_sub_1", "exchange": "binance", "market_type": "um_futures", "status": "enabled", "api_key_env": "ALPHAX_BINANCE_SUB1_API_KEY", "api_secret_env": "ALPHAX_BINANCE_SUB1_API_SECRET", }) accounts = client.get("/api/live-trading/accounts") assert page.status_code == 200 assert "实盘控制台" in page.text assert "账户总览" in page.text assert "留空=全部" in page.text assert created.status_code == 200 assert created.json()["account_code"] == "binance_sub_1" assert accounts.json()["total"] == 1 def test_live_account_overview_returns_disabled_account_without_exchange_call(): account = upsert_live_account( account_code="binance_overview_disabled", status="disabled", risk_config={"max_order_margin_usdt": 10, "max_symbol_leverage": 2, "allowed_symbols": []}, ) overview = get_live_account_overview(account["id"]) assert overview["account"]["account_code"] == "binance_overview_disabled" assert overview["risk"]["symbol_policy"] == "all" assert overview["balance"]["usdt"]["total"] == 0 assert overview["positions"] == [] def test_live_account_can_be_deleted_without_deleting_history_contract(): account = upsert_live_account(account_code="binance_delete_me", status="disabled") deleted = delete_live_account(account["id"]) missing = get_live_account(account["id"]) assert deleted["ok"] is True assert deleted["account"]["account_code"] == "binance_delete_me" assert missing == {} def test_live_order_intent_fans_out_to_multiple_enabled_accounts(): set_config("system", "live_trading", { "enabled": True, "execution_mode": "exchange_api", "require_human_approval": True, "exchange": "binance", "market_type": "um_futures", "default_leverage": 1, "risk": { "max_order_margin_usdt": 50, "max_order_notional_usdt": 100, "max_symbol_leverage": 2, "max_cumulative_leverage": 2, "allowed_symbols": ["BTC/USDT"], }, }, source="test") a1 = upsert_live_account(account_code="binance_main", status="enabled", risk_config={"max_order_notional_usdt": 100}) a2 = upsert_live_account(account_code="binance_sub", status="enabled", risk_config={"max_order_notional_usdt": 20}) result = create_live_order_intents_for_accounts({ "symbol": "BTC/USDT", "side": "long", "notional_usdt": 50, "leverage": 1, }) intents = list_live_order_intents()["items"] assert result["ok"] is True assert result["total"] == 2 assert len(intents) == 2 by_account = {item["account_id"]: item for item in intents} assert by_account[a1["id"]]["status"] == "pending_approval" assert by_account[a2["id"]]["status"] == "blocked" assert by_account[a2["id"]]["reason"] == "notional_exceeds_limit" def test_live_trading_default_config_is_safe_disabled(): upsert_live_account(account_code="binance_safe", status="enabled") result = create_live_order_intents_for_accounts({ "symbol": "BTC/USDT", "side": "long", "notional_usdt": 10, "leverage": 1, }) assert list_live_accounts()["total"] == 1 assert result["ok"] is True intent = result["items"][0] assert intent["status"] == "blocked" assert intent["reason"] == "live_trading_disabled" def test_exchange_api_execution_mode_marks_intent_ready_for_executor(): set_config("system", "live_trading", { "enabled": True, "execution_mode": "exchange_api", "require_human_approval": False, "exchange": "binance", "market_type": "um_futures", "testnet": True, "risk": { "max_order_margin_usdt": 100, "max_order_notional_usdt": 100, "max_symbol_leverage": 2, "max_cumulative_leverage": 2, "allowed_symbols": ["BTC/USDT"], }, }, source="test") account = upsert_live_account(account_code="binance_exchange_ready", status="enabled") result = create_live_order_intents_for_accounts({ "symbol": "BTC/USDT", "side": "long", "notional_usdt": 10, "leverage": 1, }, account_ids=[account["id"]]) intent = result["items"][0] assert intent["status"] == "prepared" assert intent["reason"] == "exchange_ready_for_executor" def test_live_risk_blocks_when_margin_exceeds_account_limit(): set_config("system", "live_trading", { "enabled": True, "execution_mode": "exchange_api", "require_human_approval": False, "exchange": "binance", "market_type": "um_futures", "testnet": True, "risk": { "max_order_margin_usdt": 5, "max_symbol_leverage": 2, "allowed_symbols": ["BTC/USDT"], }, }, source="test") account = upsert_live_account(account_code="binance_margin_guard", status="enabled") result = create_live_order_intents_for_accounts({ "symbol": "BTC/USDT", "side": "long", "notional_usdt": 12, "leverage": 2, }, account_ids=[account["id"]]) intent = result["items"][0] assert intent["status"] == "blocked" assert intent["reason"] == "margin_exceeds_limit" def test_live_risk_allows_any_symbol_when_allowed_symbols_empty(): set_config("system", "live_trading", { "enabled": True, "execution_mode": "exchange_api", "require_human_approval": False, "exchange": "binance", "market_type": "um_futures", "testnet": True, "risk": { "max_order_margin_usdt": 20, "max_symbol_leverage": 2, "allowed_symbols": [], }, }, source="test") account = upsert_live_account( account_code="binance_all_symbols", status="enabled", risk_config={"max_order_margin_usdt": 20, "max_symbol_leverage": 2, "allowed_symbols": []}, ) result = create_live_order_intents_for_accounts({ "symbol": "DOGE/USDT", "side": "long", "notional_usdt": 10, "leverage": 1, }, account_ids=[account["id"]]) intent = result["items"][0] assert get_live_account(account["id"])["risk_config"]["allowed_symbols"] == [] assert intent["status"] == "prepared" assert intent["reason"] == "exchange_ready_for_executor" class _FakeBinanceClient: def __init__(self): self.calls = [] self.order_id = 100 def load_markets(self): self.calls.append(("load_markets",)) return {"BTC/USDT": {}} def fetch_balance(self): self.calls.append(("fetch_balance",)) return {"USDT": {"free": 1000}} def fetch_ticker(self, symbol): self.calls.append(("fetch_ticker", symbol)) return {"last": 100.0} def set_leverage(self, symbol, leverage): self.calls.append(("set_leverage", symbol, leverage)) return {"symbol": symbol, "leverage": leverage} def amount_to_precision(self, symbol, amount): self.calls.append(("amount_to_precision", symbol, amount)) return round(amount, 4) def min_notional(self, symbol): self.calls.append(("min_notional", symbol)) return 5 def _order(self, kind, *args): self.order_id += 1 self.calls.append((kind, *args)) return {"id": str(self.order_id), "kind": kind} def create_market_order(self, symbol, side, amount, params=None): return self._order("market_order", symbol, side, amount, params or {}) def create_limit_order(self, symbol, side, amount, price, params=None): return self._order("limit_order", symbol, side, amount, price, params or {}) def cancel_order(self, order_id, symbol): self.calls.append(("cancel_order", order_id, symbol)) return {"id": order_id, "status": "canceled"} def create_stop_loss_order(self, symbol, side, amount, stop_price, params=None): return self._order("stop_loss", symbol, side, amount, stop_price, params or {}) def create_take_profit_order(self, symbol, side, amount, stop_price, params=None): return self._order("take_profit", symbol, side, amount, stop_price, params or {}) def test_binance_testnet_smoke_covers_market_limit_cancel_tp_sl_interfaces(): account = upsert_live_account( account_code="binance_testnet", exchange="binance", market_type="um_futures", testnet=True, status="enabled", api_key_env="ALPHAX_BINANCE_TESTNET_API_KEY", api_secret_env="ALPHAX_BINANCE_TESTNET_API_SECRET", ) fake = _FakeBinanceClient() result = run_binance_testnet_smoke( account_id=account["id"], symbol="BTC/USDT", notional_usdt=10, leverage=1, client=fake, ) call_names = [c[0] for c in fake.calls] assert result["ok"] is True assert "fetch_balance" in call_names assert "set_leverage" in call_names assert "market_order" in call_names assert "limit_order" in call_names assert "stop_loss" in call_names assert "take_profit" in call_names assert "cancel_order" in call_names assert call_names.count("market_order") == 2 def test_binance_smoke_blocks_before_order_when_notional_below_exchange_minimum(): account = upsert_live_account( account_code="binance_smoke_min_notional", exchange="binance", market_type="um_futures", testnet=True, status="enabled", api_key_env="ALPHAX_BINANCE_TESTNET_API_KEY", api_secret_env="ALPHAX_BINANCE_TESTNET_API_SECRET", ) fake = _FakeBinanceClient() fake.min_notional = lambda symbol: 50 try: run_binance_testnet_smoke( account_id=account["id"], symbol="BTC/USDT", notional_usdt=10, leverage=1, client=fake, ) assert False, "expected minimum notional validation error" except Exception as exc: assert "minimum notional" in str(exc) call_names = [c[0] for c in fake.calls] assert "market_order" not in call_names def test_binance_client_uses_futures_demo_endpoint(monkeypatch): monkeypatch.setenv("ALPHAX_BINANCE_DEMO_API_KEY", "demo-key") monkeypatch.setenv("ALPHAX_BINANCE_DEMO_API_SECRET", "demo-secret") client = build_binance_client({ "exchange": "binance", "market_type": "um_futures", "testnet": True, "api_key_env": "ALPHAX_BINANCE_DEMO_API_KEY", "api_secret_env": "ALPHAX_BINANCE_DEMO_API_SECRET", "risk_config": {"sandbox_mode": "demo"}, }) assert client.exchange.urls["api"]["fapiPrivate"] == "https://demo-fapi.binance.com/fapi/v1" assert client.exchange.urls["api"]["fapiPublic"] == "https://demo-fapi.binance.com/fapi/v1"