# AI Trader Market Features 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:** Enrich the live AI trader model context with Binance Spot OHLCV, indicators, volume, breakout, 24h ticker, average price, and depth liquidity features.

**Architecture:** Extend the existing exchange adapter with public market-data methods, extend `Candle` with optional Binance kline fields, and keep deterministic feature calculation in `ai_trader.py`. The prediction schema, confidence threshold behavior, and local risk/execution controls remain unchanged.

**Tech Stack:** Python 3, Decimal arithmetic, httpx, Typer CLI, pytest, existing Binance Spot REST adapter.

---

## File Structure

- Modify `src/binance_quant/models.py`: add optional quote-volume, trade-count, and taker-buy kline fields to `Candle`.
- Modify `src/binance_quant/exchange.py`: add public market-data methods `depth`, `ticker_24hr`, and `avg_price`.
- Modify `src/binance_quant/ai_trader.py`: fetch new market-data payloads, calculate indicators and liquidity features, add enriched context to `_model_context`, and update prompt guidance.
- Modify `tests/test_models.py`: assert new `Candle` fields default cleanly and accept explicit values.
- Modify `tests/test_exchange.py`: assert new Binance endpoint methods call the expected paths and params.
- Modify `tests/test_ai_trader.py`: assert parsing and model context include all requested features while existing trade execution behavior still works.
- Modify `README.md`: document enriched AI model input and extra endpoint usage.

## Task 1: Extend Candle With Binance Kline Optional Fields

**Files:**
- Modify: `src/binance_quant/models.py`
- Test: `tests/test_models.py`

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

Add these tests to `tests/test_models.py` after `test_candle_reports_close_price_as_decimal`:

```python
def test_candle_defaults_optional_binance_kline_fields_to_zero():
    candle = Candle(
        symbol="BTCUSDT",
        open_time=1,
        close_time=2,
        open=Decimal("10"),
        high=Decimal("12"),
        low=Decimal("9"),
        close=Decimal("11"),
        volume=Decimal("1.5"),
    )

    assert candle.quote_volume == Decimal("0")
    assert candle.trade_count == 0
    assert candle.taker_buy_base_volume == Decimal("0")
    assert candle.taker_buy_quote_volume == Decimal("0")


def test_candle_accepts_optional_binance_kline_fields():
    candle = Candle(
        symbol="btcusdt",
        open_time=1,
        close_time=2,
        open=Decimal("10"),
        high=Decimal("12"),
        low=Decimal("9"),
        close=Decimal("11"),
        volume=Decimal("1.5"),
        quote_volume=Decimal("16.5"),
        trade_count=7,
        taker_buy_base_volume=Decimal("0.8"),
        taker_buy_quote_volume=Decimal("8.8"),
    )

    assert candle.symbol == "BTCUSDT"
    assert candle.quote_volume == Decimal("16.5")
    assert candle.trade_count == 7
    assert candle.taker_buy_base_volume == Decimal("0.8")
    assert candle.taker_buy_quote_volume == Decimal("8.8")
```

- [ ] **Step 2: Run tests to verify they fail**

Run:

```powershell
py -3 -m pytest tests/test_models.py::test_candle_defaults_optional_binance_kline_fields_to_zero tests/test_models.py::test_candle_accepts_optional_binance_kline_fields -q
```

Expected: FAIL because `Candle` has no `quote_volume`, `trade_count`, `taker_buy_base_volume`, or `taker_buy_quote_volume`.

- [ ] **Step 3: Implement minimal Candle fields**

In `src/binance_quant/models.py`, change `Candle` to:

