Files
group_xinghuo_jinrong/app/service/threshold_service.py
T
zhanghongyu_0626 793c0307f8 feat(risk): Enhance risk management functionality and access control
- Updated `RiskListAccess` and `ThresholdWriteAccess` to enforce access control in the risk repository and threshold repository, ensuring only authorized roles can perform sensitive operations.
- Introduced new methods in `RiskRepository` for counting pending alerts and listing alerts with access checks, improving data security and compliance.
- Enhanced the `chat.py` and `deps.py` files to integrate compliance roles into the risk management matrix, allowing for more granular access control.
- Updated documentation to reflect the new testing baseline of 825 passed tests, indicating improved stability and functionality across the application.

This update significantly strengthens the risk management capabilities, ensuring robust access control and compliance with organizational policies.
2026-09-11 17:07:22 +08:00

154 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""客户亏损阈值提醒(C-04)。
从 L1 槽位 threshold_pref_summary 解析百分比并写入 customer_threshold_config;
持仓查询时按组合浮动盈亏与配置比对,命中则追加提醒并写 customer_notify_log。
"""
from __future__ import annotations
import re
import json
from decimal import Decimal
from typing import Any
from app.repository.threshold_repository import ThresholdRepository
from app.repository.repo_access import ThresholdWriteAccess
_LOSS_PCT_RE = re.compile(r"(\d+(?:\.\d+)?)\s*%")
def parse_loss_threshold_pct(summary: str) -> Decimal | None:
"""从「亏10%提醒我」类摘要解析正数阈值(百分点)。"""
m = _LOSS_PCT_RE.search(summary or "")
if not m:
return None
pct = Decimal(m.group(1))
if pct <= 0 or pct > 100:
return None
return pct
def sync_threshold_from_summary(customer_id: str, summary: str) -> int | None:
"""画像摘要 → 组合级 customer_threshold_config(upsert)。"""
pct = parse_loss_threshold_pct(summary)
if pct is None:
return None
return ThresholdRepository().upsert_portfolio(
customer_id, pct, access=ThresholdWriteAccess.profile_sync()
)
def portfolio_pnl_pct(holdings: list[dict[str, Any]]) -> float | None:
"""按市值加权计算组合浮动盈亏率(%)。"""
if not holdings:
return None
total_mv = sum(float(r.get("market_value") or 0) for r in holdings)
if total_mv <= 0:
return None
weighted = sum(
float(r.get("market_value") or 0) * float(r.get("pnl_pct") or 0)
for r in holdings
)
return weighted / total_mv
def build_threshold_alert(
customer_id: str,
holdings: list[dict[str, Any]],
*,
trace_id: str = "",
) -> str | None:
"""若组合亏损达到配置阈值,返回提醒文案并留痕;否则 None。"""
repo = ThresholdRepository()
configs = repo.list_enabled(customer_id)
portfolio_cfg = next((c for c in configs if c.get("scope_type") == "portfolio"), None)
if not portfolio_cfg:
return None
pnl = portfolio_pnl_pct(holdings)
if pnl is None or pnl >= 0:
return None
threshold = float(portfolio_cfg["loss_threshold_pct"])
loss_pct = abs(pnl)
if loss_pct < threshold:
return None
alert = (
f"【阈值提醒】您的持仓组合浮动亏损约 {loss_pct:.2f}%,"
f"已达到您设置的 {threshold:.0f}% 提醒线。"
"以上为系统只读计算,不构成投资建议;如需调整提醒条件,可告诉我新的亏损提醒比例。"
)
repo.insert_notify_log(
customer_id=customer_id,
trace_id=trace_id,
threshold_config_id=int(portfolio_cfg["id"]),
payload={
"portfolio_pnl_pct": round(pnl, 4),
"threshold_pct": threshold,
"holding_count": len(holdings),
},
)
return alert
THRESHOLD_PUSH_CHANNEL = "customer:threshold:push"
def publish_threshold_push(customer_id: str, message: str) -> bool:
"""C-04 演示:Redis pub 主动推送(前端/脚本可订阅)。"""
if not message:
return False
try:
from app.config.database import get_redis_client
payload = json.dumps({"customer_id": customer_id, "message": message}, ensure_ascii=False)
get_redis_client().publish(THRESHOLD_PUSH_CHANNEL, payload)
return True
except Exception: # noqa: BLE001
return False
def run_threshold_check(
customer_id: str,
holdings: list[dict[str, Any]],
*,
trace_id: str = "",
push: bool = False,
) -> dict[str, Any]:
"""扫描持仓阈值;可选 Redis push + 留痕。"""
alert = build_threshold_alert(customer_id, holdings, trace_id=trace_id)
pushed = False
if alert and push:
pushed = publish_threshold_push(customer_id, alert)
if pushed:
ThresholdRepository().insert_notify_log(
customer_id=customer_id,
trace_id=trace_id or "threshold-push",
threshold_config_id=None,
payload={"alert": alert[:200], "channel": "redis_pub"},
channel="push",
send_status="sent",
)
return {"alert": alert, "pushed": pushed}
def append_threshold_to_tool_result(
customer_id: str,
tool_result: dict[str, Any],
*,
trace_id: str = "",
) -> dict[str, Any]:
"""持仓 tool 成功后追加阈值提醒到 fact_text。"""
if tool_result.get("tool") != "holding_query" or not tool_result.get("ok"):
return tool_result
facts = tool_result.get("facts")
if not isinstance(facts, list):
return tool_result
alert = build_threshold_alert(customer_id, facts, trace_id=trace_id)
if not alert:
return tool_result
out = dict(tool_result)
out["fact_text"] = f"{out.get('fact_text', '')}\n\n{alert}"
return out