first commit

This commit is contained in:
dekun
2026-08-01 10:33:19 +08:00
commit d9a34d4f20
72 changed files with 5499 additions and 0 deletions
+65
View File
@@ -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
+44
View File
@@ -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)
+27
View File
@@ -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
+13
View File
@@ -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
+57
View File
@@ -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
+18
View File
@@ -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
+27
View File
@@ -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")
+56
View File
@@ -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()