```python
@dataclass(frozen=True)
class Candle:
    symbol: str
    open_time: int
    close_time: int
    open: Decimal
    high: Decimal
    low: Decimal
    close: Decimal
    volume: Decimal
    quote_volume: Decimal = Decimal("0")
    trade_count: int = 0
    taker_buy_base_volume: Decimal = Decimal("0")
    taker_buy_quote_volume: Decimal = Decimal("0")

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

- [ ] **Step 4: Run tests to verify they pass**

Run:

```powershell
py -3 -m pytest tests/test_models.py -q
```

Expected: PASS.

- [ ] **Step 5: Commit**

```powershell
git add src/binance_quant/models.py tests/test_models.py
git commit -m "feat: extend candle with binance kline fields"
```

## Task 2: Add Exchange Methods For Depth, 24h Ticker, And Average Price

**Files:**
- Modify: `src/binance_quant/exchange.py`
- Test: `tests/test_exchange.py`

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

Add these tests to `tests/test_exchange.py` after `test_klines_uses_default_interval_and_limit`:

```python
def test_depth_returns_json_with_symbol_and_limit_params():
    requests = []
    payload = {"lastUpdateId": 1, "bids": [["100", "2"]], "asks": [["101", "3"]]}

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

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

    result = client.depth("btcusdt", limit=20)

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/depth")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT", "limit": "20"}


def test_ticker_24hr_returns_json_with_symbol_param():
    requests = []
    payload = {"symbol": "BTCUSDT", "priceChangePercent": "1.23", "quoteVolume": "1000"}

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

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

    result = client.ticker_24hr("btcusdt")

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/ticker/24hr")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT"}


def test_avg_price_returns_json_with_symbol_param():
    requests = []
    payload = {"mins": 5, "price": "100.5"}

    def handler(request: httpx.Request) -> httpx.Response:
        requests.append(request)
        return httpx.Response(200, json=payload)

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

    result = client.avg_price("btcusdt")

    assert result == payload
    assert requests[0].method == "GET"
    assert requests[0].url.path.endswith("/v3/avgPrice")
    assert dict(parse_qsl(requests[0].url.query.decode())) == {"symbol": "BTCUSDT"}
```

- [ ] **Step 2: Run tests to verify they fail**

Run:

```powershell
py -3 -m pytest tests/test_exchange.py::test_depth_returns_json_with_symbol_and_limit_params tests/test_exchange.py::test_ticker_24hr_returns_json_with_symbol_param tests/test_exchange.py::test_avg_price_returns_json_with_symbol_param -q
```

Expected: FAIL because `BinanceSpotClient` has no `depth`, `ticker_24hr`, or `avg_price` methods.

- [ ] **Step 3: Implement exchange methods**

In `src/binance_quant/exchange.py`, add these methods after `klines`:

```python
    def depth(self, symbol: str, limit: int = 20) -> dict:
        response = self._client.get(
            f"{self.base_url}/v3/depth",
            params={"symbol": symbol.upper(), "limit": str(limit)},
        )
        response.raise_for_status()
        return response.json()

    def ticker_24hr(self, symbol: str) -> dict:
        response = self._client.get(f"{self.base_url}/v3/ticker/24hr", params={"symbol": symbol.upper()})
        response.raise_for_status()
        return response.json()

    def avg_price(self, symbol: str) -> dict:
        response = self._client.get(f"{self.base_url}/v3/avgPrice", params={"symbol": symbol.upper()})
        response.raise_for_status()
        return response.json()
```

- [ ] **Step 4: Run tests to verify they pass**

Run:

```powershell
py -3 -m pytest tests/test_exchange.py -q
```

Expected: PASS.

- [ ] **Step 5: Commit**

```powershell
git add src/binance_quant/exchange.py tests/test_exchange.py
git commit -m "feat: add binance public market data methods"
```

## Task 3: Parse Full Binance Kline Rows In AI Trader

**Files:**
- Modify: `src/binance_quant/ai_trader.py`
- Test: `tests/test_ai_trader.py`

- [ ] **Step 1: Write failing kline parsing test**

Update the import from `binance_quant.ai_trader` in `tests/test_ai_trader.py` to include `_kline_to_candle`, then add this test near the prediction parsing tests:

```python
def test_ai_trader_kline_to_candle_captures_full_binance_fields():
    candle = _kline_to_candle(
        "btcusdt",
        [
            1,
            "100",
            "110",
            "90",
            "105",
            "12.5",
            2,
            "1312.5",
            44,
            "6.25",
            "656.25",
            "0",
        ],
    )

    assert candle.symbol == "BTCUSDT"
    assert candle.open == Decimal("100")
    assert candle.high == Decimal("110")
    assert candle.low == Decimal("90")
    assert candle.close == Decimal("105")
    assert candle.volume == Decimal("12.5")
    assert candle.quote_volume == Decimal("1312.5")
    assert candle.trade_count == 44
    assert candle.taker_buy_base_volume == Decimal("6.25")
    assert candle.taker_buy_quote_volume == Decimal("656.25")
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_ai_trader_kline_to_candle_captures_full_binance_fields -q
```

Expected: FAIL because `_kline_to_candle` ignores optional Binance fields.

- [ ] **Step 3: Implement full row parsing**

In `src/binance_quant/ai_trader.py`, replace `_kline_to_candle` with:

```python
def _optional_decimal(row: list, index: int) -> Decimal:
    if len(row) <= index:
        return Decimal("0")
    return Decimal(str(row[index]))


