187 lines
5.3 KiB
Python
187 lines
5.3 KiB
Python
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from trading_bot.models.prediction import PredictionResult
|
|
from trading_bot.models.trade import (
|
|
rebalance_alpaca_portfolio,
|
|
summarize_alpaca_portfolio,
|
|
)
|
|
|
|
|
|
def test_calculate_cash_to_spy_ratio() -> None:
|
|
account = {"cash": "1000.00"}
|
|
positions = [{"symbol": "SPY", "qty": "10", "market_value": "5000", "avg_entry_price": "500"}]
|
|
orders = []
|
|
|
|
summary = summarize_alpaca_portfolio(account, positions, orders)
|
|
|
|
assert summary.cash == 1000.0
|
|
assert summary.spy_quantity == 10.0
|
|
assert summary.spy_market_value == 5000.0
|
|
assert summary.current_cash_to_spy_ratio == 0.2
|
|
assert summary.projected_cash_to_spy_ratio == 0.2
|
|
assert summary.open_spy_order_count == 0
|
|
|
|
|
|
def test_projected_ratio_includes_open_spy_buy_order() -> None:
|
|
account = {"cash": "1000.00"}
|
|
positions = [{"symbol": "SPY", "qty": "10", "market_value": "5000", "avg_entry_price": "500"}]
|
|
orders = [
|
|
{
|
|
"symbol": "SPY",
|
|
"side": "buy",
|
|
"qty": "2",
|
|
"filled_qty": "0",
|
|
"limit_price": "500",
|
|
"status": "open",
|
|
"type": "limit",
|
|
}
|
|
]
|
|
|
|
summary = summarize_alpaca_portfolio(account, positions, orders)
|
|
|
|
assert summary.open_spy_order_count == 1
|
|
assert summary.estimated_spy_price == 500.0
|
|
assert summary.projected_cash_to_spy_ratio == 0.0
|
|
|
|
|
|
def test_projected_ratio_handles_no_spy_position_but_open_order() -> None:
|
|
account = {"cash": "1500.00"}
|
|
positions = []
|
|
orders = [
|
|
{
|
|
"symbol": "SPY",
|
|
"side": "buy",
|
|
"qty": "3",
|
|
"filled_qty": "0",
|
|
"limit_price": "500",
|
|
"status": "open",
|
|
"type": "limit",
|
|
}
|
|
]
|
|
|
|
summary = summarize_alpaca_portfolio(account, positions, orders)
|
|
|
|
assert summary.spy_quantity == 0.0
|
|
assert summary.spy_market_value == 0.0
|
|
assert summary.current_cash_to_spy_ratio is None
|
|
assert summary.projected_cash_to_spy_ratio == 0.0
|
|
assert summary.open_spy_order_count == 1
|
|
|
|
|
|
class FakeAlpacaClient:
|
|
def __init__(self) -> None:
|
|
self.cancelled = False
|
|
self.order_request = None
|
|
|
|
def get_account(self) -> dict[str, str]:
|
|
return {"cash": "1000.00"}
|
|
|
|
def get_all_positions(self) -> list[dict[str, str]]:
|
|
return [
|
|
{
|
|
"symbol": "SPY",
|
|
"qty": "10",
|
|
"market_value": "5000",
|
|
"avg_entry_price": "500",
|
|
}
|
|
]
|
|
|
|
def get_orders(self, filter=None) -> list[dict[str, Any]]:
|
|
return []
|
|
|
|
def cancel_orders(self) -> list[dict[str, Any]]:
|
|
self.cancelled = True
|
|
return []
|
|
|
|
def submit_order(self, order_request: Any) -> dict[str, Any]:
|
|
self.order_request = order_request
|
|
return {
|
|
"id": "fake-order",
|
|
"status": "new",
|
|
"symbol": getattr(order_request, "symbol", "SPY"),
|
|
"side": getattr(order_request, "side", "buy"),
|
|
"notional": getattr(order_request, "notional", 0.0),
|
|
}
|
|
|
|
|
|
def test_rebalance_submits_buy_order_when_target_exposure_is_higher(monkeypatch) -> None:
|
|
fake_client = FakeAlpacaClient()
|
|
|
|
def fake_create_client(api_key=None, secret_key=None, paper=True):
|
|
return fake_client
|
|
|
|
def fake_predict_latest_probability(**kwargs):
|
|
return PredictionResult(
|
|
prediction_date=pd.Timestamp("2026-08-03"),
|
|
probability=0.9,
|
|
base_probability=0.5,
|
|
feature_columns=["SPY_ret_5"],
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"trading_bot.models.trade.create_alpaca_trading_client",
|
|
fake_create_client,
|
|
)
|
|
monkeypatch.setattr(
|
|
"trading_bot.models.trade.predict_latest_probability",
|
|
fake_predict_latest_probability,
|
|
)
|
|
|
|
summary, order = rebalance_alpaca_portfolio(
|
|
api_key="test-key",
|
|
secret_key="test-secret",
|
|
paper=True,
|
|
symbol="SPY",
|
|
min_order_dollars=100.0,
|
|
sensitivity=5.0,
|
|
)
|
|
|
|
assert fake_client.cancelled is True
|
|
assert order is not None
|
|
assert order["side"] == "buy"
|
|
assert order["notional"] == pytest.approx(1000.0)
|
|
assert summary.cash == 1000.0
|
|
assert summary.spy_market_value == 5000.0
|
|
|
|
|
|
def test_rebalance_skips_small_orders_below_minimum(monkeypatch) -> None:
|
|
fake_client = FakeAlpacaClient()
|
|
|
|
def fake_create_client(api_key=None, secret_key=None, paper=True):
|
|
return fake_client
|
|
|
|
def fake_predict_latest_probability(**kwargs):
|
|
return PredictionResult(
|
|
prediction_date=pd.Timestamp("2026-08-03"),
|
|
probability=0.569,
|
|
base_probability=0.5,
|
|
feature_columns=["SPY_ret_5"],
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"trading_bot.models.trade.create_alpaca_trading_client",
|
|
fake_create_client,
|
|
)
|
|
monkeypatch.setattr(
|
|
"trading_bot.models.trade.predict_latest_probability",
|
|
fake_predict_latest_probability,
|
|
)
|
|
|
|
summary, order = rebalance_alpaca_portfolio(
|
|
api_key="test-key",
|
|
secret_key="test-secret",
|
|
paper=True,
|
|
symbol="SPY",
|
|
min_order_dollars=100.0,
|
|
sensitivity=5.0,
|
|
)
|
|
|
|
assert fake_client.cancelled is False
|
|
assert order is None
|
|
assert summary.cash == 1000.0
|
|
assert summary.spy_market_value == 5000.0
|