fix: enforce account data boundaries and real-data integrity
This commit is contained in:
@@ -18,7 +18,6 @@ from typing import Any
|
||||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
from database import ReviewDatabase
|
||||
from demo_data import build_demo_dragon_tiger, build_demo_stock_detail
|
||||
from heaven_agent import HeavenAgentError, interpret_heaven
|
||||
from heaven_engine import (
|
||||
_market_line_scores,
|
||||
@@ -1074,7 +1073,7 @@ class DashboardService:
|
||||
llm_access = self.llm_access_status()
|
||||
return {
|
||||
"configured": self.configured,
|
||||
"mode": "tushare" if self.configured else "demo",
|
||||
"mode": "tushare" if self.configured else "unavailable",
|
||||
"llm_configured": self.llm_configured,
|
||||
"llm_model": self.llm_primary_model if self.llm_configured else "",
|
||||
"llm_fallback_configured": self.llm_fallback_configured,
|
||||
@@ -1097,7 +1096,7 @@ class DashboardService:
|
||||
"trade_date": normalized_date,
|
||||
"regime": regime,
|
||||
"regimes": [{"id": key, "label": value} for key, value in REGIMES.items()],
|
||||
"strategies": self.database.list_screener_strategies(),
|
||||
"strategies": self.database.list_screener_strategies(self.current_user_id),
|
||||
"factor_fields": [{"id": key, "label": value} for key, value in FACTOR_FIELDS.items()],
|
||||
"factor_data": {
|
||||
"date_count": len(factor_dates),
|
||||
@@ -1111,7 +1110,7 @@ class DashboardService:
|
||||
"fallback_configured": self.llm_fallback_configured,
|
||||
"fallback_model": self.llm_fallback_model if self.llm_fallback_configured else "",
|
||||
},
|
||||
"latest_result": self.database.latest_screener_run(normalized_date),
|
||||
"latest_result": self.database.latest_screener_run(self.current_user_id, normalized_date),
|
||||
}
|
||||
|
||||
def sync_screener_data(self, trade_date: str, lookback: int = 45) -> dict[str, Any]:
|
||||
@@ -1191,14 +1190,19 @@ class DashboardService:
|
||||
if not isinstance(regimes, list) or not regimes or any(item not in REGIMES for item in regimes):
|
||||
raise ValueError("策略适用阶段不正确。")
|
||||
formula = self.screener.validate_formula(payload.get("formula") or {})
|
||||
strategy_id = self.database.save_screener_strategy(name, description, regimes, formula)
|
||||
return {"id": strategy_id, "strategies": self.database.list_screener_strategies()}
|
||||
strategy_id = self.database.save_screener_strategy(
|
||||
self.current_user_id, name, description, regimes, formula
|
||||
)
|
||||
return {
|
||||
"id": strategy_id,
|
||||
"strategies": self.database.list_screener_strategies(self.current_user_id),
|
||||
}
|
||||
|
||||
def delete_screener_strategy(self, strategy_id: int) -> dict[str, Any]:
|
||||
deleted = self.database.delete_screener_strategy(strategy_id)
|
||||
deleted = self.database.delete_screener_strategy(self.current_user_id, strategy_id)
|
||||
return {
|
||||
"deleted": deleted,
|
||||
"strategies": self.database.list_screener_strategies(),
|
||||
"strategies": self.database.list_screener_strategies(self.current_user_id),
|
||||
}
|
||||
|
||||
def mentor_setup(self, trade_date: str) -> dict[str, Any]:
|
||||
@@ -1275,6 +1279,14 @@ class DashboardService:
|
||||
"success",
|
||||
int(result.get("latency_ms") or 0),
|
||||
)
|
||||
self.database.save_mentor_exchange(
|
||||
self.current_user_id,
|
||||
mentor_id,
|
||||
trade_date,
|
||||
question,
|
||||
str(result.get("answer") or ""),
|
||||
context["data_trade_date"],
|
||||
)
|
||||
return {
|
||||
**result,
|
||||
"mentor": skill.public(),
|
||||
@@ -1284,6 +1296,21 @@ class DashboardService:
|
||||
"notice": "主模型调用失败,已自动切换辅助模型。" if compiler == "fallback" else "",
|
||||
}
|
||||
|
||||
def mentor_messages(self, mentor_id: str, trade_date: str) -> list[dict[str, Any]]:
|
||||
mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
|
||||
trade_date = normalize_date(trade_date)
|
||||
self.mentor_skills.get_skill(mentor_id)
|
||||
return self.database.list_mentor_messages(
|
||||
self.current_user_id, mentor_id, trade_date
|
||||
)
|
||||
|
||||
def clear_mentor_messages(self, mentor_id: str, trade_date: str) -> int:
|
||||
mentor_id = validate_text(mentor_id, "问师角色", 100, required=True)
|
||||
trade_date = normalize_date(trade_date)
|
||||
return self.database.delete_mentor_messages(
|
||||
self.current_user_id, mentor_id, trade_date
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _heaven_manual_schema(market_mode: str) -> dict[str, dict[str, Any]]:
|
||||
intraday = market_mode == "intraday"
|
||||
@@ -2585,7 +2612,7 @@ class DashboardService:
|
||||
except TushareError as exc:
|
||||
raise ValueError(f"实时选股行情不可用,已停止筛选:{exc}") from exc
|
||||
return self.screener.screen(
|
||||
trade_date, formula, regime, strategy_name,
|
||||
self.current_user_id, trade_date, formula, regime, strategy_name,
|
||||
bool(payload.get("run_backtest", True)),
|
||||
realtime_snapshot,
|
||||
)
|
||||
@@ -2636,17 +2663,29 @@ class DashboardService:
|
||||
self.database.save_data_snapshot(cache_kind, normalized_date, "tushare", payload)
|
||||
return payload
|
||||
|
||||
demo = self._apply_seat_aliases(build_demo_dragon_tiger(normalized_date))
|
||||
demo["meta"] = {
|
||||
**demo.get("meta", {}),
|
||||
return {
|
||||
"meta": {
|
||||
"requested_date": f"{normalized_date[:4]}-{normalized_date[4:6]}-{normalized_date[6:8]}",
|
||||
"source": "demo",
|
||||
"status": "demo",
|
||||
"trade_date": f"{normalized_date[:4]}-{normalized_date[4:6]}-{normalized_date[6:8]}",
|
||||
"source": "unavailable",
|
||||
"status": "unavailable",
|
||||
"schema_version": 3,
|
||||
"cached": False,
|
||||
"notice": "尚未配置 Tushare Token,当前展示演示数据。",
|
||||
"notice": "公共行情尚未配置,暂无龙虎榜数据。",
|
||||
},
|
||||
"summary": {
|
||||
"trader_count": 0,
|
||||
"identity_count": 0,
|
||||
"operation_count": 0,
|
||||
"active_stock_count": 0,
|
||||
"seat_net_buy_million": 0,
|
||||
"unclassified_count": 0,
|
||||
"directory_count": 0,
|
||||
},
|
||||
"traders": [],
|
||||
"unclassified_seats": [],
|
||||
"rows": [],
|
||||
}
|
||||
return demo
|
||||
|
||||
def _search_market_directory(self) -> list[dict[str, Any]]:
|
||||
cached = self.database.get_data_snapshot("search_directory", "ths") or {}
|
||||
@@ -2935,12 +2974,12 @@ class DashboardService:
|
||||
cache_key = f"{code}:{normalized_date}"
|
||||
if not force:
|
||||
cached = self.database.get_data_snapshot("stock_detail", cache_key)
|
||||
if cached:
|
||||
if cached and str((cached.get("meta") or {}).get("source") or "") != "demo":
|
||||
cached["meta"] = {**cached.get("meta", {}), "cached": True}
|
||||
return self._enrich_stock_detail(cached)
|
||||
|
||||
name, sector = self._stock_identity(code, normalized_date)
|
||||
source = "tushare" if self.configured else "demo"
|
||||
source = "tushare"
|
||||
if self.configured:
|
||||
try:
|
||||
payload = TushareClient(self.token).stock_detail(
|
||||
@@ -2949,16 +2988,31 @@ class DashboardService:
|
||||
if not payload.get("prices"):
|
||||
raise TushareError("No price history returned")
|
||||
except TushareError as exc:
|
||||
source = "demo"
|
||||
payload = build_demo_stock_detail(
|
||||
code,
|
||||
normalized_date,
|
||||
name,
|
||||
sector,
|
||||
f"个股行情接口暂不可用,已回退演示数据。原因:{exc}",
|
||||
payload = self.database.get_latest_data_snapshot(
|
||||
"stock_detail", f"{code}:", cache_key, exclude_source="demo"
|
||||
)
|
||||
if not payload:
|
||||
raise ValueError(f"暂无 {code} 的真实行情数据:{exc}") from exc
|
||||
payload = copy.deepcopy(payload)
|
||||
payload["meta"] = {
|
||||
**payload.get("meta", {}),
|
||||
"cached": True,
|
||||
"notice": "最新行情暂不可用,已沿用最近真实收盘数据。",
|
||||
}
|
||||
return self._enrich_stock_detail(payload)
|
||||
else:
|
||||
payload = build_demo_stock_detail(code, normalized_date, name, sector)
|
||||
payload = self.database.get_latest_data_snapshot(
|
||||
"stock_detail", f"{code}:", cache_key, exclude_source="demo"
|
||||
)
|
||||
if not payload:
|
||||
raise ValueError(f"暂无 {code} 的真实行情数据,请等待后台完成首次同步。")
|
||||
payload = copy.deepcopy(payload)
|
||||
payload["meta"] = {
|
||||
**payload.get("meta", {}),
|
||||
"cached": True,
|
||||
"notice": "公共行情尚未配置,已沿用最近真实收盘数据。",
|
||||
}
|
||||
return self._enrich_stock_detail(payload)
|
||||
payload["meta"]["source"] = source
|
||||
payload["meta"]["cached"] = False
|
||||
self.database.save_data_snapshot("stock_detail", cache_key, source, payload)
|
||||
@@ -3043,7 +3097,7 @@ class DashboardService:
|
||||
return {
|
||||
"meta": {
|
||||
"trade_date": resolved_date,
|
||||
"source": detail_meta.get("source") or "demo",
|
||||
"source": detail_meta.get("source") or "unavailable",
|
||||
"notice": detail_meta.get("notice") or "",
|
||||
"intraday_status": intraday_status,
|
||||
"intraday_notice": intraday_notice,
|
||||
@@ -3317,7 +3371,10 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
if parsed.path.startswith("/api/admin/") and not self.require_admin():
|
||||
return
|
||||
if parsed.path in {"/api/screener/setup", "/api/mentors/setup", "/api/heaven/setup"} and not self.require_member():
|
||||
if parsed.path in {
|
||||
"/api/screener/setup", "/api/mentors/setup", "/api/mentors/messages",
|
||||
"/api/heaven/setup",
|
||||
} and not self.require_member():
|
||||
return
|
||||
if parsed.path == "/api/admin/settings":
|
||||
self.send_json(
|
||||
@@ -3462,6 +3519,20 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
except ValueError as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
return
|
||||
if parsed.path == "/api/mentors/messages":
|
||||
query = parse_qs(parsed.query)
|
||||
try:
|
||||
self.send_json(
|
||||
{
|
||||
"items": SERVICE.mentor_messages(
|
||||
query.get("mentor_id", [""])[0],
|
||||
query.get("trade_date", [date.today().isoformat()])[0],
|
||||
)
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
return
|
||||
if parsed.path == "/api/heaven/setup":
|
||||
query = parse_qs(parsed.query)
|
||||
trade_date = query.get("trade_date", [date.today().isoformat()])[0]
|
||||
@@ -3537,12 +3608,18 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
self.save_note()
|
||||
return
|
||||
if parsed.path == "/api/reasons":
|
||||
if not self.require_admin():
|
||||
return
|
||||
self.save_reason()
|
||||
return
|
||||
if parsed.path == "/api/seat-aliases":
|
||||
if not self.require_admin():
|
||||
return
|
||||
self.save_seat_alias()
|
||||
return
|
||||
if parsed.path == "/api/heaven/sector-phases":
|
||||
if not self.require_admin():
|
||||
return
|
||||
self.save_sector_phase_override()
|
||||
return
|
||||
if parsed.path == "/api/backfill":
|
||||
@@ -3582,6 +3659,19 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
deleted = SERVICE.database.delete_user_birth_profile(SERVICE.current_user_id)
|
||||
self.send_json({"ok": True, "deleted": deleted})
|
||||
return
|
||||
if parsed.path == "/api/mentors/messages":
|
||||
if not self.require_member():
|
||||
return
|
||||
query = parse_qs(parsed.query)
|
||||
try:
|
||||
deleted = SERVICE.clear_mentor_messages(
|
||||
query.get("mentor_id", [""])[0],
|
||||
query.get("trade_date", [date.today().isoformat()])[0],
|
||||
)
|
||||
self.send_json({"ok": True, "deleted": deleted})
|
||||
except ValueError as exc:
|
||||
self.send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
|
||||
return
|
||||
strategy_match = re.fullmatch(r"/api/screener/strategies/(\d+)", parsed.path)
|
||||
if strategy_match:
|
||||
if not self.require_member():
|
||||
@@ -3608,7 +3698,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
sector_phase_match = re.fullmatch(r"/api/heaven/sector-phases/(.+)", parsed.path)
|
||||
if sector_phase_match:
|
||||
if not self.require_member():
|
||||
if not self.require_admin():
|
||||
return
|
||||
name = unquote(sector_phase_match.group(1)).strip()
|
||||
deleted = SERVICE.database.delete_sector_phase_override(name)
|
||||
|
||||
Reference in New Issue
Block a user