def _optional_int(row: list, index: int) -> int:
    if len(row) <= index:
        return 0
    return int(row[index])


def _kline_to_candle(symbol: str, row: list) -> Candle:
    return Candle(
        symbol=symbol,
        open_time=int(row[0]),
        close_time=int(row[6]),
        open=Decimal(str(row[1])),
        high=Decimal(str(row[2])),
        low=Decimal(str(row[3])),
        close=Decimal(str(row[4])),
        volume=Decimal(str(row[5])),
        quote_volume=_optional_decimal(row, 7),
        trade_count=_optional_int(row, 8),
        taker_buy_base_volume=_optional_decimal(row, 9),
        taker_buy_quote_volume=_optional_decimal(row, 10),
    )
```

- [ ] **Step 4: Run targeted tests**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_ai_trader_kline_to_candle_captures_full_binance_fields -q
```

Expected: PASS.

- [ ] **Step 5: Commit**

```powershell
git add src/binance_quant/ai_trader.py tests/test_ai_trader.py
git commit -m "feat: parse enriched ai trader klines"
```

## Task 4: Add Indicator And Feature Helper Functions

**Files:**
- Modify: `src/binance_quant/ai_trader.py`
- Test: `tests/test_ai_trader.py`

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

Update the import from `binance_quant.ai_trader` in `tests/test_ai_trader.py` to include `_timeframe_features`, then add these helpers and tests near `FakeAiExchange`:

```python
def rich_candle(index: int, close: Decimal, volume: Decimal = Decimal("100")) -> Candle:
    return Candle(
        symbol="BTCUSDT",
        open_time=index,
        close_time=index + 1,
        open=close - Decimal("1"),
        high=close + Decimal("2"),
        low=close - Decimal("2"),
        close=close,
        volume=volume,
        quote_volume=volume * close,
        trade_count=index + 10,
        taker_buy_base_volume=volume / Decimal("2"),
        taker_buy_quote_volume=(volume * close) / Decimal("2"),
    )


def test_timeframe_features_include_ohlcv_indicators_volume_and_breakout():
    candles = [
        rich_candle(index, Decimal(index), Decimal("100") + Decimal(index))
        for index in range(1, 31)
    ]

    features = _timeframe_features(candles)

    assert features["opens"][0] == "0"
    assert features["highs"][-1] == "32"
    assert features["lows"][-1] == "28"
    assert features["closes"][-1] == "30"
    assert features["volumes"][-1] == "130"
    assert features["quote_volumes"][-1] == "3900"
    assert features["trade_counts"][-1] == 40
    assert features["taker_buy_base_volumes"][-1] == "65"
    assert features["taker_buy_quote_volumes"][-1] == "1950"
    assert features["indicators"]["sma_7"] == "27"
    assert features["indicators"]["sma_20"] == "20.5"
    assert features["indicators"]["ema_12"] is not None
    assert features["indicators"]["ema_26"] is not None
    assert features["indicators"]["rsi_14"] == "100"
    assert features["indicators"]["macd"] is not None
    assert features["indicators"]["macd_signal"] is not None
    assert features["indicators"]["macd_histogram"] is not None
    assert features["indicators"]["atr_14"] == "4"
    assert features["volume"]["latest_volume"] == "130"
    assert features["volume"]["average_volume_20"] == "119.5"
    assert features["volume"]["volume_ratio_20"] is not None
    assert features["range"]["high_20"] == "32"
    assert features["range"]["low_20"] == "9"
    assert features["range"]["breakout_up_20"] is False
    assert features["range"]["breakdown_down_20"] is False


def test_timeframe_features_return_nulls_for_insufficient_history():
    features = _timeframe_features([rich_candle(1, Decimal("10"))])

    assert features["indicators"]["sma_7"] is None
    assert features["indicators"]["rsi_14"] is None
    assert features["indicators"]["macd"] is None
    assert features["indicators"]["atr_14"] is None
    assert features["range"]["breakout_up_20"] is False
    assert features["range"]["breakdown_down_20"] is False
```

