Initialize crypto_monitor_user (user edition) from monitor codebase.
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"""hub_supervisor_lib 单元测试."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import pytest
|
||||
except ImportError: # pragma: no cover
|
||||
import unittest
|
||||
|
||||
raise unittest.SkipTest("pytest not installed")
|
||||
|
||||
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", "contracts": 1.0}}
|
||||
curr = {
|
||||
"0|ETH/USDT|long": {"symbol": "ETH/USDT", "contracts": 1.0},
|
||||
"1|BTC/USDT|short": {"symbol": "BTC/USDT", "contracts": 2.0, "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_opens_skips_existing_holdings():
|
||||
prev = {
|
||||
"2|ZEC/USDT|short": {"symbol": "ZEC/USDT:USDT", "contracts": 5.0},
|
||||
"2|HYPE/USDT|short": {"symbol": "HYPE/USDT:USDT", "contracts": 3.0},
|
||||
}
|
||||
curr = {
|
||||
"2|ZEC/USDT|short": {"symbol": "ZEC/USDT:USDT", "contracts": 5.0},
|
||||
"2|HYPE/USDT|short": {"symbol": "HYPE/USDT:USDT", "contracts": 3.0},
|
||||
}
|
||||
assert sup.detect_new_opens(prev, curr) == []
|
||||
|
||||
|
||||
def test_detect_new_opens_only_from_flat():
|
||||
prev = {"2|ZEC/USDT|short": {"symbol": "ZEC/USDT", "contracts": 0.0}}
|
||||
curr = {"2|ZEC/USDT|short": {"symbol": "ZEC/USDT:USDT", "contracts": 2.0}}
|
||||
events = sup.detect_new_opens(prev, curr)
|
||||
assert len(events) == 1
|
||||
assert events[0]["symbol"] == "ZEC/USDT:USDT"
|
||||
|
||||
|
||||
def test_normalize_position_symbol():
|
||||
assert sup._normalize_position_symbol("ZEC/USDT:USDT") == "ZEC/USDT"
|
||||
assert sup._position_key("2", "ZEC/USDT:USDT", "short") == "2|ZEC/USDT|short"
|
||||
|
||||
|
||||
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": [{"id": "0", "enabled": True, "agent": {"ok": True, "positions": []}}],
|
||||
}
|
||||
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
|
||||
|
||||
board2 = {
|
||||
"ok": True,
|
||||
"rows": [
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Gate",
|
||||
"enabled": True,
|
||||
"agent": {
|
||||
"ok": True,
|
||||
"positions": [
|
||||
{"symbol": "ZEC/USDT:USDT", "side": "short", "contracts": 1.0},
|
||||
{"symbol": "HYPE/USDT:USDT", "side": "short", "contracts": 1.0},
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
r3 = sup.process_supervisor_tick(dash, board2, settings, ai_reply_fn=None)
|
||||
assert r3.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",
|
||||
}
|
||||
]
|
||||
r4 = sup.process_supervisor_tick(dash2, board2, settings, ai_reply_fn=None)
|
||||
assert r4.get("events") == 1
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_supervisor_fallback_reply_program_sl():
|
||||
text = sup.build_supervisor_fallback_reply(
|
||||
{
|
||||
"event_type": sup.EVENT_PROGRAM_SL,
|
||||
"symbol": "ZEC/USDT",
|
||||
"pnl_amount": -0.9557,
|
||||
}
|
||||
)
|
||||
assert "程序止损" in text
|
||||
assert "AI 生成失败" not in text
|
||||
|
||||
|
||||
def test_supervisor_fallback_not_error_reply():
|
||||
from hub_ai.text_util import is_ai_error_reply
|
||||
|
||||
text = sup.build_supervisor_fallback_reply(
|
||||
{"event_type": sup.EVENT_MANUAL_CLOSE, "symbol": "ETH/USDT", "pnl_amount": -1}
|
||||
)
|
||||
assert text
|
||||
assert not is_ai_error_reply(text)
|
||||
Reference in New Issue
Block a user