Files
MoFin/venv/lib/python3.12/site-packages/alphasift/risk.py
T
知微 fa45d8aa5f fix: 小果地址统一node122(兼容LAN+EasyTier)
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
  Privoxy对node122:18003返回500,直连正常
2026-06-30 02:56:35 +08:00

282 lines
9.8 KiB
Python

# -*- coding: utf-8 -*-
"""Independent risk overlay for shortlisted picks."""
from __future__ import annotations
from alphasift.models import Pick
_DEFAULT_RISK_PROFILE = {
"chase_change_pct": 8.0,
"chase_points": 4.0,
"breakdown_change_pct": -7.0,
"breakdown_points": 3.5,
"abnormal_volume_ratio": 6.0,
"abnormal_volume_ratio_points": 3.0,
"high_turnover_rate": 15.0,
"high_turnover_points": 3.0,
"invalid_pe_points": 3.0,
"high_pb": 8.0,
"high_pb_points": 2.0,
"weak_signal_score": 45.0,
"weak_signal_points": 2.5,
"macd_bearish_points": 2.0,
"rsi_overbought_points": 1.5,
"low_llm_confidence": 0.35,
"low_llm_confidence_points": 1.5,
"llm_risk_points": 1.2,
"llm_risk_points_cap": 4.0,
"deep_risk_points": 1.5,
"deep_risk_points_cap": 4.5,
}
_DEFAULT_PORTFOLIO_BUCKETS = {
"金融": ("券商", "银行", "保险", "金融"),
"地产链": ("地产", "房地产", "建材", "家居", "物业"),
"新能源": ("新能源", "光伏", "锂电", "电池", "储能"),
"AI算力": ("AI算力", "算力", "数据中心", "服务器", "光模块"),
"消费": ("白酒", "食品", "家电", "零售", "消费"),
"医药": ("医药", "医疗", "创新药"),
"半导体": ("半导体", "芯片"),
}
def apply_risk_overlay(
picks: list[Pick],
*,
max_penalty: float = 12.0,
veto_high_risk: bool = False,
profile: dict[str, object] | None = None,
) -> tuple[list[Pick], list[str]]:
"""Attach risk flags and subtract a bounded penalty from final_score."""
if not picks:
return picks, []
max_penalty = max(float(max_penalty), 0.0)
degradation: list[str] = []
kept: list[Pick] = []
risk_profile = _risk_profile(profile)
for pick in picks:
points, flags = assess_pick_risk(pick, profile=risk_profile)
penalty = min(points, max_penalty)
pick.risk_penalty = round(penalty, 4)
pick.risk_score = round(0.0 if max_penalty == 0 else min(points / max_penalty * 100, 100), 4)
pick.risk_level = _risk_level(points, max_penalty)
pick.risk_flags = _unique([*pick.risk_flags, *flags])
pick.final_score = round(float(pick.final_score) - penalty, 4)
pick.excluded_by_risk = veto_high_risk and pick.risk_level == "high"
if pick.excluded_by_risk:
degradation.append(f"Risk veto excluded {pick.code}: {', '.join(pick.risk_flags)}")
continue
kept.append(pick)
kept.sort(key=lambda item: item.final_score, reverse=True)
for i, pick in enumerate(kept, start=1):
pick.rank = i
return kept, degradation
def apply_portfolio_overlay(
picks: list[Pick],
*,
max_same_sector: int = 1,
concentration_penalty: float = 4.0,
profile: dict[str, object] | None = None,
) -> tuple[list[Pick], list[str]]:
"""Penalize repeated LLM sectors so Top N is not only one crowded trade."""
if not picks:
return picks, []
portfolio_profile = profile or {}
max_same_sector = max(int(portfolio_profile.get("max_same_bucket", max_same_sector)), 1)
penalty_step = max(float(portfolio_profile.get("concentration_penalty", concentration_penalty)), 0.0)
if penalty_step == 0:
return picks, []
ordered = sorted(picks, key=lambda item: item.final_score, reverse=True)
if not any(_canonical_sector(_pick_sector(pick)) for pick in ordered):
return ordered, []
bucket_counts: dict[str, int] = {}
bucket_penalties: dict[str, list[tuple[str, str, float]]] = {}
for pick in ordered:
sector = _canonical_sector(_pick_sector(pick))
if not sector:
continue
bucket = _portfolio_bucket(sector, pick.llm_theme, buckets=portfolio_profile.get("buckets"))
bucket_counts[bucket] = bucket_counts.get(bucket, 0) + 1
excess = bucket_counts[bucket] - max_same_sector
if excess <= 0:
continue
penalty = min(penalty_step * excess, penalty_step * 3)
flag = f"portfolio_sector_concentration:{bucket}"
pick.portfolio_penalty = round(float(pick.portfolio_penalty) + penalty, 4)
pick.portfolio_flags = _unique([*pick.portfolio_flags, flag])
pick.risk_flags = _unique([*pick.risk_flags, flag])
pick.final_score = round(float(pick.final_score) - penalty, 4)
bucket_penalties.setdefault(bucket, []).append((pick.code, sector, penalty))
ordered.sort(key=lambda item: item.final_score, reverse=True)
for i, pick in enumerate(ordered, start=1):
pick.rank = i
notes = [
(
f"Portfolio concentration bucket={bucket}: "
f"penalized={len(items)}, "
f"codes={_format_penalty_codes(items)}"
)
for bucket, items in bucket_penalties.items()
]
return ordered, notes
def assess_pick_risk(
pick: Pick,
*,
profile: dict[str, float] | None = None,
) -> tuple[float, list[str]]:
"""Return risk points and human-readable flags for one pick."""
profile = _risk_profile(profile)
points = 0.0
flags: list[str] = []
if pick.change_pct >= profile["chase_change_pct"]:
points += profile["chase_points"]
flags.append("single_day_chase_risk")
elif pick.change_pct <= profile["breakdown_change_pct"]:
points += profile["breakdown_points"]
flags.append("single_day_breakdown_risk")
if pick.volume_ratio is not None and pick.volume_ratio >= profile["abnormal_volume_ratio"]:
points += profile["abnormal_volume_ratio_points"]
flags.append("abnormal_volume_ratio")
if pick.turnover_rate is not None and pick.turnover_rate >= profile["high_turnover_rate"]:
points += profile["high_turnover_points"]
flags.append("high_turnover")
if pick.pe_ratio is not None and pick.pe_ratio <= 0:
points += profile["invalid_pe_points"]
flags.append("negative_or_invalid_pe")
if pick.pb_ratio is not None and pick.pb_ratio >= profile["high_pb"]:
points += profile["high_pb_points"]
flags.append("high_pb")
if pick.signal_score is not None and pick.signal_score < profile["weak_signal_score"]:
points += profile["weak_signal_points"]
flags.append("weak_daily_signal")
if pick.macd_status == "bearish":
points += profile["macd_bearish_points"]
flags.append("macd_bearish")
if pick.rsi_status == "overbought":
points += profile["rsi_overbought_points"]
flags.append("rsi_overbought")
if pick.llm_confidence is not None and pick.llm_confidence < profile["low_llm_confidence"]:
points += profile["low_llm_confidence_points"]
flags.append("low_llm_confidence")
llm_risks = [risk for risk in pick.llm_risks if risk]
if llm_risks:
points += min(len(llm_risks) * profile["llm_risk_points"], profile["llm_risk_points_cap"])
flags.extend(llm_risks)
if pick.deep_analysis_risk_flags:
points += min(
len(pick.deep_analysis_risk_flags) * profile["deep_risk_points"],
profile["deep_risk_points_cap"],
)
flags.extend(pick.deep_analysis_risk_flags)
return points, _unique(flags)
def _risk_level(points: float, max_penalty: float) -> str:
if max_penalty <= 0:
return "low"
if points >= max_penalty * 0.66:
return "high"
if points >= max_penalty * 0.33:
return "medium"
return "low"
def _canonical_sector(label: str) -> str:
cleaned = str(label or "").strip()[:40]
if not cleaned:
return ""
aliases = {
"券商": ("券商", "证券"),
"银行": ("银行",),
"保险": ("保险",),
"地产": ("地产", "房地产"),
"医药": ("医药", "医疗", "创新药"),
"白酒": ("白酒", "酿酒"),
"半导体": ("半导体", "芯片"),
"AI算力": ("AI算力", "算力", "数据中心"),
"新能源": ("新能源", "光伏", "锂电", "电池"),
}
for canonical, needles in aliases.items():
if any(needle in cleaned for needle in needles):
return canonical
return cleaned
def _pick_sector(pick: Pick) -> str:
return pick.llm_sector or pick.industry
def _risk_profile(profile: dict[str, object] | None) -> dict[str, float]:
result = dict(_DEFAULT_RISK_PROFILE)
for key, value in (profile or {}).items():
if key in result:
result[key] = float(value)
return result
def _portfolio_bucket(
sector: str,
theme: str = "",
*,
buckets: object = None,
) -> str:
text = f"{sector} {theme or ''}"
bucket_map = _portfolio_buckets(buckets)
for bucket, needles in bucket_map.items():
if any(needle in text for needle in needles):
return bucket
return sector
def _portfolio_buckets(custom_buckets: object = None) -> dict[str, tuple[str, ...]]:
bucket_map = {key: tuple(value) for key, value in _DEFAULT_PORTFOLIO_BUCKETS.items()}
if not isinstance(custom_buckets, dict):
return bucket_map
for bucket, needles in custom_buckets.items():
if isinstance(needles, str):
items = [needles]
elif isinstance(needles, list):
items = [str(item) for item in needles]
else:
continue
if items:
bucket_map[str(bucket)] = tuple(items)
return bucket_map
def _format_penalty_codes(items: list[tuple[str, str, float]], limit: int = 5) -> str:
shown = [
f"{code}:{sector}(-{penalty:.1f})"
for code, sector, penalty in items[:limit]
]
if len(items) > limit:
shown.append(f"+{len(items) - limit} more")
return ",".join(shown)
def _unique(items: list[str]) -> list[str]:
seen = set()
result = []
for item in items:
key = str(item).strip()
if key and key not in seen:
seen.add(key)
result.append(key)
return result