claude强化功能

This commit is contained in:
2026-06-14 11:54:45 +08:00
parent cc8dff4e57
commit e524a3589a
43 changed files with 13421 additions and 73 deletions

390
backend/smart_selector.py Normal file
View File

@@ -0,0 +1,390 @@
"""智能选股引擎 — 可视化选股器、策略保存、回测验证。
功能:
1. 多条件组合选股
2. 选股策略保存/加载
3. 选股结果历史回测
4. 选股结果对比(新入选/退出)
5. 条件预警
"""
import datetime as dt
import json
from typing import Dict, List, Any, Optional
from sqlalchemy import select, and_, or_, func
import numpy as np
from db import get_session
from models import StockMetric, DailyQuote, Security
class Condition:
"""选股条件"""
def __init__(self, field: str, operator: str, value: Any):
self.field = field
self.operator = operator
self.value = value
def to_dict(self):
return {
"field": self.field,
"operator": self.operator,
"value": self.value
}
@classmethod
def from_dict(cls, data: Dict):
return cls(data["field"], data["operator"], data["value"])
def to_sql(self, model):
"""转换为 SQLAlchemy 查询条件"""
field = getattr(model, self.field, None)
if field is None:
return None
op = self.operator
val = self.value
if op == "==":
return field == val
elif op == ">":
return field > val
elif op == ">=":
return field >= val
elif op == "<":
return field < val
elif op == "<=":
return field <= val
elif op == "between":
return and_(field >= val[0], field <= val[1])
elif op == "in":
return field.in_(val)
else:
return None
class Strategy:
"""选股策略"""
def __init__(self, name: str, description: str = ""):
self.name = name
self.description = description
self.conditions: List[Condition] = []
self.logic = "and" # and / or
def add_condition(self, field: str, operator: str, value: Any):
"""添加条件"""
self.conditions.append(Condition(field, operator, value))
def to_dict(self):
return {
"name": self.name,
"description": self.description,
"logic": self.logic,
"conditions": [c.to_dict() for c in self.conditions]
}
@classmethod
def from_dict(cls, data: Dict):
strategy = cls(data["name"], data.get("description", ""))
strategy.logic = data.get("logic", "and")
for cond in data.get("conditions", []):
strategy.conditions.append(Condition.from_dict(cond))
return strategy
def to_json(self) -> str:
"""序列化为JSON"""
return json.dumps(self.to_dict(), ensure_ascii=False)
@classmethod
def from_json(cls, json_str: str):
"""从JSON反序列化"""
return cls.from_dict(json.loads(json_str))
def run_selector(strategy: Strategy, date: Optional[dt.date] = None) -> Dict[str, Any]:
"""执行选股
Args:
strategy: 选股策略
date: 选股日期None表示最新
Returns:
选股结果
"""
with get_session() as s:
# 确定日期
if date is None:
date = s.execute(select(func.max(StockMetric.date))).scalar()
if not date:
return {"ok": False, "msg": "暂无数据"}
# 构建查询
query = select(StockMetric).where(StockMetric.date == date)
# 应用条件
sql_conditions = []
for cond in strategy.conditions:
sql_cond = cond.to_sql(StockMetric)
if sql_cond is not None:
sql_conditions.append(sql_cond)
if sql_conditions:
if strategy.logic == "and":
query = query.where(and_(*sql_conditions))
else: # or
query = query.where(or_(*sql_conditions))
# 执行查询
rows = s.execute(query).scalars().all()
# 格式化结果
results = [{
"code": r.code,
"name": r.name,
"close": round(r.close, 2),
"pct": round(r.pct, 2),
"ret5": round(r.ret5, 2),
"ret20": round(r.ret20, 2),
"vol_ratio": round(r.vol_ratio, 2),
"rsi14": round(r.rsi14, 2),
"pos60": round(r.pos60 * 100, 1),
"amount": round(r.amount, 2),
"ma_bull": r.ma_bull,
"macd_gold": r.macd_gold
} for r in rows]
return {
"ok": True,
"date": date.isoformat(),
"strategy": strategy.name,
"count": len(results),
"results": results
}
def backtest_selector(strategy: Strategy, days: int = 60) -> Dict[str, Any]:
"""选股策略回测
Args:
strategy: 选股策略
days: 回测天数
Returns:
回测结果
"""
with get_session() as s:
# 获取最近N个交易日
latest_date = s.execute(select(func.max(StockMetric.date))).scalar()
if not latest_date:
return {"ok": False, "msg": "暂无数据"}
start_date = latest_date - dt.timedelta(days=days)
# 获取这段时间内的所有交易日
dates = s.execute(
select(StockMetric.date)
.where(StockMetric.date >= start_date)
.group_by(StockMetric.date)
.order_by(StockMetric.date)
).scalars().all()
if len(dates) < 5:
return {"ok": False, "msg": "数据不足"}
# 逐日选股并统计后续N日收益
daily_results = []
for i, date in enumerate(dates[:-5]): # 至少保留5日用于计算收益
# 执行选股
result = run_selector(strategy, date)
if not result["ok"] or not result["results"]:
continue
selected_codes = [r["code"] for r in result["results"]]
# 查询5日后的收益
future_date = dates[min(i + 5, len(dates) - 1)]
with get_session() as s:
# 获取选中股票5日后的表现
future_rows = s.execute(
select(DailyQuote.code, DailyQuote.close)
.where(
and_(
DailyQuote.code.in_(selected_codes),
DailyQuote.date == future_date
)
)
).all()
current_rows = s.execute(
select(DailyQuote.code, DailyQuote.close)
.where(
and_(
DailyQuote.code.in_(selected_codes),
DailyQuote.date == date
)
)
).all()
# 计算收益
current_prices = {r.code: float(r.close) for r in current_rows}
future_prices = {r.code: float(r.close) for r in future_rows}
returns = []
for code in selected_codes:
if code in current_prices and code in future_prices:
ret = (future_prices[code] / current_prices[code] - 1) * 100
returns.append(ret)
if returns:
daily_results.append({
"date": date.isoformat(),
"count": len(selected_codes),
"avg_return": round(np.mean(returns), 2),
"median_return": round(np.median(returns), 2),
"win_rate": round(sum(1 for r in returns if r > 0) / len(returns) * 100, 1),
"max_return": round(max(returns), 2),
"min_return": round(min(returns), 2)
})
if not daily_results:
return {"ok": False, "msg": "回测数据不足"}
# 汇总统计
avg_returns = [r["avg_return"] for r in daily_results]
win_rates = [r["win_rate"] for r in daily_results]
summary = {
"total_days": len(daily_results),
"avg_return": round(np.mean(avg_returns), 2),
"avg_win_rate": round(np.mean(win_rates), 1),
"best_day": max(daily_results, key=lambda x: x["avg_return"]),
"worst_day": min(daily_results, key=lambda x: x["avg_return"])
}
return {
"ok": True,
"strategy": strategy.name,
"days": days,
"summary": summary,
"daily": daily_results
}
def compare_results(date1: dt.date, date2: dt.date, strategy: Strategy) -> Dict[str, Any]:
"""对比两个日期的选股结果
Args:
date1: 日期1通常是昨日
date2: 日期2通常是今日
strategy: 选股策略
Returns:
对比结果
"""
result1 = run_selector(strategy, date1)
result2 = run_selector(strategy, date2)
if not result1["ok"] or not result2["ok"]:
return {"ok": False, "msg": "数据不足"}
codes1 = set(r["code"] for r in result1["results"])
codes2 = set(r["code"] for r in result2["results"])
# 新入选
new_in = codes2 - codes1
# 退出
dropped = codes1 - codes2
# 持续入选
continued = codes1 & codes2
# 获取详细信息
new_in_stocks = [r for r in result2["results"] if r["code"] in new_in]
dropped_stocks = [r for r in result1["results"] if r["code"] in dropped]
continued_stocks = [r for r in result2["results"] if r["code"] in continued]
return {
"ok": True,
"date1": date1.isoformat(),
"date2": date2.isoformat(),
"count1": len(codes1),
"count2": len(codes2),
"new_in": {
"count": len(new_in),
"stocks": new_in_stocks
},
"dropped": {
"count": len(dropped),
"stocks": dropped_stocks
},
"continued": {
"count": len(continued),
"stocks": continued_stocks
}
}
# 预设策略
PRESET_STRATEGIES = {
"momentum": Strategy("动量突破", "短期强势+放量"),
"value": Strategy("价值洼地", "超跌低位+基本面支撑"),
"growth": Strategy("成长加速", "持续上涨+量价齐升"),
"reversal": Strategy("反转抄底", "超跌企稳+技术反转")
}
# 动量突破
PRESET_STRATEGIES["momentum"].add_condition("ret5", ">", 10)
PRESET_STRATEGIES["momentum"].add_condition("vol_ratio", ">", 2)
PRESET_STRATEGIES["momentum"].add_condition("rsi14", "<", 80)
# 价值洼地
PRESET_STRATEGIES["value"].add_condition("pos60", "<", 0.3)
PRESET_STRATEGIES["value"].add_condition("pct", ">", 0)
PRESET_STRATEGIES["value"].add_condition("amount", ">", 5)
# 成长加速
PRESET_STRATEGIES["growth"].add_condition("ret20", ">", 15)
PRESET_STRATEGIES["growth"].add_condition("ma_bull", "==", True)
PRESET_STRATEGIES["growth"].add_condition("up_streak", ">=", 2)
# 反转抄底
PRESET_STRATEGIES["reversal"].add_condition("ret20", "<", -15)
PRESET_STRATEGIES["reversal"].add_condition("rsi14", "<", 30)
PRESET_STRATEGIES["reversal"].add_condition("pct", ">", 2)
def get_preset_strategies() -> List[Dict[str, Any]]:
"""获取预设策略列表"""
return [
{
"id": key,
"name": strategy.name,
"description": strategy.description,
"conditions_count": len(strategy.conditions)
}
for key, strategy in PRESET_STRATEGIES.items()
]
def get_available_fields() -> List[Dict[str, Any]]:
"""获取可用的选股字段"""
return [
{"field": "close", "name": "现价", "type": "number", "unit": ""},
{"field": "pct", "name": "涨跌幅", "type": "number", "unit": "%"},
{"field": "ret5", "name": "5日涨幅", "type": "number", "unit": "%"},
{"field": "ret20", "name": "20日涨幅", "type": "number", "unit": "%"},
{"field": "ret60", "name": "60日涨幅", "type": "number", "unit": "%"},
{"field": "ma5", "name": "MA5", "type": "number", "unit": ""},
{"field": "ma10", "name": "MA10", "type": "number", "unit": ""},
{"field": "ma20", "name": "MA20", "type": "number", "unit": ""},
{"field": "ma60", "name": "MA60", "type": "number", "unit": ""},
{"field": "vol_ratio", "name": "量比", "type": "number", "unit": ""},
{"field": "rsi14", "name": "RSI", "type": "number", "unit": ""},
{"field": "pos60", "name": "60日分位", "type": "number", "unit": ""},
{"field": "amount", "name": "成交额", "type": "number", "unit": "亿"},
{"field": "up_streak", "name": "连涨天数", "type": "number", "unit": ""},
{"field": "ma_bull", "name": "均线多头", "type": "boolean", "unit": ""},
{"field": "macd_gold", "name": "MACD金叉", "type": "boolean", "unit": ""}
]