- [ ] **Step 2: Run tests to verify they fail**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_timeframe_features_include_ohlcv_indicators_volume_and_breakout tests/test_ai_trader.py::test_timeframe_features_return_nulls_for_insufficient_history -q
```

Expected: FAIL because `_timeframe_features` does not exist.

- [ ] **Step 3: Implement helper functions**

In `src/binance_quant/ai_trader.py`, add these helpers above `_model_context`:

```python
def _decimal_text(value: Decimal | None) -> str | None:
    if value is None:
        return None
    return format(value.normalize(), "f")


def _average(values: list[Decimal]) -> Decimal | None:
    if not values:
        return None
    return sum(values, Decimal("0")) / Decimal(len(values))


def _sma(values: list[Decimal], period: int) -> Decimal | None:
    if len(values) < period:
        return None
    return _average(values[-period:])


def _ema_series(values: list[Decimal], period: int) -> list[Decimal]:
    if not values:
        return []
    multiplier = Decimal("2") / Decimal(period + 1)
    ema_values = [values[0]]
    for value in values[1:]:
        ema_values.append((value - ema_values[-1]) * multiplier + ema_values[-1])
    return ema_values


def _ema(values: list[Decimal], period: int) -> Decimal | None:
    if len(values) < period:
        return None
    return _ema_series(values, period)[-1]


def _rsi(values: list[Decimal], period: int = 14) -> Decimal | None:
    if len(values) <= period:
        return None
    gains: list[Decimal] = []
    losses: list[Decimal] = []
    for previous, current in zip(values[-period - 1 : -1], values[-period:]):
        change = current - previous
        gains.append(max(change, Decimal("0")))
        losses.append(abs(min(change, Decimal("0"))))
    average_gain = _average(gains)
    average_loss = _average(losses)
    if average_gain is None or average_loss is None:
        return None
    if average_loss == 0:
        return Decimal("100")
    relative_strength = average_gain / average_loss
    return Decimal("100") - (Decimal("100") / (Decimal("1") + relative_strength))


def _macd(values: list[Decimal]) -> tuple[Decimal | None, Decimal | None, Decimal | None]:
    if len(values) < 26:
        return None, None, None
    ema_12 = _ema_series(values, 12)
    ema_26 = _ema_series(values, 26)
    macd_values = [short - long for short, long in zip(ema_12[-len(ema_26) :], ema_26)]
    current_macd = macd_values[-1]
    if len(macd_values) < 9:
        return current_macd, None, None
    signal = _ema_series(macd_values, 9)[-1]
    return current_macd, signal, current_macd - signal


def _atr(candles: list[Candle], period: int = 14) -> Decimal | None:
    if len(candles) <= period:
        return None
    true_ranges: list[Decimal] = []
    for previous, current in zip(candles[-period - 1 : -1], candles[-period:]):
        true_ranges.append(
            max(
                current.high - current.low,
                abs(current.high - previous.close),
                abs(current.low - previous.close),
            )
        )
    return _average(true_ranges)


def _ratio(numerator: Decimal, denominator: Decimal | None) -> Decimal | None:
    if denominator is None or denominator == 0:
        return None
    return numerator / denominator


