Files
ai-exchange/tests/unit/test_trading_plan_service.py
T
2026-05-06 11:50:28 +08:00

251 lines
9.4 KiB
Python

from datetime import date, datetime, timezone
import os
import unittest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from scripts import run_trading_plans
from src.services.trading_plans import ACTIVE_PLAN_STATUS, DRAFT_PLAN_STATUS, TradingPlanService
from src.services.trading_plans.trading_plan_service import build_trading_plan_from_inputs
class TradingPlanServiceTests(unittest.TestCase):
def test_builds_active_plan_when_draw_and_invalidation_exist(self) -> None:
plan = build_trading_plan_from_inputs(
instrument_id=1,
plan_date=date(2026, 5, 6),
timeframe_set={"bias_tf": "1h", "setup_tf": "15m", "trigger_tf": "5m"},
range_row=_range_row(),
liquidity_pools=[
_liquidity_pool(pool_id=21, side="buy_side"),
_liquidity_pool(pool_id=22, side="sell_side"),
],
mss=_mss_row(direction="bullish", accepted=True),
fvgs=[_fvg_row()],
sessions=[_session_row("LONDON"), _session_row("NY_AM"), _session_row("ASIA")],
created_ts=_created_ts(),
)
self.assertEqual(plan.status, ACTIVE_PLAN_STATUS)
self.assertEqual(plan.bias, "bullish")
self.assertEqual(plan.primary_draw["side"], "buy_side")
self.assertEqual(plan.primary_draw["source"], "liquidity_pool")
self.assertEqual(plan.primary_draw["pool_id"], 21)
self.assertEqual(plan.allowed_sessions, ["LONDON", "NY_AM"])
self.assertTrue(plan.invalidations)
self.assertEqual(plan.no_trade_conditions, [])
self.assertTrue(plan.quality["has_draw"])
self.assertTrue(plan.quality["has_invalidation"])
self.assertTrue(plan.quality["active_eligible"])
self.assertIsNotNone(plan.activated_ts)
def test_plan_without_draw_cannot_be_active_even_when_invalidation_exists(self) -> None:
plan = build_trading_plan_from_inputs(
instrument_id=1,
plan_date=date(2026, 5, 6),
timeframe_set={"bias_tf": "1h", "setup_tf": "15m", "trigger_tf": "5m"},
range_row=None,
liquidity_pools=[],
mss=_mss_row(direction="bullish", accepted=True),
fvgs=[],
sessions=[],
created_ts=_created_ts(),
)
self.assertEqual(plan.status, DRAFT_PLAN_STATUS)
self.assertEqual(plan.primary_draw, {})
self.assertTrue(plan.invalidations)
self.assertIn("missing_primary_draw", plan.no_trade_conditions)
self.assertFalse(plan.quality["active_eligible"])
self.assertIsNone(plan.activated_ts)
def test_plan_without_invalidation_cannot_be_active_even_when_draw_exists(self) -> None:
plan = build_trading_plan_from_inputs(
instrument_id=1,
plan_date=date(2026, 5, 6),
timeframe_set={"bias_tf": "1h", "setup_tf": "15m", "trigger_tf": "5m"},
range_row=None,
liquidity_pools=[_liquidity_pool(pool_id=41, side="buy_side")],
mss=None,
fvgs=[],
sessions=[],
created_ts=_created_ts(),
)
self.assertEqual(plan.status, DRAFT_PLAN_STATUS)
self.assertEqual(plan.primary_draw["side"], "buy_side")
self.assertEqual(plan.invalidations, [])
self.assertIn("missing_invalidation", plan.no_trade_conditions)
self.assertIn("missing_directional_bias", plan.no_trade_conditions)
self.assertFalse(plan.quality["active_eligible"])
def test_mss_must_be_accepted_for_active_plan(self) -> None:
plan = build_trading_plan_from_inputs(
instrument_id=1,
plan_date=date(2026, 5, 6),
timeframe_set={"bias_tf": "1h", "setup_tf": "15m", "trigger_tf": "5m"},
range_row=_range_row(),
liquidity_pools=[_liquidity_pool(pool_id=51, side="sell_side")],
mss=_mss_row(direction="bearish", accepted=False),
fvgs=[_fvg_row(direction="bearish")],
sessions=[],
created_ts=_created_ts(),
)
self.assertEqual(plan.status, DRAFT_PLAN_STATUS)
self.assertEqual(plan.bias, "bearish")
self.assertIn("mss_not_accepted", plan.no_trade_conditions)
def test_default_btc_eth_symbols_can_generate_structured_daily_plans(self) -> None:
repository = _TradingPlanStubRepository()
service = TradingPlanService(repository=repository)
plans = []
for symbol in run_trading_plans.build_trading_plan_symbols_from_env({}):
instrument = repository.fetch_instrument_by_symbol(symbol)
self.assertIsNotNone(instrument)
assert instrument is not None
plans.append(
service.build_plan(
instrument_id=instrument["id"],
bias_tf="1h",
setup_tf="15m",
trigger_tf="5m",
plan_date=date(2026, 5, 6),
)
)
self.assertEqual([plan.instrument_id for plan in plans], [1, 2])
self.assertEqual([plan.status for plan in plans], [ACTIVE_PLAN_STATUS, ACTIVE_PLAN_STATUS])
for plan in plans:
with self.subTest(instrument_id=plan.instrument_id):
self.assertTrue(plan.primary_draw)
self.assertTrue(plan.invalidations)
self.assertIn(plan.bias, {"bullish", "bearish"})
class TradingPlanScriptConfigTests(unittest.TestCase):
def test_symbol_env_defaults_to_btc_eth(self) -> None:
self.assertEqual(run_trading_plans.build_trading_plan_symbols_from_env({}), ("BTC-USDT", "ETH-USDT"))
def test_symbol_env_accepts_comma_separated_override(self) -> None:
self.assertEqual(
run_trading_plans.build_trading_plan_symbols_from_env({"TRADING_PLAN_SYMBOLS": " BTC-USDT , ETH-USDT "}),
("BTC-USDT", "ETH-USDT"),
)
def test_timeframe_env_reads_overrides(self) -> None:
self.assertEqual(
run_trading_plans.build_trading_plan_timeframe_set_from_env(
{
"AI_ICT_BIAS_TIMEFRAME": "4h",
"AI_ICT_SETUP_TIMEFRAME": "1h",
"AI_ICT_TRIGGER_TIMEFRAME": "15m",
}
),
{"bias_tf": "4h", "setup_tf": "1h", "trigger_tf": "15m"},
)
class _TradingPlanStubRepository:
def __init__(self) -> None:
self.instruments = {
"BTC-USDT": {"id": 1, "symbol": "BTC-USDT", "active": True},
"ETH-USDT": {"id": 2, "symbol": "ETH-USDT", "active": True},
}
def fetch_instrument_by_symbol(self, symbol: str):
return self.instruments.get(symbol)
def fetch_latest_range(self, instrument_id: int, timeframe: str):
if instrument_id == 1:
return _range_row(range_id=101, high=72000.0, low=70000.0)
return _range_row(range_id=201, high=3800.0, low=3600.0)
def fetch_recent_liquidity_pools(self, instrument_id: int, timeframe: str, limit: int = 20):
if instrument_id == 1:
return [_liquidity_pool(pool_id=111, side="buy_side", price_low=71980.0, price_high=72020.0)]
return [_liquidity_pool(pool_id=211, side="sell_side", price_low=3590.0, price_high=3610.0)]
def fetch_latest_mss(self, instrument_id: int, timeframe: str):
if instrument_id == 1:
return _mss_row(mss_id=121, direction="bullish", accepted=True, broken_level=71000.0)
return _mss_row(mss_id=221, direction="bearish", accepted=True, broken_level=3700.0)
def fetch_recent_fvgs(self, instrument_id: int, timeframe: str, limit: int = 20):
if instrument_id == 1:
return [_fvg_row(fvg_id=131, direction="bullish")]
return [_fvg_row(fvg_id=231, direction="bearish")]
def fetch_sessions_for_date(self, instrument_id: int, plan_date):
return [_session_row("LONDON"), _session_row("NY_AM")]
def upsert_trading_plan(self, **kwargs):
return int(kwargs["instrument_id"])
def _created_ts() -> datetime:
return datetime(2026, 5, 6, 0, 0, tzinfo=timezone.utc)
def _range_row(range_id: int = 10, high: float = 101.0, low: float = 99.0) -> dict:
return {"id": range_id, "high": high, "low": low, "equilibrium": (high + low) / 2, "state": "valid"}
def _liquidity_pool(
*,
pool_id: int,
side: str,
price_low: float = 100.8,
price_high: float = 101.2,
status: str = "untouched",
) -> dict:
return {
"id": pool_id,
"side": side,
"pool_type": "equal_highs" if side == "buy_side" else "equal_lows",
"price_low": price_low,
"price_high": price_high,
"status": status,
}
def _mss_row(
*,
mss_id: int = 30,
direction: str = "bullish",
accepted: bool = True,
broken_level: float = 100.0,
) -> dict:
return {
"id": mss_id,
"direction": direction,
"accepted": accepted,
"broken_level": broken_level,
"displacement_event_id": 7,
"meta": {"state": "accepted" if accepted else "rejected"},
}
def _fvg_row(fvg_id: int = 40, direction: str = "bullish") -> dict:
return {
"id": fvg_id,
"direction": direction,
"lower": 100.0,
"upper": 100.2,
"status": "open",
"fill_pct": 0.0,
}
def _session_row(code: str) -> dict:
return {
"id": len(code),
"session_code": code,
"status": "planned",
}
if __name__ == "__main__":
unittest.main()