feat: complete strategy and market data improvements
This commit is contained in:
@@ -12,18 +12,20 @@ from screener import (
|
||||
FACTOR_GROUPS,
|
||||
ScreenerEngine,
|
||||
_broken_reversal_metrics,
|
||||
_earnings_event_rows,
|
||||
_popularity_factor_rows,
|
||||
_risk_flags,
|
||||
_rsi,
|
||||
_quarter_periods,
|
||||
)
|
||||
from server import automatic_screener_jobs
|
||||
from server import DashboardService, automatic_screener_jobs
|
||||
|
||||
|
||||
class CuratedScreenerTests(unittest.TestCase):
|
||||
def test_curated_library_contains_original_and_advanced_strategies(self):
|
||||
self.assertEqual(13, len(ADVANCED_CURATED_STRATEGIES))
|
||||
self.assertEqual(23, len(CURATED_STRATEGIES))
|
||||
self.assertEqual(23, len({item["name"] for item in CURATED_STRATEGIES}))
|
||||
self.assertEqual(19, len(ADVANCED_CURATED_STRATEGIES))
|
||||
self.assertEqual(29, len(CURATED_STRATEGIES))
|
||||
self.assertEqual(29, len({item["name"] for item in CURATED_STRATEGIES}))
|
||||
self.assertTrue(
|
||||
{"行业动量轮动", "主力资金行业流入"}.issubset(
|
||||
{item["name"] for item in CURATED_STRATEGIES}
|
||||
@@ -32,6 +34,16 @@ class CuratedScreenerTests(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
all(item["formula"]["meta"]["library"] == "curated" for item in CURATED_STRATEGIES)
|
||||
)
|
||||
self.assertTrue(
|
||||
{
|
||||
"景气-趋势-拥挤三维行业打分",
|
||||
"大小盘/成长价值风格切换(元策略)",
|
||||
"业绩超预期漂移(SUE/PEAD)",
|
||||
"多因子综合打分(IC动态加权)",
|
||||
"热度突增潜伏(另类数据)",
|
||||
"机构榜溢价",
|
||||
}.issubset({item["name"] for item in CURATED_STRATEGIES})
|
||||
)
|
||||
|
||||
def test_every_curated_strategy_explains_environment_and_failure_risk(self):
|
||||
for strategy in CURATED_STRATEGIES:
|
||||
@@ -85,6 +97,51 @@ class CuratedScreenerTests(unittest.TestCase):
|
||||
}
|
||||
self.assertTrue(fields.issubset(FACTOR_FIELDS), strategy["name"])
|
||||
|
||||
def test_server_gate_blocks_specialized_strategies_until_sources_are_ready(self):
|
||||
factor_dates = [f"2026{index + 1:04d}" for index in range(260)]
|
||||
health = {
|
||||
"market": True,
|
||||
"auction": True,
|
||||
"benchmark": True,
|
||||
"valuation": True,
|
||||
"fundamental": True,
|
||||
"dividend_history": True,
|
||||
"moneyflow_history": True,
|
||||
"earnings_events": False,
|
||||
"popularity": False,
|
||||
"institutions": False,
|
||||
}
|
||||
expected = {
|
||||
"业绩超预期漂移(SUE/PEAD)": "业绩预告与快报",
|
||||
"热度突增潜伏(另类数据)": "当日人气榜",
|
||||
"机构榜溢价": "龙虎榜机构席位",
|
||||
}
|
||||
by_name = {strategy["name"]: strategy for strategy in CURATED_STRATEGIES}
|
||||
|
||||
for name, missing_label in expected.items():
|
||||
self.assertEqual(
|
||||
[missing_label],
|
||||
DashboardService._strategy_missing_data(
|
||||
by_name[name], factor_dates, health
|
||||
),
|
||||
name,
|
||||
)
|
||||
|
||||
ready_health = {
|
||||
**health,
|
||||
"earnings_events": True,
|
||||
"popularity": True,
|
||||
"institutions": True,
|
||||
}
|
||||
for name in expected:
|
||||
self.assertEqual(
|
||||
[],
|
||||
DashboardService._strategy_missing_data(
|
||||
by_name[name], factor_dates, ready_health
|
||||
),
|
||||
name,
|
||||
)
|
||||
|
||||
def test_factor_groups_cover_every_quant_factor(self):
|
||||
grouped = [field for fields in FACTOR_GROUPS.values() for field in fields]
|
||||
self.assertEqual(set(FACTOR_FIELDS), set(grouped))
|
||||
@@ -109,6 +166,9 @@ class CuratedScreenerTests(unittest.TestCase):
|
||||
self.assertTrue({"pe_ttm", "pb", "ps_ttm", "dv_ttm"}.issubset(indicator_columns))
|
||||
self.assertIn("fundamental_indicators", tables)
|
||||
self.assertIn("benchmark_bars", tables)
|
||||
self.assertIn("earnings_events", tables)
|
||||
self.assertIn("popularity_factors", tables)
|
||||
self.assertIn("lhb_institution_daily", tables)
|
||||
|
||||
def test_advanced_strategies_declare_history_and_backtest_contracts(self):
|
||||
for strategy in ADVANCED_CURATED_STRATEGIES:
|
||||
@@ -327,6 +387,129 @@ class CuratedScreenerTests(unittest.TestCase):
|
||||
self.assertLess(laggard["net_flow_5d_million"], 0)
|
||||
self.assertEqual(leader["sector_flow_rank"], 1)
|
||||
|
||||
def test_stage_three_event_and_composite_factors_are_date_scoped(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
database = ReviewDatabase(Path(root) / "review.db")
|
||||
stocks = [
|
||||
("600001.SH", "成长样本", "电子", 0.08),
|
||||
("600002.SH", "价值样本", "银行", 0.02),
|
||||
]
|
||||
database.upsert_stock_master([
|
||||
{
|
||||
"ts_code": code, "name": name, "industry": industry,
|
||||
"market": "主板", "list_date": "20000101",
|
||||
}
|
||||
for code, name, industry, _ in stocks
|
||||
])
|
||||
dates = []
|
||||
cursor = datetime(2026, 3, 1)
|
||||
while len(dates) < 80:
|
||||
if cursor.weekday() < 5:
|
||||
dates.append(cursor.strftime("%Y%m%d"))
|
||||
cursor += timedelta(days=1)
|
||||
database.upsert_daily_bars([
|
||||
{
|
||||
"trade_date": trade_date, "ts_code": code,
|
||||
"open": 10 + index * slope - 0.02,
|
||||
"high": 10 + index * slope + 0.08,
|
||||
"low": 10 + index * slope - 0.08,
|
||||
"close": 10 + index * slope,
|
||||
"pct_chg": slope, "vol": 1000 + index, "amount": 300000,
|
||||
}
|
||||
for index, trade_date in enumerate(dates)
|
||||
for code, _, _, slope in stocks
|
||||
])
|
||||
database.upsert_daily_indicators([
|
||||
{
|
||||
"trade_date": dates[-1], "ts_code": "600001.SH",
|
||||
"turnover_rate": 3, "volume_ratio": 1.4, "total_mv": 900000,
|
||||
"circ_mv": 700000, "pe_ttm": 25, "pb": 3, "ps_ttm": 4,
|
||||
},
|
||||
{
|
||||
"trade_date": dates[-1], "ts_code": "600002.SH",
|
||||
"turnover_rate": 1, "volume_ratio": 0.9, "total_mv": 5000000,
|
||||
"circ_mv": 4000000, "pe_ttm": 8, "pb": 0.8, "ps_ttm": 1,
|
||||
},
|
||||
])
|
||||
database.upsert_fundamental_indicators([
|
||||
{
|
||||
"end_date": "20260331", "ann_date": dates[-10],
|
||||
"ts_code": "600001.SH", "roe": 16, "roic": 13,
|
||||
"grossprofit_margin": 35, "netprofit_yoy": 45, "or_yoy": 30,
|
||||
},
|
||||
{
|
||||
"end_date": "20260331", "ann_date": dates[-10],
|
||||
"ts_code": "600002.SH", "roe": 9, "roic": 7,
|
||||
"grossprofit_margin": 18, "netprofit_yoy": 5, "or_yoy": 3,
|
||||
},
|
||||
])
|
||||
database.upsert_earnings_events([{
|
||||
"end_date": "20260331", "ann_date": dates[-3],
|
||||
"ts_code": "600001.SH", "forecast_profit": 100,
|
||||
"actual_profit": 125, "surprise_pct": 25,
|
||||
"revenue_yoy": 30, "netprofit_yoy": 45,
|
||||
"source": "forecast+express",
|
||||
}])
|
||||
database.upsert_popularity_factors([{
|
||||
"trade_date": dates[-1], "ts_code": "600001.SH",
|
||||
"ths_rank": 5, "dc_rank": 8, "combined_score": 75,
|
||||
"rank_change": 12, "dual_source": True,
|
||||
}])
|
||||
database.upsert_lhb_institutions([{
|
||||
"trade_date": dates[-1], "ts_code": "600001.SH",
|
||||
"exalter": "机构专用", "buy": 80_000_000,
|
||||
"sell": 20_000_000, "net_buy": 60_000_000,
|
||||
}])
|
||||
|
||||
factors, actual_date = ScreenerEngine(database).build_factors(
|
||||
dates[-1], history_days=80
|
||||
)
|
||||
by_code = {item["ts_code"]: item for item in factors}
|
||||
factor = by_code["600001.SH"]
|
||||
self.assertEqual(actual_date, dates[-1])
|
||||
self.assertEqual(factor["earnings_days_since_announce"], 2)
|
||||
self.assertEqual(factor["earnings_surprise_pct"], 25)
|
||||
self.assertEqual(factor["popularity_score"], 75)
|
||||
self.assertEqual(factor["popularity_dual_source"], 1)
|
||||
self.assertEqual(factor["institution_net_buy_million"], 60)
|
||||
self.assertEqual(factor["institution_seat_count"], 1)
|
||||
self.assertIsNotNone(factor["sector_composite_score"])
|
||||
self.assertIsNotNone(factor["style_fit_score"])
|
||||
self.assertIsNotNone(factor["multi_factor_composite"])
|
||||
health = database.factor_health_summary(dates[-1])
|
||||
self.assertTrue(health["earnings_events"])
|
||||
self.assertTrue(health["popularity"])
|
||||
self.assertTrue(health["institutions"])
|
||||
|
||||
def test_stage_three_sources_normalize_units_and_rank_changes(self):
|
||||
earnings = _earnings_event_rows(
|
||||
[{
|
||||
"ts_code": "600001.SH", "ann_date": "20260401",
|
||||
"end_date": "20260331", "net_profit_min": 10000,
|
||||
"net_profit_max": 12000,
|
||||
}],
|
||||
[{
|
||||
"ts_code": "600001.SH", "ann_date": "20260420",
|
||||
"end_date": "20260331", "n_income": 132_000_000,
|
||||
"yoy_net_profit": 30, "yoy_sales": 18,
|
||||
}],
|
||||
"20260420",
|
||||
)
|
||||
self.assertEqual(len(earnings), 1)
|
||||
self.assertEqual(round(earnings[0]["actual_profit"]), 13200)
|
||||
self.assertEqual(round(earnings[0]["surprise_pct"]), 20)
|
||||
|
||||
popularity = _popularity_factor_rows(
|
||||
"20260420",
|
||||
[{"data_type": "热股", "ts_code": "600001.SH", "rank": 5}],
|
||||
[{"data_type": "A股市场", "ts_code": "600001.SH", "rank": 8}],
|
||||
[{"data_type": "热股", "ts_code": "600001.SH", "rank": 20}],
|
||||
[{"data_type": "A股市场", "ts_code": "600001.SH", "rank": 30}],
|
||||
)
|
||||
self.assertEqual(len(popularity), 1)
|
||||
self.assertEqual(popularity[0]["rank_change"], 15)
|
||||
self.assertTrue(popularity[0]["dual_source"])
|
||||
|
||||
def test_screen_reports_signal_health(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
database = ReviewDatabase(Path(root) / "review.db")
|
||||
|
||||
Reference in New Issue
Block a user