feat(v0.2): 市场阶段与主线识别 + 因子挖掘全链路 + 数据层完善

- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动,
  EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合,
  可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存
- 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档),
  周度调度默认关闭且永不自动发布
- 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益,
  信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错)
- 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复
- 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
This commit is contained in:
shy3130
2026-08-16 23:39:07 +08:00
parent eb869c7ad2
commit 697c27bb02
129 changed files with 20324 additions and 602 deletions
+123
View File
@@ -0,0 +1,123 @@
"""preferences mtime 缓存测试 — 读盘去重, 且外部修改/自身写入后立即可见。"""
from __future__ import annotations
import json
import os
import pytest
from app.services import preferences
@pytest.fixture(autouse=True)
def _isolated(tmp_path, monkeypatch):
path = tmp_path / "preferences.json"
monkeypatch.setattr(preferences, "_path", lambda: path)
preferences._invalidate_cache()
yield path
preferences._invalidate_cache()
def _patched_loads(monkeypatch, counter: dict):
real_loads = json.loads
def _counting(text):
counter["loads"] += 1
return real_loads(text)
monkeypatch.setattr(preferences.json, "loads", _counting)
def test_second_load_hits_cache_without_disk_parse(_isolated, monkeypatch):
_isolated.write_text(json.dumps({"realtime_quotes_enabled": True}), encoding="utf-8")
counter = {"loads": 0}
_patched_loads(monkeypatch, counter)
assert preferences.load()["realtime_quotes_enabled"] is True
assert preferences.load()["realtime_quotes_enabled"] is True
assert counter["loads"] == 1, "签名未变时第二次 load 不得重复读盘+parse"
def test_external_file_change_invalidates_cache(_isolated, monkeypatch):
_isolated.write_text(json.dumps({"realtime_quote_interval": 6.0}), encoding="utf-8")
assert preferences.load()["realtime_quote_interval"] == 6.0
_isolated.write_text(json.dumps({"realtime_quote_interval": 3.0}), encoding="utf-8")
# 同尺寸修改且 mtime 粒度可能不变时, 显式推进 mtime 模拟真实场景
st = _isolated.stat()
os.utime(_isolated, ns=(st.st_atime_ns, st.st_mtime_ns + 1_000_000))
assert preferences.load()["realtime_quote_interval"] == 3.0
def test_save_then_load_sees_merged_values(_isolated):
_isolated.write_text(json.dumps({"a": 1}), encoding="utf-8")
out = preferences.save({"b": 2})
assert out == {"a": 1, "b": 2}
assert preferences.load() == {"a": 1, "b": 2}
def test_interval_setter_invalidates_cache(_isolated):
preferences.set_realtime_quote_interval(2.0)
assert preferences.load()["realtime_quote_interval"] == 2.0
def test_load_returns_copy_not_cached_object(_isolated):
_isolated.write_text(json.dumps({"k": [1, 2]}), encoding="utf-8")
first = preferences.load()
first["k"].append(3)
first["extra"] = True
again = preferences.load()
assert again == {"k": [1, 2]}
def test_mining_schedule_defaults_are_disabled(_isolated):
assert preferences.get_mining_schedule() == {
"mining_schedule_enabled": False,
"mining_schedule_weekday": 4,
"mining_budget_profile": "balanced",
}
def test_mining_schedule_invalid_stored_values_fail_closed(_isolated):
_isolated.write_text(
json.dumps(
{
"mining_schedule_enabled": "false",
"mining_schedule_weekday": True,
"mining_budget_profile": None,
}
),
encoding="utf-8",
)
assert preferences.get_mining_schedule() == {
"mining_schedule_enabled": False,
"mining_schedule_weekday": 4,
"mining_budget_profile": "balanced",
}
def test_mining_schedule_setter_saves_group_once(monkeypatch):
calls = []
monkeypatch.setattr(preferences, "save", lambda updates: calls.append(updates) or updates)
result = preferences.set_mining_schedule(True, 2, "strict")
assert result == {
"mining_schedule_enabled": True,
"mining_schedule_weekday": 2,
"mining_budget_profile": "strict",
}
assert calls == [result]
@pytest.mark.parametrize("weekday", [-1, 5, True])
def test_mining_schedule_setter_rejects_invalid_weekday(weekday):
with pytest.raises(ValueError, match="weekday"):
preferences.set_mining_schedule(True, weekday, "balanced")
def test_mining_schedule_setter_rejects_invalid_profile():
with pytest.raises(ValueError, match="profile"):
preferences.set_mining_schedule(True, 4, "exploratory")