# AI Futures Trading Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a Binance USDT-M perpetual futures AI batch trader with dry-run, futures testnet, and production-live support, while keeping the existing Spot AI trader unchanged.

**Architecture:** Build a separate futures subsystem with futures-specific config, models, exchange client, storage, risk, execution, AI runner, CLI command, and runner script wiring. Reuse existing patterns for settings validation, HMAC signing, audit storage, Codex schema execution, and Rich CLI output, but keep futures action semantics independent from Spot `BUY`/`SELL`.

**Tech Stack:** Python 3.11+, Pydantic settings, httpx, Typer, Rich, SQLite, pytest, Decimal arithmetic, Binance USD-M Futures REST API.

---

## Reference Notes

Use the current Binance USD-M Futures docs while implementing:

- New regular order: `POST /fapi/v1/order` for market open/close orders.
- New algo order: `POST /fapi/v1/algoOrder` for conditional `STOP_MARKET` and `TAKE_PROFIT_MARKET` protection orders.
- Cancel algo order: `DELETE /fapi/v1/algoOrder` with either `algoId` or `clientAlgoId`.
- Current position mode: `GET /fapi/v1/positionSide/dual`; response `dualSidePosition=false` means one-way mode.
- Change margin type: `POST /fapi/v1/marginType`.
- Change initial leverage: `POST /fapi/v1/leverage`.
- Position information: `GET /fapi/v2/positionRisk`.

Do not implement protection orders through `POST /fapi/v1/order`; USD-M Futures TP/SL conditional orders should use the algo-order endpoints.

## File Structure

- Create `src/binance_quant/futures_models.py`: futures enums and dataclasses only.
- Create `src/binance_quant/futures_exchange.py`: USD-M Futures REST client and response parsers only.
- Create `src/binance_quant/futures_risk.py`: pure risk calculation and validation only.
- Create `src/binance_quant/futures_execution.py`: order quantity calculation, dry-run/testnet/live open and close execution, protection order rollback handling.
- Create `src/binance_quant/futures_ai_trader.py`: prediction schema/parsing, prompt, market/account context construction, batch orchestration.
- Modify `src/binance_quant/config.py`: futures settings and URL validation.
- Modify `src/binance_quant/storage.py`: futures audit tables and helper methods.
- Modify `src/binance_quant/cli.py`: futures batch command and exit-code rules.
- Modify `scripts/run.ps1`: runner action and live safety preflight.
- Modify `README.md`: document command and settings.
- Add tests: `tests/test_futures_models.py`, `tests/test_futures_exchange.py`, `tests/test_futures_risk.py`, `tests/test_futures_execution.py`, `tests/test_futures_ai_trader.py`.
- Modify tests: `tests/test_config.py`, `tests/test_storage.py`, `tests/test_cli.py`, `tests/test_run_script.py`.

## Task 1: Futures Config

**Files:**
- Modify: `src/binance_quant/config.py`
- Test: `tests/test_config.py`

- [ ] **Step 1: Write failing config tests**

Append these tests to `tests/test_config.py`:

```python
def test_futures_ai_trader_defaults_are_disabled_and_conservative():
    settings = Settings(_env_file=None)

    assert settings.live_futures_ai_trader_enabled is False
    assert settings.live_futures_confirm_production is False
    assert settings.futures_ai_symbols == DEFAULT_TRADING_SYMBOLS
    assert settings.futures_margin_amount == Decimal("5")
    assert settings.futures_default_leverage == 2
    assert settings.futures_max_leverage == 3
    assert settings.futures_max_total_margin == Decimal("20")
    assert settings.futures_ai_min_confidence == Decimal("0.70")
    assert settings.futures_ai_max_actions_per_run == 5
    assert settings.futures_daily_realized_loss_limit == Decimal("10")
    assert settings.futures_max_margin_loss_percent == Decimal("50")
    assert settings.futures_min_liquidation_buffer_percent == Decimal("2")
    assert settings.futures_min_stop_distance_percent == Decimal("0.3")
    assert settings.futures_max_stop_distance_percent == Decimal("5")
    assert settings.futures_min_take_profit_distance_percent == Decimal("0.3")
    assert settings.futures_max_take_profit_distance_percent == Decimal("10")
    assert settings.futures_rest_base_url == "https://testnet.binancefuture.com"
    assert settings.futures_stream_base_url == "wss://stream.binancefuture.com/ws"
```

Append this environment parsing test:

```python
def test_futures_ai_trader_settings_parse_environment(monkeypatch):
    monkeypatch.setenv("BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED", "true")
    monkeypatch.setenv("BINANCE_LIVE_FUTURES_CONFIRM_PRODUCTION", "true")
    monkeypatch.setenv("BINANCE_FUTURES_AI_SYMBOLS", " ethusdt , btcusdt ")
    monkeypatch.setenv("BINANCE_FUTURES_MARGIN_AMOUNT", "7.5")
    monkeypatch.setenv("BINANCE_FUTURES_DEFAULT_LEVERAGE", "4")
    monkeypatch.setenv("BINANCE_FUTURES_MAX_LEVERAGE", "5")
    monkeypatch.setenv("BINANCE_FUTURES_MAX_TOTAL_MARGIN", "30")
    monkeypatch.setenv("BINANCE_FUTURES_AI_MIN_CONFIDENCE", "0.82")
    monkeypatch.setenv("BINANCE_FUTURES_AI_MAX_ACTIONS_PER_RUN", "3")
    monkeypatch.setenv("BINANCE_FUTURES_DAILY_REALIZED_LOSS_LIMIT", "12.5")
    monkeypatch.setenv("BINANCE_FUTURES_MAX_MARGIN_LOSS_PERCENT", "40")
    monkeypatch.setenv("BINANCE_FUTURES_MIN_LIQUIDATION_BUFFER_PERCENT", "3")
    monkeypatch.setenv("BINANCE_FUTURES_MIN_STOP_DISTANCE_PERCENT", "0.4")
    monkeypatch.setenv("BINANCE_FUTURES_MAX_STOP_DISTANCE_PERCENT", "4")
    monkeypatch.setenv("BINANCE_FUTURES_MIN_TAKE_PROFIT_DISTANCE_PERCENT", "0.5")
    monkeypatch.setenv("BINANCE_FUTURES_MAX_TAKE_PROFIT_DISTANCE_PERCENT", "8")

    settings = Settings(_env_file=None)

    assert settings.live_futures_ai_trader_enabled is True
    assert settings.live_futures_confirm_production is True
    assert settings.futures_ai_symbols == ("ETHUSDT", "BTCUSDT")
    assert settings.futures_margin_amount == Decimal("7.5")
    assert settings.futures_default_leverage == 4
    assert settings.futures_max_leverage == 5
    assert settings.futures_max_total_margin == Decimal("30")
    assert settings.futures_ai_min_confidence == Decimal("0.82")
    assert settings.futures_ai_max_actions_per_run == 3
    assert settings.futures_daily_realized_loss_limit == Decimal("12.5")
    assert settings.futures_max_margin_loss_percent == Decimal("40")
    assert settings.futures_min_liquidation_buffer_percent == Decimal("3")
    assert settings.futures_min_stop_distance_percent == Decimal("0.4")
    assert settings.futures_max_stop_distance_percent == Decimal("4")
    assert settings.futures_min_take_profit_distance_percent == Decimal("0.5")
    assert settings.futures_max_take_profit_distance_percent == Decimal("8")
```

Append this validation test:

```python
def test_futures_ai_trader_rejects_invalid_settings():
    with pytest.raises(ValueError, match="futures_ai_symbols must contain at least one symbol"):
        Settings(_env_file=None, futures_ai_symbols="")
    with pytest.raises(ValueError, match="futures_ai_symbols must be USDT futures symbols"):
        Settings(_env_file=None, futures_ai_symbols="BTCUSDT,ETHBTC")
    with pytest.raises(ValueError, match="futures_margin_amount must be positive"):
        Settings(_env_file=None, futures_margin_amount=Decimal("0"))
    with pytest.raises(ValueError, match="futures_default_leverage must be positive"):
        Settings(_env_file=None, futures_default_leverage=0)
    with pytest.raises(ValueError, match="futures_max_leverage must be positive"):
        Settings(_env_file=None, futures_max_leverage=0)
    with pytest.raises(ValueError, match="futures_default_leverage must be less than or equal to futures_max_leverage"):
        Settings(_env_file=None, futures_default_leverage=4, futures_max_leverage=3)
    with pytest.raises(ValueError, match="futures_ai_min_confidence must be greater than 0 and less than or equal to 1"):
        Settings(_env_file=None, futures_ai_min_confidence=Decimal("0"))
    with pytest.raises(ValueError, match="futures_ai_max_actions_per_run must be positive"):
        Settings(_env_file=None, futures_ai_max_actions_per_run=0)
    with pytest.raises(ValueError, match="futures_min_stop_distance_percent must be less than or equal to futures_max_stop_distance_percent"):
        Settings(
            _env_file=None,
            futures_min_stop_distance_percent=Decimal("6"),
            futures_max_stop_distance_percent=Decimal("5"),
        )
    with pytest.raises(ValueError, match="futures_min_take_profit_distance_percent must be less than or equal to futures_max_take_profit_distance_percent"):
        Settings(
            _env_file=None,
            futures_min_take_profit_distance_percent=Decimal("11"),
            futures_max_take_profit_distance_percent=Decimal("10"),
        )
```

- [ ] **Step 2: Verify config tests fail**

Run:

```powershell
python -m pytest tests/test_config.py::test_futures_ai_trader_defaults_are_disabled_and_conservative tests/test_config.py::test_futures_ai_trader_settings_parse_environment tests/test_config.py::test_futures_ai_trader_rejects_invalid_settings -q
```

Expected: FAIL because the futures settings are not defined.

- [ ] **Step 3: Implement futures config fields**

In `src/binance_quant/config.py`, add constants near the existing REST constants:

```python
FUTURES_TESTNET_REST_BASE_URL = "https://testnet.binancefuture.com"
FUTURES_TESTNET_STREAM_BASE_URL = "wss://stream.binancefuture.com/ws"
FUTURES_LIVE_REST_BASE_URL = "https://fapi.binance.com"
FUTURES_LIVE_REST_BASE_URLS = (
    "https://fapi.binance.com",
    "https://fapi1.binance.com",
    "https://fapi2.binance.com",
    "https://fapi3.binance.com",
)
FUTURES_LIVE_STREAM_BASE_URL = "wss://fstream.binance.com/ws"
FUTURES_TESTNET_REST_HOSTS = {"testnet.binancefuture.com"}
FUTURES_TESTNET_STREAM_HOSTS = {"stream.binancefuture.com"}
FUTURES_LIVE_REST_HOSTS = {"fapi.binance.com", "fapi1.binance.com", "fapi2.binance.com", "fapi3.binance.com"}
FUTURES_LIVE_STREAM_HOSTS = {"fstream.binance.com"}
```

Add these fields to `Settings` next to the existing AI trader fields:

```python
live_futures_ai_trader_enabled: bool = False
live_futures_confirm_production: bool = False
futures_ai_symbols: tuple[str, ...] | str = DEFAULT_TRADING_SYMBOLS
futures_margin_amount: Decimal = Decimal("5")
futures_default_leverage: int = Field(default=2, ge=1)
futures_max_leverage: int = Field(default=3, ge=1)
futures_max_total_margin: Decimal = Decimal("20")
futures_ai_min_confidence: Decimal = Decimal("0.70")
futures_ai_max_actions_per_run: int = Field(default=5, ge=1)
futures_daily_realized_loss_limit: Decimal = Decimal("10")
futures_max_margin_loss_percent: Decimal = Decimal("50")
futures_min_liquidation_buffer_percent: Decimal = Decimal("2")
futures_min_stop_distance_percent: Decimal = Decimal("0.3")
futures_max_stop_distance_percent: Decimal = Decimal("5")
futures_min_take_profit_distance_percent: Decimal = Decimal("0.3")
futures_max_take_profit_distance_percent: Decimal = Decimal("10")
futures_rest_base_url: str | None = None
futures_rest_base_urls: tuple[str, ...] | str | None = None
futures_stream_base_url: str | None = None
```

Add a parser that mirrors `parse_ai_trader_symbols` but uses a futures-specific error:

```python
@field_validator("futures_ai_symbols", mode="before")
@classmethod
def parse_futures_ai_symbols(cls, value: Any) -> tuple[str, ...]:
    if isinstance(value, str):
        symbols = tuple(item.strip().upper() for item in value.split(",") if item.strip())
    else:
        if not isinstance(value, Iterable):
            raise ValueError("futures_ai_symbols must be a string or iterable collection of symbols")
        symbols = tuple(str(item).strip().upper() for item in value if str(item).strip())
    if not symbols:
        raise ValueError("futures_ai_symbols must contain at least one symbol")
    if any(not symbol.endswith("USDT") for symbol in symbols):
        raise ValueError("futures_ai_symbols must be USDT futures symbols")
    return symbols
```

Add validators:

```python
@field_validator(
    "futures_margin_amount",
    "futures_max_total_margin",
    "futures_daily_realized_loss_limit",
    "futures_max_margin_loss_percent",
    "futures_min_liquidation_buffer_percent",
    "futures_min_stop_distance_percent",
    "futures_max_stop_distance_percent",
    "futures_min_take_profit_distance_percent",
    "futures_max_take_profit_distance_percent",
)
@classmethod
def positive_futures_decimal(cls, value: Decimal, info) -> Decimal:
    if value <= 0:
        raise ValueError(f"{info.field_name} must be positive")
    return value

@field_validator("futures_ai_min_confidence")
@classmethod
def valid_futures_ai_min_confidence(cls, value: Decimal) -> Decimal:
    if value <= 0 or value > 1:
        raise ValueError("futures_ai_min_confidence must be greater than 0 and less than or equal to 1")
    return value

@field_validator("futures_ai_max_actions_per_run")
@classmethod
def positive_futures_ai_max_actions_per_run(cls, value: int) -> int:
    if value <= 0:
        raise ValueError("futures_ai_max_actions_per_run must be positive")
    return value
```

Add futures URL parsing by reusing existing helpers:

```python
@field_validator("futures_rest_base_url")
@classmethod
def validate_futures_rest_base_url(cls, value: str | None) -> str | None:
    if value is None:
        return None
    return _normalize_base_url(value, label="futures REST", expected_scheme="https")

@field_validator("futures_rest_base_urls", mode="before")
@classmethod
def parse_futures_rest_base_urls(cls, value: Any) -> tuple[str, ...] | None:
    if value is None:
        return None
    if isinstance(value, str):
        urls = tuple(item.strip() for item in value.split(",") if item.strip())
    else:
        if not isinstance(value, Iterable):
            raise ValueError("futures_rest_base_urls must be a string or iterable collection of URLs")
        urls = tuple(str(item).strip() for item in value if str(item).strip())
    if not urls:
        raise ValueError("futures_rest_base_urls must contain at least one URL")
    return urls

@field_validator("futures_rest_base_urls")
@classmethod
def validate_futures_rest_base_urls(cls, value: tuple[str, ...] | None) -> tuple[str, ...] | None:
    if value is None:
        return None
    return tuple(_normalize_base_url(item, label="futures REST", expected_scheme="https") for item in value)

@field_validator("futures_stream_base_url")
@classmethod
def validate_futures_stream_base_url(cls, value: str | None) -> str | None:
    if value is None:
        return None
    return _normalize_base_url(value, label="futures stream", expected_scheme="wss")
```