def _timeframe_features(candles: list[Candle]) -> dict[str, Any]:
    closes = [candle.close for candle in candles]
    volumes = [candle.volume for candle in candles]
    quote_volumes = [candle.quote_volume for candle in candles]
    macd_value, macd_signal, macd_histogram = _macd(closes)
    previous_volumes = volumes[-21:-1] if len(volumes) > 1 else []
    previous_quote_volumes = quote_volumes[-21:-1] if len(quote_volumes) > 1 else []
    latest = candles[-1] if candles else None
    recent = candles[-20:]
    previous_recent = candles[-21:-1] if len(candles) > 1 else []
    high_20 = max((candle.high for candle in recent), default=None)
    low_20 = min((candle.low for candle in recent), default=None)
    previous_high_20 = max((candle.high for candle in previous_recent), default=None)
    previous_low_20 = min((candle.low for candle in previous_recent), default=None)
    average_volume_20 = _average(previous_volumes)
    average_quote_volume_20 = _average(previous_quote_volumes)
    latest_volume = latest.volume if latest else Decimal("0")
    latest_quote_volume = latest.quote_volume if latest else Decimal("0")

    return {
        "opens": [_decimal_text(candle.open) for candle in candles],
        "highs": [_decimal_text(candle.high) for candle in candles],
        "lows": [_decimal_text(candle.low) for candle in candles],
        "closes": [_decimal_text(candle.close) for candle in candles],
        "volumes": [_decimal_text(candle.volume) for candle in candles],
        "quote_volumes": [_decimal_text(candle.quote_volume) for candle in candles],
        "trade_counts": [candle.trade_count for candle in candles],
        "taker_buy_base_volumes": [_decimal_text(candle.taker_buy_base_volume) for candle in candles],
        "taker_buy_quote_volumes": [_decimal_text(candle.taker_buy_quote_volume) for candle in candles],
        "indicators": {
            "sma_7": _decimal_text(_sma(closes, 7)),
            "sma_20": _decimal_text(_sma(closes, 20)),
            "ema_12": _decimal_text(_ema(closes, 12)),
            "ema_26": _decimal_text(_ema(closes, 26)),
            "rsi_14": _decimal_text(_rsi(closes, 14)),
            "macd": _decimal_text(macd_value),
            "macd_signal": _decimal_text(macd_signal),
            "macd_histogram": _decimal_text(macd_histogram),
            "atr_14": _decimal_text(_atr(candles, 14)),
        },
        "volume": {
            "latest_volume": _decimal_text(latest_volume),
            "average_volume_20": _decimal_text(average_volume_20),
            "volume_ratio_20": _decimal_text(_ratio(latest_volume, average_volume_20)),
            "latest_quote_volume": _decimal_text(latest_quote_volume),
            "average_quote_volume_20": _decimal_text(average_quote_volume_20),
            "quote_volume_ratio_20": _decimal_text(_ratio(latest_quote_volume, average_quote_volume_20)),
        },
        "range": {
            "high_20": _decimal_text(high_20),
            "low_20": _decimal_text(low_20),
            "distance_from_high_20_bps": _decimal_text(
                ((latest.close - high_20) / high_20) * BPS if latest and high_20 and high_20 != 0 else None
            ),
            "distance_from_low_20_bps": _decimal_text(
                ((latest.close - low_20) / low_20) * BPS if latest and low_20 and low_20 != 0 else None
            ),
            "breakout_up_20": bool(latest and previous_high_20 is not None and latest.close > previous_high_20),
            "breakdown_down_20": bool(latest and previous_low_20 is not None and latest.close < previous_low_20),
        },
    }
```

- [ ] **Step 4: Run targeted tests**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_timeframe_features_include_ohlcv_indicators_volume_and_breakout tests/test_ai_trader.py::test_timeframe_features_return_nulls_for_insufficient_history -q
```

Expected: PASS. If Decimal strings include additional precision for EMA/MACD fields, keep assertions to `is not None` for those fields.

- [ ] **Step 5: Commit**

```powershell
git add src/binance_quant/ai_trader.py tests/test_ai_trader.py
git commit -m "feat: calculate ai trader market indicators"
```

## Task 5: Fetch And Include Full Market Context

**Files:**
- Modify: `src/binance_quant/ai_trader.py`
- Test: `tests/test_ai_trader.py`

- [ ] **Step 1: Write failing model-context test**

Modify `FakeAiExchange.symbol_order_book_ticker` in `tests/test_ai_trader.py` to return quantities:

