rebuild(screener): add controlled formulas and rolling backtests

This commit is contained in:
leefer
2026-07-30 10:45:40 +08:00
parent 08f69b0641
commit b79b4ba280
21 changed files with 616 additions and 37 deletions
+70 -5
View File
@@ -9,12 +9,20 @@ from backend.data.contracts import SnapshotState
from backend.data.gateway import DataGateway
from backend.data.repository import MarketRepository
from backend.database.connection import Database
from backend.features.accounts.models import Principal
from backend.features.screener.backtest import attach_historical_estimate, rolling_backtest
from backend.features.screener.catalog import (
CatalogError,
factor_catalog,
strategy_catalog,
validate_formula,
)
from backend.features.screener.compiler import (
PROMPT_VERSION,
compile_messages,
description_key,
parse_compiled_formula,
)
from backend.features.screener.engine import execute_formula
from backend.features.screener.factors import build_factor_snapshot
from backend.features.screener.repository import (
@@ -23,6 +31,7 @@ from backend.features.screener.repository import (
decode_run,
decode_track,
)
from backend.llm.gateway import LLMGateway
SHANGHAI = ZoneInfo("Asia/Shanghai")
PHASE_REGIMES = {
@@ -47,11 +56,13 @@ class ScreenerService:
repository: ScreenerRepository,
market_repository: MarketRepository,
gateway: DataGateway,
llm: LLMGateway,
) -> None:
self._database = database
self._repository = repository
self._market_repository = market_repository
self._gateway = gateway
self._llm = llm
def catalog(self) -> dict[str, Any]:
factors = factor_catalog()
@@ -89,10 +100,16 @@ class ScreenerService:
decode_run(row)
for row in self._repository.latest_runs(connection, "curated", through)
]
custom_runs = [
decode_run(row)
for row in self._repository.latest_runs(connection, "custom", through, user_id)
]
custom_runs = []
for row in self._repository.latest_runs(connection, "custom", through, user_id):
decoded = decode_run(row)
if decoded is not None:
custom_runs.append(
attach_historical_estimate(
decoded,
self._repository.backtest(connection, int(row["id"])),
)
)
return {
"trade_date": through,
"message": "",
@@ -161,6 +178,28 @@ class ScreenerService:
self._repository.save_custom_strategy(connection, user_id, normalized, formula)
)
def compile_formula(self, principal: Principal, description: str) -> dict[str, Any]:
normalized = " ".join(description.split())
if not 5 <= len(normalized) <= 500:
raise ScreenerError("自然语言条件应为5至500个字符")
call = self._llm.prepare(
principal,
feature="screener_formula",
prompt_version=PROMPT_VERSION,
business_id=description_key(normalized),
input_chars=len(normalized),
)
content = "".join(
event.content
for event in self._llm.stream(call, compile_messages(normalized))
if event.type == "delta"
)
try:
formula = parse_compiled_formula(content)
except CatalogError as exc:
raise ScreenerError(f"公式转换失败:{exc}") from exc
return {"formula": formula, "prompt_version": PROMPT_VERSION}
def delete_custom(self, user_id: int, strategy_id: int) -> None:
with self._database.transaction() as connection:
if not self._repository.delete_custom_strategy(connection, user_id, strategy_id):
@@ -184,7 +223,7 @@ class ScreenerService:
result = self._run(int(snapshot["id"]), str(snapshot["trade_date"]), strategy, user_id)
if result is None:
raise ScreenerError("自定义策略执行失败")
return result
return self._with_backtest(result, strategy["formula"], str(snapshot["trade_date"]))
def tracks(self, user_id: int) -> list[dict[str, Any]]:
with self._database.read() as connection:
@@ -274,6 +313,32 @@ class ScreenerService:
).fetchone()
)
def _with_backtest(
self,
run: dict[str, Any],
formula: dict[str, Any],
through: str,
) -> dict[str, Any]:
run_id = int(run["id"])
with self._database.read() as connection:
existing = self._repository.backtest(connection, run_id)
if existing is None:
with self._database.read() as connection:
rows = self._repository.factor_snapshots_through(connection, through, 63)
history = [
{
"trade_date": str(row["trade_date"]),
"coverage": json.loads(str(row["coverage_json"])),
"rows": self._repository.factor_rows(connection, int(row["id"])),
}
for row in reversed(rows)
]
calculated = rolling_backtest(history, formula)
with self._database.transaction() as connection:
self._repository.save_backtest(connection, run_id, calculated)
existing = self._repository.backtest(connection, run_id)
return attach_historical_estimate(run, existing)
def _market_snapshot(self, trade_date: str) -> dict[str, Any]:
with self._database.read() as connection:
row = self._market_repository.latest_summary(connection, trade_date)