In `validate_windows_and_credentials`, after existing spot URL setup, add:

```python
if self.futures_default_leverage > self.futures_max_leverage:
    raise ValueError("futures_default_leverage must be less than or equal to futures_max_leverage")
if self.futures_min_stop_distance_percent > self.futures_max_stop_distance_percent:
    raise ValueError("futures_min_stop_distance_percent must be less than or equal to futures_max_stop_distance_percent")
if self.futures_min_take_profit_distance_percent > self.futures_max_take_profit_distance_percent:
    raise ValueError(
        "futures_min_take_profit_distance_percent must be less than or equal to futures_max_take_profit_distance_percent"
    )
if self.mode is TradingMode.LIVE:
    if self.futures_rest_base_urls is None:
        if self.futures_rest_base_url is None:
            self.futures_rest_base_urls = FUTURES_LIVE_REST_BASE_URLS
        else:
            self.futures_rest_base_urls = (self.futures_rest_base_url,)
    if self.futures_rest_base_url is None:
        self.futures_rest_base_url = self.futures_rest_base_urls[0]
    if self.futures_stream_base_url is None:
        self.futures_stream_base_url = FUTURES_LIVE_STREAM_BASE_URL
    for rest_base_url in self.futures_rest_base_urls:
        _validate_url_host(
            rest_base_url,
            allowed_hosts=FUTURES_LIVE_REST_HOSTS,
            label="futures REST",
            environment="production",
        )
    _validate_url_host(
        self.futures_stream_base_url,
        allowed_hosts=FUTURES_LIVE_STREAM_HOSTS,
        label="futures stream",
        environment="production",
    )
else:
    if self.futures_rest_base_urls is None:
        if self.futures_rest_base_url is None:
            self.futures_rest_base_urls = (FUTURES_TESTNET_REST_BASE_URL,)
        else:
            self.futures_rest_base_urls = (self.futures_rest_base_url,)
    if self.futures_rest_base_url is None:
        self.futures_rest_base_url = self.futures_rest_base_urls[0]
    if self.futures_stream_base_url is None:
        self.futures_stream_base_url = FUTURES_TESTNET_STREAM_BASE_URL
    for rest_base_url in self.futures_rest_base_urls:
        _validate_url_host(
            rest_base_url,
            allowed_hosts=FUTURES_TESTNET_REST_HOSTS,
            label="futures REST",
            environment="Testnet",
        )
    _validate_url_host(
        self.futures_stream_base_url,
        allowed_hosts=FUTURES_TESTNET_STREAM_HOSTS,
        label="futures stream",
        environment="Testnet",
    )
```

- [ ] **Step 4: Verify config tests pass**

Run:

```powershell
python -m pytest tests/test_config.py::test_futures_ai_trader_defaults_are_disabled_and_conservative tests/test_config.py::test_futures_ai_trader_settings_parse_environment tests/test_config.py::test_futures_ai_trader_rejects_invalid_settings -q
```

Expected: PASS.

- [ ] **Step 5: Commit config task**

Run:

```powershell
git add src/binance_quant/config.py tests/test_config.py
git commit -m "feat: add futures ai trader settings"
```

## Task 2: Futures Models

**Files:**
- Create: `src/binance_quant/futures_models.py`
- Test: `tests/test_futures_models.py`

- [ ] **Step 1: Write failing model tests**

Create `tests/test_futures_models.py`:

```python
from dataclasses import replace
from decimal import Decimal

from binance_quant.futures_models import (
    FuturesAction,
    FuturesOrderSide,
    FuturesPosition,
    FuturesPositionSide,
    FuturesPrediction,
    FuturesSymbolMetadata,
)


def test_futures_position_derives_side_from_signed_quantity():
    long_position = FuturesPosition(
        symbol="btcusdt",
        quantity=Decimal("0.01"),
        entry_price=Decimal("60000"),
        mark_price=Decimal("60100"),
        liquidation_price=Decimal("55000"),
        isolated_margin=Decimal("10"),
        unrealized_pnl=Decimal("1"),
        leverage=3,
        margin_type="isolated",
    )
    short_position = FuturesPosition(
        symbol="ethusdt",
        quantity=Decimal("-0.5"),
        entry_price=Decimal("3000"),
        mark_price=Decimal("2990"),
        liquidation_price=Decimal("3300"),
        isolated_margin=Decimal("20"),
        unrealized_pnl=Decimal("5"),
        leverage=2,
        margin_type="isolated",
    )

    assert long_position.symbol == "BTCUSDT"
    assert long_position.side is FuturesPositionSide.LONG
    assert short_position.symbol == "ETHUSDT"
    assert short_position.side is FuturesPositionSide.SHORT
    assert short_position.absolute_quantity == Decimal("0.5")


def test_futures_prediction_normalizes_symbol_and_action():
    prediction = FuturesPrediction(
        symbol="btcusdt",
        action=FuturesAction.OPEN_LONG,
        confidence=Decimal("0.82"),
        reason="trend",
        leverage=3,
        stop_loss_price=Decimal("62000"),
        take_profit_price=Decimal("66000"),
    )

    assert prediction.symbol == "BTCUSDT"
    assert prediction.action is FuturesAction.OPEN_LONG
    assert prediction.close_percent is None


def test_futures_symbol_metadata_normalizes_assets():
    metadata = FuturesSymbolMetadata(
        symbol="btcusdt",
        base_asset="btc",
        quote_asset="usdt",
        min_qty=Decimal("0.001"),
        step_size=Decimal("0.001"),
        tick_size=Decimal("0.1"),
        min_notional=Decimal("100"),
    )

    assert metadata.symbol == "BTCUSDT"
    assert metadata.base_asset == "BTC"
    assert metadata.quote_asset == "USDT"
```

- [ ] **Step 2: Verify model tests fail**

Run:

```powershell
python -m pytest tests/test_futures_models.py -q
```

Expected: FAIL because `binance_quant.futures_models` does not exist.

- [ ] **Step 3: Implement futures models**

Create `src/binance_quant/futures_models.py`:

```python
from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
from typing import Any


class FuturesAction(str, Enum):
    OPEN_LONG = "OPEN_LONG"
    OPEN_SHORT = "OPEN_SHORT"
    CLOSE = "CLOSE"
    HOLD = "HOLD"


class FuturesPositionSide(str, Enum):
    LONG = "LONG"
    SHORT = "SHORT"
    FLAT = "FLAT"


class FuturesOrderSide(str, Enum):
    BUY = "BUY"
    SELL = "SELL"


@dataclass(frozen=True)
class FuturesSymbolMetadata:
    symbol: str
    base_asset: str
    quote_asset: str
    min_qty: Decimal
    step_size: Decimal
    tick_size: Decimal
    min_notional: Decimal

    def __post_init__(self) -> None:
        object.__setattr__(self, "symbol", self.symbol.upper())
        object.__setattr__(self, "base_asset", self.base_asset.upper())
        object.__setattr__(self, "quote_asset", self.quote_asset.upper())


@dataclass(frozen=True)
class FuturesPosition:
    symbol: str
    quantity: Decimal
    entry_price: Decimal
    mark_price: Decimal
    liquidation_price: Decimal
    isolated_margin: Decimal
    unrealized_pnl: Decimal
    leverage: int
    margin_type: str

    def __post_init__(self) -> None:
        object.__setattr__(self, "symbol", self.symbol.upper())
        object.__setattr__(self, "margin_type", self.margin_type.lower())

    @property
    def side(self) -> FuturesPositionSide:
        if self.quantity > 0:
            return FuturesPositionSide.LONG
        if self.quantity < 0:
            return FuturesPositionSide.SHORT
        return FuturesPositionSide.FLAT

    @property
    def absolute_quantity(self) -> Decimal:
        return abs(self.quantity)


@dataclass(frozen=True)
class FuturesPrediction:
    symbol: str
    action: FuturesAction
    confidence: Decimal
    reason: str
    leverage: int | None = None
    stop_loss_price: Decimal | None = None
    take_profit_price: Decimal | None = None
    close_percent: int | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "symbol", self.symbol.upper())


@dataclass(frozen=True)
class FuturesRiskDecision:
    approved: bool
    reason: str
    prediction: FuturesPrediction
    leverage: int | None = None
    quantity: Decimal | None = None
    margin_amount: Decimal | None = None
    notional: Decimal | None = None


@dataclass(frozen=True)
class FuturesExecutionResult:
    run_id: int | None
    status: str
    symbol: str
    action: str
    reason: str
    confidence: Decimal | None = None
    leverage: int | None = None
    margin_amount: Decimal | None = None
    close_percent: int | None = None
    raw: dict[str, Any] | None = None

    def __post_init__(self) -> None:
        object.__setattr__(self, "symbol", self.symbol.upper() if self.symbol else "")
```

- [ ] **Step 4: Verify model tests pass**

Run:

```powershell
python -m pytest tests/test_futures_models.py -q
```

Expected: PASS.

- [ ] **Step 5: Commit model task**

Run:

```powershell
git add src/binance_quant/futures_models.py tests/test_futures_models.py
git commit -m "feat: add futures domain models"
```

## Task 3: Futures Exchange Client

**Files:**
- Create: `src/binance_quant/futures_exchange.py`
- Test: `tests/test_futures_exchange.py`

- [ ] **Step 1: Write failing exchange client tests**

Create `tests/test_futures_exchange.py`:

```python
from decimal import Decimal
from urllib.parse import parse_qsl, urlencode

import httpx

from binance_quant.exchange import sign_query
from binance_quant.futures_exchange import BinanceUsdmFuturesClient
from binance_quant.futures_models import FuturesOrderSide, FuturesSymbolMetadata


def test_futures_client_uses_testnet_base_url_by_default():
    client = BinanceUsdmFuturesClient(api_key="key", api_secret="secret")

    assert client.base_url == "https://testnet.binancefuture.com"


def test_futures_exchange_info_parses_usdt_perpetual_metadata():
    def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            200,
            json={
                "symbols": [
                    {
                        "symbol": "BTCUSDT",
                        "contractType": "PERPETUAL",
                        "status": "TRADING",
                        "baseAsset": "BTC",
                        "quoteAsset": "USDT",
                        "filters": [
                            {"filterType": "LOT_SIZE", "minQty": "0.001", "stepSize": "0.001"},
                            {"filterType": "PRICE_FILTER", "tickSize": "0.10"},
                            {"filterType": "MIN_NOTIONAL", "notional": "100"},
                        ],
                    }
                ]
            },
        )

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    metadata = client.exchange_info("btcusdt")

    assert isinstance(metadata, FuturesSymbolMetadata)
    assert metadata.symbol == "BTCUSDT"
    assert metadata.base_asset == "BTC"
    assert metadata.quote_asset == "USDT"
    assert metadata.min_qty == Decimal("0.001")
    assert metadata.step_size == Decimal("0.001")
    assert metadata.tick_size == Decimal("0.10")
    assert metadata.min_notional == Decimal("100")


def test_futures_create_market_order_sends_signed_params():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"status": "FILLED", "clientOrderId": "bqf-open"})

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    payload = client.create_market_order(
        "btcusdt",
        side=FuturesOrderSide.BUY,
        quantity=Decimal("0.002"),
        client_order_id="bqf-open",
        reduce_only=False,
        timestamp_ms=1,
    )

    body = requests[0].content.decode()
    body_pairs = parse_qsl(body, keep_blank_values=True)
    signature = dict(body_pairs)["signature"]
    sent_without_signature = urlencode([(key, value) for key, value in body_pairs if key != "signature"])
    assert payload["status"] == "FILLED"
    assert requests[0].method == "POST"
    assert requests[0].url.path == "/fapi/v1/order"
    assert "symbol=BTCUSDT" in body
    assert "side=BUY" in body
    assert "type=MARKET" in body
    assert "quantity=0.002" in body
    assert "newClientOrderId=bqf-open" in body
    assert "reduceOnly=true" not in body
    assert sign_query(sent_without_signature, "secret") == signature


def test_futures_create_algo_order_sends_conditional_algo_params():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"algoId": 123, "clientAlgoId": "bqf-stop"})

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    payload = client.create_conditional_algo_order(
        "btcusdt",
        side=FuturesOrderSide.SELL,
        order_type="STOP_MARKET",
        trigger_price=Decimal("59000"),
        quantity=Decimal("0.002"),
        client_algo_id="bqf-stop",
        timestamp_ms=1,
    )

    body = requests[0].content.decode()
    assert payload["algoId"] == 123
    assert requests[0].method == "POST"
    assert requests[0].url.path == "/fapi/v1/algoOrder"
    assert "algoType=CONDITIONAL" in body
    assert "type=STOP_MARKET" in body
    assert "triggerPrice=59000" in body
    assert "reduceOnly=true" in body
    assert "workingType=MARK_PRICE" in body
    assert "clientAlgoId=bqf-stop" in body


def test_futures_cancel_algo_order_uses_client_algo_id():
    requests = []

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json={"clientAlgoId": "bqf-stop", "status": "CANCELED"})

    client = BinanceUsdmFuturesClient(
        api_key="key",
        api_secret="secret",
        http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    )

    result = client.cancel_algo_order("bqf-stop", timestamp_ms=1)

    assert result["status"] == "CANCELED"
    assert requests[0].method == "DELETE"
    assert requests[0].url.path == "/fapi/v1/algoOrder"
    assert "clientAlgoId=bqf-stop" in requests[0].content.decode()
```

- [ ] **Step 2: Verify exchange tests fail**

Run:

```powershell
python -m pytest tests/test_futures_exchange.py -q
```

Expected: FAIL because `futures_exchange.py` does not exist.

- [ ] **Step 3: Implement futures exchange client**

Create `src/binance_quant/futures_exchange.py` with this structure:

```python
from __future__ import annotations

import time
from collections.abc import Callable
from decimal import Decimal
from typing import Any
from urllib.parse import urlencode

import httpx

from .audit_log import AuditLogger
from .exchange import sign_query
from .futures_models import FuturesOrderSide, FuturesPosition, FuturesSymbolMetadata


FUTURES_TESTNET_REST_BASE_URL = "https://testnet.binancefuture.com"


class BinanceUsdmFuturesClient:
    def __init__(
        self,
        api_key: str,
        api_secret: str,
        base_url: str = FUTURES_TESTNET_REST_BASE_URL,
        base_urls: tuple[str, ...] | None = None,
        http_client: httpx.Client | None = None,
        recv_window: int = 5000,
        proxy_url: str | None = None,
        audit_logger: AuditLogger | None = None,
        clock: Callable[[], float] | None = None,
    ) -> None:
        self.api_key = api_key
        self.api_secret = api_secret
        self.base_urls = tuple(item.rstrip("/") for item in (base_urls or (base_url,)))
        self.base_url = self.base_urls[0]
        self.recv_window = recv_window
        self._client = http_client or httpx.Client(proxy=proxy_url, timeout=10, trust_env=False)
        self.audit_logger = audit_logger or AuditLogger()
        self._clock = clock or time.time

    def exchange_info(self, symbol: str) -> FuturesSymbolMetadata:
        payload = self._public_request("GET", "/fapi/v1/exchangeInfo", {"symbol": symbol.upper()})
        symbol_info = payload["symbols"][0]
        filters = {item["filterType"]: item for item in symbol_info["filters"]}
        lot_size = filters.get("LOT_SIZE", {})
        price_filter = filters.get("PRICE_FILTER", {})
        min_notional_filter = filters.get("MIN_NOTIONAL") or filters.get("NOTIONAL") or {}
        return FuturesSymbolMetadata(
            symbol=symbol_info["symbol"],
            base_asset=symbol_info["baseAsset"],
            quote_asset=symbol_info["quoteAsset"],
            min_qty=Decimal(lot_size.get("minQty", "0")),
            step_size=Decimal(lot_size.get("stepSize", "0")),
            tick_size=Decimal(price_filter.get("tickSize", "0")),
            min_notional=Decimal(min_notional_filter.get("notional", min_notional_filter.get("minNotional", "0"))),
        )

    def klines(self, symbol: str, interval: str, limit: int) -> list:
        return self._public_request(
            "GET",
            "/fapi/v1/klines",
            {"symbol": symbol.upper(), "interval": interval, "limit": str(limit)},
        )

    def depth(self, symbol: str, limit: int = 20) -> dict:
        return self._public_request("GET", "/fapi/v1/depth", {"symbol": symbol.upper(), "limit": str(limit)})

    def ticker_24hr(self, symbol: str) -> dict:
        return self._public_request("GET", "/fapi/v1/ticker/24hr", {"symbol": symbol.upper()})

    def aggregate_trades(self, symbol: str, limit: int = 500) -> list:
        return self._public_request("GET", "/fapi/v1/aggTrades", {"symbol": symbol.upper(), "limit": str(limit)})

    def mark_price(self, symbol: str) -> dict:
        return self._public_request("GET", "/fapi/v1/premiumIndex", {"symbol": symbol.upper()})

    def open_interest(self, symbol: str) -> dict:
        return self._public_request("GET", "/fapi/v1/openInterest", {"symbol": symbol.upper()})

    def global_long_short_ratio(self, symbol: str, period: str = "5m", limit: int = 1) -> list:
        return self._public_request(
            "GET",
            "/futures/data/globalLongShortAccountRatio",
            {"symbol": symbol.upper(), "period": period, "limit": str(limit)},
        )

    def taker_long_short_ratio(self, symbol: str, period: str = "5m", limit: int = 1) -> list:
        return self._public_request(
            "GET",
            "/futures/data/takerlongshortRatio",
            {"symbol": symbol.upper(), "period": period, "limit": str(limit)},
        )

    def current_position_mode(self, timestamp_ms: int | None = None) -> bool:
        payload = self._signed_request("GET", "/fapi/v1/positionSide/dual", {}, timestamp_ms)
        return bool(payload["dualSidePosition"])

    def position_risk(self, symbol: str | None = None, timestamp_ms: int | None = None) -> list[dict]:
        params = {"symbol": symbol.upper()} if symbol else {}
        payload = self._signed_request("GET", "/fapi/v2/positionRisk", params, timestamp_ms)
        if not isinstance(payload, list):
            raise ValueError("position risk response must be a list")
        return payload

    def open_orders(self, symbol: str | None = None, timestamp_ms: int | None = None) -> list[dict]:
        params = {"symbol": symbol.upper()} if symbol else {}
        payload = self._signed_request("GET", "/fapi/v1/openOrders", params, timestamp_ms)
        if not isinstance(payload, list):
            raise ValueError("open orders response must be a list")
        return payload

    def all_algo_orders(self, symbol: str | None = None, timestamp_ms: int | None = None) -> list[dict]:
        params = {"symbol": symbol.upper()} if symbol else {}
        payload = self._signed_request("GET", "/fapi/v1/allAlgoOrders", params, timestamp_ms)
        if not isinstance(payload, list):
            raise ValueError("algo orders response must be a list")
        return payload

    def income(self, start_time_ms: int, end_time_ms: int, timestamp_ms: int | None = None) -> list[dict]:
        payload = self._signed_request(
            "GET",
            "/fapi/v1/income",
            {"startTime": str(start_time_ms), "endTime": str(end_time_ms), "limit": "1000"},
            timestamp_ms,
        )
        if not isinstance(payload, list):
            raise ValueError("income response must be a list")
        return payload

    def change_margin_type(self, symbol: str, margin_type: str = "ISOLATED", timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "POST",
            "/fapi/v1/marginType",
            {"symbol": symbol.upper(), "marginType": margin_type.upper()},
            timestamp_ms,
        )

    def change_leverage(self, symbol: str, leverage: int, timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "POST",
            "/fapi/v1/leverage",
            {"symbol": symbol.upper(), "leverage": str(leverage)},
            timestamp_ms,
        )

    def create_market_order(
        self,
        symbol: str,
        side: FuturesOrderSide,
        quantity: Decimal,
        client_order_id: str,
        *,
        reduce_only: bool,
        timestamp_ms: int | None = None,
    ) -> dict:
        params = {
            "symbol": symbol.upper(),
            "side": side.value,
            "type": "MARKET",
            "quantity": format(quantity, "f"),
            "newClientOrderId": client_order_id,
        }
        if reduce_only:
            params["reduceOnly"] = "true"
        return self._signed_request("POST", "/fapi/v1/order", params, timestamp_ms)

    def create_conditional_algo_order(
        self,
        symbol: str,
        side: FuturesOrderSide,
        order_type: str,
        trigger_price: Decimal,
        quantity: Decimal,
        client_algo_id: str,
        timestamp_ms: int | None = None,
    ) -> dict:
        return self._signed_request(
            "POST",
            "/fapi/v1/algoOrder",
            {
                "symbol": symbol.upper(),
                "side": side.value,
                "algoType": "CONDITIONAL",
                "type": order_type,
                "triggerPrice": format(trigger_price, "f"),
                "quantity": format(quantity, "f"),
                "reduceOnly": "true",
                "workingType": "MARK_PRICE",
                "clientAlgoId": client_algo_id,
            },
            timestamp_ms,
        )

    def cancel_algo_order(self, client_algo_id: str, timestamp_ms: int | None = None) -> dict:
        return self._signed_request(
            "DELETE",
            "/fapi/v1/algoOrder",
            {"clientAlgoId": client_algo_id},
            timestamp_ms,
        )

    def _signed_request(self, method: str, path: str, params: dict[str, str], timestamp_ms: int | None = None) -> Any:
        signed_params = dict(params)
        signed_params["recvWindow"] = str(self.recv_window)
        signed_params["timestamp"] = str(timestamp_ms if timestamp_ms is not None else int(self._clock() * 1000))
        query = urlencode(signed_params)
        signed_params["signature"] = sign_query(query, self.api_secret)
        headers = {"X-MBX-APIKEY": self.api_key}
        request_kwargs = {"params": signed_params} if method.upper() == "GET" else {"data": signed_params}
        response = self._client.request(method, f"{self.base_url}{path}", headers=headers, **request_kwargs)
        response.raise_for_status()
        return response.json()

    def _public_request(self, method: str, path: str, params: dict[str, str]) -> Any:
        response = self._client.request(method, f"{self.base_url}{path}", params=params)
        response.raise_for_status()
        return response.json()


def parse_position(row: dict) -> FuturesPosition:
    return FuturesPosition(
        symbol=str(row["symbol"]),
        quantity=Decimal(str(row.get("positionAmt", "0"))),
        entry_price=Decimal(str(row.get("entryPrice", "0"))),
        mark_price=Decimal(str(row.get("markPrice", "0"))),
        liquidation_price=Decimal(str(row.get("liquidationPrice", "0"))),
        isolated_margin=Decimal(str(row.get("isolatedMargin", row.get("isolatedWallet", "0")))),
        unrealized_pnl=Decimal(str(row.get("unRealizedProfit", "0"))),
        leverage=int(row.get("leverage", "1")),
        margin_type=str(row.get("marginType", "")),
    )
```

- [ ] **Step 4: Verify exchange tests pass**

Run:

```powershell
python -m pytest tests/test_futures_exchange.py -q
```

Expected: PASS.

- [ ] **Step 5: Commit exchange task**

Run:

```powershell
git add src/binance_quant/futures_exchange.py tests/test_futures_exchange.py
git commit -m "feat: add usdm futures client"
```

## Task 4: Futures Storage

**Files:**
- Modify: `src/binance_quant/storage.py`
- Test: `tests/test_storage.py`

- [ ] **Step 1: Write failing storage tests**

Append these tests to `tests/test_storage.py`:

```python
def test_storage_records_futures_ai_trade_run(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()

    run_id = storage.record_futures_ai_trade_run(
        symbol="BTCUSDT",
        action="OPEN_LONG",
        confidence=Decimal("0.82"),
        status="predicted",
        reason="trend",
        leverage=3,
        margin_amount=Decimal("5"),
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
        close_percent=None,
        raw={"confidence": Decimal("0.82")},
        created_at_ms=0,
    )

    [run] = storage.list_futures_ai_trade_runs()
    assert run_id == 1
    assert run["created_at_ms"] == 0
    assert run["symbol"] == "BTCUSDT"
    assert run["action"] == "OPEN_LONG"
    assert run["confidence"] == "0.82"
    assert run["leverage"] == 3
    assert run["margin_amount"] == "5"
    assert run["stop_loss_price"] == "59000"
    assert run["take_profit_price"] == "63000"
    assert run["close_percent"] is None
    assert json.loads(run["raw_json"]) == {"confidence": "0.82"}


def test_storage_records_futures_order_event_and_position_snapshot(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    run_id = storage.record_futures_ai_trade_run(
        "BTCUSDT",
        "OPEN_LONG",
        Decimal("0.82"),
        "predicted",
        "trend",
        raw={},
    )

    event_id = storage.record_futures_order_event(
        run_id=run_id,
        event_type="stop_algo",
        symbol="BTCUSDT",
        client_order_id=None,
        client_algo_id="bqf-stop",
        status="NEW",
        raw={"algoId": 123},
        created_at_ms=0,
    )
    snapshot_id = storage.record_futures_position_snapshot(
        run_id=run_id,
        symbol="BTCUSDT",
        side="LONG",
        quantity=Decimal("0.002"),
        entry_price=Decimal("60000"),
        mark_price=Decimal("60100"),
        liquidation_price=Decimal("55000"),
        isolated_margin=Decimal("5"),
        unrealized_pnl=Decimal("0.2"),
        created_at_ms=0,
    )

    assert event_id == 1
    assert snapshot_id == 1
    [event] = storage.list_futures_order_events(run_id)
    assert event["event_type"] == "stop_algo"
    assert event["client_algo_id"] == "bqf-stop"
    [snapshot] = storage.list_futures_position_snapshots(run_id)
    assert snapshot["side"] == "LONG"
    assert snapshot["quantity"] == "0.002"


def test_storage_updates_futures_ai_trade_run_status(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    run_id = storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.82"), "predicted", "trend", raw={})

    storage.update_futures_ai_trade_run_status(run_id, "protected", "protected", raw={"final": "protected"})

    [run] = storage.list_futures_ai_trade_runs()
    assert run["status"] == "protected"
    assert run["reason"] == "protected"
    assert json.loads(run["raw_json"]) == {"final": "protected"}
```

- [ ] **Step 2: Verify storage tests fail**

Run:

```powershell
python -m pytest tests/test_storage.py::test_storage_records_futures_ai_trade_run tests/test_storage.py::test_storage_records_futures_order_event_and_position_snapshot tests/test_storage.py::test_storage_updates_futures_ai_trade_run_status -q
```

Expected: FAIL because futures storage helpers do not exist.

- [ ] **Step 3: Implement futures tables**

In `Storage.initialize`, add this SQL after `ai_trade_runs`:

```sql
CREATE TABLE IF NOT EXISTS futures_ai_trade_runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at_ms INTEGER NOT NULL,
    symbol TEXT,
    action TEXT NOT NULL,
    confidence TEXT NOT NULL,
    status TEXT NOT NULL,
    reason TEXT NOT NULL,
    leverage INTEGER,
    margin_amount TEXT,
    stop_loss_price TEXT,
    take_profit_price TEXT,
    close_percent INTEGER,
    raw_json TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS futures_order_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at_ms INTEGER NOT NULL,
    run_id INTEGER NOT NULL,
    event_type TEXT NOT NULL,
    symbol TEXT,
    client_order_id TEXT,
    client_algo_id TEXT,
    status TEXT,
    raw_json TEXT NOT NULL,
    FOREIGN KEY(run_id) REFERENCES futures_ai_trade_runs(id)
);
CREATE TABLE IF NOT EXISTS futures_position_snapshots (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at_ms INTEGER NOT NULL,
    run_id INTEGER NOT NULL,
    symbol TEXT NOT NULL,
    side TEXT NOT NULL,
    quantity TEXT NOT NULL,
    entry_price TEXT NOT NULL,
    mark_price TEXT NOT NULL,
    liquidation_price TEXT NOT NULL,
    isolated_margin TEXT NOT NULL,
    unrealized_pnl TEXT NOT NULL,
    FOREIGN KEY(run_id) REFERENCES futures_ai_trade_runs(id)
);
```

Add methods to `Storage`:

```python
def record_futures_ai_trade_run(
    self,
    symbol: str | None,
    action: str,
    confidence: Decimal,
    status: str,
    reason: str,
    raw: dict | None = None,
    leverage: int | None = None,
    margin_amount: Decimal | None = None,
    stop_loss_price: Decimal | None = None,
    take_profit_price: Decimal | None = None,
    close_percent: int | None = None,
    created_at_ms: int | None = None,
) -> int:
    with self._connect() as connection:
        cursor = connection.execute(
            """
            INSERT INTO futures_ai_trade_runs (
                created_at_ms, symbol, action, confidence, status, reason, leverage,
                margin_amount, stop_loss_price, take_profit_price, close_percent, raw_json
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                created_at_ms if created_at_ms is not None else _now_ms(),
                symbol.upper() if symbol else None,
                action.upper(),
                str(confidence),
                status,
                reason,
                leverage,
                str(margin_amount) if margin_amount is not None else None,
                str(stop_loss_price) if stop_loss_price is not None else None,
                str(take_profit_price) if take_profit_price is not None else None,
                close_percent,
                json.dumps(raw or {}, sort_keys=True, default=str),
            ),
        )
        return int(cursor.lastrowid)

def update_futures_ai_trade_run_status(self, run_id: int, status: str, reason: str, raw: dict | None = None) -> None:
    with self._connect() as connection:
        connection.execute(
            """
            UPDATE futures_ai_trade_runs
            SET status = ?, reason = ?, raw_json = COALESCE(?, raw_json)
            WHERE id = ?
            """,
            (status, reason, json.dumps(raw, sort_keys=True, default=str) if raw is not None else None, run_id),
        )

def record_futures_order_event(
    self,
    run_id: int,
    event_type: str,
    symbol: str | None,
    client_order_id: str | None,
    client_algo_id: str | None,
    status: str | None,
    raw: dict,
    created_at_ms: int | None = None,
) -> int:
    with self._connect() as connection:
        cursor = connection.execute(
            """
            INSERT INTO futures_order_events (
                created_at_ms, run_id, event_type, symbol, client_order_id, client_algo_id, status, raw_json
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                created_at_ms if created_at_ms is not None else _now_ms(),
                run_id,
                event_type,
                symbol.upper() if symbol else None,
                client_order_id,
                client_algo_id,
                status,
                json.dumps(raw, sort_keys=True, default=str),
            ),
        )
        return int(cursor.lastrowid)

def record_futures_position_snapshot(
    self,
    run_id: int,
    symbol: str,
    side: str,
    quantity: Decimal,
    entry_price: Decimal,
    mark_price: Decimal,
    liquidation_price: Decimal,
    isolated_margin: Decimal,
    unrealized_pnl: Decimal,
    created_at_ms: int | None = None,
) -> int:
    with self._connect() as connection:
        cursor = connection.execute(
            """
            INSERT INTO futures_position_snapshots (
                created_at_ms, run_id, symbol, side, quantity, entry_price,
                mark_price, liquidation_price, isolated_margin, unrealized_pnl
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                created_at_ms if created_at_ms is not None else _now_ms(),
                run_id,
                symbol.upper(),
                side.upper(),
                str(quantity),
                str(entry_price),
                str(mark_price),
                str(liquidation_price),
                str(isolated_margin),
                str(unrealized_pnl),
            ),
        )
        return int(cursor.lastrowid)

def list_futures_ai_trade_runs(self, limit: int = 50) -> list[dict[str, Any]]:
    if limit < 0:
        raise ValueError("limit must be non-negative")
    with self._connect() as connection:
        rows = connection.execute("SELECT * FROM futures_ai_trade_runs ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
        return [dict(row) for row in rows]

def list_futures_order_events(self, run_id: int) -> list[dict[str, Any]]:
    with self._connect() as connection:
        rows = connection.execute(
            "SELECT * FROM futures_order_events WHERE run_id = ? ORDER BY id",
            (run_id,),
        ).fetchall()
        return [dict(row) for row in rows]

def list_futures_position_snapshots(self, run_id: int) -> list[dict[str, Any]]:
    with self._connect() as connection:
        rows = connection.execute(
            "SELECT * FROM futures_position_snapshots WHERE run_id = ? ORDER BY id",
            (run_id,),
        ).fetchall()
        return [dict(row) for row in rows]
```

- [ ] **Step 4: Verify storage tests pass**

Run:

```powershell
python -m pytest tests/test_storage.py::test_storage_records_futures_ai_trade_run tests/test_storage.py::test_storage_records_futures_order_event_and_position_snapshot tests/test_storage.py::test_storage_updates_futures_ai_trade_run_status -q
```

Expected: PASS.

- [ ] **Step 5: Commit storage task**

Run:

```powershell
git add src/binance_quant/storage.py tests/test_storage.py
git commit -m "feat: add futures audit storage"
```

## Task 5: Prediction Parsing And Schema

**Files:**
- Create: `src/binance_quant/futures_ai_trader.py`
- Test: `tests/test_futures_ai_trader.py`

- [ ] **Step 1: Write failing prediction tests**

Create `tests/test_futures_ai_trader.py`:

```python
import json
from decimal import Decimal

from binance_quant.futures_ai_trader import FuturesAiPrediction, _prediction_schema
from binance_quant.futures_models import FuturesAction


def test_futures_prediction_list_parses_valid_payload():
    predictions = FuturesAiPrediction.from_json_list(
        json.dumps(
            {
                "predictions": [
                    {
                        "symbol": "btcusdt",
                        "action": "OPEN_LONG",
                        "confidence": "0.82",
                        "leverage": 5,
                        "stop_loss_price": "59000",
                        "take_profit_price": "63000",
                        "reason": "BTC trend",
                    },
                    {
                        "symbol": "ethusdt",
                        "action": "CLOSE",
                        "confidence": "0.77",
                        "close_percent": 50,
                        "reason": "ETH risk",
                    },
                ]
            }
        ),
        allowed_symbols=("BTCUSDT", "ETHUSDT"),
    )

    assert [item.symbol for item in predictions] == ["BTCUSDT", "ETHUSDT"]
    assert predictions[0].action is FuturesAction.OPEN_LONG
    assert predictions[0].leverage == 5
    assert predictions[0].stop_loss_price == Decimal("59000")
    assert predictions[0].take_profit_price == Decimal("63000")
    assert predictions[1].action is FuturesAction.CLOSE
    assert predictions[1].close_percent == 50


def test_futures_prediction_rejects_invalid_close_percent_to_hold():
    [prediction] = FuturesAiPrediction.from_json_list(
        json.dumps(
            {
                "predictions": [
                    {
                        "symbol": "BTCUSDT",
                        "action": "CLOSE",
                        "confidence": "0.88",
                        "close_percent": 75,
                        "reason": "invalid percent",
                    }
                ]
            }
        ),
        allowed_symbols=("BTCUSDT",),
    )

    assert prediction.action is FuturesAction.HOLD
    assert prediction.reason == "模型返回的该交易对预测无效"


def test_futures_prediction_schema_uses_futures_actions_and_symbols():
    schema = _prediction_schema(("BTCUSDT", "ETHUSDT"))

    item = schema["properties"]["predictions"]["items"]
    assert item["properties"]["symbol"]["enum"] == ["BTCUSDT", "ETHUSDT"]
    assert item["properties"]["action"]["enum"] == ["OPEN_LONG", "OPEN_SHORT", "CLOSE", "HOLD"]
    assert item["properties"]["close_percent"]["enum"] == [25, 50, 100]
```

- [ ] **Step 2: Verify prediction tests fail**

Run:

```powershell
python -m pytest tests/test_futures_ai_trader.py::test_futures_prediction_list_parses_valid_payload tests/test_futures_ai_trader.py::test_futures_prediction_rejects_invalid_close_percent_to_hold tests/test_futures_ai_trader.py::test_futures_prediction_schema_uses_futures_actions_and_symbols -q
```

Expected: FAIL because `futures_ai_trader.py` does not exist.

- [ ] **Step 3: Implement prediction parser and schema**

Create `src/binance_quant/futures_ai_trader.py` with the parser and schema first:

```python
from __future__ import annotations

import json
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from typing import Any

from .futures_models import FuturesAction, FuturesPrediction


FUTURES_ACTIONS = {item.value for item in FuturesAction}
FUTURES_CLOSE_PERCENTS = {25, 50, 100}


@dataclass(frozen=True)
class FuturesAiPrediction(FuturesPrediction):
    @classmethod
    def hold(cls, reason: str, symbol: str = "") -> "FuturesAiPrediction":
        return cls(symbol=symbol, action=FuturesAction.HOLD, confidence=Decimal("0"), reason=reason)

    @classmethod
    def from_json_list(cls, text: str, allowed_symbols: tuple[str, ...]) -> list["FuturesAiPrediction"]:
        allowed = tuple(item.upper() for item in allowed_symbols)
        try:
            payload = json.loads(text)
        except json.JSONDecodeError:
            return [cls.hold("模型响应无效", symbol) for symbol in allowed]
        if not isinstance(payload, dict) or not isinstance(payload.get("predictions"), list):
            return [cls.hold("模型响应无效", symbol) for symbol in allowed]

        by_symbol: dict[str, FuturesAiPrediction] = {}
        invalid_symbols: set[str] = set()
        for item in payload["predictions"]:
            symbol = str(item.get("symbol", "")).strip().upper() if isinstance(item, dict) else ""
            prediction = cls.from_payload(item, allowed)
            if prediction is None:
                if symbol in allowed:
                    invalid_symbols.add(symbol)
                continue
            by_symbol.setdefault(prediction.symbol, prediction)

        result: list[FuturesAiPrediction] = []
        for symbol in allowed:
            if symbol in by_symbol:
                result.append(by_symbol[symbol])
            elif symbol in invalid_symbols:
                result.append(cls.hold("模型返回的该交易对预测无效", symbol))
            else:
                result.append(cls.hold("模型未返回该交易对预测", symbol))
        return result

    @classmethod
    def from_payload(cls, payload: object, allowed_symbols: tuple[str, ...]) -> "FuturesAiPrediction | None":
        if not isinstance(payload, dict):
            return None
        symbol = str(payload.get("symbol", "")).strip().upper()
        if symbol not in allowed_symbols:
            return None
        action_text = str(payload.get("action", "")).strip().upper()
        if action_text not in FUTURES_ACTIONS:
            return None
        try:
            confidence = Decimal(str(payload.get("confidence", "")))
        except (InvalidOperation, ValueError):
            return None
        if not confidence.is_finite() or confidence < 0 or confidence > 1:
            return None
        reason = str(payload.get("reason", "")).strip()
        if not reason:
            return None

        action = FuturesAction(action_text)
        leverage = _optional_int(payload.get("leverage"))
        stop_loss_price = _optional_decimal(payload.get("stop_loss_price"))
        take_profit_price = _optional_decimal(payload.get("take_profit_price"))
        close_percent = _optional_int(payload.get("close_percent"))
        if action in {FuturesAction.OPEN_LONG, FuturesAction.OPEN_SHORT}:
            if stop_loss_price is None or take_profit_price is None:
                return None
        if action is FuturesAction.CLOSE and close_percent not in FUTURES_CLOSE_PERCENTS:
            return None

        return cls(
            symbol=symbol,
            action=action,
            confidence=confidence,
            reason=reason,
            leverage=leverage,
            stop_loss_price=stop_loss_price,
            take_profit_price=take_profit_price,
            close_percent=close_percent,
        )


def _prediction_schema(allowed_symbols: tuple[str, ...]) -> dict[str, Any]:
    symbols = [symbol.upper() for symbol in allowed_symbols]
    return {
        "type": "object",
        "additionalProperties": False,
        "required": ["predictions"],
        "properties": {
            "predictions": {
                "type": "array",
                "minItems": len(symbols),
                "maxItems": len(symbols),
                "items": {
                    "type": "object",
                    "additionalProperties": False,
                    "required": ["symbol", "action", "confidence", "reason"],
                    "properties": {
                        "symbol": {"type": "string", "enum": symbols},
                        "action": {"type": "string", "enum": ["OPEN_LONG", "OPEN_SHORT", "CLOSE", "HOLD"]},
                        "confidence": {"type": "string", "pattern": r"^(0(\.\d{1,4})?|1(\.0{1,4})?)$"},
                        "leverage": {"type": "integer", "minimum": 1},
                        "stop_loss_price": {"type": "string", "minLength": 1},
                        "take_profit_price": {"type": "string", "minLength": 1},
                        "close_percent": {"type": "integer", "enum": [25, 50, 100]},
                        "reason": {"type": "string", "minLength": 1},
                    },
                },
            }
        },
    }


def _optional_decimal(value: object) -> Decimal | None:
    if value is None or value == "":
        return None
    try:
        return Decimal(str(value))
    except (InvalidOperation, ValueError):
        return None


def _optional_int(value: object) -> int | None:
    if value is None or value == "":
        return None
    try:
        return int(value)
    except (TypeError, ValueError):
        return None
```

- [ ] **Step 4: Verify prediction tests pass**

Run:

```powershell
python -m pytest tests/test_futures_ai_trader.py::test_futures_prediction_list_parses_valid_payload tests/test_futures_ai_trader.py::test_futures_prediction_rejects_invalid_close_percent_to_hold tests/test_futures_ai_trader.py::test_futures_prediction_schema_uses_futures_actions_and_symbols -q
```

Expected: PASS.

- [ ] **Step 5: Commit prediction task**

Run:

```powershell
git add src/binance_quant/futures_ai_trader.py tests/test_futures_ai_trader.py
git commit -m "feat: parse futures ai predictions"
```

## Task 6: Futures Risk Engine

**Files:**
- Create: `src/binance_quant/futures_risk.py`
- Test: `tests/test_futures_risk.py`

- [ ] **Step 1: Write failing risk tests**

Create `tests/test_futures_risk.py`:

```python
from decimal import Decimal

from binance_quant.futures_models import (
    FuturesAction,
    FuturesPosition,
    FuturesPrediction,
    FuturesSymbolMetadata,
)
from binance_quant.futures_risk import FuturesRiskContext, FuturesRiskEngine, FuturesRiskSettings


def metadata():
    return FuturesSymbolMetadata(
        "BTCUSDT",
        "BTC",
        "USDT",
        min_qty=Decimal("0.001"),
        step_size=Decimal("0.001"),
        tick_size=Decimal("0.1"),
        min_notional=Decimal("100"),
    )


def flat_position():
    return FuturesPosition("BTCUSDT", Decimal("0"), Decimal("0"), Decimal("60000"), Decimal("0"), Decimal("0"), Decimal("0"), 2, "isolated")


def long_position():
    return FuturesPosition("BTCUSDT", Decimal("0.002"), Decimal("60000"), Decimal("60100"), Decimal("55000"), Decimal("5"), Decimal("0.2"), 2, "isolated")


def settings():
    return FuturesRiskSettings(
        margin_amount=Decimal("5"),
        default_leverage=2,
        max_leverage=3,
        max_total_margin=Decimal("20"),
        min_confidence=Decimal("0.70"),
        daily_realized_loss_limit=Decimal("10"),
        max_margin_loss_percent=Decimal("50"),
        min_liquidation_buffer_percent=Decimal("2"),
        min_stop_distance_percent=Decimal("0.3"),
        max_stop_distance_percent=Decimal("5"),
        min_take_profit_distance_percent=Decimal("0.3"),
        max_take_profit_distance_percent=Decimal("10"),
    )


def test_risk_approves_open_long_and_clips_leverage():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=10,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    context = FuturesRiskContext(
        prediction=prediction,
        metadata=metadata(),
        position=flat_position(),
        mark_price=Decimal("60000"),
        total_isolated_margin=Decimal("5"),
        daily_realized_pnl=Decimal("0"),
        hedge_mode=False,
    )

    decision = FuturesRiskEngine(settings()).evaluate(context)

    assert decision.approved
    assert decision.reason == "approved"
    assert decision.leverage == 3
    assert decision.notional == Decimal("15")
    assert decision.margin_amount == Decimal("5")


def test_risk_rejects_open_when_daily_loss_limit_reached_but_allows_close():
    engine = FuturesRiskEngine(settings())
    open_prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    close_prediction = FuturesPrediction("BTCUSDT", FuturesAction.CLOSE, Decimal("0.82"), "risk", close_percent=50)

    open_decision = engine.evaluate(
        FuturesRiskContext(open_prediction, metadata(), flat_position(), Decimal("60000"), Decimal("5"), Decimal("-10"), False)
    )
    close_decision = engine.evaluate(
        FuturesRiskContext(close_prediction, metadata(), long_position(), Decimal("60000"), Decimal("5"), Decimal("-10"), False)
    )

    assert open_decision.approved is False
    assert open_decision.reason == "daily realized loss limit reached"
    assert close_decision.approved is True


def test_risk_rejects_same_direction_add_and_opposite_open():
    engine = FuturesRiskEngine(settings())
    open_long = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    open_short = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_SHORT,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("61000"),
        take_profit_price=Decimal("57000"),
    )

    same = engine.evaluate(FuturesRiskContext(open_long, metadata(), long_position(), Decimal("60000"), Decimal("5"), Decimal("0"), False))
    opposite = engine.evaluate(FuturesRiskContext(open_short, metadata(), long_position(), Decimal("60000"), Decimal("5"), Decimal("0"), False))

    assert same.reason == "same-direction position exists"
    assert opposite.reason == "opposite position requires close first"


def test_risk_rejects_invalid_stop_take_profit_direction():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("61000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(prediction, metadata(), flat_position(), Decimal("60000"), Decimal("5"), Decimal("0"), False)
    )

    assert decision.approved is False
    assert decision.reason == "invalid stop or take-profit direction"


def test_risk_rejects_open_when_estimated_liquidation_buffer_is_too_small():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "high leverage",
        leverage=50,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    risky_settings = replace(settings(), max_leverage=50, min_liquidation_buffer_percent=Decimal("3"))

    decision = FuturesRiskEngine(risky_settings).evaluate(
        FuturesRiskContext(prediction, metadata(), flat_position(), Decimal("60000"), Decimal("0"), Decimal("0"), False)
    )

    assert decision.approved is False
    assert decision.reason == "estimated liquidation buffer below minimum"
```

- [ ] **Step 2: Verify risk tests fail**

Run:

```powershell
python -m pytest tests/test_futures_risk.py -q
```

Expected: FAIL because `futures_risk.py` does not exist.

- [ ] **Step 3: Implement risk engine**

Create `src/binance_quant/futures_risk.py`:

```python
from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal

from .futures_models import (
    FuturesAction,
    FuturesPosition,
    FuturesPositionSide,
    FuturesPrediction,
    FuturesRiskDecision,
    FuturesSymbolMetadata,
)


@dataclass(frozen=True)
class FuturesRiskSettings:
    margin_amount: Decimal
    default_leverage: int
    max_leverage: int
    max_total_margin: Decimal
    min_confidence: Decimal
    daily_realized_loss_limit: Decimal
    max_margin_loss_percent: Decimal
    min_liquidation_buffer_percent: Decimal
    min_stop_distance_percent: Decimal
    max_stop_distance_percent: Decimal
    min_take_profit_distance_percent: Decimal
    max_take_profit_distance_percent: Decimal


@dataclass(frozen=True)
class FuturesRiskContext:
    prediction: FuturesPrediction
    metadata: FuturesSymbolMetadata
    position: FuturesPosition
    mark_price: Decimal
    total_isolated_margin: Decimal
    daily_realized_pnl: Decimal
    hedge_mode: bool


class FuturesRiskEngine:
    def __init__(self, settings: FuturesRiskSettings) -> None:
        self.settings = settings

    def evaluate(self, context: FuturesRiskContext) -> FuturesRiskDecision:
        prediction = context.prediction
        if context.hedge_mode:
            return FuturesRiskDecision(False, "hedge mode active", prediction)
        if prediction.confidence < self.settings.min_confidence:
            return FuturesRiskDecision(False, "confidence below minimum", prediction)
        if prediction.symbol != context.metadata.symbol:
            return FuturesRiskDecision(False, "metadata symbol mismatch", prediction)
        if prediction.action is FuturesAction.HOLD:
            return FuturesRiskDecision(False, "hold signal", prediction)
        if prediction.action is FuturesAction.CLOSE:
            return self._evaluate_close(context)
        if context.daily_realized_pnl <= -self.settings.daily_realized_loss_limit:
            return FuturesRiskDecision(False, "daily realized loss limit reached", prediction)
        return self._evaluate_open(context)

    def _evaluate_close(self, context: FuturesRiskContext) -> FuturesRiskDecision:
        prediction = context.prediction
        if context.position.side is FuturesPositionSide.FLAT:
            return FuturesRiskDecision(False, "no position to close", prediction)
        if prediction.close_percent not in {25, 50, 100}:
            return FuturesRiskDecision(False, "invalid close percent", prediction)
        return FuturesRiskDecision(True, "approved", prediction)

    def _evaluate_open(self, context: FuturesRiskContext) -> FuturesRiskDecision:
        prediction = context.prediction
        if context.position.side is not FuturesPositionSide.FLAT:
            if (
                prediction.action is FuturesAction.OPEN_LONG
                and context.position.side is FuturesPositionSide.LONG
            ) or (
                prediction.action is FuturesAction.OPEN_SHORT
                and context.position.side is FuturesPositionSide.SHORT
            ):
                return FuturesRiskDecision(False, "same-direction position exists", prediction)
            return FuturesRiskDecision(False, "opposite position requires close first", prediction)

        leverage = min(prediction.leverage or self.settings.default_leverage, self.settings.max_leverage)
        notional = self.settings.margin_amount * Decimal(leverage)
        if context.total_isolated_margin + self.settings.margin_amount > self.settings.max_total_margin:
            return FuturesRiskDecision(False, "total margin above max", prediction)
        if notional < context.metadata.min_notional:
            return FuturesRiskDecision(False, "notional below min notional", prediction)
        if prediction.stop_loss_price is None or prediction.take_profit_price is None:
            return FuturesRiskDecision(False, "missing protection prices", prediction)
        if not self._valid_protection_direction(prediction, context.mark_price):
            return FuturesRiskDecision(False, "invalid stop or take-profit direction", prediction)
        if not self._distance_in_range(context.mark_price, prediction.stop_loss_price, self.settings.min_stop_distance_percent, self.settings.max_stop_distance_percent):
            return FuturesRiskDecision(False, "stop distance outside limits", prediction)
        if not self._distance_in_range(context.mark_price, prediction.take_profit_price, self.settings.min_take_profit_distance_percent, self.settings.max_take_profit_distance_percent):
            return FuturesRiskDecision(False, "take-profit distance outside limits", prediction)
        stop_loss_amount = notional * self._distance_percent(context.mark_price, prediction.stop_loss_price) / Decimal("100")
        max_loss_amount = self.settings.margin_amount * self.settings.max_margin_loss_percent / Decimal("100")
        if stop_loss_amount > max_loss_amount:
            return FuturesRiskDecision(False, "estimated stop loss above margin risk budget", prediction)
        if self._estimated_liquidation_buffer_percent(leverage) <= self.settings.min_liquidation_buffer_percent:
            return FuturesRiskDecision(False, "estimated liquidation buffer below minimum", prediction)
        return FuturesRiskDecision(True, "approved", prediction, leverage=leverage, margin_amount=self.settings.margin_amount, notional=notional)

    def _valid_protection_direction(self, prediction: FuturesPrediction, mark_price: Decimal) -> bool:
        if prediction.stop_loss_price is None or prediction.take_profit_price is None:
            return False
        if prediction.action is FuturesAction.OPEN_LONG:
            return prediction.stop_loss_price < mark_price < prediction.take_profit_price
        if prediction.action is FuturesAction.OPEN_SHORT:
            return prediction.take_profit_price < mark_price < prediction.stop_loss_price
        return False

    def _distance_in_range(self, reference: Decimal, price: Decimal | None, minimum: Decimal, maximum: Decimal) -> bool:
        if price is None:
            return False
        distance = self._distance_percent(reference, price)
        return minimum <= distance <= maximum

    def _distance_percent(self, reference: Decimal, price: Decimal) -> Decimal:
        return abs(reference - price) / reference * Decimal("100")

    def _estimated_liquidation_buffer_percent(self, leverage: int) -> Decimal:
        return Decimal("100") / Decimal(leverage)
```

- [ ] **Step 4: Verify risk tests pass**

Run:

```powershell
python -m pytest tests/test_futures_risk.py -q
```

Expected: PASS.

- [ ] **Step 5: Commit risk task**

Run:

```powershell
git add src/binance_quant/futures_risk.py tests/test_futures_risk.py
git commit -m "feat: add futures risk engine"
```

## Task 7: Futures Execution Engine

**Files:**
- Create: `src/binance_quant/futures_execution.py`
- Test: `tests/test_futures_execution.py`

- [ ] **Step 1: Write failing execution tests**

Create `tests/test_futures_execution.py`:

```python
from decimal import Decimal

from binance_quant.futures_execution import FuturesExecutionEngine, build_futures_client_order_id, round_down
from binance_quant.futures_models import (
    FuturesAction,
    FuturesOrderSide,
    FuturesPrediction,
    FuturesRiskDecision,
    FuturesSymbolMetadata,
)
from binance_quant.models import TradingMode
from binance_quant.storage import Storage


class FakeFuturesExchange:
    def __init__(self, protection_fails=False):
        self.calls = []
        self.protection_fails = protection_fails

    def change_margin_type(self, symbol, margin_type="ISOLATED"):
        self.calls.append(("change_margin_type", symbol, margin_type))
        return {"code": 200}

    def change_leverage(self, symbol, leverage):
        self.calls.append(("change_leverage", symbol, leverage))
        return {"leverage": leverage}

    def create_market_order(self, symbol, side, quantity, client_order_id, *, reduce_only):
        self.calls.append(("create_market_order", symbol, side, quantity, client_order_id, reduce_only))
        return {"status": "FILLED", "clientOrderId": client_order_id, "executedQty": str(quantity)}

    def create_conditional_algo_order(self, symbol, side, order_type, trigger_price, quantity, client_algo_id):
        self.calls.append(("create_conditional_algo_order", symbol, side, order_type, trigger_price, quantity, client_algo_id))
        if self.protection_fails:
            raise RuntimeError("algo rejected")
        return {"algoId": 1, "clientAlgoId": client_algo_id, "status": "NEW"}


def metadata():
    return FuturesSymbolMetadata("BTCUSDT", "BTC", "USDT", Decimal("0.001"), Decimal("0.001"), Decimal("0.1"), Decimal("100"))


def approved_open_prediction():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.88"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    return FuturesRiskDecision(True, "approved", prediction, leverage=2, margin_amount=Decimal("5"), notional=Decimal("10"))


def test_round_down_uses_exchange_step_size():
    assert round_down(Decimal("0.0029"), Decimal("0.001")) == Decimal("0.002")


def test_futures_execution_dry_run_records_no_exchange_calls(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    exchange = FakeFuturesExchange()
    engine = FuturesExecutionEngine(TradingMode.DRY_RUN, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "dry_run"
    assert exchange.calls == []
    [event] = storage.list_futures_order_events(1)
    assert event["event_type"] == "dry_run_open"


def test_futures_execution_live_open_places_market_and_two_algo_orders(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "protected"
    assert exchange.calls[0] == ("change_margin_type", "BTCUSDT", "ISOLATED")
    assert exchange.calls[1] == ("change_leverage", "BTCUSDT", 2)
    assert exchange.calls[2][0] == "create_market_order"
    assert exchange.calls[2][2] is FuturesOrderSide.BUY
    assert exchange.calls[3][2] is FuturesOrderSide.SELL
    assert exchange.calls[3][3] == "STOP_MARKET"
    assert exchange.calls[4][3] == "TAKE_PROFIT_MARKET"


def test_futures_execution_rolls_back_when_protection_fails(tmp_path):
    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "OPEN_LONG", Decimal("0.88"), "predicted", "trend", raw={})
    exchange = FakeFuturesExchange(protection_fails=True)
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)

    result = engine.execute_open(1, approved_open_prediction(), metadata(), reference_price=Decimal("60000"))

    assert result.status == "rollback_completed"
    rollback_call = exchange.calls[-1]
    assert rollback_call[0] == "create_market_order"
    assert rollback_call[2] is FuturesOrderSide.SELL
    assert rollback_call[5] is True
```

- [ ] **Step 2: Verify execution tests fail**

Run:

```powershell
python -m pytest tests/test_futures_execution.py -q
```

Expected: FAIL because `futures_execution.py` does not exist.

- [ ] **Step 3: Implement execution engine**

Create `src/binance_quant/futures_execution.py`:

```python
from __future__ import annotations

import uuid
from decimal import Decimal, ROUND_DOWN

from .futures_models import FuturesAction, FuturesExecutionResult, FuturesOrderSide, FuturesRiskDecision, FuturesSymbolMetadata
from .models import TradingMode
from .storage import Storage


def build_futures_client_order_id(prefix: str = "bqf") -> str:
    return f"{prefix}-{uuid.uuid4().hex[:24]}"


def round_down(value: Decimal, increment: Decimal) -> Decimal:
    if increment <= 0:
        return value
    return (value / increment).to_integral_value(rounding=ROUND_DOWN) * increment


class FuturesExecutionEngine:
    def __init__(self, mode: TradingMode, exchange_client, storage: Storage) -> None:
        self.mode = mode
        self.exchange_client = exchange_client
        self.storage = storage

    def execute_open(
        self,
        run_id: int,
        decision: FuturesRiskDecision,
        metadata: FuturesSymbolMetadata,
        *,
        reference_price: Decimal,
    ) -> FuturesExecutionResult:
        prediction = decision.prediction
        leverage = decision.leverage or prediction.leverage or 1
        notional = decision.notional or Decimal("0")
        quantity = round_down(notional / reference_price, metadata.step_size)
        open_side = FuturesOrderSide.BUY if prediction.action is FuturesAction.OPEN_LONG else FuturesOrderSide.SELL
        close_side = FuturesOrderSide.SELL if open_side is FuturesOrderSide.BUY else FuturesOrderSide.BUY
        if self.mode is TradingMode.DRY_RUN:
            self.storage.record_futures_order_event(run_id, "dry_run_open", prediction.symbol, None, None, "DRY_RUN", {"quantity": quantity})
            return FuturesExecutionResult(run_id, "dry_run", prediction.symbol, prediction.action.value, "dry run", prediction.confidence, leverage, decision.margin_amount)

        self.exchange_client.change_margin_type(prediction.symbol, "ISOLATED")
        self.exchange_client.change_leverage(prediction.symbol, leverage)
        open_client_order_id = build_futures_client_order_id("bqf-open")
        open_payload = self.exchange_client.create_market_order(
            prediction.symbol,
            open_side,
            quantity,
            open_client_order_id,
            reduce_only=False,
        )
        self.storage.record_futures_order_event(run_id, "open", prediction.symbol, open_client_order_id, None, open_payload.get("status"), open_payload)
        try:
            stop_algo_id = build_futures_client_order_id("bqf-stop")
            stop_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                close_side,
                "STOP_MARKET",
                prediction.stop_loss_price,
                quantity,
                stop_algo_id,
            )
            self.storage.record_futures_order_event(run_id, "stop_algo", prediction.symbol, None, stop_algo_id, stop_payload.get("status"), stop_payload)
            take_algo_id = build_futures_client_order_id("bqf-tp")
            take_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                close_side,
                "TAKE_PROFIT_MARKET",
                prediction.take_profit_price,
                quantity,
                take_algo_id,
            )
            self.storage.record_futures_order_event(run_id, "take_profit_algo", prediction.symbol, None, take_algo_id, take_payload.get("status"), take_payload)
        except Exception:
            rollback_id = build_futures_client_order_id("bqf-rb")
            rollback_payload = self.exchange_client.create_market_order(
                prediction.symbol,
                close_side,
                quantity,
                rollback_id,
                reduce_only=True,
            )
            self.storage.record_futures_order_event(run_id, "rollback", prediction.symbol, rollback_id, None, rollback_payload.get("status"), rollback_payload)
            return FuturesExecutionResult(run_id, "rollback_completed", prediction.symbol, prediction.action.value, "protection failed; rollback completed", prediction.confidence, leverage, decision.margin_amount)

        return FuturesExecutionResult(run_id, "protected", prediction.symbol, prediction.action.value, "protected", prediction.confidence, leverage, decision.margin_amount)
```

- [ ] **Step 4: Verify execution tests pass**

Run:

```powershell
python -m pytest tests/test_futures_execution.py -q
```

Expected: PASS.

- [ ] **Step 5: Commit execution task**

Run:

```powershell
git add src/binance_quant/futures_execution.py tests/test_futures_execution.py
git commit -m "feat: execute futures open orders"
```

## Task 8: Futures Batch Runner

**Files:**
- Modify: `src/binance_quant/futures_ai_trader.py`
- Test: `tests/test_futures_ai_trader.py`

- [ ] **Step 1: Write failing runner tests**

Append to `tests/test_futures_ai_trader.py`:

```python
from dataclasses import dataclass
from decimal import Decimal

from binance_quant.config import Settings
from binance_quant.futures_ai_trader import FuturesAiBatchResult, run_live_futures_ai_trade_batch_once
from binance_quant.futures_exchange import parse_position


class StaticFuturesPredictor:
    def __init__(self, predictions):
        self.predictions = predictions
        self.contexts = []

    def predict(self, context, allowed_symbols):
        self.contexts.append((context, allowed_symbols))
        return self.predictions


class FakeFuturesExchange:
    def __init__(self):
        self.calls = []

    def current_position_mode(self):
        self.calls.append(("current_position_mode",))
        return False

    def position_risk(self, symbol=None):
        self.calls.append(("position_risk", symbol))
        return [
            {
                "symbol": symbol or "BTCUSDT",
                "positionAmt": "0",
                "entryPrice": "0",
                "markPrice": "60000",
                "liquidationPrice": "0",
                "isolatedMargin": "0",
                "unRealizedProfit": "0",
                "leverage": "2",
                "marginType": "isolated",
            }
        ]

    def exchange_info(self, symbol):
        self.calls.append(("exchange_info", symbol))
        from binance_quant.futures_models import FuturesSymbolMetadata

        return FuturesSymbolMetadata(symbol, "BTC", "USDT", Decimal("0.001"), Decimal("0.001"), Decimal("0.1"), Decimal("1"))

    def mark_price(self, symbol):
        self.calls.append(("mark_price", symbol))
        return {"markPrice": "60000", "lastFundingRate": "0.0001", "nextFundingTime": 1}

    def depth(self, symbol, limit=20):
        return {"bids": [["59999", "1"]], "asks": [["60001", "1"]]}

    def ticker_24hr(self, symbol):
        return {"priceChangePercent": "1", "quoteVolume": "100000"}

    def aggregate_trades(self, symbol, limit=500):
        return []

    def klines(self, symbol, interval, limit):
        return [[1, "60000", "60100", "59900", "60050", "10", 2, "600500", 1, "5", "300250"]]

    def open_interest(self, symbol):
        return {"openInterest": "123"}

    def global_long_short_ratio(self, symbol, period="5m", limit=1):
        return [{"longShortRatio": "1.2"}]

    def taker_long_short_ratio(self, symbol, period="5m", limit=1):
        return [{"buySellRatio": "1.1"}]

    def all_algo_orders(self, symbol=None):
        return []

    def income(self, start_time_ms, end_time_ms):
        return [
            {"incomeType": "REALIZED_PNL", "income": "-1.5"},
            {"incomeType": "COMMISSION", "income": "-0.1"},
            {"incomeType": "FUNDING_FEE", "income": "0.05"},
        ]


def futures_settings(tmp_path, **overrides):
    values = {
        "mode": "live",
        "api_key": "key",
        "api_secret": "secret",
        "database_path": tmp_path / "audit.sqlite3",
        "live_futures_ai_trader_enabled": True,
        "live_futures_confirm_production": True,
        "futures_rest_base_url": "https://fapi.binance.com",
        "futures_stream_base_url": "wss://fstream.binance.com/ws",
        "futures_ai_symbols": ("BTCUSDT",),
        "futures_margin_amount": Decimal("5"),
        "futures_default_leverage": 2,
        "futures_max_leverage": 3,
        "futures_max_total_margin": Decimal("20"),
    }
    values.update(overrides)
    return Settings(_env_file=None, **values)


def test_futures_batch_rejects_when_live_futures_disabled(tmp_path):
    settings = futures_settings(tmp_path, live_futures_ai_trader_enabled=False)

    result = run_live_futures_ai_trade_batch_once(settings, exchange_client=FakeFuturesExchange(), predictor=StaticFuturesPredictor([]))

    assert result.status == "rejected"
    assert result.reason == "live futures ai trader disabled"


def test_futures_batch_records_hold_prediction(tmp_path):
    prediction = FuturesAiPrediction("BTCUSDT", FuturesAction.HOLD, Decimal("0.33"), "BTC震荡")
    result = run_live_futures_ai_trade_batch_once(
        futures_settings(tmp_path),
        exchange_client=FakeFuturesExchange(),
        predictor=StaticFuturesPredictor([prediction]),
    )

    assert result.status == "no_action"
    assert result.prediction_count == 1
    assert result.executed_count == 0
    assert result.results[0].status == "no_action"


def test_futures_batch_context_includes_futures_market_and_account_features(tmp_path):
    predictor = StaticFuturesPredictor([FuturesAiPrediction("BTCUSDT", FuturesAction.HOLD, Decimal("0.33"), "BTC震荡")])

    result = run_live_futures_ai_trade_batch_once(
        futures_settings(tmp_path),
        exchange_client=FakeFuturesExchange(),
        predictor=predictor,
        now_ms=1_700_000_000_000,
    )

    context, allowed_symbols = predictor.contexts[0]
    symbol_context = context["market"][0]
    assert allowed_symbols == ("BTCUSDT",)
    assert result.daily_realized_pnl == Decimal("-1.55")
    assert result.total_isolated_margin == Decimal("0")
    assert symbol_context["funding"]["last_funding_rate"] == "0.0001"
    assert symbol_context["open_interest"] == "123"
    assert symbol_context["long_short_ratio"] == "1.2"
    assert symbol_context["taker_long_short_ratio"] == "1.1"
    assert symbol_context["order_book"]["spread"] == "2"


def test_futures_batch_preflight_rejects_hedge_mode(tmp_path):
    class HedgeExchange(FakeFuturesExchange):
        def current_position_mode(self):
            return True

    result = run_live_futures_ai_trade_batch_once(
        futures_settings(tmp_path),
        exchange_client=HedgeExchange(),
        predictor=StaticFuturesPredictor([]),
    )

    assert result.status == "rejected"
    assert result.reason == "hedge mode active"
```

- [ ] **Step 2: Verify runner tests fail**

Run:

```powershell
python -m pytest tests/test_futures_ai_trader.py::test_futures_batch_rejects_when_live_futures_disabled tests/test_futures_ai_trader.py::test_futures_batch_records_hold_prediction tests/test_futures_ai_trader.py::test_futures_batch_context_includes_futures_market_and_account_features tests/test_futures_ai_trader.py::test_futures_batch_preflight_rejects_hedge_mode -q
```

Expected: FAIL because runner classes/functions do not exist.

- [ ] **Step 3: Implement minimal batch runner**

Add to `src/binance_quant/futures_ai_trader.py`:

```python
import time
from dataclasses import dataclass
from decimal import Decimal
from pathlib import Path
from typing import Any

from .config import Settings
from .futures_exchange import BinanceUsdmFuturesClient, parse_position
from .futures_models import FuturesAction, FuturesAiPrediction
from .futures_models import FuturesExecutionResult
from .storage import Storage


@dataclass(frozen=True)
class FuturesAiBatchResult:
    status: str
    reason: str
    max_actions: int
    prediction_count: int
    executed_count: int
    daily_realized_pnl: Decimal
    total_isolated_margin: Decimal
    results: tuple[FuturesExecutionResult, ...]


class StaticNoopFuturesPredictor:
    def predict(self, context: dict[str, Any], allowed_symbols: tuple[str, ...]) -> list[FuturesAiPrediction]:
        return [FuturesAiPrediction.hold("no predictor configured", symbol) for symbol in allowed_symbols]


def run_live_futures_ai_trade_batch_once(
    settings: Settings,
    exchange_client=None,
    predictor=None,
    now_ms: int | None = None,
) -> FuturesAiBatchResult:
    now_ms = now_ms if now_ms is not None else int(time.time() * 1000)
    storage = Storage(settings.database_path)
    storage.initialize()
    if settings.mode.value == "live" and not settings.live_futures_ai_trader_enabled:
        return FuturesAiBatchResult("rejected", "live futures ai trader disabled", settings.futures_ai_max_actions_per_run, 0, 0, Decimal("0"), Decimal("0"), ())
    if settings.mode.value == "live" and not settings.live_futures_confirm_production:
        return FuturesAiBatchResult("rejected", "live futures production confirmation disabled", settings.futures_ai_max_actions_per_run, 0, 0, Decimal("0"), Decimal("0"), ())
    exchange = exchange_client or BinanceUsdmFuturesClient(
        settings.api_key,
        settings.api_secret.get_secret_value(),
        base_url=settings.futures_rest_base_url,
        base_urls=settings.futures_rest_base_urls,
        recv_window=settings.recv_window,
        proxy_url=settings.proxy_url,
    )
    hedge_mode = exchange.current_position_mode()
    if hedge_mode:
        return FuturesAiBatchResult("rejected", "hedge mode active", settings.futures_ai_max_actions_per_run, 0, 0, Decimal("0"), Decimal("0"), ())
    daily_realized_pnl = _daily_realized_pnl(exchange, now_ms)
    total_isolated_margin = _total_isolated_margin(exchange, settings.futures_ai_symbols)
    context = _model_context(settings, exchange, daily_realized_pnl, total_isolated_margin)
    predictions = (predictor or StaticNoopFuturesPredictor()).predict(context, settings.futures_ai_symbols)
    results: list[FuturesExecutionResult] = []
    prediction_rows: list[tuple[FuturesAiPrediction, int]] = []
    for prediction in predictions:
        run_id = storage.record_futures_ai_trade_run(
            prediction.symbol,
            prediction.action.value,
            prediction.confidence,
            "predicted",
            prediction.reason,
            raw=_prediction_raw(prediction),
            leverage=prediction.leverage,
            stop_loss_price=prediction.stop_loss_price,
            take_profit_price=prediction.take_profit_price,
            close_percent=prediction.close_percent,
            created_at_ms=now_ms,
        )
        prediction_rows.append((prediction, run_id))
        if prediction.action is FuturesAction.HOLD:
            storage.update_futures_ai_trade_run_status(run_id, "no_action", prediction.reason, raw=_prediction_raw(prediction))
            results.append(FuturesExecutionResult(run_id, "no_action", prediction.symbol, prediction.action.value, prediction.reason, prediction.confidence))
    status = "no_action" if not any(item.status != "no_action" for item in results) else "completed"
    reason = "无合约动作" if status == "no_action" else "AI合约批量交易完成"
    return FuturesAiBatchResult(
        status,
        reason,
        settings.futures_ai_max_actions_per_run,
        len(predictions),
        0,
        daily_realized_pnl,
        total_isolated_margin,
        tuple(results),
    )


def _prediction_raw(prediction: FuturesAiPrediction) -> dict[str, str]:
    return {
        "symbol": prediction.symbol,
        "action": prediction.action.value,
        "confidence": str(prediction.confidence),
        "leverage": "" if prediction.leverage is None else str(prediction.leverage),
        "stop_loss_price": "" if prediction.stop_loss_price is None else str(prediction.stop_loss_price),
        "take_profit_price": "" if prediction.take_profit_price is None else str(prediction.take_profit_price),
        "close_percent": "" if prediction.close_percent is None else str(prediction.close_percent),
        "reason": prediction.reason,
    }


def _model_context(
    settings: Settings,
    exchange,
    daily_realized_pnl: Decimal,
    total_isolated_margin: Decimal,
) -> dict[str, Any]:
    market = []
    for symbol in settings.futures_ai_symbols:
        mark_price = exchange.mark_price(symbol)
        position_payload = exchange.position_risk(symbol)
        position = parse_position(position_payload[0])
        depth = exchange.depth(symbol)
        best_bid = Decimal(str(depth["bids"][0][0]))
        best_ask = Decimal(str(depth["asks"][0][0]))
        long_short = exchange.global_long_short_ratio(symbol)
        taker_ratio = exchange.taker_long_short_ratio(symbol)
        market.append(
            {
                "symbol": symbol,
                "mark_price": str(mark_price.get("markPrice", "")),
                "funding": {
                    "last_funding_rate": str(mark_price.get("lastFundingRate", "")),
                    "next_funding_time": str(mark_price.get("nextFundingTime", "")),
                },
                "open_interest": str(exchange.open_interest(symbol).get("openInterest", "")),
                "long_short_ratio": str(long_short[0].get("longShortRatio", "")) if long_short else "",
                "taker_long_short_ratio": str(taker_ratio[0].get("buySellRatio", "")) if taker_ratio else "",
                "order_book": {"best_bid": str(best_bid), "best_ask": str(best_ask), "spread": str(best_ask - best_bid)},
                "ticker_24h": exchange.ticker_24hr(symbol),
                "recent_klines": exchange.klines(symbol, "5m", 120)[-5:],
                "recent_aggregate_trades": exchange.aggregate_trades(symbol, 100)[-20:],
                "position": {
                    "side": position.side.value,
                    "quantity": str(position.quantity),
                    "entry_price": str(position.entry_price),
                    "unrealized_pnl": str(position.unrealized_pnl),
                    "liquidation_price": str(position.liquidation_price),
                    "isolated_margin": str(position.isolated_margin),
                },
            }
        )
    return {
        "symbols": list(settings.futures_ai_symbols),
        "daily_realized_pnl": str(daily_realized_pnl),
        "total_isolated_margin": str(total_isolated_margin),
        "market": market,
    }


def _daily_realized_pnl(exchange, now_ms: int) -> Decimal:
    day_start_ms = now_ms - (now_ms % 86_400_000)
    income_rows = exchange.income(day_start_ms, now_ms)
    included_types = {"REALIZED_PNL", "COMMISSION", "FUNDING_FEE"}
    return sum(
        Decimal(str(row.get("income", "0")))
        for row in income_rows
        if str(row.get("incomeType", "")) in included_types
    )


def _total_isolated_margin(exchange, symbols: tuple[str, ...]) -> Decimal:
    return sum(
        parse_position(row).isolated_margin
        for symbol in symbols
        for row in exchange.position_risk(symbol)
    )
```

