first commit
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
from packages.domain.aggregate import aggregate_leverage, summarize_values
|
||||
from packages.domain.range import resolve_range
|
||||
|
||||
|
||||
def test_summarize_values():
|
||||
s = summarize_values([100, 200, 300, 400], min_leverage=200)
|
||||
assert s["n"] == 4
|
||||
assert s["mean"] == 250
|
||||
assert s["median"] == 250
|
||||
assert s["pct_ge_min"] == 0.75
|
||||
|
||||
|
||||
def test_summarize_empty():
|
||||
s = summarize_values([], min_leverage=100)
|
||||
assert s["n"] == 0
|
||||
assert s["mean"] is None
|
||||
|
||||
|
||||
def test_resolve_range_day():
|
||||
info = resolve_range("day", "2026-07-31")
|
||||
assert info["start_ymd"] == "2026-07-31"
|
||||
assert info["end_ymd"] == "2026-07-31"
|
||||
assert info["days"] == 1
|
||||
assert info["end_ms"] > info["start_ms"]
|
||||
|
||||
|
||||
def test_resolve_range_week():
|
||||
info = resolve_range("week", "2026-07-31")
|
||||
assert info["start_ymd"] == "2026-07-25"
|
||||
assert info["end_ymd"] == "2026-07-31"
|
||||
assert info["days"] == 7
|
||||
|
||||
|
||||
def test_resolve_range_month_rolling():
|
||||
info = resolve_range("month", "2026-07-31", month_mode="rolling_30")
|
||||
assert info["start_ymd"] == "2026-07-02"
|
||||
assert info["days"] == 30
|
||||
|
||||
|
||||
def test_resolve_range_month_calendar():
|
||||
info = resolve_range("month", "2026-07-31", month_mode="calendar")
|
||||
assert info["start_ymd"] == "2026-07-01"
|
||||
assert info["end_ymd"] == "2026-07-31"
|
||||
|
||||
|
||||
def test_aggregate_leverage_buckets():
|
||||
# 2026-07-31 14:30 Asia/Shanghai
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
sh = ZoneInfo("Asia/Shanghai")
|
||||
ts = int(datetime(2026, 7, 31, 14, 30, tzinfo=sh).timestamp() * 1000)
|
||||
rows = [
|
||||
{"ts_ms": ts, "side": "C", "leverage": 120},
|
||||
{"ts_ms": ts, "side": "P", "leverage": 80},
|
||||
{"ts_ms": ts, "side": "C", "leverage": 180},
|
||||
]
|
||||
buckets = aggregate_leverage(rows, bucket_minutes=60, min_leverage=100, side="C")
|
||||
assert len(buckets) == 24
|
||||
b14 = next(b for b in buckets if b["bucket_start_min"] == 14 * 60)
|
||||
assert b14["n"] == 2
|
||||
assert b14["mean"] == 150
|
||||
assert b14["label"] == "14:00"
|
||||
empty = next(b for b in buckets if b["bucket_start_min"] == 0)
|
||||
assert empty["n"] == 0
|
||||
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
|
||||
from packages.config.settings import get_settings
|
||||
|
||||
|
||||
def _with_env(**kwargs):
|
||||
old = {}
|
||||
for k, v in kwargs.items():
|
||||
old[k] = os.environ.get(k)
|
||||
os.environ[k] = v
|
||||
get_settings.cache_clear()
|
||||
return old
|
||||
|
||||
|
||||
def _restore(old: dict):
|
||||
for k, v in old.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_issue_token_ok():
|
||||
from apps.api.auth import expected_token, issue_token
|
||||
|
||||
old = _with_env(AUTH_SECRET="unit-secret", ADMIN_PASSWORD="pass123")
|
||||
try:
|
||||
tok = issue_token("pass123")
|
||||
assert tok is not None
|
||||
assert tok == expected_token()
|
||||
assert issue_token("wrong") is None
|
||||
finally:
|
||||
_restore(old)
|
||||
|
||||
|
||||
def test_auth_disabled():
|
||||
from apps.api.auth import auth_disabled
|
||||
|
||||
old = _with_env(AUTH_SECRET="disabled")
|
||||
try:
|
||||
assert auth_disabled() is True
|
||||
finally:
|
||||
_restore(old)
|
||||
@@ -0,0 +1,27 @@
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from packages.domain.buckets import shanghai_bucket, shanghai_bucket_hour, shanghai_day
|
||||
|
||||
_SH = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
|
||||
def _ms(y, m, d, hh, mm=0):
|
||||
return int(datetime(y, m, d, hh, mm, tzinfo=_SH).timestamp() * 1000)
|
||||
|
||||
|
||||
def test_shanghai_day():
|
||||
# UTC 2026-07-31 16:00 = 上海 2026-08-01 00:00
|
||||
ts = int(datetime(2026, 7, 31, 16, 0, tzinfo=ZoneInfo("UTC")).timestamp() * 1000)
|
||||
assert shanghai_day(ts) == "2026-08-01"
|
||||
|
||||
|
||||
def test_bucket_hour():
|
||||
ts = _ms(2026, 7, 31, 14, 35)
|
||||
assert shanghai_bucket(ts, 60) == 14 * 60
|
||||
assert shanghai_bucket_hour(ts) == 14
|
||||
|
||||
|
||||
def test_bucket_30m():
|
||||
ts = _ms(2026, 7, 31, 14, 35)
|
||||
assert shanghai_bucket(ts, 30) == 14 * 60 + 30
|
||||
@@ -0,0 +1,13 @@
|
||||
from packages.domain import option_leverage
|
||||
|
||||
|
||||
def test_leverage_basic():
|
||||
assert option_leverage(2000.0, 20.0) == 100.0
|
||||
assert option_leverage(3500.0, 35.0) == 100.0
|
||||
|
||||
|
||||
def test_leverage_invalid():
|
||||
assert option_leverage(0, 10) is None
|
||||
assert option_leverage(100, 0) is None
|
||||
assert option_leverage(100, None) is None
|
||||
assert option_leverage(-1, 1) is None
|
||||
@@ -0,0 +1,57 @@
|
||||
from packages.domain.aggregate import aggregate_move_points, build_move_samples, move_points_stats_payload
|
||||
from packages.domain.range import resolve_range
|
||||
|
||||
|
||||
def test_build_move_samples_settled_and_pending():
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
sh = ZoneInfo("Asia/Shanghai")
|
||||
ts = int(datetime(2026, 7, 30, 10, 0, tzinfo=sh).timestamp() * 1000)
|
||||
rows = [
|
||||
{"ts_ms": ts, "side": "C", "expiry_ymd": "260720", "index_px": 3400}, # settled
|
||||
{"ts_ms": ts, "side": "P", "expiry_ymd": "260720", "index_px": 3450},
|
||||
{"ts_ms": ts, "side": "C", "expiry_ymd": "991231", "index_px": 3500}, # pending far
|
||||
]
|
||||
settlements = {
|
||||
"260720": {"settle_ts_ms": ts - 1000, "settle_index_px": 3500},
|
||||
}
|
||||
samples, meta = build_move_samples(rows, settlements, side="both", now_ms=ts)
|
||||
assert meta["settled_count"] == 2
|
||||
assert meta["pending_count"] == 1
|
||||
assert meta["pending_expiry"] is True
|
||||
assert samples[0]["move_signed"] == 100.0 # 3500-3400
|
||||
assert samples[1]["move_abs"] == 50.0
|
||||
|
||||
|
||||
def test_aggregate_move_points():
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
sh = ZoneInfo("Asia/Shanghai")
|
||||
ts = int(datetime(2026, 7, 30, 14, 20, tzinfo=sh).timestamp() * 1000)
|
||||
samples = [
|
||||
{"ts_ms": ts, "move_signed": 100, "move_abs": 100},
|
||||
{"ts_ms": ts, "move_signed": -40, "move_abs": 40},
|
||||
]
|
||||
buckets = aggregate_move_points(samples, bucket_minutes=60)
|
||||
b14 = next(b for b in buckets if b["bucket_start_min"] == 14 * 60)
|
||||
assert b14["n"] == 2
|
||||
assert b14["mean_signed"] == 30.0
|
||||
assert b14["mean_abs"] == 70.0
|
||||
|
||||
|
||||
def test_move_points_payload_pending_message():
|
||||
info = resolve_range("day", "2026-07-30")
|
||||
payload = move_points_stats_payload(
|
||||
[{"ts_ms": info["start_ms"] + 3600_000, "side": "C", "expiry_ymd": "991231", "index_px": 1}],
|
||||
{},
|
||||
range_info=info,
|
||||
bucket_minutes=60,
|
||||
side="both",
|
||||
now_ms=info["start_ms"],
|
||||
)
|
||||
assert payload["status"] == "ok"
|
||||
assert payload["pending_expiry"] is True
|
||||
assert payload["settled_count"] == 0
|
||||
assert payload["sample_count"] == 0
|
||||
@@ -0,0 +1,18 @@
|
||||
from packages.domain import move_points
|
||||
from packages.domain.expiry import expiry_ms_from_ymd
|
||||
|
||||
|
||||
def test_move_points_signed():
|
||||
assert move_points(3600.0, 3500.0) == 100.0
|
||||
assert move_points(3400.0, 3500.0) == -100.0
|
||||
assert abs(move_points(3400.0, 3500.0)) == 100.0
|
||||
|
||||
|
||||
def test_expiry_ms_utc8():
|
||||
# 260731 → 2026-07-31 08:00 UTC
|
||||
ms = expiry_ms_from_ymd("260731")
|
||||
from datetime import datetime, timezone
|
||||
|
||||
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
||||
assert dt.year == 2026 and dt.month == 7 and dt.day == 31
|
||||
assert dt.hour == 8 and dt.minute == 0
|
||||
@@ -0,0 +1,27 @@
|
||||
from apps.collector.selectors import pick_atm_strike, select_atm_pair
|
||||
|
||||
|
||||
def test_pick_atm_strike():
|
||||
assert pick_atm_strike([3490, 3500, 3510], 3502) == 3500
|
||||
assert pick_atm_strike([3490, 3510], 3500) == 3490 # 等距取较小
|
||||
|
||||
|
||||
def test_select_atm_pair():
|
||||
# 构造远到期,避免 min_hours 过滤
|
||||
contracts = []
|
||||
for k in (3490.0, 3500.0, 3510.0):
|
||||
for side in ("C", "P"):
|
||||
contracts.append(
|
||||
{
|
||||
"inst_id": f"ETH-USD_UM-991231-{int(k)}-{side}",
|
||||
"expiry_ymd": "991231",
|
||||
"expiry_ms": 4102358400000, # 远未来
|
||||
"strike": k,
|
||||
"side": side,
|
||||
}
|
||||
)
|
||||
pair = select_atm_pair(contracts, index_px=3501.0, min_hours=0)
|
||||
assert pair is not None
|
||||
assert pair.strike == 3500.0
|
||||
assert pair.call_inst_id.endswith("-C")
|
||||
assert pair.put_inst_id.endswith("-P")
|
||||
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
|
||||
from packages.config.settings import get_settings
|
||||
from packages.notify import wecom
|
||||
|
||||
|
||||
def _with_env(**kwargs):
|
||||
old = {}
|
||||
for k, v in kwargs.items():
|
||||
old[k] = os.environ.get(k)
|
||||
os.environ[k] = v
|
||||
get_settings.cache_clear()
|
||||
return old
|
||||
|
||||
|
||||
def _restore(old: dict):
|
||||
for k, v in old.items():
|
||||
if v is None:
|
||||
os.environ.pop(k, None)
|
||||
else:
|
||||
os.environ[k] = v
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_build_markdown_contains_tag():
|
||||
md = wecom.build_markdown(tag="FAULT", title="测试", lines=["a", "b"])
|
||||
assert "`FAULT`" in md
|
||||
assert "比特骆驼行情采集分析" in md
|
||||
assert "测试" in md
|
||||
|
||||
|
||||
def test_fault_below_threshold():
|
||||
wecom.reset_alert_state()
|
||||
old = _with_env(WECOM_ENABLED="0", ALERT_FAIL_THRESHOLD="5")
|
||||
try:
|
||||
ok, msg = wecom.notify_collector_fault(error="x", consecutive_failures=2)
|
||||
assert ok is False
|
||||
assert "below threshold" in msg
|
||||
finally:
|
||||
_restore(old)
|
||||
wecom.reset_alert_state()
|
||||
|
||||
|
||||
def test_fault_when_disabled():
|
||||
wecom.reset_alert_state()
|
||||
old = _with_env(WECOM_ENABLED="0", ALERT_FAIL_THRESHOLD="3")
|
||||
try:
|
||||
ok, msg = wecom.notify_collector_fault(error="boom", consecutive_failures=3)
|
||||
assert ok is False
|
||||
assert "未开启" in msg
|
||||
ok2, msg2 = wecom.notify_collector_fault(error="boom", consecutive_failures=3)
|
||||
assert ok2 is False
|
||||
assert msg2 == "dedup"
|
||||
finally:
|
||||
_restore(old)
|
||||
wecom.reset_alert_state()
|
||||
Reference in New Issue
Block a user