feat(signal): M8.1 交易信号引擎(规则 + signal_event 落库 + /api/signals)
- SignalRules(买入 rank 阈值/趋势 MA/动量 + 卖出区间/破位警示)+ SignalEvent (BUY/WATCH/SELL,score/price/trigger_reason 可解释)+ SignalResult/Meta - quant/signal.generate_signals:与选股同一评分引擎取全市场 rank,按规则分类输出 - signal_snapshot/signal_event 表(migration d8e0b2f3c4d5,MySQL 已应用)+ Repo - SignalService + POST /api/signals(同步+落库)、GET 详情/列表 - tests/test_signals.py(引擎分类/排序/破位不 BUY、service、API 提交读回);全量 pytest 通过
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""交易信号领域实体(M8.1,v2 §15 Signal Engine)。
|
||||
|
||||
Signal 输入 = Selection 排序(score)+ 价格/技术条件 + 规则;输出事件可解释:
|
||||
BUY / WATCH / SELL(破位警示),每条带 trigger_reason —— 回答
|
||||
「某日为什么对该股票给 BUY/SELL」(v2 §8)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SignalRules(BaseModel):
|
||||
"""规则(结构化,MVP):买入区间 + 趋势/动量条件 + 卖出/警示区间。"""
|
||||
|
||||
buy_rank_threshold: int = Field(default=20, ge=1, le=500, description="rank<=此值进入买入候选")
|
||||
buy_require_trend: bool = Field(default=True, description="买入需 close > MA(trend_ma)")
|
||||
buy_require_momentum: bool = Field(default=False, description="买入需 close > 20 日前 close")
|
||||
trend_ma: int = Field(default=60, ge=10, le=250)
|
||||
sell_rank_threshold: int = Field(default=50, ge=1, le=1000, description="rank>此值或破位 → SELL 警示")
|
||||
sell_on_trend_break: bool = Field(default=True, description="买入区间内 close < MA(trend_ma) → SELL")
|
||||
max_output_rank: int = Field(default=80, ge=1, le=2000, description="仅输出排名前 N 的信号")
|
||||
|
||||
|
||||
class SignalEvent(BaseModel):
|
||||
symbol: str
|
||||
signal_date: date
|
||||
signal_type: str = Field(pattern="^(BUY|WATCH|SELL)$")
|
||||
score: float | None = None
|
||||
price: float | None = None
|
||||
trigger_reason: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SignalStatistics(BaseModel):
|
||||
universe_size: int = 0
|
||||
buy: int = 0
|
||||
watch: int = 0
|
||||
sell: int = 0
|
||||
|
||||
|
||||
class SignalResult(BaseModel):
|
||||
as_of_date: date
|
||||
rules: SignalRules
|
||||
statistics: SignalStatistics
|
||||
events: list[SignalEvent] = Field(default_factory=list)
|
||||
config_snapshot: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class SignalMeta(BaseModel):
|
||||
id: str
|
||||
as_of: date
|
||||
buy: int = 0
|
||||
watch: int = 0
|
||||
sell: int = 0
|
||||
created_at: datetime | None = None
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Signal Repository Protocol(M8.1 落库)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from typing import Protocol
|
||||
|
||||
from app.domain.entities.signal import SignalMeta, SignalResult
|
||||
|
||||
|
||||
class SignalRepository(Protocol):
|
||||
def save(self, signal_id: str, result: SignalResult) -> None:
|
||||
"""snapshot 一行 + 事件逐行(同事务,调用方 commit)。"""
|
||||
|
||||
def get(self, signal_id: str) -> SignalResult | None: ...
|
||||
|
||||
def list_recent(
|
||||
self, as_of: date | None = None, limit: int = 20
|
||||
) -> list[SignalMeta]: ...
|
||||
Reference in New Issue
Block a user