- [ ] **Step 4: Verify runner tests pass**

Run:

```powershell
python -m pytest tests/test_futures_ai_trader.py::test_futures_batch_rejects_when_live_futures_disabled tests/test_futures_ai_trader.py::test_futures_batch_records_hold_prediction tests/test_futures_ai_trader.py::test_futures_batch_context_includes_futures_market_and_account_features tests/test_futures_ai_trader.py::test_futures_batch_preflight_rejects_hedge_mode -q
```

Expected: PASS.

- [ ] **Step 5: Commit runner skeleton**

Run:

```powershell
git add src/binance_quant/futures_ai_trader.py tests/test_futures_ai_trader.py
git commit -m "feat: add futures ai batch preflight"
```

## Task 9: Wire Risk And Open Execution Into Batch Runner

**Files:**
- Modify: `src/binance_quant/futures_ai_trader.py`
- Test: `tests/test_futures_ai_trader.py`

- [ ] **Step 1: Write failing open execution runner test**

Append to `tests/test_futures_ai_trader.py`:

```python
class ExecutingExchange(FakeFuturesExchange):
    def __init__(self):
        super().__init__()
        self.orders = []

    def change_margin_type(self, symbol, margin_type="ISOLATED"):
        self.orders.append(("change_margin_type", symbol, margin_type))
        return {"code": 200}

    def change_leverage(self, symbol, leverage):
        self.orders.append(("change_leverage", symbol, leverage))
        return {"leverage": leverage}

    def create_market_order(self, symbol, side, quantity, client_order_id, *, reduce_only):
        self.orders.append(("market", symbol, side.value, quantity, reduce_only))
        return {"status": "FILLED", "clientOrderId": client_order_id, "executedQty": str(quantity)}

    def create_conditional_algo_order(self, symbol, side, order_type, trigger_price, quantity, client_algo_id):
        self.orders.append(("algo", symbol, side.value, order_type, trigger_price, quantity))
        return {"status": "NEW", "clientAlgoId": client_algo_id}


def test_futures_batch_executes_approved_open_long(tmp_path):

    prediction = FuturesAiPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.88"),
        "BTC转强",
        leverage=3,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    exchange = ExecutingExchange()

    result = run_live_futures_ai_trade_batch_once(
        futures_settings(tmp_path),
        exchange_client=exchange,
        predictor=StaticFuturesPredictor([prediction]),
    )

    assert result.status == "completed"
    assert result.executed_count == 1
    assert result.results[0].status == "protected"
    assert any(call[0] == "market" for call in exchange.orders)
    assert [call[3] for call in exchange.orders if call[0] == "algo"] == ["STOP_MARKET", "TAKE_PROFIT_MARKET"]


def test_futures_batch_skips_remaining_actions_after_max_actions(tmp_path):
    first = FuturesAiPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.88"),
        "first",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    second = FuturesAiPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.87"),
        "second",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    settings = futures_settings(tmp_path, futures_ai_max_actions_per_run=1)

    result = run_live_futures_ai_trade_batch_once(
        settings,
        exchange_client=ExecutingExchange(),
        predictor=StaticFuturesPredictor([first, second]),
    )

    assert result.executed_count == 1
    assert [item.status for item in result.results] == ["protected", "skipped"]
```

- [ ] **Step 2: Verify open execution runner test fails**

Run:

```powershell
python -m pytest tests/test_futures_ai_trader.py::test_futures_batch_executes_approved_open_long tests/test_futures_ai_trader.py::test_futures_batch_skips_remaining_actions_after_max_actions -q
```

Expected: FAIL because actionable predictions are recorded but not executed.

- [ ] **Step 3: Implement open candidate execution**

In `run_live_futures_ai_trade_batch_once`, after recording predictions, build actionable candidates:

```python
actionable = [
    (prediction, run_id)
    for prediction, run_id in prediction_rows
    if prediction.action in {FuturesAction.OPEN_LONG, FuturesAction.OPEN_SHORT}
]
symbol_order = {symbol: index for index, symbol in enumerate(settings.futures_ai_symbols)}
actionable.sort(key=lambda item: (-item[0].confidence, symbol_order.get(item[0].symbol, 9999)))
```

Add a helper to build risk settings:

```python
def _risk_settings(settings: Settings) -> FuturesRiskSettings:
    return FuturesRiskSettings(
        margin_amount=settings.futures_margin_amount,
        default_leverage=settings.futures_default_leverage,
        max_leverage=settings.futures_max_leverage,
        max_total_margin=settings.futures_max_total_margin,
        min_confidence=settings.futures_ai_min_confidence,
        daily_realized_loss_limit=settings.futures_daily_realized_loss_limit,
        max_margin_loss_percent=settings.futures_max_margin_loss_percent,
        min_liquidation_buffer_percent=settings.futures_min_liquidation_buffer_percent,
        min_stop_distance_percent=settings.futures_min_stop_distance_percent,
        max_stop_distance_percent=settings.futures_max_stop_distance_percent,
        min_take_profit_distance_percent=settings.futures_min_take_profit_distance_percent,
        max_take_profit_distance_percent=settings.futures_max_take_profit_distance_percent,
    )
```

For each actionable candidate, refresh state and call the risk/execution engine:

```python
executed_count = 0
for prediction, run_id in actionable:
    if executed_count >= settings.futures_ai_max_actions_per_run:
        storage.update_futures_ai_trade_run_status(run_id, "skipped", "max actions reached", raw=_prediction_raw(prediction))
        results.append(FuturesExecutionResult(run_id, "skipped", prediction.symbol, prediction.action.value, "max actions reached", prediction.confidence))
        continue

    metadata = exchange.exchange_info(prediction.symbol)
    position = parse_position(exchange.position_risk(prediction.symbol)[0])
    mark = Decimal(str(exchange.mark_price(prediction.symbol)["markPrice"]))
    total_isolated_margin = _total_isolated_margin(exchange, settings.futures_ai_symbols)
    daily_realized_pnl = _daily_realized_pnl(exchange, int(time.time() * 1000))
    context = FuturesRiskContext(
        prediction=prediction,
        metadata=metadata,
        position=position,
        mark_price=mark,
        total_isolated_margin=total_isolated_margin,
        daily_realized_pnl=daily_realized_pnl,
        hedge_mode=False,
    )
    decision = FuturesRiskEngine(_risk_settings(settings)).evaluate(context)
    if not decision.approved:
        storage.update_futures_ai_trade_run_status(run_id, "rejected", decision.reason, raw=_prediction_raw(prediction))
        results.append(FuturesExecutionResult(run_id, "rejected", prediction.symbol, prediction.action.value, decision.reason, prediction.confidence))
        continue
    execution_result = FuturesExecutionEngine(settings.mode, exchange, storage).execute_open(
        run_id,
        decision,
        metadata,
        reference_price=mark,
    )
    storage.update_futures_ai_trade_run_status(run_id, execution_result.status, execution_result.reason, raw=_prediction_raw(prediction))
    results.append(execution_result)
    executed_count += 1
```

Replace the `FuturesAiBatchResult` construction at the end of `run_live_futures_ai_trade_batch_once` so it reports the refreshed run totals:

```python
status = "no_action" if not any(item.status not in {"no_action", "rejected", "skipped"} for item in results) else "completed"
reason = "无合约动作" if status == "no_action" else "AI合约批量交易完成"
return FuturesAiBatchResult(
    status,
    reason,
    settings.futures_ai_max_actions_per_run,
    len(predictions),
    executed_count,
    daily_realized_pnl,
    total_isolated_margin,
    tuple(results),
)
```

Import `FuturesExecutionEngine`, `FuturesRiskContext`, `FuturesRiskEngine`, and `FuturesRiskSettings`.

- [ ] **Step 4: Verify open execution runner test passes**

Run:

```powershell
python -m pytest tests/test_futures_ai_trader.py::test_futures_batch_executes_approved_open_long tests/test_futures_ai_trader.py::test_futures_batch_skips_remaining_actions_after_max_actions -q
```

Expected: PASS.

- [ ] **Step 5: Commit runner execution task**

Run:

```powershell
git add src/binance_quant/futures_ai_trader.py tests/test_futures_ai_trader.py
git commit -m "feat: execute futures ai open signals"
```

## Task 10: Close Execution

**Files:**
- Modify: `src/binance_quant/futures_execution.py`
- Modify: `src/binance_quant/futures_ai_trader.py`
- Test: `tests/test_futures_execution.py`
- Test: `tests/test_futures_ai_trader.py`

- [ ] **Step 1: Write failing close execution tests**

Append to `tests/test_futures_execution.py`:

```python
def test_futures_execution_close_cancels_algo_and_submits_reduce_only_market(tmp_path):
    class CloseExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            return {"status": "CANCELED", "clientAlgoId": client_algo_id}

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = CloseExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction("BTCUSDT", FuturesAction.CLOSE, Decimal("0.88"), "risk", close_percent=50)

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=("bqf-stop", "bqf-tp"),
        existing_stop_loss_price=None,
        existing_take_profit_price=None,
    )

    assert result.status == "partially_closed"
    assert ("cancel_algo_order", "bqf-stop") in exchange.calls
    assert ("cancel_algo_order", "bqf-tp") in exchange.calls
    close_call = next(call for call in exchange.calls if call[0] == "create_market_order")
    assert close_call[2] is FuturesOrderSide.SELL
    assert close_call[3] == Decimal("0.002")
    assert close_call[5] is True


def test_futures_execution_partial_close_recreates_remaining_protection(tmp_path):
    class CloseExchange(FakeFuturesExchange):
        def cancel_algo_order(self, client_algo_id):
            self.calls.append(("cancel_algo_order", client_algo_id))
            return {"status": "CANCELED", "clientAlgoId": client_algo_id}

    storage = Storage(tmp_path / "audit.sqlite3")
    storage.initialize()
    storage.record_futures_ai_trade_run("BTCUSDT", "CLOSE", Decimal("0.88"), "predicted", "risk", raw={})
    exchange = CloseExchange()
    engine = FuturesExecutionEngine(TradingMode.LIVE, exchange, storage)
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.CLOSE,
        Decimal("0.88"),
        "risk",
        close_percent=50,
    )

    result = engine.execute_close(
        1,
        prediction,
        metadata(),
        position_quantity=Decimal("0.004"),
        protection_client_algo_ids=("bqf-stop", "bqf-tp"),
        existing_stop_loss_price=Decimal("59000"),
        existing_take_profit_price=Decimal("63000"),
    )

    assert result.status == "partially_closed"
    protection_calls = [call for call in exchange.calls if call[0] == "create_conditional_algo_order"]
    assert [call[3] for call in protection_calls] == ["STOP_MARKET", "TAKE_PROFIT_MARKET"]
    assert all(call[5] == Decimal("0.002") for call in protection_calls)
```

- [ ] **Step 2: Verify close execution test fails**

Run:

```powershell
python -m pytest tests/test_futures_execution.py::test_futures_execution_close_cancels_algo_and_submits_reduce_only_market tests/test_futures_execution.py::test_futures_execution_partial_close_recreates_remaining_protection -q
```

Expected: FAIL because `execute_close` does not exist.

- [ ] **Step 3: Implement close execution**

Add this method to `FuturesExecutionEngine`:

```python
def execute_close(
    self,
    run_id: int,
    prediction,
    metadata: FuturesSymbolMetadata,
    *,
    position_quantity: Decimal,
    protection_client_algo_ids: tuple[str, ...],
    existing_stop_loss_price: Decimal | None,
    existing_take_profit_price: Decimal | None,
) -> FuturesExecutionResult:
    quantity_abs = abs(position_quantity)
    close_quantity = round_down(quantity_abs * Decimal(prediction.close_percent or 100) / Decimal("100"), metadata.step_size)
    if close_quantity <= 0:
        return FuturesExecutionResult(run_id, "rejected", prediction.symbol, prediction.action.value, "close quantity below minimum", prediction.confidence)
    if self.mode is TradingMode.DRY_RUN:
        self.storage.record_futures_order_event(run_id, "dry_run_close", prediction.symbol, None, None, "DRY_RUN", {"quantity": close_quantity})
        return FuturesExecutionResult(run_id, "dry_run", prediction.symbol, prediction.action.value, "dry run", prediction.confidence, close_percent=prediction.close_percent)
    for client_algo_id in protection_client_algo_ids:
        payload = self.exchange_client.cancel_algo_order(client_algo_id)
        self.storage.record_futures_order_event(run_id, "cancel_algo", prediction.symbol, None, client_algo_id, payload.get("status"), payload)
    side = FuturesOrderSide.SELL if position_quantity > 0 else FuturesOrderSide.BUY
    client_order_id = build_futures_client_order_id("bqf-close")
    payload = self.exchange_client.create_market_order(
        prediction.symbol,
        side,
        close_quantity,
        client_order_id,
        reduce_only=True,
    )
    self.storage.record_futures_order_event(run_id, "close", prediction.symbol, client_order_id, None, payload.get("status"), payload)
    status = "closed" if close_quantity == quantity_abs else "partially_closed"
    remaining_quantity = quantity_abs - close_quantity
    if remaining_quantity > 0:
        if existing_stop_loss_price is None or existing_take_profit_price is None:
            return FuturesExecutionResult(run_id, "close_protection_failed", prediction.symbol, prediction.action.value, "missing existing protection prices", prediction.confidence, close_percent=prediction.close_percent)
        try:
            stop_algo_id = build_futures_client_order_id("bqf-stop")
            stop_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                side,
                "STOP_MARKET",
                existing_stop_loss_price,
                remaining_quantity,
                stop_algo_id,
            )
            self.storage.record_futures_order_event(run_id, "stop_algo", prediction.symbol, None, stop_algo_id, stop_payload.get("status"), stop_payload)
            take_algo_id = build_futures_client_order_id("bqf-tp")
            take_payload = self.exchange_client.create_conditional_algo_order(
                prediction.symbol,
                side,
                "TAKE_PROFIT_MARKET",
                existing_take_profit_price,
                remaining_quantity,
                take_algo_id,
            )
            self.storage.record_futures_order_event(run_id, "take_profit_algo", prediction.symbol, None, take_algo_id, take_payload.get("status"), take_payload)
        except Exception:
            return FuturesExecutionResult(run_id, "close_protection_failed", prediction.symbol, prediction.action.value, "close protection failed", prediction.confidence, close_percent=prediction.close_percent)
    return FuturesExecutionResult(run_id, status, prediction.symbol, prediction.action.value, status, prediction.confidence, close_percent=prediction.close_percent)
```