```python
    def symbol_order_book_ticker(self, symbol):
        self.calls.append(("symbol_order_book_ticker", symbol))
        return {
            "symbol": symbol,
            "bidPrice": "104.99",
            "bidQty": "200",
            "askPrice": "105",
            "askQty": "180",
        }
```

Add these methods to `FakeAiExchange`:

```python
    def depth(self, symbol, limit=20):
        self.calls.append(("depth", symbol, limit))
        return {
            "lastUpdateId": 1,
            "bids": [["104", "10"], ["103", "20"]],
            "asks": [["106", "8"], ["107", "12"]],
        }

    def ticker_24hr(self, symbol):
        self.calls.append(("ticker_24hr", symbol))
        return {
            "symbol": symbol,
            "priceChange": "2",
            "priceChangePercent": "1.94",
            "weightedAvgPrice": "103",
            "highPrice": "110",
            "lowPrice": "95",
            "volume": "1234",
            "quoteVolume": "127102",
            "count": 456,
        }

    def avg_price(self, symbol):
        self.calls.append(("avg_price", symbol))
        return {"mins": 5, "price": "104.5"}
```

Then add this test after `test_run_live_ai_trade_once_passes_configured_klines_to_ai_context`:

```python
def test_run_live_ai_trade_once_passes_enriched_market_features_to_ai_context(tmp_path):
    fake = FakeAiExchange()
    predictor = StaticPredictor([AiTradePrediction("BTCUSDT", "HOLD", Decimal("0.3"), "BTC震荡")])

    run_live_ai_trade_once(ai_settings(tmp_path), exchange_client=fake, predictor=predictor)

    context = predictor.contexts[0][0]
    [market] = context["market"]
    hourly = market["timeframes"]["1h"]
    assert hourly["opens"]
    assert hourly["highs"]
    assert hourly["lows"]
    assert hourly["closes"]
    assert hourly["quote_volumes"]
    assert hourly["trade_counts"]
    assert hourly["indicators"]["sma_7"] is not None
    assert "volume_ratio_20" in hourly["volume"]
    assert "distance_from_high_20_bps" in hourly["range"]
    assert "breakout_up_20" in hourly["range"]
    assert market["ticker_24h"]["price_change_percent"] == "1.94"
    assert market["average_price"]["price"] == "104.5"
    assert market["average_price"]["mins"] == 5
    assert market["liquidity"]["best_bid_qty"] == "200"
    assert market["liquidity"]["best_ask_qty"] == "180"
    assert market["liquidity"]["depth_limit"] == 20
    assert market["liquidity"]["depth_bid_notional"] == "3100"
    assert market["liquidity"]["depth_ask_notional"] == "2132"
    assert market["liquidity"]["spread_rating"] == "tight"
    assert market["liquidity"]["liquidity_rating"] == "poor"
    assert ("depth", "BTCUSDT", 20) in fake.calls
    assert ("ticker_24hr", "BTCUSDT") in fake.calls
    assert ("avg_price", "BTCUSDT") in fake.calls
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_run_live_ai_trade_once_passes_enriched_market_features_to_ai_context -q
```

Expected: FAIL because `_SymbolContext` does not store depth/ticker/avg-price data and `_model_context` does not include enriched features.

- [ ] **Step 3: Extend `_SymbolContext` and data loading**

In `src/binance_quant/ai_trader.py`, extend `_SymbolContext`:

```python
@dataclass(frozen=True)
class _SymbolContext:
    symbol: str
    metadata: SymbolMetadata
    bid: Decimal
    ask: Decimal
    spread_bps: Decimal
    best_bid_qty: Decimal
    best_ask_qty: Decimal
    candles_by_interval: dict[str, list[Candle]]
    open_orders: list[dict]
    depth: dict[str, Any]
    ticker_24hr: dict[str, Any]
    avg_price: dict[str, Any]
```

In `_load_symbol_context`, parse quantities and fetch new payloads:

```python
    best_bid_qty = Decimal(str(ticker.get("bidQty", "0")))
    best_ask_qty = Decimal(str(ticker.get("askQty", "0")))
    depth = exchange.depth(normalized_symbol, limit=20)
    ticker_24hr = exchange.ticker_24hr(normalized_symbol)
    avg_price = exchange.avg_price(normalized_symbol)
```

