rebuild(screener): add controlled formulas and rolling backtests
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from backend.features.screener.catalog import CatalogError, factor_catalog, validate_formula
|
||||
|
||||
PROMPT_VERSION = "screener:formula-compiler:v1"
|
||||
|
||||
|
||||
def compile_messages(description: str) -> list[dict[str, str]]:
|
||||
factors = factor_catalog()["factors"]
|
||||
schema = {
|
||||
"universe": {"exclude_st": True, "listed_days_min": 120},
|
||||
"filters": [{"field": "amount_billion", "op": ">=", "value": 1}],
|
||||
"score": [{"field": "return_20d", "weight": 1.0, "direction": "desc"}],
|
||||
"limit": 30,
|
||||
"min_score": 0.5,
|
||||
}
|
||||
return [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"你是受控选股公式编译器。只输出一个JSON对象,不得输出Markdown、解释、股票或代码。"
|
||||
"只能使用给定因子;filters运算符仅限 >、>=、<、<=、==、!=、between、in;"
|
||||
"score权重必须为0至1小数且总和等于1;direction仅限asc或desc;"
|
||||
"limit为1至50,min_score为0至1。无法完全表达时选择最接近的已知因子,不得创造字段。"
|
||||
f"\nJSON结构:{json.dumps(schema, ensure_ascii=False, separators=(',', ':'))}"
|
||||
f"\n可用因子:{json.dumps(factors, ensure_ascii=False, separators=(',', ':'))}"
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": description},
|
||||
]
|
||||
|
||||
|
||||
def description_key(description: str) -> str:
|
||||
digest = hashlib.sha256(description.encode("utf-8")).hexdigest()[:16]
|
||||
return f"formula:{digest}"
|
||||
|
||||
|
||||
def parse_compiled_formula(content: str) -> dict[str, Any]:
|
||||
text = content.strip()
|
||||
if text.startswith("```"):
|
||||
lines = text.splitlines()
|
||||
if len(lines) < 3 or lines[-1].strip() != "```":
|
||||
raise CatalogError("模型返回的公式格式无效")
|
||||
text = "\n".join(lines[1:-1]).strip()
|
||||
try:
|
||||
raw = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise CatalogError("模型未返回有效JSON公式") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError("模型返回的公式必须是对象")
|
||||
return normalize_custom_formula(raw)
|
||||
|
||||
|
||||
def normalize_custom_formula(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
universe = raw.get("universe")
|
||||
filters = raw.get("filters")
|
||||
scores = raw.get("score")
|
||||
if not isinstance(universe, dict) or not isinstance(filters, list) or not isinstance(
|
||||
scores, list
|
||||
):
|
||||
raise CatalogError("模型返回的公式结构不完整")
|
||||
exclude_st = universe.get("exclude_st", True)
|
||||
listed_days = universe.get("listed_days_min", 120)
|
||||
if not isinstance(exclude_st, bool) or not isinstance(listed_days, int):
|
||||
raise CatalogError("股票范围设置无效")
|
||||
normalized_scores = []
|
||||
for score in scores:
|
||||
if not isinstance(score, dict):
|
||||
raise CatalogError("评分因子结构无效")
|
||||
normalized_scores.append(
|
||||
{
|
||||
"field": score.get("field"),
|
||||
"weight": score.get("weight"),
|
||||
"direction": score.get("direction", "desc"),
|
||||
}
|
||||
)
|
||||
if any(not isinstance(item, dict) for item in filters):
|
||||
raise CatalogError("筛选条件结构无效")
|
||||
numeric_weights = [item["weight"] for item in normalized_scores]
|
||||
if all(
|
||||
isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
for value in numeric_weights
|
||||
):
|
||||
total = sum(float(value) for value in numeric_weights)
|
||||
if abs(total - 100) <= 0.0001:
|
||||
for item in normalized_scores:
|
||||
item["weight"] = float(item["weight"]) / 100
|
||||
minimum = raw.get("min_score")
|
||||
if isinstance(minimum, (int, float)) and not isinstance(minimum, bool) and 1 < minimum <= 100:
|
||||
minimum = float(minimum) / 100
|
||||
formula = {
|
||||
"universe": {
|
||||
"exclude_st": exclude_st,
|
||||
"listed_days_min": listed_days,
|
||||
},
|
||||
"filters": [
|
||||
{
|
||||
"field": item.get("field"),
|
||||
"op": item.get("op"),
|
||||
"value": item.get("value"),
|
||||
}
|
||||
for item in filters
|
||||
],
|
||||
"score": normalized_scores,
|
||||
"limit": raw.get("limit"),
|
||||
"min_score": minimum,
|
||||
}
|
||||
validate_formula(formula)
|
||||
return formula
|
||||
Reference in New Issue
Block a user