- [ ] **Step 4: Wire close into runner and verify tests pass**

In `run_live_futures_ai_trade_batch_once`, extend the actionable filter from Task 9 to include `CLOSE`:

```python
actionable = [
    (prediction, run_id)
    for prediction, run_id in prediction_rows
    if prediction.action in {FuturesAction.OPEN_LONG, FuturesAction.OPEN_SHORT, FuturesAction.CLOSE}
]
```

In `futures_ai_trader.py`, add a helper that extracts this bot's existing protection prices from open algo orders:

```python
def _existing_protection_prices(open_algo_orders: list[dict]) -> tuple[Decimal | None, Decimal | None, tuple[str, ...]]:
    stop_loss_price = None
    take_profit_price = None
    client_algo_ids: list[str] = []
    for order in open_algo_orders:
        client_algo_id = str(order.get("clientAlgoId", ""))
        if not client_algo_id.startswith("bqf-"):
            continue
        client_algo_ids.append(client_algo_id)
        order_type = str(order.get("type", ""))
        trigger_price = Decimal(str(order.get("triggerPrice", order.get("price", "0"))))
        if order_type == "STOP_MARKET":
            stop_loss_price = trigger_price
        if order_type == "TAKE_PROFIT_MARKET":
            take_profit_price = trigger_price
    return stop_loss_price, take_profit_price, tuple(client_algo_ids)
```

Then when `prediction.action is FuturesAction.CLOSE`, call:

```python
stop_loss_price, take_profit_price, protection_ids = _existing_protection_prices(exchange.all_algo_orders(prediction.symbol))
execution_result = FuturesExecutionEngine(settings.mode, exchange, storage).execute_close(
    run_id,
    prediction,
    metadata,
    position_quantity=position.quantity,
    protection_client_algo_ids=protection_ids,
    existing_stop_loss_price=stop_loss_price,
    existing_take_profit_price=take_profit_price,
)
```

Run:

```powershell
python -m pytest tests/test_futures_execution.py tests/test_futures_ai_trader.py -q
```

Expected: PASS for the futures tests added so far.

- [ ] **Step 5: Commit close task**

Run:

```powershell
git add src/binance_quant/futures_execution.py src/binance_quant/futures_ai_trader.py tests/test_futures_execution.py tests/test_futures_ai_trader.py
git commit -m "feat: execute futures close signals"
```

## Task 11: CLI Command

**Files:**
- Modify: `src/binance_quant/cli.py`
- Test: `tests/test_cli.py`

- [ ] **Step 1: Write failing CLI tests**

Append to `tests/test_cli.py`:

```python
from binance_quant.futures_models import FuturesExecutionResult
from binance_quant.futures_ai_trader import FuturesAiBatchResult


def test_live_futures_ai_trade_batch_once_command_prints_summary_and_rows(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "\n".join(
            [
                "mode: live",
                "api_key: key",
                "api_secret: secret",
                "live_futures_ai_trader_enabled: true",
                "live_futures_confirm_production: true",
            ]
        ),
        encoding="utf-8",
    )
    monkeypatch.setattr(
        "binance_quant.cli.run_live_futures_ai_trade_batch_once",
        lambda settings: FuturesAiBatchResult(
            status="completed",
            reason="AI合约批量交易完成",
            max_actions=5,
            prediction_count=1,
            executed_count=1,
            daily_realized_pnl=Decimal("0"),
            total_isolated_margin=Decimal("5"),
            results=(
                FuturesExecutionResult(1, "protected", "BTCUSDT", "OPEN_LONG", "protected", Decimal("0.88"), 3, Decimal("5")),
            ),
        ),
    )

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 0
    assert "Status: completed" in result.stdout
    assert "Daily realized PnL: 0" in result.stdout
    assert "Total isolated margin: 5" in result.stdout
    assert "BTCUSDT" in result.stdout
    assert "OPEN_LONG" in result.stdout
    assert "protected" in result.stdout


def test_live_futures_ai_trade_batch_once_command_exits_nonzero_for_failure(tmp_path, monkeypatch):
    config_path = tmp_path / "config.yml"
    config_path.write_text(
        "mode: live\napi_key: key\napi_secret: secret\nlive_futures_ai_trader_enabled: true\nlive_futures_confirm_production: true\n",
        encoding="utf-8",
    )
    monkeypatch.setattr(
        "binance_quant.cli.run_live_futures_ai_trade_batch_once",
        lambda settings: FuturesAiBatchResult(
            status="completed",
            reason="failure",
            max_actions=5,
            prediction_count=1,
            executed_count=1,
            daily_realized_pnl=Decimal("0"),
            total_isolated_margin=Decimal("5"),
            results=(FuturesExecutionResult(1, "rollback_failed", "BTCUSDT", "OPEN_LONG", "failed", Decimal("0.88")),),
        ),
    )

    result = runner.invoke(app, ["live-futures-ai-trade-batch-once", "--config", str(config_path)])

    assert result.exit_code == 1
    assert "rollback_failed" in result.stdout
```

- [ ] **Step 2: Verify CLI tests fail**

Run:

```powershell
python -m pytest tests/test_cli.py::test_live_futures_ai_trade_batch_once_command_prints_summary_and_rows tests/test_cli.py::test_live_futures_ai_trade_batch_once_command_exits_nonzero_for_failure -q
```

Expected: FAIL because the CLI command is not wired.

- [ ] **Step 3: Implement CLI command**

In `src/binance_quant/cli.py`, import:

```python
from .futures_ai_trader import run_live_futures_ai_trade_batch_once
```

Add failure statuses:

```python
_FAILED_FUTURES_AI_TRADE_STATUSES = {"rollback_failed", "close_failed", "close_protection_failed"}
```

Add helper:

```python
def _live_futures_ai_trade_batch_exit_code(result) -> int:
    if result.status == "failed":
        return 1
    if any(item.status in _FAILED_FUTURES_AI_TRADE_STATUSES for item in result.results):
        return 1
    return 0
```

Add command:

```python
@app.command("live-futures-ai-trade-batch-once")
def live_futures_ai_trade_batch_once(config: Path | None = _config_option()) -> None:
    settings = load_settings(config)
    try:
        result = run_live_futures_ai_trade_batch_once(settings)
    except httpx.HTTPError:
        _exit_for_http_error()
    console.print(f"Status: {result.status}")
    console.print(f"Reason: {result.reason}")
    console.print(f"Max actions: {result.max_actions}")
    console.print(f"Executed actions: {result.executed_count}")
    console.print(f"Predictions: {result.prediction_count}")
    console.print(f"Daily realized PnL: {result.daily_realized_pnl}")
    console.print(f"Total isolated margin: {result.total_isolated_margin}")
    table = Table("Run ID", "Status", "Symbol", "Action", "Confidence", "Leverage", "Margin", "Close %", "Reason")
    for item in result.results:
        table.add_row(
            str(item.run_id) if item.run_id is not None else "-",
            item.status,
            item.symbol or "-",
            item.action,
            _format_optional_decimal(item.confidence),
            "-" if item.leverage is None else str(item.leverage),
            _format_optional_decimal(item.margin_amount),
            "-" if item.close_percent is None else str(item.close_percent),
            item.reason,
        )
    console.print(table)
    exit_code = _live_futures_ai_trade_batch_exit_code(result)
    if exit_code:
        raise typer.Exit(exit_code)
```

- [ ] **Step 4: Verify CLI tests pass**

Run:

```powershell
python -m pytest tests/test_cli.py::test_live_futures_ai_trade_batch_once_command_prints_summary_and_rows tests/test_cli.py::test_live_futures_ai_trade_batch_once_command_exits_nonzero_for_failure -q
```

Expected: PASS.

- [ ] **Step 5: Commit CLI task**

Run:

```powershell
git add src/binance_quant/cli.py tests/test_cli.py
git commit -m "feat: add futures ai cli command"
```

## Task 12: Runner Script And README

**Files:**
- Modify: `scripts/run.ps1`
- Modify: `README.md`
- Test: `tests/test_run_script.py`

- [ ] **Step 1: Write failing runner and README tests**

Append to `tests/test_run_script.py`:

```python
def test_run_script_has_live_futures_ai_trade_batch_once_action():
    text = SCRIPT.read_text(encoding="utf-8")

    assert '"live-futures-ai-trade-batch-once"' in text
    assert "Invoke-BinanceQuant live-futures-ai-trade-batch-once" in text
    assert "Refusing live futures AI batch trade because BINANCE_MODE" in text
    assert "BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED is false" in text
    assert "BINANCE_LIVE_FUTURES_CONFIRM_PRODUCTION is false" in text
    assert text.index("BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED is false") < text.index("Invoke-BinanceQuant live-futures-ai-trade-batch-once")


def test_readme_documents_live_futures_ai_trader_disabled_default():
    readme = README.read_text(encoding="utf-8")

    assert "live-futures-ai-trade-batch-once" in readme
    assert "BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED=false" in readme
    assert "BINANCE_LIVE_FUTURES_CONFIRM_PRODUCTION=false" in readme
    assert "BINANCE_FUTURES_MARGIN_AMOUNT=5" in readme
    assert "BINANCE_FUTURES_MAX_LEVERAGE=3" in readme
    assert "BINANCE_FUTURES_MAX_TOTAL_MARGIN=20" in readme
```

- [ ] **Step 2: Verify runner tests fail**

Run:

```powershell
python -m pytest tests/test_run_script.py::test_run_script_has_live_futures_ai_trade_batch_once_action tests/test_run_script.py::test_readme_documents_live_futures_ai_trader_disabled_default -q
```

Expected: FAIL because runner and README are not updated.

- [ ] **Step 3: Update runner script**

In `scripts/run.ps1`, add `"live-futures-ai-trade-batch-once"` to the `ValidateSet`.

Add a switch branch following the existing live AI batch pattern:

```powershell
"live-futures-ai-trade-batch-once" {
    $mode = Get-SettingValue "BINANCE_MODE"
    if ($mode -ne "live") {
        throw "Refusing live futures AI batch trade because BINANCE_MODE must be live."
    }
    $enabled = Get-SettingValue "BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED"
    if ($enabled -ne "true") {
        throw "Refusing live futures AI batch trade because BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED is false."
    }
    $confirmed = Get-SettingValue "BINANCE_LIVE_FUTURES_CONFIRM_PRODUCTION"
    if ($confirmed -ne "true") {
        throw "Refusing live futures AI batch trade because BINANCE_LIVE_FUTURES_CONFIRM_PRODUCTION is false."
    }
    Invoke-BinanceQuant live-futures-ai-trade-batch-once
}
```

- [ ] **Step 4: Update README**

Add a futures section near the live AI trader documentation:

````markdown
### Live Futures AI Trader

The futures AI command is disabled by default and targets Binance USDT-M perpetual futures:

```powershell
.\scripts\run.ps1 -Action live-futures-ai-trade-batch-once
```

Conservative defaults:

```text
BINANCE_LIVE_FUTURES_AI_TRADER_ENABLED=false
BINANCE_LIVE_FUTURES_CONFIRM_PRODUCTION=false
BINANCE_FUTURES_MARGIN_AMOUNT=5
BINANCE_FUTURES_DEFAULT_LEVERAGE=2
BINANCE_FUTURES_MAX_LEVERAGE=3
BINANCE_FUTURES_MAX_TOTAL_MARGIN=20
BINANCE_FUTURES_AI_MAX_ACTIONS_PER_RUN=5
```

The futures runner requires one-way position mode and isolated margin. AI can suggest `OPEN_LONG`, `OPEN_SHORT`, `CLOSE`, or `HOLD`, but local risk controls enforce leverage, margin, stop loss, take profit, daily realized loss, and close-percentage limits.
````

- [ ] **Step 5: Verify runner and README tests pass**

Run:

```powershell
python -m pytest tests/test_run_script.py -q
```

Expected: PASS.

- [ ] **Step 6: Commit runner/docs task**

Run:

```powershell
git add scripts/run.ps1 README.md tests/test_run_script.py
git commit -m "docs: document futures ai runner"
```

## Task 13: Final Verification

**Files:**
- All changed files.

- [ ] **Step 1: Run futures-focused tests**

Run:

```powershell
python -m pytest tests/test_futures_models.py tests/test_futures_exchange.py tests/test_futures_risk.py tests/test_futures_execution.py tests/test_futures_ai_trader.py -q
```

Expected: PASS.

- [ ] **Step 2: Run integration-adjacent tests**

Run:

```powershell
python -m pytest tests/test_config.py tests/test_storage.py tests/test_cli.py tests/test_run_script.py -q
```

Expected: PASS.

- [ ] **Step 3: Run full test suite**

Run:

```powershell
python -m pytest
```

Expected: PASS.

- [ ] **Step 4: Inspect git status and diff**

Run:

```powershell
git status --short
git log --oneline -12
```

Expected: working tree clean after task commits, with focused commits for config, models, exchange, storage, parsing, risk, execution, runner, CLI, and docs.

- [ ] **Step 5: Record final implementation notes**

If execution used subagents, add a short implementation note to the final response with:

```text
Implemented:
- futures settings and URL validation
- futures REST client with algo-order protection support
- futures AI prediction parsing, risk, execution, batch runner, CLI, script, and docs

Verification:
- python -m pytest
```