Pass these fields into `_SymbolContext`.

- [ ] **Step 4: Add liquidity/ticker serialization helpers**

Add these helpers above `_model_context`:

```python
def _sum_depth(rows: object) -> tuple[Decimal, Decimal]:
    if not isinstance(rows, list):
        return Decimal("0"), Decimal("0")
    quantity_total = Decimal("0")
    notional_total = Decimal("0")
    for row in rows:
        if not isinstance(row, list) or len(row) < 2:
            continue
        price = Decimal(str(row[0]))
        quantity = Decimal(str(row[1]))
        quantity_total += quantity
        notional_total += price * quantity
    return quantity_total, notional_total


def _spread_rating(spread_bps: Decimal) -> str:
    if spread_bps <= Decimal("2"):
        return "tight"
    if spread_bps <= Decimal("5"):
        return "normal"
    if spread_bps <= Decimal("10"):
        return "wide"
    return "very_wide"


def _liquidity_rating(depth_bid_notional: Decimal, depth_ask_notional: Decimal) -> str:
    smaller_side = min(depth_bid_notional, depth_ask_notional)
    if smaller_side >= Decimal("100000"):
        return "excellent"
    if smaller_side >= Decimal("25000"):
        return "good"
    if smaller_side >= Decimal("5000"):
        return "fair"
    return "poor"


def _ticker_24h_features(payload: dict[str, Any]) -> dict[str, Any]:
    return {
        "price_change": str(payload.get("priceChange", "0")),
        "price_change_percent": str(payload.get("priceChangePercent", "0")),
        "weighted_avg_price": str(payload.get("weightedAvgPrice", "0")),
        "high_price": str(payload.get("highPrice", "0")),
        "low_price": str(payload.get("lowPrice", "0")),
        "volume": str(payload.get("volume", "0")),
        "quote_volume": str(payload.get("quoteVolume", "0")),
        "trade_count": int(payload.get("count", 0)),
    }


def _average_price_features(payload: dict[str, Any]) -> dict[str, Any]:
    return {
        "mins": int(payload.get("mins", 0)),
        "price": str(payload.get("price", "0")),
    }


def _liquidity_features(context: _SymbolContext) -> dict[str, Any]:
    depth_bid_qty, depth_bid_notional = _sum_depth(context.depth.get("bids", []))
    depth_ask_qty, depth_ask_notional = _sum_depth(context.depth.get("asks", []))
    depth_total_notional = depth_bid_notional + depth_ask_notional
    depth_imbalance = (
        (depth_bid_notional - depth_ask_notional) / depth_total_notional
        if depth_total_notional
        else None
    )
    best_bid_notional = context.bid * context.best_bid_qty
    best_ask_notional = context.ask * context.best_ask_qty
    return {
        "best_bid_qty": _decimal_text(context.best_bid_qty),
        "best_ask_qty": _decimal_text(context.best_ask_qty),
        "best_bid_notional": _decimal_text(best_bid_notional),
        "best_ask_notional": _decimal_text(best_ask_notional),
        "depth_limit": 20,
        "depth_bid_qty": _decimal_text(depth_bid_qty),
        "depth_ask_qty": _decimal_text(depth_ask_qty),
        "depth_bid_notional": _decimal_text(depth_bid_notional),
        "depth_ask_notional": _decimal_text(depth_ask_notional),
        "depth_imbalance": _decimal_text(depth_imbalance),
        "spread_rating": _spread_rating(context.spread_bps),
        "liquidity_rating": _liquidity_rating(depth_bid_notional, depth_ask_notional),
    }
```

- [ ] **Step 5: Update `_model_context`**

In `_model_context`, replace the timeframe object creation with:

```python
            timeframes[interval] = _timeframe_features(candles)
```

And add fields to the market object:

```python
                "best_bid_qty": str(context.best_bid_qty),
                "best_ask_qty": str(context.best_ask_qty),
                "ticker_24h": _ticker_24h_features(context.ticker_24hr),
                "average_price": _average_price_features(context.avg_price),
                "liquidity": _liquidity_features(context),
```

