"""hub_supervisor_lib 单元测试。""" from __future__ import annotations import json import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "manual_trading_hub")) import hub_supervisor_lib as sup @pytest.fixture def state_path(tmp_path, monkeypatch): p = tmp_path / "hub_supervisor_state.json" monkeypatch.setattr(sup, "STATE_PATH", p) return p def test_classify_close_result(): assert sup.classify_close_result("手动平仓") == sup.EVENT_MANUAL_CLOSE assert sup.classify_close_result("强制清仓") == sup.EVENT_HUB_CLOSE assert sup.classify_close_result("止盈") == sup.EVENT_PROGRAM_TP assert sup.classify_close_result("止损") == sup.EVENT_PROGRAM_SL assert sup.classify_close_result("外部平仓") == sup.EVENT_EXTERNAL def test_detect_new_opens(): prev = {"0|ETH/USDT|long": {"symbol": "ETH/USDT"}} curr = { "0|ETH/USDT|long": {"symbol": "ETH/USDT"}, "1|BTC/USDT|short": {"symbol": "BTC/USDT", "exchange_name": "OKX"}, } events = sup.detect_new_opens(prev, curr) assert len(events) == 1 assert events[0]["event_type"] == sup.EVENT_OPEN assert events[0]["symbol"] == "BTC/USDT" def test_detect_new_closes_dedup(): trades = [ { "account_name": "OKX", "symbol": "ETH/USDT", "result": "手动平仓", "pnl_amount": -5, "closed_at": "2026-06-14 10:00:00", } ] eid = f"close:{sup._trade_event_id(trades[0])}" events = sup.detect_new_closes(set(), trades) assert len(events) == 1 assert events[0]["event_type"] == sup.EVENT_MANUAL_CLOSE assert sup.detect_new_closes({eid}, trades) == [] def test_evaluate_frequency_warnings_interval(): stats = { "2026-06-14": { "supervised_closes": [ {"closed_at": "2026-06-14 09:50:00", "pnl_amount": -1}, ], "supervised_opens": [], } } event = { "event_type": sup.EVENT_MANUAL_CLOSE, "closed_at": "2026-06-14 10:00:00", "pnl_amount": -2, } settings = sup.normalize_supervisor_settings({}) warnings = sup.evaluate_frequency_warnings( trading_day="2026-06-14", event=event, stats=stats, settings=settings, ) rules = {w["rule"] for w in warnings} assert "INTERVAL_SHORT" in rules def test_process_supervisor_tick_seeds_without_events(state_path, monkeypatch, tmp_path): chat_path = tmp_path / "hub_ai_chat.json" monkeypatch.setattr("hub_ai.store.CHAT_PATH", chat_path) dash = { "ok": True, "trading_day": "2026-06-14", "closed_trades": [ { "account_name": "Binance", "symbol": "ETH/USDT", "result": "手动平仓", "pnl_amount": 1, "closed_at": "2026-06-14 08:30:00", } ], } board = {"ok": True, "rows": []} settings = {"supervisor": sup.normalize_supervisor_settings({"enabled": True, "wechat_webhook": ""})} r1 = sup.process_supervisor_tick(dash, board, settings, ai_reply_fn=None) assert r1.get("seeded") is True assert r1.get("events") == 0 r2 = sup.process_supervisor_tick(dash, board, settings, ai_reply_fn=None) assert r2.get("events") == 0 dash2 = dict(dash) dash2["closed_trades"] = dash["closed_trades"] + [ { "account_name": "Binance", "symbol": "BTC/USDT", "result": "手动平仓", "pnl_amount": -3, "closed_at": "2026-06-14 11:00:00", } ] r3 = sup.process_supervisor_tick(dash2, board, settings, ai_reply_fn=None) assert r3.get("events") == 1 assert chat_path.is_file() data = json.loads(chat_path.read_text(encoding="utf-8")) sessions = [s for s in data.get("sessions") or [] if s.get("bot_mode") == "supervisor"] assert sessions msgs = sessions[0].get("messages") or [] assert any(m.get("role") == "system" for m in msgs) def test_normalize_supervisor_settings_env(monkeypatch): monkeypatch.setenv("SUPERVISOR_WECHAT_WEBHOOK", "https://example.com/hook") monkeypatch.setenv("SUPERVISOR_WECHAT_LINK", "https://hub.example/ai?mode=supervisor") cfg = sup.normalize_supervisor_settings({}) assert cfg["wechat_webhook"] == "https://example.com/hook" assert cfg["wechat_link_base"] == "https://hub.example/ai?mode=supervisor"