mirror of
https://github.com/OpenByteInc/QuantDinger.git
synced 2026-09-28 15:22:56 +08:00
160 lines
5.0 KiB
Python
160 lines
5.0 KiB
Python
from datetime import datetime, timezone
|
|
|
|
import pandas as pd
|
|
|
|
from app.services.ai_decision_context import (
|
|
build_market_evidence,
|
|
build_quick_trade_decision_context,
|
|
build_strategy_decision_context,
|
|
summarize_market_bars,
|
|
)
|
|
|
|
|
|
def _bars(count=80, start=2_000.0, step=2.0):
|
|
now = datetime.now(timezone.utc).timestamp()
|
|
return [
|
|
{
|
|
"time": now - (count - index) * 60,
|
|
"open": start + index * step - 0.5,
|
|
"high": start + index * step + 2.0,
|
|
"low": start + index * step - 2.0,
|
|
"close": start + index * step,
|
|
"volume": 100 + index,
|
|
}
|
|
for index in range(count)
|
|
]
|
|
|
|
|
|
def test_market_summary_contains_directional_and_risk_evidence():
|
|
summary = summarize_market_bars(_bars(), timeframe="15m", reference_price=2_160.0)
|
|
|
|
assert summary["available"] is True
|
|
assert summary["trend"] in {"uptrend", "strong_uptrend"}
|
|
assert summary["return_20_bar_pct"] > 0
|
|
assert summary["rsi14"] is not None
|
|
assert summary["atr14_pct"] > 0
|
|
assert summary["data_age_seconds"] >= 0
|
|
assert summary["interval_seconds"] == 900
|
|
assert summary["is_stale"] is False
|
|
|
|
|
|
def test_runtime_frequency_frames_are_reused_without_market_requests():
|
|
class Klines:
|
|
def __init__(self):
|
|
self.cache = self
|
|
self.keys = []
|
|
|
|
def get(self, key):
|
|
self.keys.append(key)
|
|
return None
|
|
|
|
service = Klines()
|
|
frame = pd.DataFrame(_bars()).set_index("time")
|
|
evidence = build_market_evidence(
|
|
market="Crypto",
|
|
symbol="ETH/USDT",
|
|
timeframe="15m",
|
|
exchange_id="binance",
|
|
market_type="spot",
|
|
reference_price=2_160.0,
|
|
primary_frame=frame,
|
|
frame_bundle={
|
|
"15m": frame,
|
|
"1h": frame,
|
|
"4h": frame,
|
|
},
|
|
kline_service=service,
|
|
)
|
|
|
|
assert evidence["data_quality"] == "complete"
|
|
assert set(evidence["available_timeframes"]) == {"15m", "1h", "4h"}
|
|
assert service.keys == []
|
|
|
|
|
|
def test_missing_market_evidence_reads_cache_only():
|
|
class Cache:
|
|
def __init__(self):
|
|
self.keys = []
|
|
|
|
def get(self, key):
|
|
self.keys.append(key)
|
|
return _bars() if "kline:latest:Crypto:binance:spot::ETH/USDT:1h" == key else None
|
|
|
|
class Klines:
|
|
def __init__(self):
|
|
self.cache = Cache()
|
|
|
|
def get_kline(self, **kwargs):
|
|
raise AssertionError("The order boundary must not fetch market data")
|
|
|
|
service = Klines()
|
|
evidence = build_market_evidence(
|
|
market="Crypto",
|
|
symbol="ETH/USDT",
|
|
timeframe="15m",
|
|
exchange_id="binance",
|
|
market_type="spot",
|
|
kline_service=service,
|
|
)
|
|
|
|
assert evidence["data_quality"] == "partial"
|
|
assert evidence["available_timeframes"] == ["1h"]
|
|
assert service.cache.keys
|
|
|
|
|
|
def test_strategy_context_includes_bound_parameters_and_freshness(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"app.services.ai_decision_context._strategy_performance",
|
|
lambda _strategy_id: {"completed_exits": 2},
|
|
)
|
|
frame = pd.DataFrame(_bars()).set_index("time")
|
|
context = build_strategy_decision_context(
|
|
values={
|
|
"symbol": "ETH/USDT",
|
|
"market_category": "Crypto",
|
|
"market_type": "spot",
|
|
"current_price": 2_160.0,
|
|
"strategy_id": 9,
|
|
"market_frame": frame,
|
|
"market_frames": {"15m": frame, "1h": frame, "4h": frame},
|
|
"trading_config": {
|
|
"direction_mode": "long_only",
|
|
"params": {"fast_period": 20, "slow_period": 60, "private": {"ignored": True}},
|
|
},
|
|
},
|
|
strategy={"strategy_name": "Dual Moving Average", "timeframe": "15m"},
|
|
order_budget={"allowed": True},
|
|
strategy_equity=950.0,
|
|
initial_capital=1_000.0,
|
|
entry_percent=50.0,
|
|
)
|
|
|
|
assert context["strategy"]["parameters"] == {"fast_period": 20, "slow_period": 60}
|
|
assert context["market_evidence"]["data_quality"] == "complete"
|
|
assert context["market_evidence"]["stale_timeframes"] == []
|
|
assert context["portfolio_risk"]["drawdown_from_initial_pct"] == -5.0
|
|
|
|
|
|
def test_quick_trade_context_includes_live_evidence_and_protection(monkeypatch):
|
|
monkeypatch.setattr(
|
|
"app.services.ai_decision_context._cached_market_rows",
|
|
lambda *args, **kwargs: _bars(),
|
|
)
|
|
context = build_quick_trade_decision_context({
|
|
"symbol": "ETH/USDT",
|
|
"side": "buy",
|
|
"market_type": "spot",
|
|
"exchange_id": "binance",
|
|
"base_qty": 0.1,
|
|
"order_notional_usdt": 216.0,
|
|
"usdt_amount": 216.0,
|
|
"tp_price": 2_220.0,
|
|
"sl_price": 2_120.0,
|
|
"bal": {"available": 500.0, "total": 700.0},
|
|
})
|
|
|
|
assert context["context_version"] == 2
|
|
assert context["market_evidence"]["data_quality"] == "complete"
|
|
assert context["portfolio_risk"]["available_balance"] == 500.0
|
|
assert context["portfolio_risk"]["risk_reward_ratio"] == 1.5
|