Keep existing `bid`, `ask`, `spread_bps`, `symbol`, `timeframes`, and `open_sell_orders`.

- [ ] **Step 6: Run targeted test**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_run_live_ai_trade_once_passes_enriched_market_features_to_ai_context -q
```

Expected: PASS.

- [ ] **Step 7: Run existing AI trader tests**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py -q
```

Expected: PASS. If existing `FakeAiExchange` tests fail because call expectations need new public-data calls, update assertions to include the new calls, not to avoid them.

- [ ] **Step 8: Commit**

```powershell
git add src/binance_quant/ai_trader.py tests/test_ai_trader.py
git commit -m "feat: enrich ai trader market context"
```

## Task 6: Update Prompt Guidance

**Files:**
- Modify: `src/binance_quant/ai_trader.py`
- Test: `tests/test_ai_trader.py`

- [ ] **Step 1: Write failing prompt test**

In `test_codex_exec_predictor_builds_command_and_parses_response`, add:

```python
    assert "trend, momentum, volatility, volume confirmation, breakout, 24h context, spread, and liquidity" in command[-1]
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_codex_exec_predictor_builds_command_and_parses_response -q
```

Expected: FAIL because the prompt does not mention enriched feature categories yet.

- [ ] **Step 3: Update prompt**

In `_build_prompt`, add this line after `Use confidence from 0.00 to 1.00.`:

```python
            "When assigning action and confidence, consider trend, momentum, volatility, volume confirmation, breakout, 24h context, spread, and liquidity.",
```

- [ ] **Step 4: Run prompt test**

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py::test_codex_exec_predictor_builds_command_and_parses_response -q
```

Expected: PASS.

- [ ] **Step 5: Commit**

```powershell
git add src/binance_quant/ai_trader.py tests/test_ai_trader.py
git commit -m "feat: guide ai trader prompt with market features"
```

## Task 7: Document Enriched AI Analysis Input

**Files:**
- Modify: `README.md`

- [ ] **Step 1: Update README AI input section**

In `README.md`, update the `AI 分析输入数据` section so it states:

```markdown
| `market[].timeframes[].opens/highs/lows/closes/volumes` | `/api/v3/klines` | K 线 OHLCV 原始序列。 |
| `market[].timeframes[].quote_volumes/trade_counts/taker_buy_*` | `/api/v3/klines` | 成交额、成交笔数和主动买入成交量。 |
| `market[].timeframes[].indicators` | 本地计算 | SMA、EMA、RSI、MACD、ATR。历史不足时字段为 `null`。 |
| `market[].timeframes[].volume` | 本地计算 | 最近成交量、20 根均量和放大倍数。 |
| `market[].timeframes[].range` | 本地计算 | 20 根高低点、距离高低点 bps、突破/跌破标记。 |
| `market[].ticker_24h` | `/api/v3/ticker/24hr` | 24h 涨跌幅、高低点、成交量、成交额和成交笔数。 |
| `market[].average_price` | `/api/v3/avgPrice` | 当前平均价及统计分钟数。 |
| `market[].liquidity` | `/api/v3/ticker/bookTicker` 和 `/api/v3/depth?limit=20` | 最优价数量、20 档深度、盘口不平衡、点差评级和流动性评级。 |
```

Also add one sentence after the table:

```markdown
这些增强特征只影响模型判断输入；本地置信度阈值、余额检查、点差上限、冷却、每日限制、最小名义金额和保护性止损仍然是最终风控边界。
```

- [ ] **Step 2: Check README diff**

Run:

```powershell
git diff -- README.md
```

Expected: Diff only touches AI trader documentation.

- [ ] **Step 3: Commit**

```powershell
git add README.md
git commit -m "docs: describe enriched ai trader market input"
```

## Task 8: Full Verification

**Files:**
- Verify all changed files.

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

Run:

```powershell
py -3 -m pytest tests/test_ai_trader.py tests/test_exchange.py tests/test_models.py -q
```

Expected: PASS.

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

Run:

```powershell
py -3 -m pytest -q
```

Expected: PASS.

- [ ] **Step 3: Inspect final diff**

Run:

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

Expected: working tree clean except for any intentional uncommitted user changes. Recent commits should show the feature steps above.
