rebuild(screener): add controlled formulas and rolling backtests
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user