mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
点位监控(双击图表): - 日K/单日分时/多日分时价格主图支持双击, 自动预填目标价并按最新价判断上穿/下穿 - 成交量区与指标子图不触发; 自动生成通知文案, 用户手改后不再覆盖 - 已启用点位监控以横虚线显示 (上穿红/下穿绿), 并纳入三图 Y 轴范围 - 复用现有 PriceAlertDialog, 未改变后端 close>=/<= 规则语义与冷却机制 分钟K数据: - 个股补齐强制回溯请求天数 (force_full_days), 修复切换日期范围只显示3天 - 单股并发补齐复用仓库写锁, 修复 Windows Parquet 临时文件替换失败 - DuckDB 读路径改用短生命周期 cursor, 释放对 Parquet 文件的占用 设置页: - 数据任务超时从 DataSources 抽出为独立"超时设置"页签 - 设置侧栏垂直居中 验证: pytest 38 passed, pnpm build ok, git diff --check ok, 浏览器端到端 GUI 验证通过
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
"""指数资产路由 — repository 层测试。"""
|
|
import os
|
|
|
|
import polars as pl
|
|
import pytest
|
|
|
|
from app.tickflow.repository import DataStore, KlineRepository
|
|
|
|
|
|
@pytest.fixture()
|
|
def repo(tmp_path):
|
|
return KlineRepository(DataStore(tmp_path))
|
|
|
|
|
|
def _write_index_instruments(repo, rows):
|
|
pl.DataFrame(rows).write_parquet(
|
|
repo.store.data_dir / "instruments_index" / "part.parquet"
|
|
)
|
|
repo._refresh_index_instruments()
|
|
|
|
|
|
def test_name_map_includes_index(repo):
|
|
_write_index_instruments(repo, {
|
|
"symbol": ["000001.SH"], "name": ["上证指数"],
|
|
"code": ["000001"], "asset_type": ["index"],
|
|
})
|
|
names = repo.get_name_map(["000001.SH", "600000.SH"])
|
|
assert names.get("000001.SH") == "上证指数"
|
|
assert "600000.SH" not in names # 未收录不造名
|
|
|
|
|
|
def test_name_map_stock_beats_index(repo):
|
|
"""同名 symbol 同时出现在股票/指数维表时, 股票名称优先。"""
|
|
_write_index_instruments(repo, {
|
|
"symbol": ["600000.SH"], "name": ["某指数"],
|
|
"code": ["600000"], "asset_type": ["index"],
|
|
})
|
|
pl.DataFrame({
|
|
"symbol": ["600000.SH"], "name": ["浦发银行"], "code": ["600000"],
|
|
"exchange": ["SH"], "region": ["CN"], "type": ["stock"],
|
|
"listing_date": [None], "total_shares": [None], "float_shares": [None],
|
|
"tick_size": [None], "limit_up": [None], "limit_down": [None],
|
|
"as_of": ["2026-07-25"],
|
|
}).write_parquet(repo.store.data_dir / "instruments" / "instruments.parquet")
|
|
repo._refresh_instruments()
|
|
assert repo.get_name_map(["600000.SH"]).get("600000.SH") == "浦发银行"
|
|
|
|
|
|
import datetime as _dt
|
|
|
|
|
|
def test_execute_one_releases_parquet_file(repo):
|
|
minute_dir = repo.store.data_dir / "kline_minute" / "date=2026-07-23"
|
|
minute_dir.mkdir(parents=True, exist_ok=True)
|
|
part = minute_dir / "part.parquet"
|
|
replacement = minute_dir / "part.parquet.tmp"
|
|
minute = pl.DataFrame({
|
|
"symbol": ["600000.SH"],
|
|
"datetime": [_dt.datetime(2026, 7, 23, 9, 30)],
|
|
"close": [10.0],
|
|
})
|
|
minute.write_parquet(part)
|
|
repo.rebuild_views()
|
|
|
|
assert repo.execute_one("SELECT max(datetime) FROM kline_minute")[0] == _dt.datetime(2026, 7, 23, 9, 30)
|
|
|
|
minute.write_parquet(replacement)
|
|
os.replace(replacement, part)
|
|
|
|
|
|
def _write_index_enriched(repo, dates_rows):
|
|
for ds, rows in dates_rows.items():
|
|
d = repo.store.data_dir / "kline_index_enriched" / f"date={ds}"
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
pl.DataFrame(rows).write_parquet(d / "part.parquet")
|
|
|
|
|
|
def _index_rows(ds, close=3000.0):
|
|
return [{
|
|
"symbol": "000001.SH", "date": _dt.date.fromisoformat(ds),
|
|
"open": close - 10, "high": close + 20, "low": close - 20, "close": close,
|
|
"volume": 1_000_000, "amount": 1e9,
|
|
}]
|
|
|
|
|
|
def test_get_enriched_latest_asset_index(repo):
|
|
_write_index_enriched(repo, {
|
|
"2026-07-23": _index_rows("2026-07-23", 2990.0),
|
|
"2026-07-24": _index_rows("2026-07-24", 3000.0),
|
|
})
|
|
df, dt = repo.get_enriched_latest_asset("index")
|
|
assert str(dt) == "2026-07-24"
|
|
assert df["symbol"].to_list() == ["000001.SH"]
|
|
assert "ma5" in df.columns or "rsi_14" in df.columns # 重算产出指标列
|
|
|
|
|
|
def test_get_enriched_latest_asset_index_cold_no_refresh(repo):
|
|
df, dt = repo.get_enriched_latest_asset("index", refresh=False)
|
|
assert df.is_empty() and dt is None
|
|
|
|
|
|
def test_flush_live_enriched_asset_index_updates_cache(repo):
|
|
df = pl.DataFrame([{
|
|
"symbol": "000001.SH", "date": _dt.date(2026, 7, 25),
|
|
"open": 3000.0, "high": 3010.0, "low": 2990.0, "close": 3005.0,
|
|
"volume": 1_000_000, "amount": 1e9, "ma5": 3001.0, "rsi_14": 55.0,
|
|
}])
|
|
repo.flush_live_enriched_asset("index", df)
|
|
cached, dt = repo.get_enriched_latest_asset("index", refresh=False)
|
|
assert str(dt) == "2026-07-25"
|
|
assert cached["close"].to_list() == [3005.0]
|
|
assert (repo.store.data_dir / "kline_index_enriched" / "date=2026-07-25" / "part.parquet").exists()
|
|
|
|
|
|
def _merge_row(symbol, close):
|
|
return {
|
|
"symbol": symbol, "date": _dt.date(2026, 7, 25),
|
|
"open": close - 5, "high": close + 5, "low": close - 6, "close": close,
|
|
"volume": 1_000, "amount": 1e6,
|
|
}
|
|
|
|
|
|
def test_merge_live_enriched_asset_index_merges_cache(repo):
|
|
"""merge 路径: 两次合并缓存取并集 (不 NameError, 不丢已有缓存)。"""
|
|
repo.merge_live_enriched_asset("index", pl.DataFrame([_merge_row("000001.SH", 3000.0)]))
|
|
repo.merge_live_enriched_asset("index", pl.DataFrame([_merge_row("000300.SH", 4000.0)]))
|
|
cached, dt = repo.get_enriched_latest_asset("index", refresh=False)
|
|
assert str(dt) == "2026-07-25"
|
|
assert set(cached["symbol"].to_list()) == {"000001.SH", "000300.SH"}
|
|
|