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, get_live_account_snapshot, list_live_accounts, list_live_order_events, 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 import live_trading_account from app.services.live_trading_smoke import run_binance_testnet_smoke from app.services.live_trading_sync import run_live_trading_sync, sync_live_protection_from_paper, sync_paper_trade_to_live from app.web import web_server from app.db.schema import get_conn 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 "历史仓位" 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_edit_updates_existing_account_instead_of_creating_new_one(): token = _login_user("admin-live-edit@example.com", admin=True) client = TestClient(web_server.app) client.cookies.set("altcoin_session", token) created = client.post("/api/live-trading/accounts", json={ "account_code": "binance_edit_before", "exchange": "binance", "market_type": "um_futures", "status": "disabled", "api_key_env": "ALPHAX_BINANCE_OLD_KEY", "api_secret_env": "ALPHAX_BINANCE_OLD_SECRET", }) account_id = created.json()["id"] updated = client.put(f"/api/live-trading/accounts/{account_id}", json={ "account_code": "binance_edit_after", "exchange": "binance", "market_type": "um_futures", "status": "enabled", "api_key_env": "ALPHAX_BINANCE_NEW_KEY", "api_secret_env": "ALPHAX_BINANCE_NEW_SECRET", "risk_config": {"max_order_margin_usdt": 25, "allowed_symbols": ["BTC/USDT"]}, }) accounts = client.get("/api/live-trading/accounts").json() assert updated.status_code == 200 assert updated.json()["id"] == account_id assert updated.json()["account_code"] == "binance_edit_after" assert updated.json()["status"] == "enabled" assert updated.json()["risk_config"]["max_order_margin_usdt"] == 25 assert accounts["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_overview_does_not_hit_exchange_without_refresh(monkeypatch): account = upsert_live_account( account_code="binance_overview_fast", status="enabled", risk_config={"max_order_margin_usdt": 10, "max_symbol_leverage": 2, "allowed_symbols": []}, ) def fail_build(*args, **kwargs): raise AssertionError("exchange should not be called without refresh") monkeypatch.setattr(live_trading_account, "build_binance_client", fail_build) overview = get_live_account_overview(account["id"], refresh=False) assert overview["exchange_cache"]["requires_refresh"] is True assert overview["balance"]["usdt"]["total"] == 0 assert overview["positions"] == [] def test_live_account_overview_refresh_compacts_position_value_side_and_leverage(): account = upsert_live_account( account_code="binance_overview_refresh", status="enabled", risk_config={"max_order_margin_usdt": 10, "max_symbol_leverage": 3, "allowed_symbols": []}, ) class Client: def load_markets(self): return {} def fetch_balance(self): return {"total": {"USDT": 1000}, "free": {"USDT": 900}, "used": {"USDT": 100}} def fetch_positions(self, symbols=None): return [{ "symbol": "BTC/USDT", "contracts": 0.02, "entryPrice": 75000, "markPrice": 76000, "unrealizedPnl": 20, "info": {"positionAmt": "0.02"}, }] def fetch_open_orders(self, symbol=None): return [] def fetch_orders(self, symbol=None, limit=30): assert symbol == "BTC/USDT" return [] overview = get_live_account_overview(account["id"], refresh=True, client_factory=lambda account: Client()) pos = overview["positions"][0] assert overview["exchange_cache"]["loaded"] is True assert pos["side_label"] == "多" assert pos["position_value_usdt"] == 1520 assert pos["leverage"] == 3 assert pos["leverage_source"] == "account_config" assert round(pos["pnl_pct"], 2) == 1.32 assert overview["performance"]["equity_usdt"] == 1000 assert overview["performance"]["unrealized_pnl_usdt"] == 20 def test_live_account_overview_refresh_persists_database_snapshot_and_reads_without_exchange(monkeypatch): account = upsert_live_account( account_code="binance_snapshot_cache", status="enabled", risk_config={"max_order_margin_usdt": 10, "max_symbol_leverage": 2, "allowed_symbols": ["BTC/USDT"]}, ) class Client: def load_markets(self): return {} def fetch_balance(self): return {"total": {"USDT": 1234}, "free": {"USDT": 1200}, "used": {"USDT": 34}} def fetch_positions(self, symbols=None): return [] def fetch_open_orders(self, symbol=None): return [] def fetch_orders(self, symbol=None, limit=30): return [] refreshed = get_live_account_overview(account["id"], refresh=True, client_factory=lambda account: Client()) snapshot = get_live_account_snapshot(account["id"]) def fail_build(*args, **kwargs): raise AssertionError("database snapshot read must not call exchange") monkeypatch.setattr(live_trading_account, "build_binance_client", fail_build) cached = get_live_account_overview(account["id"], refresh=False) assert refreshed["balance"]["usdt"]["total"] == 1234 assert snapshot["status"] == "ok" assert snapshot["snapshot"]["balance"]["usdt"]["total"] == 1234 assert cached["balance"]["usdt"]["total"] == 1234 assert cached["exchange_cache"]["source"] == "database" assert cached["exchange_cache"]["cached"] is True def test_live_account_overview_fetches_order_history_per_symbol(): account = upsert_live_account( account_code="binance_overview_orders_by_symbol", status="enabled", risk_config={"max_order_margin_usdt": 10, "max_symbol_leverage": 2, "allowed_symbols": ["BTC/USDT", "ETHUSDT"]}, ) class Client: def __init__(self): self.order_symbols = [] def load_markets(self): return {} def fetch_balance(self): return {"total": {"USDT": 1000}, "free": {"USDT": 1000}, "used": {"USDT": 0}} def fetch_positions(self, symbols=None): return [] def fetch_open_orders(self, symbol=None): return [] def fetch_orders(self, symbol=None, limit=30): if symbol is None: raise AssertionError("fetch_orders must be called with a symbol for binanceusdm") self.order_symbols.append(symbol) return [{ "id": f"{symbol}-1", "symbol": symbol, "type": "limit", "side": "buy", "status": "closed", "price": 1, "amount": 2, "filled": 2, "datetime": "2026-05-30T08:00:00", "info": {"realizedPnl": "3.5"}, }] client = Client() overview = get_live_account_overview(account["id"], refresh=True, client_factory=lambda account: client) assert client.order_symbols == ["BTC/USDT", "ETH/USDT"] assert len(overview["order_history"]) == 2 assert overview["historical_positions"][0]["result"] == "盈利" assert overview["errors"] == [] def test_live_account_overview_normalizes_futures_symbols_and_pairs_closed_positions(): account = upsert_live_account( account_code="binance_overview_futures_symbol_history", status="enabled", risk_config={"max_order_margin_usdt": 10, "max_symbol_leverage": 2, "allowed_symbols": ["BANK/USDT"]}, ) class Client: def load_markets(self): return {} def fetch_balance(self): return {"total": {"USDT": 1000}, "free": {"USDT": 980}, "used": {"USDT": 20}} def fetch_positions(self, symbols=None): return [{ "symbol": "BANK/USDT:USDT", "contracts": 100, "entryPrice": 0.1, "markPrice": 0.11, "unrealizedPnl": 1, "info": {"positionAmt": "100", "leverage": "2"}, }] def fetch_open_orders(self, symbol=None): return [{ "id": "open-1", "symbol": "BANK/USDT:USDT", "type": "limit", "side": "sell", "status": "open", "price": 0.12, "amount": 100, "datetime": "2026-06-07T08:00:00", }] def fetch_orders(self, symbol=None, limit=30): assert symbol == "BANK/USDT" return [ { "id": "entry-1", "symbol": "BANK/USDT:USDT", "type": "market", "side": "buy", "status": "closed", "average": 0.1, "filled": 100, "datetime": "2026-06-07T08:00:00", "info": {"reduceOnly": "false"}, }, { "id": "exit-1", "symbol": "BANK/USDT:USDT", "type": "market", "side": "sell", "status": "closed", "average": 0.12, "filled": 100, "datetime": "2026-06-07T09:00:00", "info": {"reduceOnly": "true", "realizedPnl": "2"}, }, ] overview = get_live_account_overview(account["id"], refresh=True, client_factory=lambda account: Client()) position = overview["positions"][0] open_order = overview["open_orders"][0] history = overview["historical_positions"][0] assert position["symbol"] == "BANK/USDT" assert open_order["symbol"] == "BANK/USDT" assert overview["order_history"][0]["symbol"] == "BANK/USDT" assert history["symbol"] == "BANK/USDT" assert history["side"] == "long" assert history["entry_price"] == 0.1 assert history["exit_price"] == 0.12 assert history["realized_pnl"] == 2 assert history["realized_pnl_pct"] == 20 assert history["result"] == "盈利" 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 fetch_positions(self, symbols=None): self.calls.append(("fetch_positions", symbols)) return [] 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 cancel_algo_order(self, *, algo_id=None, client_algo_id=None): self.calls.append(("cancel_algo_order", algo_id, client_algo_id)) return {"algoId": algo_id, "clientAlgoId": client_algo_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 _insert_paper_trade(symbol="DOGE/USDT", notional=5000, leverage=5): conn = get_conn() try: row = conn.execute( """ INSERT INTO paper_trades ( recommendation_id, symbol, side, status, opened_at, entry_price, qty, notional_usdt, margin_usdt, leverage, stop_loss, tp1, tp2, max_price, min_price, current_price, pnl_pct, fee_usdt, source_status, source_action, strategy_version, created_at, updated_at ) VALUES (%s,%s,'long','open','2026-05-22T00:00:00',0.1,100,%s,%s,%s,0.09,0.12,0.13,0.1,0.1,0.1,0,0,'buy_now','buy_now','test','2026-05-22T00:00:00','2026-05-22T00:00:00') RETURNING * """, (990000 + int(notional), symbol, notional, notional / leverage, leverage), ).fetchone() conn.commit() finally: conn.close() return dict(row) def test_paper_trade_sync_executes_scaled_live_order_once(): 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_auto_sync", status="enabled", risk_config={"max_order_margin_usdt": 20, "max_symbol_leverage": 2, "max_cumulative_leverage": 5, "allowed_symbols": []}, ) trade = _insert_paper_trade() fake = _FakeBinanceClient() fake.fetch_balance = lambda: {"total": {"USDT": 1000}} result = sync_paper_trade_to_live( trade["id"], account_ids=[account["id"]], execute=True, client_factory=lambda acct: fake, ) again = sync_paper_trade_to_live( trade["id"], account_ids=[account["id"]], execute=True, client_factory=lambda acct: fake, ) call_names = [c[0] for c in fake.calls] assert result["ok"] is True assert result["items"][0]["executed"] is True assert result["items"][0]["sizing"]["notional_usdt"] == 40 assert "market_order" in call_names assert "stop_loss" in call_names assert "take_profit" in call_names assert again["items"][0]["reason"] == "already_synced" def test_live_trading_sync_job_refreshes_snapshots_and_syncs_open_paper_trade(): 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_scheduler_sync", status="enabled", risk_config={"max_order_margin_usdt": 20, "max_symbol_leverage": 2, "max_cumulative_leverage": 5, "allowed_symbols": []}, ) trade = _insert_paper_trade(symbol="BTC/USDT") fake = _FakeBinanceClient() fake.fetch_balance = lambda: {"total": {"USDT": 1000}, "free": {"USDT": 900}, "used": {"USDT": 100}} fake.fetch_open_orders = lambda symbol=None: [] fake.fetch_orders = lambda symbol=None, limit=30: [] result = run_live_trading_sync(limit=20, execute=True, client_factory=lambda acct: fake) snapshot = get_live_account_snapshot(account["id"]) intents = list_live_order_intents(account_id=account["id"])["items"] assert result["ok"] is True assert result["snapshots"]["ok_count"] == 1 assert result["paper_sync"]["processed_count"] == 1 assert snapshot["snapshot"]["balance"]["usdt"]["total"] == 1000 assert intents[0]["paper_trade_id"] == trade["id"] assert intents[0]["status"] == "submitted" def test_live_protection_sync_replaces_live_stop_when_paper_trailing_moves(): 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_protection_move", status="enabled", risk_config={"max_order_margin_usdt": 20, "max_symbol_leverage": 2, "max_cumulative_leverage": 5, "allowed_symbols": []}, ) trade = _insert_paper_trade(symbol="BTC/USDT") fake = _FakeBinanceClient() fake.fetch_balance = lambda: {"total": {"USDT": 1000}} sync_paper_trade_to_live(trade["id"], account_ids=[account["id"]], execute=True, client_factory=lambda acct: fake) conn = get_conn() try: conn.execute("UPDATE paper_trades SET trailing_stop=0.105, updated_at='2026-05-22T00:10:00' WHERE id=%s", (trade["id"],)) conn.commit() finally: conn.close() result = sync_live_protection_from_paper(client_factory=lambda acct: fake) intent = list_live_order_intents(account_id=account["id"])["items"][0] events = list_live_order_events(limit=20)["items"] call_names = [c[0] for c in fake.calls] assert result["ok"] is True assert result["results"][0]["action"] == "replace_stop" assert intent["stop_loss"] == 0.105 assert "cancel_algo_order" in call_names assert call_names.count("stop_loss") == 2 assert any(e["event_type"] == "live_protection_stop_replace" for e in events) def test_live_protection_sync_closes_live_position_when_paper_trade_closed(): 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_protection_close", status="enabled", risk_config={"max_order_margin_usdt": 20, "max_symbol_leverage": 2, "max_cumulative_leverage": 5, "allowed_symbols": []}, ) trade = _insert_paper_trade(symbol="BTC/USDT") fake = _FakeBinanceClient() fake.fetch_balance = lambda: {"total": {"USDT": 1000}} sync_paper_trade_to_live(trade["id"], account_ids=[account["id"]], execute=True, client_factory=lambda acct: fake) conn = get_conn() try: conn.execute( "UPDATE paper_trades SET status='closed', exit_reason='trailing_stop', closed_at='2026-05-22T00:20:00', updated_at='2026-05-22T00:20:00' WHERE id=%s", (trade["id"],), ) conn.commit() finally: conn.close() result = sync_live_protection_from_paper(client_factory=lambda acct: fake) intent = list_live_order_intents(account_id=account["id"])["items"][0] events = list_live_order_events(limit=20)["items"] market_orders = [c for c in fake.calls if c[0] == "market_order"] assert result["ok"] is True assert result["results"][0]["action"] == "close" assert intent["status"] == "closed" assert market_orders[-1][-1]["reduceOnly"] is True assert any(e["event_type"] == "live_sync_close" for e in events) 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"