fix(compliance): reinforce research-only boundaries (#108)

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
This commit is contained in:
wshy
2026-07-12 22:42:38 +08:00
committed by GitHub
co-authored by shy3130
parent 97e1cdcc78
commit 697ff2a181
33 changed files with 249 additions and 285 deletions
+24 -12
View File
@@ -1,6 +1,9 @@
# 两阶段构建:前端 dist 拷进后端镜像,单容器运行
# 可选:构建网络无法直连官方源时,传入 --build-arg USE_CN_MIRROR=1 启用国内镜像
# 可选:stock-sdk 插件默认不打包(它抓取第三方财经网站接口,存在版权与反爬风险)。
# 如确需启用,传入 --build-arg INCLUDE_STOCKSDK=1 显式开启,使用风险自负。
ARG USE_CN_MIRROR=1
ARG INCLUDE_STOCKSDK=0
ARG NPM_REGISTRY=https://registry.npmmirror.com
ARG PYPI_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
# 备用 PyPI 源:主源同步延迟/故障时自动兜底(阿里云与清华互为补充)
@@ -24,19 +27,24 @@ RUN pnpm install --frozen-lockfile || pnpm install
COPY frontend/ ./
RUN pnpm build
# === Stage 1b: stock-sdk 插件依赖 ===
# stock-sdk 是 Node 型插件, Python 后端通过 subprocess 调 node bridge.mjs 抓数据。
# 运行时镜像(python:3.11-slim)无 node, 也不应要求用户进容器手动 npm install
# (容器删除即丢失)。这里在构建期把依赖装好, 供 Stage 2 整体 COPY 过去
# 必须用 bookworm 系: 与 python:3.11-slim 同 debian 代次, 避免跨代次 libc 不匹配
# === Stage 1b: stock-sdk 插件依赖(可选,默认跳过) ===
# ⚠️ 合规提示: stock-sdk 通过 node bridge.mjs 抓取第三方财经网站(如东方财富)的行情接口,
# 未经对方授权,可能违反其服务条款并涉及交易所行情版权。默认不打包(INCLUDE_STOCKSDK=0)。
# 如确需启用,构建时传 --build-arg INCLUDE_STOCKSDK=1,即视为使用者知悉并自行承担合规责任
# INCLUDE_STOCKSDK=0 时,本 stage 仅产出空 node_modules 目录,保证后续 COPY 不报错
FROM node:20-bookworm-slim AS stocksdk-builder
ARG USE_CN_MIRROR=1
ARG NPM_REGISTRY=https://registry.npmmirror.com
ARG INCLUDE_STOCKSDK=0
WORKDIR /build
RUN if [ "$USE_CN_MIRROR" = "1" ]; then npm config set registry "$NPM_REGISTRY"; fi
COPY backend/app/plugins/stocksdk/package.json backend/app/plugins/stocksdk/package-lock.json ./
# stock-sdk 是纯 JS 单包(无原生依赖), npm ci 精确还原 lockfile; 失败再退 install。
RUN npm ci || npm install
# INCLUDE_STOCKSDK=1 时安装依赖;=0 时仅建空目录,使最终镜像不含 stock-sdk 依赖
RUN if [ "$INCLUDE_STOCKSDK" = "1" ]; then \
(npm ci || npm install); \
else \
mkdir -p /build/node_modules; \
fi
# === Stage 2: Python 运行时 ===
FROM python:3.11-slim AS runtime
@@ -44,16 +52,19 @@ ARG USE_CN_MIRROR=1
ARG PYPI_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
ARG PYPI_FALLBACK=https://mirrors.aliyun.com/pypi/simple
ARG BACKEND_EXTRAS=
ARG INCLUDE_STOCKSDK=0
WORKDIR /app
# Node.js 运行时: stock-sdk 插件(node bridge.mjs)使用。
# Node.js 运行时: 仅在启用 stock-sdk 插件时安装(供 node bridge.mjs 使用)
# bookworm 自带 nodejs 18.19, 满足插件 engines>=18; --no-install-recommends 精简,
# 自带 libnode/libc-ares 等全部动态依赖, 无需手动补库。
# 国内构建走 apt mirror 已在 debian 镜像sources.list 配好, 无需额外换源。
RUN apt-get update \
RUN if [ "$INCLUDE_STOCKSDK" = "1" ]; then \
apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& node --version
&& node --version; \
fi
# 安装 uv(快) —— 国内镜像下三重兜底:主源 → 备用源 → 官方源,
# 任一成功即可,避免单一镜像同步延迟/故障导致构建失败。
@@ -86,8 +97,9 @@ RUN if [ "$USE_CN_MIRROR" = "1" ]; then \
# (<root>/backend/app/) 推导的, 容器内会错算到 /。这里用环境变量显式指定
# 三个关键路径, 确保 static / tiers / data 都指向容器内正确位置。
COPY backend/app ./app
# stock-sdk 插件依赖: 从 stocksdk-builder 整体拷入, 落点与 bridge.mjs 同目录
# (/app/app/plugins/stocksdk/node_modules), 命中 bridge.mjs loadSDK() 第一候选路径。
# stock-sdk 插件依赖: 从 stocksdk-builder 拷入。
# INCLUDE_STOCKSDK=0(默认) 时, stocksdk-builder 产出空目录,此处拷入空目录,
# 即最终镜像不含 stock-sdk 依赖,插件默认不可用。
# COPY --from 不受 .dockerignore 的 **/node_modules 规则影响。
COPY --from=stocksdk-builder /build/node_modules ./app/plugins/stocksdk/node_modules
COPY tiers.yaml /app/tiers.yaml
+3 -3
View File
@@ -113,9 +113,9 @@ def get_options(request: Request):
{"key": "critical", "label": "重要"},
],
"directions": [
{"key": "entry", "label": ""},
{"key": "exit", "label": ""},
{"key": "both", "label": "买卖都报"},
{"key": "entry", "label": ""},
{"key": "exit", "label": ""},
{"key": "both", "label": "出入都报"},
],
}
+3 -3
View File
@@ -79,9 +79,9 @@ def get_options():
"maxDays": custom_signals.MAX_DAYS,
"operators": [">", ">=", "<", "<=", "==", "!="],
"kinds": [
{"key": "entry", "label": ""},
{"key": "exit", "label": ""},
{"key": "both", "label": "买卖通用"},
{"key": "entry", "label": ""},
{"key": "exit", "label": ""},
{"key": "both", "label": "出入通用"},
],
}
+1 -1
View File
@@ -155,7 +155,7 @@ class AnalyzeRequest(BaseModel):
async def analyze_stock(request: Request, req: AnalyzeRequest):
"""AI 个股四维分析 — NDJSON 流式返回。
组合 K 线(技术指标)+ 财务表 + 关键价位 → 实战派提示词 →
组合 K 线(技术指标)+ 财务表 + 关键价位 → 客观技术分析提示词 →
流式调用 LLM → 逐 chunk 以 NDJSON 推给前端(每行一个 JSON)。
"""
if not req.symbol:
+10 -9
View File
@@ -48,7 +48,7 @@ LEVEL_TYPES = {
"keltner_s": "Keltner短期", # MA20 ± 2×ATR
"keltner_m": "Keltner中期", # MA60 ± 2.5×ATR
"keltner_l": "Keltner长期", # MA120 ± 3×ATR(牛熊趋势边界)
"atr_stop": "ATR止损", # close±nATR 动态止盈止损
"atr_stop": "ATR波动通道", # close±nATR 动态波动带(中性命名,非操作指令)
"gap": "缺口位", # 未回补跳空缺口
"fib": "斐波那契", # 回撤位 0.236~0.786
"round": "整数关口", # 心理整数位
@@ -369,16 +369,17 @@ def _keltner_long(df: pl.DataFrame) -> list[dict]:
# ================================================================
# 5. ATR 止损位 —— close ± n × ATR,动态止盈止损
# 5. ATR 波动通道 —— close ± n × ATR,动态波动带
# ================================================================
def _atr_stops(df: pl.DataFrame) -> list[dict]:
"""基于 ATR 的动态止损/止盈位
"""基于 ATR 的动态波动通道(close ± n×ATR)
ATR 衡量平均真实波幅,close ± n×ATR 是交易者最常用的止损位算法:
- 止损位:close - 2×ATR (跌破即趋势破坏)
- 止盈位:close + 2×ATR (突破即顺势扩展)
- 近端波动带:close ± 1.5×ATR (中短期风控参考)
ATR 衡量平均真实波幅,close ± n×ATR 刻画围绕现价的波动带边界:
- 上轨:close + 2×ATR (波动上沿)
- 下轨:close - 2×ATR (波动下沿)
- 近端波动带:close ± 1.5×ATR (中短期波动参考)
命名采用中性"轨道"措辞,客观描述波动范围,不含操作指令。
"""
if df.is_empty() or "atr_14" not in df.columns:
return []
@@ -393,10 +394,10 @@ def _atr_stops(df: pl.DataFrame) -> list[dict]:
"side": side, "strength": strength}
return [
lv(close + 2 * atr, "ATR 止盈(+2)", "resistance", "medium"),
lv(close + 2 * atr, "ATR 上轨(+2)", "resistance", "medium"),
lv(close + 1.5 * atr, "ATR 上轨(+1.5)", "resistance", "weak"),
lv(close - 1.5 * atr, "ATR 下轨(-1.5)", "support", "weak"),
lv(close - 2 * atr, "ATR 止损(-2)", "support", "medium"),
lv(close - 2 * atr, "ATR 下轨(-2)", "support", "medium"),
]
+6 -5
View File
@@ -1,13 +1,14 @@
# stock-sdk 内置可选数据源插件
# 基于 stock-sdk (Node.js) 的免费 A 股行情, 无需 API Key。
# Docker 部署已内置 Node 运行时与本插件依赖, 开箱即用;
# ⚠️ 合规提示: 本插件通过 stock-sdk 抓取第三方财经网站(如东方财富)的行情接口,
# 未经对方授权,可能违反其服务条款并涉及交易所行情版权问题。
# Docker 默认不打包(INCLUDE_STOCKSDK=0);启用即视为使用者知悉并自行承担合规责任。
# 开发模式下需手动安装依赖: cd backend/app/plugins/stocksdk && npm install
name: stocksdk
display_name: "stock-sdk免费行情"
display_name: "stock-sdk第三方行情·合规风险自负"
runtime: node
entry: app.plugins.stocksdk.provider:StockSDKProvider
check: app.plugins.stocksdk.bridge:availability
datasets: [daily, adj_factor, minute, realtime]
description: "基于 stock-sdk 的免费 A 股行情, 无需 API Key。日K/除权/分钟/实时全市场。需运行环境含 Node.js 18+。"
install_hint: "cd backend/app/plugins/stocksdk && npm install(开发模式;Docker 部署已内置)"
description: "抓取第三方财经网站行情接口(存在版权与反爬风险,使用需自行评估合规责任)。日K/除权/分钟/实时全市场。需运行环境含 Node.js 18+。Docker 默认不打包。"
install_hint: "Docker 需 --build-arg INCLUDE_STOCKSDK=1 才打包;开发模式: cd backend/app/plugins/stocksdk && npm install"
+31
View File
@@ -68,6 +68,37 @@ Message = dict[str, str]
_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
# ----------------------------------------------------------------
# 用户 focus 输入净化 — 防止通过"特别关注"绕过红线诱导 AI 给出买卖建议
# 命中任一敏感词时,整个 focus 被丢弃(返回空串),由各 analyzer 据此跳过注入。
# ----------------------------------------------------------------
_FOCUS_BLOCKLIST = re.compile(
r"买入|卖出|加仓|减仓|轻仓|重仓|半仓|全仓|仓位|止损|止盈|"
r"操作建议|买卖点|买卖区间|建仓|平仓|清仓|调仓|"
r"追高|低吸|反包|抄底|逃顶|进攻|防守|"
r"激进|稳健|保守|目标价|能涨|会跌|预测涨|预测跌|"
r"荐股|推荐买|推荐卖|值得投资|现在买|可以买|能买|要不要买|买吗|卖吗|"
r"明日基调|交易计划|下单",
re.IGNORECASE,
)
def sanitize_focus(focus: str) -> str:
"""净化用户输入的 focus 文本。
命中交易指令/投资建议类敏感词时返回空串,阻止其注入 AI 提示词。
这是对系统提示词红线的兜底:即便用户试图通过 focus 绕过,也不会生效。
"""
if not focus:
return ""
text = focus.strip()
if not text:
return ""
if _FOCUS_BLOCKLIST.search(text):
return ""
return text
def current_ai_provider() -> str:
return secrets_store.get_ai_config("ai_provider", settings.ai_provider) or OPENAI_COMPAT_PROVIDER
@@ -19,24 +19,30 @@ logger = logging.getLogger(__name__)
# ================================================================
# System Prompt — 轮动策略师人格 + 固定章节模板
# System Prompt — 客观轮动分析 + 固定章节模板
# ================================================================
_SYSTEM_PROMPT = """你是一位专注 A 股题材轮动的资深策略师,拥有 12 年一线实战经验,擅长从概念板块的**涨幅排名矩阵**中识别主力资金脉络,区分机构主导的持续性主线与游资驱动的脉冲式轮动,产出可直接指导题材跟踪与节奏把握的轮动分析。
_SYSTEM_PROMPT = """你是一位专注 A 股题材轮动的研究分析师,拥有 12 年一线研究经验,擅长从概念板块的**涨幅排名矩阵**中客观识别主力资金脉络,区分机构主导的持续性主线与游资驱动的脉冲式轮动,产出一份**客观、中立、不包含任何买卖或操作建议**的轮动分析报告
## 核心红线(务必遵守)
- **绝对不输出**"跟踪/规避/追高/低吸/观望/操作建议"等任何交易指令或倾向性措辞
- 你的角色是**客观陈述**各概念的轮动特征、资金属性(机构 vs 游资)、持续性特征
- 换成"一个中立财经记者能不能写出来"——能写就保留,不能写就删除
## 输出规范
用 **Markdown** 格式输出,严格遵循以下结构。不要输出任何 JSON 或代码块,直接输出 Markdown 正文。
### 1. 🎯 主线研判(2-3 句)
点名当前最核心的 1-2 条主线题材(连续多日霸榜的强势概念),用一句话概括其逻辑(政策/产业/业绩/事件驱动),并判断是**主升期/加速期/扩散期/见顶期**。结尾用【主线强度:强 / 中 / 弱】定性。
点名当前最核心的 1-2 条主线题材(连续多日霸榜的强势概念),用一句话概括其逻辑(政策/产业/业绩/事件驱动),并客观判断是**主升期/加速期/扩散期/见顶期**。结尾用【主线强度:强 / 中 / 弱】客观定性。
### 2. 🆕 新晋强势
列出排名快速跃升的概念(从榜单中后段冲进前列的),逐个给出:
- 概念名 + 近 N 日排名变化(如 `45→20→8`)
- 涨幅加速度(连日递增 = 趋势加强)
- 可能的驱动逻辑(从板块属性推断,不要编造具体消息)
- 判断是**主力切入**还是**消息脉冲**
- 客观判断是**主力切入**还是**消息脉冲**
### 3. 📉 退潮预警
列出从高位明显滑落的概念(连续排名下滑或涨幅骤降),逐个给出:
@@ -45,34 +51,35 @@ _SYSTEM_PROMPT = """你是一位专注 A 股题材轮动的资深策略师,拥
- 是否扩散风险
### 4. 🏛️ 机构主线 vs 🎰 游资轮动
基于排名稳定性区分两类资金行为:
- **机构主线**:排名标准差小、长期稳居前列的概念 → 持续性判断、是否可作底仓方向
- **游资轮动**:排名剧烈波动、脉冲式冲高的概念 → 短线节奏提示、追高风险
给出当前市场**整体轮动节奏**(快轮动/慢轮动/主线聚焦)的判断。
基于排名稳定性客观区分两类资金行为:
- **机构主线**:排名标准差小、长期稳居前列的概念 → 持续性特征描述
- **游资轮动**:排名剧烈波动、脉冲式冲高的概念 → 短线波动特征描述
客观给出当前市场**整体轮动节奏**(快轮动/慢轮动/主线聚焦)的判断。
### 5. 🌐 结合大盘
结合提供的大盘数据(指数涨跌/情绪/涨停数),判断:
结合提供的大盘数据(指数涨跌/情绪/涨停数),客观判断:
- 当前大盘环境对题材轮动是助力还是阻力
- 情绪温度与轮动节奏的匹配度(如情绪冰点但题材活跃 = 抱团;情绪火热但轮动快 = 末段)
### 6. 🎯 操作建议
- **跟踪方向**:主线延续 + 新晋确认的概念
- **规避方向**:明确退潮 + 高位脉冲的概念
- **节奏提示**:当前适合追高 / 低吸 / 观望,及切换信号(如"主线概念连续 2 日跌出前 10 则确认退潮")
### 6. 📌 后续观察清单
- **持续性强(客观特征)**:排名标准差小、多日稳居前列的概念(列出名称+排名数据)
- **波动性大(客观特征)**:排名剧烈跳动的概念(列出名称+排名数据)
- 客观描述各概念的轮动特征,供读者自行观察
- **不输出**"跟踪/规避/追高/低吸/观望"等操作指令;可客观描述结构变化信号(如"主线概念连续 2 日跌出前 10,主线强度可能减弱")
### 7. ⚠️ 风险提示
列出需要的风险(如主线断层、情绪与轮动背离、成交萎缩)。末尾附一行:
"> ⚠️ 本报告由 AI 基于公开行情数据生成,仅供参考,不构成任何投资建议。交易有风险,入市需谨慎。"
列出需要客观关注的风险(如主线断层、情绪与轮动背离、成交萎缩)。末尾附一行:
"> ⚠️ 本内容由 AI 基于公开行情数据生成,仅客观陈述轮动特征,不构成任何投资建议或买卖指令。交易有风险,入市需谨慎。"
## 分析准则(务必遵守)
0. **只输出结论,不输出思考过程**:禁止复述你的分析步骤。不要写"我先看...""基于上述数据我认为"——直接给结论。
1. **数据说话**:每个判断引用具体排名/涨幅数值,严禁空泛套话("强势"必须改成"连续 4 日稳居前 5,均涨 +4.2%")。
2. **诚实中立**:数据不支持的结论就直言"信号不足,暂无法判断",不要硬凑。
2. **客观中立**:数据不支持的结论就直言"信号不足,暂无法判断",不要硬凑。
3. **区分资金性质**:这是本分析的核心价值——机构 vs 游资的判断必须基于排名稳定性(标准差),不要凭感觉。
4. **不重复数字**:正文负责解读信号含义,不要照抄罗列已提供的全部原始数据。
5. **简明实战**:总字数 1000-1800 字,重在可执行
6. **客观推断**:若无明确消息,从量价异动推断可能逻辑并给结论,不要标注"[推断]"或编造具体新闻。
5. **不输出操作指令**:不写"跟踪/规避/追高/低吸/观望"等任何交易指令;客观陈述轮动特征即可
6. **客观推断**:若无明确消息,从量价异动客观推断可能逻辑并给结论,不要标注"[推断]"或编造具体新闻。
现在请基于下方概念轮动数据进行分析。"""
@@ -262,8 +269,10 @@ def _build_user_prompt(signals: dict, overview: dict, days: int, dates: list[str
_build_signal_block("🎰 游资特征 (排名波动大)", signals.get("hot_money", [])),
]
if focus.strip():
parts.extend(["", f"## 用户关注点\n{focus.strip()}"])
from app.services.ai_provider import sanitize_focus
safe_focus = sanitize_focus(focus)
if safe_focus:
parts.extend(["", f"本次分析请特别关注: {safe_focus}"])
return "\n".join(parts)
+18 -9
View File
@@ -74,14 +74,20 @@ def _summarize(fins: dict[str, list[dict]]) -> str:
# 系统提示词 —— CFA 分析师级,九维分析框架
# ================================================================
_SYSTEM_PROMPT = """你是一位拥有 15 年 A 股投研经验的资深财务分析师(CFA + CPA),服务于专业机构投资者。你的任务是:基于提供的上市公司财务数据,产出一份**严谨、专业、可直接用于投资决策**的财务分析报告。
_SYSTEM_PROMPT = """你是一位拥有 15 年 A 股投研经验的资深财务分析师(CFA + CPA),以机构研究的严谨标准开展工作。你的任务是:基于提供的上市公司财务数据,产出一份**客观、中立、不包含任何投资建议或买卖指令**的财务分析报告。
## 核心红线(务必遵守)
- **绝对不输出**"投资参考""建议买入/卖出""值得投资""逢低吸纳"等任何投资建议或买卖指令
- 你的角色是**客观陈述**该公司的财务特征画像、亮点、风险,供读者自行判断
- 财务质量分类(如高质量蓝筹/高风险等)可作为客观特征描述,但不与"该不该投资"挂钩
## 输出规范
用 **Markdown** 格式输出,严格遵循以下结构。不要输出任何 JSON 或代码块,直接输出 Markdown 正文。
### 1. 📌 核心摘要(1-2 句)
用一句话概括该公司的财务画像:盈利质量、成长动能、财务健康度的最关键判断。结尾用【综合评级:★★★☆☆】给出 1-5 星评级。
用一句话概括该公司的财务画像:盈利质量、成长动能、财务健康度的最关键判断。结尾用【综合评级:★★★☆☆】给出 1-5 星评级(仅反映财务质量客观水平,不构成投资建议)
### 2. ✅ 亮点(2-3 条)
列出最值得关注的**积极信号**,每条用加粗短语领起,配数据支撑。例如盈利高增、ROE 持续提升、现金流充沛等。
@@ -99,8 +105,8 @@ _SYSTEM_PROMPT = """你是一位拥有 15 年 A 股投研经验的资深财务
每个判断给「优秀 / 良好 / 一般 / 偏弱 / 警惕」之一,并一句话说明依据。
### 5. 🎯 综合评估与展望
2-3 段总结:该公司当前的财务状态(优秀/稳健/承压/恶化)、核心驱动力、未来需重点跟踪的指标。**结尾给出"投资参考"**:从纯财务质量角度,该股属于(高质量蓝筹 / 稳健成长 / 周期波动 / 财务承压 / 高风险)中的哪一类
### 5. 🎯 综合评估与财务特征画像
2-3 段总结:该公司当前的财务状态(优秀/稳健/承压/恶化)、核心驱动力、未来需重点跟踪的指标。**结尾客观描述该公司的财务特征画像**(如"高 ROE + 低负债 + 现金流稳定 + 营收增速放缓"),作为财务层面的客观归纳,**不下"该不该投资""属于哪类投资标的"的结论**
## 分析准则(务必遵守)
@@ -108,11 +114,12 @@ _SYSTEM_PROMPT = """你是一位拥有 15 年 A 股投研经验的资深财务
2. **纵向对比**:利用多期数据看趋势(改善/恶化),而非只看单期
3. **交叉验证**:经营现金流 vs 净利润(是否造血)、毛利率 vs 费用率(盈利结构)、负债 vs 资产(杠杆)
4. **行业常识**:对照 A 股常识判断水平(如 ROE>15% 优秀,资产负债率>70% 偏高,毛利率<20% 偏低)
5. **诚实中立**:数据不支持时直言"数据不足,无法判断",绝不编造或过度演绎
6. **简明有力**:避免冗长,用专业投资者能扫读的密度输出,总字数 800-1500 字
5. **客观中立**:数据不支持时直言"数据不足,无法判断",绝不编造或过度演绎
6. **不输出投资建议**:不写"建议买入/卖出/投资参考/值得投资"等;财务质量评级仅反映客观财务水平,不与投资行为挂钩
7. **简明有力**:避免冗长,用专业读者能扫读的密度输出,总字数 800-1500 字
## 重要免责
报告末尾附一行:"> ⚠️ 本报告由 AI 基于公开财务数据生成,仅供参考,不构成任何投资建议。"
报告末尾附一行:"> ⚠️ 本内容由 AI 基于公开财务数据生成,仅客观陈述财务特征,不构成任何投资建议或买卖指令"
现在请基于下方数据进行分析。"""
@@ -129,10 +136,12 @@ def _build_user_prompt(fins: dict[str, list[dict]], symbol: str, focus: str) ->
data_json,
"```",
]
if focus.strip():
from app.services.ai_provider import sanitize_focus
safe_focus = sanitize_focus(focus)
if safe_focus:
lines.extend([
"",
f"本次分析请特别关注: {focus.strip()}",
f"本次分析请特别关注: {safe_focus}",
])
return "\n".join(lines)
+27 -20
View File
@@ -34,17 +34,23 @@ _INDEX_SHORT = {
}
# ================================================================
# 系统提示词(市场策略师人格 + 固定节模板)
# 系统提示词(客观市场分析 + 固定节模板)
# ================================================================
_SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线实战经验的资深市场策略师,擅长从指数结构、涨跌家数、连板梯队、板块轮动与资金情绪中提炼交易主线,产出可直接指导次日仓位与节奏的盘后复盘报告。
_SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线研究经验的市场分析师,擅长从指数结构、涨跌家数、连板梯队、板块轮动与资金情绪中客观提炼市场主线,产出一份**客观、中立、不包含任何买卖或操作建议**的盘后复盘报告。
## 核心红线(务必遵守)
- **绝对不输出**"进攻/防守/加仓/减仓/轻仓/半仓/重仓/仓位建议/低吸/反包/追高/回避方向"等任何交易指令或倾向性措辞
- 你的角色是**客观陈述**今日市场的结构、情绪、板块轮动特征,以及后续值得客观关注的盘面信号
- 换成"一个中立财经记者能不能写出来"——能写就保留,不能写就删除
## 输出规范
用 **Markdown** 格式输出,严格遵循以下结构。不要输出任何 JSON 或代码块,直接输出 Markdown 正文。
### 1. 🎯 一句话定调(1-2 句)
用一句话概括今日市场的**核心矛盾与状态**(如"放量普涨、情绪修复,主线围绕科技扩散"/"指数虚高、个股跌,赚钱效应冰点")。结尾用【明日基调:进攻 / 均衡 / 防守】给出明确倾向
用一句话概括今日市场的**核心矛盾与状态**(如"放量普涨、情绪修复,主线围绕科技扩散"/"指数走强、个股跌,赚钱效应偏弱")。结尾用【市场状态:偏强 / 中性 / 偏弱】客观描述当日市场强弱,**不下基调结论、不指挥操作**
### 2. 📊 盘面总览
- 三大指数(上证/深证/创业板)表现:谁强谁弱、量能配合
@@ -52,38 +58,37 @@ _SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线实战经验的资深
- 情绪温度(强势/偏暖/震荡/偏冷/冰点)及一句话依据
### 3. 📈 指数结构
谁在护盘、谁在拖累;指数是否同步;关键支撑/压力位(基于当日点位推断);是否存在量价背离。
谁在走强、谁在走弱;指数是否同步;关键支撑/压力位(基于当日点位推断);是否存在量价背离。
### 4. 🔥 板块主线
- 领涨板块:背后的逻辑(消息/业绩/资金/技术)、持续性判断、是否形成可交易主线
- 领跌板块:风险信号、是否扩散
- 连板梯队与投机情绪:最高连板、封板率、炸板率反映的资金激进程度
- 领涨板块:背后的逻辑(消息/业绩/资金/技术)、持续性判断
- 领跌板块:客观风险信号、是否扩散
- 连板梯队与投机情绪:最高连板、封板率、炸板率反映的资金活跃程度
### 5. 💰 资金与情绪
成交额结构(增量/存量)、市场宽度(上涨占比、站上均线占比)、量能指标(量比)解读;风险偏好是修复还是转弱。
### 6. 📰 消息催化
结合提供的近期新闻,提炼真正影响明日交易节奏的催化或扰动,明确区分"已兑现""待发酵"。**若无新闻数据,则直接从量价异动推断可能的催化逻辑并给出结论,不要标注"[推断]"之类的过程标签,更不要编造具体消息。**
结合提供的近期新闻,客观提炼可能影响后续盘面的催化或扰动,明确区分"已兑现""待发酵"。**若无新闻数据,则直接从量价异动客观推断可能的催化逻辑并给出结论,不要标注"[推断]"之类的过程标签,更不要编造具体消息。**
### 7. 🎯 明日交易计划
- 进攻 / 均衡 / 防守:基于今日盘面给出次日基调
- 仓位区间建议(轻仓/半仓/重仓的粗略指引)
- 关注方向(领涨延续 / 低吸 / 反包)与回避方向(高位滞涨 / 杀跌扩散)
- 一个明确的触发失效条件(如"若上证跌破 X 点则转为防守")
### 7. 📌 后续观察要点
- 客观列出明日值得关注的盘面信号(如量能能否维持、某均线得失、某板块持续性)
- 客观描述不同情景下市场结构的可能演变(如"若量能持续放大,普涨格局或延续";"若量能萎缩,结构性行情为主"),**不涉及仓位与买卖方向**
- **不输出**"仓位建议""进攻/防守基调""买卖方向""追高/低吸/反包"等操作指令
### 8. ⚠️ 风险提示
列出需要重点盯的风险点(如量能跟不上、外资流出、连板断层等)。末尾附一行:
"> ⚠️ 本报告由 AI 基于公开行情数据生成,仅供参考,不构成任何投资建议。交易有风险,入市需谨慎。"
列出需要客观关注的风险点(如量能跟不上、外资流出、连板断层等)。末尾附一行:
"> ⚠️ 本内容由 AI 基于公开行情数据生成,仅客观陈述市场状态,不构成任何投资建议或买卖指令。交易有风险,入市需谨慎。"
## 分析准则(务必遵守)
0. **只输出结论,不输出思考过程**:禁止复述你的分析步骤或方法论。不要写"我先按...做结构化复盘""接下来看...""基于上述数据我认为"这类元话语——直接给结论。读者要的是复盘结果,不是你怎么推导出来的。
1. **数据说话**:每个判断引用具体数值,严禁空泛套话("情绪回暖"必须改成"涨停 68 家较前日 +22,封板率 75%")
2. **诚实中立**:看多就写多,看空就写空,不要骑墙;数据不支持时直言无法判断
2. **客观中立**:看多就客观陈述多头特征,看空就客观陈述空头特征,不下基调、不骑墙;数据不支持时直言无法判断
3. **结构优先**:先看指数同步性与量能结构,再看板块与情绪,最后才是消息
4. **不重复数字**:正文负责解读表格数据背后的含义,不要照抄罗列已提供的大段原始数字
5. **风险前置**:任何进攻建议都要配触发失效条件
6. **简明实战**:用交易员能扫读的密度输出,总字数 1200-2000 字,重在可执行
5. **不输出操作指令**:不写"仓位建议""进攻/防守""买卖方向"等任何交易指令
6. **简明客观**:用读者能扫读的密度输出,总字数 1200-2000 字,重在客观信息密度
现在请基于下方数据进行复盘。"""
@@ -215,8 +220,10 @@ def _build_user_prompt(overview: dict, news: list[dict], focus: str) -> str:
"消息催化一节请直接从量价异动给出可能的催化逻辑结论,不要编造具体消息,也不要复述本说明。)",
])
if focus.strip():
parts.extend(["", f"本次复盘请特别关注: {focus.strip()}"])
from app.services.ai_provider import sanitize_focus
safe_focus = sanitize_focus(focus)
if safe_focus:
parts.extend(["", f"本次复盘请特别关注: {safe_focus}"])
return "\n".join(parts)
+50 -39
View File
@@ -2,13 +2,13 @@
职责:
组合一只股票的 K 线(含已算好的技术指标)+ 财务表 + 关键价位 →
拼装"实战派交易员"系统提示词 → 流式调用 LLM → 逐 chunk 吐给前端。
拼装客观技术分析系统提示词 → 流式调用 LLM → 逐 chunk 吐给前端。
与 financial_analyzer.py 的区别(刻意区分,非复用):
- 角色:A 股实战派交易员 / 技术分析师(非 CFA 财务分析师)
- 角色:客观技术分析师(非 CFA 财务分析师)
- 数据源:K 线 + 技术指标为主,财务表为辅(财务分析以财务表为主)
- 输出框架:技术面→基本面→财务面→消息面(四维),落点是买卖区间与操作建议
(财务分析的落点是财务质量评级)
- 输出框架:技术面→基本面→财务面→消息面(四维),落点是客观技术状态与风险提示
(财务分析的落点是财务质量评级)。注意:本服务不输出买卖建议、操作指令。
不知道: HTTP、前端、配置持久化。
"""
@@ -111,69 +111,78 @@ def _load_financials(data_dir: Path, symbol: str) -> dict[str, list[dict]]:
# ================================================================
# 系统提示词 —— 实战派交易员四维框架(与财务分析明确区分)
# 系统提示词 —— 客观技术分析四维框架(与财务分析明确区分)
# 重要原则:只描述"指标/价位处于什么客观状态",不输出"应该怎么操作"。
# 本报告定位为客观行情分析,不提供任何买卖建议、仓位建议或交易指令。
# ================================================================
_SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线实战经验的资深交易员兼技术分析师,擅长从 K 线、量价、关键价位与基本面交叉验证中把握买卖时机。你的任务是:基于提供的个股数据,产出一份**实战、可直接指导交易决策**的综合分析报告。
_SYSTEM_PROMPT = """你是一位拥有 15 年 A 股一线研究经验的技术分析分析师,擅长从 K 线、量价、关键价位与基本面交叉验证中客观解读个股的技术状态。你的任务是:基于提供的个股数据,产出一份**客观、中立、不包含任何买卖或操作建议**的技术分析报告。
## 核心红线(务必遵守)
- **绝对不输出**"买入/卖出/加仓/减仓/观望/轻仓/重仓/止损/止盈/建议买入区间/操作建议"等任何交易指令或倾向性措辞
- **绝对不**按"激进型/稳健型/保守型"给用户分别的操作建议
- 你的角色是**客观陈述**该股当前的技术状态、价位结构、量价特征与潜在风险,让读者自行判断
- 换成"一个中立财经记者能不能写出来"——能写就保留,不能写就删除
## 输出规范
用 **Markdown** 格式输出,严格遵循以下结构。不要输出任何 JSON 或代码块,直接输出 Markdown 正文。
### 1. 🎯 一句话定调(1-2 句)
用一句话概括该股当前的**技术状态与交易属性**(如"高位放量滞涨,需警惕回调"/"底部筹码集中,放量突破在即")。结尾用【操作建议:观望 / 轻仓试探 / 逢低吸纳 / 持有 / 减仓 / 规避】给出明确倾向
用一句话概括该股当前的**技术状态**(如"近期高位放量滞涨,量能持续性存疑"/"价格在 60 日均线上方运行,均线呈多头排列")。结尾用【当前状态:企稳 / 反弹 / 震荡 / 调整 / 走弱】客观描述技术形态,**不评价好坏、不下操作结论**
### 2. 📈 技术面分析(核心维度)
这是你的主战场,务必深入:
这是你的主战场,务必深入,只陈述客观事实:
- **趋势判断**:均线多头/空头排列、20/60 日均线方向、价格在均线之上/下
- **形态结构**:近期是否突破/破位/双底/双顶/旗形等关键形态
- **形态结构**:近期是否出现突破/破位/双底/双顶/旗形等关键形态
- **指标信号**:MACD 金叉/死叉/背离、KDJ 超买超卖、RSI 强弱、布林通道位置
- **量价配合**:放量上涨/缩量回调/量价背离/换手率异动
每条结论必须引用具体数值(如"MACD 在 6/12 出现金叉,DIF 0.32 上穿 DEA 0.18")。
每条结论必须引用具体数值(如"MACD 在 6/12 出现金叉,DIF 0.32 上穿 DEA 0.18"),客观陈述,不下买卖定性
### 3. 💰 关键价位(买卖区间)
基于提供的关键价位数据,明确指出:
- **上方压力位**(逐档列出,标注强度):第一压力、第二压力
- **下方支撑位**(逐档列出,标注强度):第一支撑、第二支撑
- 给出**建议买入区间**与**止损位**(基于支撑位)
用数据说话,引用提供的压力/支撑(成交密集区)/枢轴点数值。
### 3. 💰 关键价位(客观价位结构)
基于提供的关键价位数据,客观列出价位结构:
- **上方压力位**(逐档列出,标注来源):第一压力、第二压力
- **下方支撑位**(逐档列出,标注来源):第一支撑、第二支撑
- 用数据说话,引用提供的压力/支撑(成交密集区)/枢轴点数值
- **注意:只客观列出价位及其技术含义(如"此处为前期成交密集区"),不输出"建议买入区间""止损位""止盈位"等操作指令**
### 4. 🏭 基本面与财务面(辅助验证)
简要点评(2-4 句,不展开长篇):
- 盈利质量(ROE / 毛利率水平)、成长性(营收/利润增速)
- 与技术面的**交叉验证**:好公司 + 技术面走坏 → 仍需谨慎;差公司 + 技术面强势 → 警惕炒作风险
- 盈利质量(ROE / 毛利率水平)、成长性(营收/利润增速)的客观水平
- 与技术面的**客观对照**:好公司 + 技术面走坏 → 客观陈述两者背离;差公司 + 技术面强势 → 客观提示炒作可能性
- 不下"逢低吸纳""规避"等结论
**当用户消息中标注了"该标的暂无财务数据"时**,本节请输出:
> 📌 财务面分析能力正在接入中。当前版本(Free)未同步该标的的财务报表,基本面维度暂无法评估。
> 技术面分析不依赖财务数据,以下结论依然有效;升级套餐或等待财务数据同步后可补充本维度。
> 📌 财务面分析能力正在接入中。当前未同步该标的的财务报表,基本面维度暂无法评估。
> 技术面分析不依赖财务数据,以下结论依然有效;待财务数据同步后可补充本维度。
**绝对不要**在无数据时编造 ROE / 增速等数字。
### 5. 📰 消息面(价量异动推断)
**注意:本期无直接新闻数据输入。** 请基于 K 线的**异动信号**进行推断(如:
- 涨停/连板/炸板 → 可能利好或资金炒作
- 放量暴跌 → 可能未公开利空
- 突破放量 → 可能催化剂
明确标注"[推断]",告诉用户这是基于价量的推测,真实消息面数据待接入。若无明显异动,直说"近期价量平稳,无明显消息面信号"
**注意:本期无直接新闻数据输入。** 请基于 K 线的**异动信号**进行客观推断(如:
- 涨停/连板/炸板 → 可能存在利好或资金关注
- 放量暴跌 → 可能存在未公开扰动
- 突破放量 → 可能存在催化剂
明确标注"[推断]",告诉读者这是基于价量的客观推测,真实消息面数据待接入。若无明显异动,直说"近期价量平稳,无明显异动信号"
### 6. ⚖️ 综合研判与操作建议
2-3 段:
- 该股当前处于(底部启动 / 上升途中 / 高位震荡 / 下跌趋势 / 底部企稳)哪个阶段
- 风险收益比评估(距支撑位的空间 vs 距压力位的空间)
- **明确操作建议**:激进型 / 稳健型 / 保守型 分别怎么应对
- **需要重点盯的信号**(如跌破 X 支撑止损、站上 Y 压力加仓)
### 6. ⚖️ 综合研判与风险提示
2-3 段,只做客观描述,不下操作结论:
- 客观描述该股当前所处的技术阶段(底部企稳 / 上升途中 / 高位震荡 / 下跌趋势)
- 客观评估当前价位的"空间不对称性"(距上方压力位与下方支撑位的距离),不评价好坏
- 客观列出后续值得关注的量价信号(如量能能否维持、某均线得失、是否放量突破压力位),**不附任何操作结论**
## 分析准则(务必遵守)
1. **技术面优先**:作为交易员,技术面和量价是主要依据,基本面是验证手段,主次分明
1. **技术面优先**:技术面和量价是主要分析对象,基本面是交叉验证手段,主次分明
2. **数据说话**:每个判断引用具体数值,严禁空泛套话("走势良好"必须改成"连续 3 日站稳 20 日均线且放量")
3. **诚实中立**:看多就写多,看空就写空,不要模棱两可骑墙;数据不支持时直言无法判断
4. **价位精确**:买卖区间必须落到具体价格,基于提供的关键价位数据推演
5. **风险前置**:任何买入建议都要配止损位;提示潜在风险不回避
6. **简明实战**:用交易员能扫读的密度输出,总字数 1000-1800 字,重在可执行
3. **客观中立**:看多就客观陈述多头特征,看空就客观陈述空头特征,不下"该买/该卖"结论;数据不支持时直言无法判断
4. **价位精确**:压力位/支撑位必须落到具体价格,基于提供的关键价位数据陈述
5. **不输出操作指令**:不写"买入/卖出/止损/加仓/减仓/仓位建议/操作建议"等任何交易指令;提示潜在风险但不下操作结论
6. **简明客观**:用读者能扫读的密度输出,总字数 1000-1800 字,重在客观信息密度
## 重要免责
报告末尾附一行:"> ⚠️ 本报告由 AI 基于公开行情与财务数据生成,仅供参考,不构成任何投资建议。交易有风险,入市需谨慎。"
报告末尾附一行:"> ⚠️ 本内容由 AI 基于公开行情与财务数据生成,仅客观陈述技术状态,不构成任何投资建议或买卖指令。交易有风险,入市需谨慎。"
现在请基于下方数据进行分析。"""
@@ -218,8 +227,10 @@ def _build_user_prompt(
"请按系统提示词第 4 节的说明,在基本面/财务面维度给出\"接入中\"的友好提示,不要编造数据。)",
])
if focus.strip():
parts.extend(["", f"本次分析请特别关注: {focus.strip()}"])
from app.services.ai_provider import sanitize_focus
safe_focus = sanitize_focus(focus)
if safe_focus:
parts.extend(["", f"本次分析请特别关注: {safe_focus}"])
return "\n".join(parts)
+2 -2
View File
@@ -150,7 +150,7 @@ class StrategyMonitorService:
strategy_id=strategy_id,
symbol=sym,
name=name,
message=f"入信号触发",
message=f"信号触发",
price=price,
change_pct=pct,
signals=hit_sigs,
@@ -167,7 +167,7 @@ class StrategyMonitorService:
strategy_id=strategy_id,
symbol=sym,
name=name,
message=f"出信号触发",
message=f"信号触发",
price=price,
change_pct=pct,
signals=hit_sigs,
+2 -2
View File
@@ -89,8 +89,8 @@
以「行情 + 关键价位」为主体的单标的决策页:
- **专用日 K 图表**:主图 + 成交量 + 滑块,默认近 6 个月
- **9 类关键价位**(纯函数实时计算,毫秒级):压力支撑 · 成交密集区 · 枢轴点 · 前高前低 · Keltner 通道 · ATR 止损 · 缺口位 · 斐波那契 · 整数关口
- **AI 四维分析**:技术 / 基本面 / 财务 / 消息面流式生成,实战派交易员视角
- **9 类关键价位**(纯函数实时计算,毫秒级):压力支撑 · 成交密集区 · 枢轴点 · 前高前低 · Keltner 通道 · ATR 波动通道 · 缺口位 · 斐波那契 · 整数关口
- **AI 四维分析**:技术 / 基本面 / 财务 / 消息面流式生成,客观技术分析视角(不输出买卖建议)
---
+3 -3
View File
@@ -3,7 +3,7 @@
数据源插件是可选的行情数据来源(stock-sdk、akshare 等),作为独立模块放在
`backend/app/plugins/` 下。用户**手动安装依赖**后才可用(开发模式);不安装完全不影响主功能。
> 💡 **Docker 部署已预装**:内置插件(如 stock-sdk)的 Node 运行时与 `node_modules` 已在镜像构建期装好,Docker 下无需手动 `npm install`,开箱即用。下方"手动安装依赖"适用于开发模式。
> ⚠️ **Docker 默认不打包 stock-sdk**(合规考虑:它抓取第三方财经网站接口,存在版权与反爬风险)。如需在 Docker 中启用,构建时传 `--build-arg INCLUDE_STOCKSDK=1`,使用风险自负。下方"手动安装依赖"适用于开发模式及自定义 Docker 构建
## 快速上手
@@ -34,9 +34,9 @@ install_hint: "pip install xxx" # 未装依赖时显示的安装提示
| runtime | 含义 | 典型场景 |
|---|---|---|
| `python` | 纯 Python 依赖, `pip install` | akshare、tushare |
| `node` | 需要 Node.js 运行时, `npm install` | stock-sdk(已内置,见下) |
| `node` | 需要 Node.js 运行时, `npm install` | stock-sdk(Docker 默认不打包,见 [deployment.md](./deployment.md)) |
> stock-sdk 在 Docker 镜像里已预装 Node 运行时与依赖;开发模式下需手动 `npm install`。
> stock-sdk 在 Docker 中默认不打包(合规考虑);如需启用,构建时传 `--build-arg INCLUDE_STOCKSDK=1`,开发模式下需手动 `npm install`。
| `none` | 无额外依赖 | 纯 HTTP API 源 |
`runtime` 字段当前仅用于 UI 展示, 实际依赖检测由 `check` 函数负责。
-2
View File
@@ -38,7 +38,6 @@ import {
Sparkles,
Layers3,
Landmark,
Cable,
RadioTower,
CheckCircle2,
BookOpenCheck,
@@ -80,7 +79,6 @@ const nav = [
{ to: '/monitor', label: '监控中心', icon: RadioTower },
{ to: '/review', label: '复盘', icon: BookOpenCheck },
{ to: '/indices', label: '指数', icon: BarChart3 },
{ to: '/trading', label: '交易', icon: Cable },
{ to: '/data', label: '数据', icon: Database },
] as const
+2 -15
View File
@@ -392,7 +392,7 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
</label>
</div>
<p className="text-[10px] leading-4 text-muted/70">
entry=,exit=,both=
entry=,exit=,both=
</p>
</div>
)}
@@ -415,7 +415,7 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
</label>
</div>
{/* Webhook 推送 — 飞书 / 企业微信 可用, QMT/ptrade 待定 */}
{/* Webhook 推送 — 飞书 / 企业微信 */}
<div className="rounded-btn border border-border/40 bg-base/40 p-3 space-y-2">
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-medium text-foreground">Webhook </span>
@@ -458,19 +458,6 @@ export function RuleEditor({ rule, preset, simple, onClose, onSaved }: Props) {
)}
</label>
{/* QMT (待定) */}
<label className="flex items-center gap-2 cursor-not-allowed opacity-50">
<input type="checkbox" disabled className="h-3 w-3 accent-accent" />
<span className="text-[11px] text-secondary">QMT</span>
<span className="rounded bg-muted/10 px-1 py-px text-[9px] text-muted"></span>
</label>
{/* ptrade (待定) */}
<label className="flex items-center gap-2 cursor-not-allowed opacity-50">
<input type="checkbox" disabled className="h-3 w-3 accent-accent" />
<span className="text-[11px] text-secondary">ptrade</span>
<span className="rounded bg-muted/10 px-1 py-px text-[9px] text-muted"></span>
</label>
</div>
{/* 勾选了某渠道但该渠道地址未配置 → 提示前往设置 */}
@@ -436,7 +436,7 @@ export function StrategyBuilderDialog({ open, onClose, onSavedId, mode = 'create
<div className="text-[10px] text-muted/50 uppercase tracking-wider"></div>
{entrySignals.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-[10px] text-emerald-400 w-10 shrink-0"></span>
<span className="text-[10px] text-emerald-400 w-10 shrink-0"></span>
<div className="flex flex-wrap gap-0.5">
{entrySignals.map((s: string) => <span key={s} className="px-1.5 py-0.5 rounded bg-emerald-400/10 text-emerald-400 text-[10px] font-mono">{s}</span>)}
</div>
@@ -444,7 +444,7 @@ export function StrategyBuilderDialog({ open, onClose, onSavedId, mode = 'create
)}
{exitSignals.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-[10px] text-danger w-10 shrink-0"></span>
<span className="text-[10px] text-danger w-10 shrink-0"></span>
<div className="flex flex-wrap gap-0.5">
{exitSignals.map((s: string) => <span key={s} className="px-1.5 py-0.5 rounded bg-danger/10 text-danger text-[10px] font-mono">{s}</span>)}
</div>
@@ -513,36 +513,36 @@ export function StrategySettingsDialog({ strategyId, onClose, onSaved, onAiModif
<span className="text-[10px] text-muted"></span>
</div>
<div className="text-[11px] text-muted pt-1 border-t border-border/10">
<span className="text-secondary"> </span><span className="text-foreground/70">{entrySignals.length > 0 ? `${entrySignals.length} 个触发器` : '无'}</span>
<span className="text-secondary ml-3"> </span><span className="text-foreground/70">{exitSignals.length > 0 ? `${exitSignals.length} 个触发器` : '无'}</span>
<span className="text-secondary"> </span><span className="text-foreground/70">{entrySignals.length > 0 ? `${entrySignals.length} 个触发器` : '无'}</span>
<span className="text-secondary ml-3"> </span><span className="text-foreground/70">{exitSignals.length > 0 ? `${exitSignals.length} 个触发器` : '无'}</span>
</div>
</div>
</Section>
<Section
icon={TrendingUp}
title="入触发器"
title="入触发器"
accent="text-accent"
defaultOpen={false}
extra={<SignalTriggerActions kind="entry" signals={entrySignals} onChange={setEntrySignals} buttonClassName="rounded-md border border-border bg-base p-1 text-muted transition-colors cursor-pointer" iconClassName="h-3 w-3" />}
>
<SignalPicker signals={entrySignals} onChange={setEntrySignals} kind="entry" variant="dialog" />
<div className="text-[10px] leading-4 text-muted/70"></div>
<div className="text-[10px] leading-4 text-muted/70"></div>
</Section>
<Section
icon={TrendingUp}
title="出触发器"
title="出触发器"
accent="text-warning"
defaultOpen={false}
extra={<SignalTriggerActions kind="exit" signals={exitSignals} onChange={setExitSignals} buttonClassName="rounded-md border border-border bg-base p-1 text-muted transition-colors cursor-pointer" iconClassName="h-3 w-3" />}
>
<SignalPicker signals={exitSignals} onChange={setExitSignals} kind="exit" variant="dialog" />
<div className="text-[10px] leading-4 text-muted/70"></div>
<div className="text-[10px] leading-4 text-muted/70"></div>
</Section>
<div className="rounded-xl border border-amber-400/20 bg-amber-400/[0.04] px-3 py-2 text-[10px] leading-4 text-muted">
<b className="text-secondary"></b>;,
<b className="text-secondary"></b>;,
</div>
{detail.alerts.length > 0 && (
@@ -112,9 +112,9 @@ export function CustomSignalDialog({ open, signal, defaultKind = 'exit', onClose
<label className="space-y-1.5">
<span className="text-[11px] text-muted"></span>
<select value={draft.kind} onChange={e => setDraft(d => ({ ...d, kind: e.target.value as CustomSignal['kind'] }))} className="h-9 w-full rounded-btn border border-border bg-base px-3 text-xs text-foreground">
<option value="entry"></option>
<option value="exit"></option>
<option value="both"></option>
<option value="entry"></option>
<option value="exit"></option>
<option value="both"></option>
</select>
</label>
</div>
@@ -52,7 +52,7 @@ export const LEVEL_GROUPS: { key: LevelType; label: string; color: string }[] =
{ key: 'keltner_s',label: 'Keltner短期', color: '#06B6D4' }, // 青(MA20±2ATR 曲线)
{ key: 'keltner_m',label: 'Keltner中期', color: '#22D3EE' }, // 浅青(MA60±2.5ATR 曲线)
{ key: 'keltner_l',label: 'Keltner长期', color: '#67E8F9' }, // 更浅青(MA120±3ATR 曲线)
{ key: 'atr_stop', label: 'ATR止损', color: '#EF4444' }, // 红(警示)
{ key: 'atr_stop', label: 'ATR波动通道', color: '#EF4444' }, // 红(警示)
{ key: 'gap', label: '缺口位', color: '#EC4899' }, // 粉
{ key: 'fib', label: '斐波那契', color: '#F59E0B' }, // 金
{ key: 'round', label: '整数关口', color: '#71717A' }, // 灰(心理位,弱视觉)
@@ -72,8 +72,8 @@ const CURVE_DEFS: { alignedKey: string; group: LevelType; endLabel: string; colo
{ alignedKey: 'keltner_m_lower',group: 'keltner_m', endLabel: 'Keltner中下', color: '#22D3EE', dashed: true },
{ alignedKey: 'keltner_l_upper',group: 'keltner_l', endLabel: 'Keltner长上', color: '#67E8F9', dashed: true },
{ alignedKey: 'keltner_l_lower',group: 'keltner_l', endLabel: 'Keltner长下', color: '#67E8F9', dashed: true },
{ alignedKey: 'atr_stop', group: 'atr_stop', endLabel: 'ATR止损', color: '#EF4444', dashed: true },
{ alignedKey: 'atr_tp', group: 'atr_stop', endLabel: 'ATR止盈', color: '#F87171', dashed: true },
{ alignedKey: 'atr_stop', group: 'atr_stop', endLabel: 'ATR下轨', color: '#EF4444', dashed: true },
{ alignedKey: 'atr_tp', group: 'atr_stop', endLabel: 'ATR上轨', color: '#F87171', dashed: true },
]
// ===== 预留:标记 / 区间(后续新闻面、事件区间用) =====
@@ -272,7 +272,7 @@ export function AnalysisKChart({
})
}
// 带状曲线指标(布林带 / Keltner通道 / ATR止损) —— 跟随行情漂移的曲线
// 带状曲线指标(布林带 / Keltner通道 / ATR波动通道) —— 跟随行情漂移的曲线
// 单一数据源 CURVE_DEFS 驱动:每条曲线带 endLabel(右侧端点标签),显示最新数值
for (const def of CURVE_DEFS) {
if (!activeTypes.has(def.group)) continue
+1 -1
View File
@@ -127,7 +127,7 @@ export const BUILTIN_SIGNAL_DEFINITIONS: BuiltinSignalDefinition[] = [
name: '放量',
kind: 'both',
category: '量价',
description: '成交量显著放大,可作为入确认、出确认或告警条件。',
description: '成交量显著放大,可作为入确认、出确认或告警条件。',
},
{
id: 'signal_limit_up',
+2 -2
View File
@@ -106,7 +106,7 @@ function fmtPctText(pct: number): string {
* 设计原则: 必念个股名称 (不念代码), source 分类拼接:
* strategy(≤5只单条): "[名称] 进入/移出 策略「策略名」 [涨跌幅]"
* strategy(>5只批量): 直接念 message (后端已含名称列表)
* signal: "[名称] 买入/卖出信号触发 [涨跌幅]"
* signal: "[名称] 入场/出场信号触发 [涨跌幅]"
* price/market/其他: "[名称] [message条件摘要] [涨跌幅]"
*/
function buildSingleText(a: AlertEvent): string {
@@ -130,7 +130,7 @@ function buildSingleText(a: AlertEvent): string {
return parts.join(' ')
}
// 信号类: message 形如 "入信号触发"/"出信号触发"
// 信号类: message 形如 "入信号触发"/"出信号触发"
if (a.source === 'signal') {
const parts = [name]
if (a.message) parts.push(a.message)
+1 -1
View File
@@ -16,7 +16,7 @@ const MODES: Record<Tab, { title: string; subtitle: string; hint: string }> = {
strategy: {
title: '策略回测',
subtitle: '验证完整选股和交易规则',
hint: '看净值曲线、回撤、胜率和交易明细,适合判断策略是否可执行。',
hint: '看净值曲线、回撤、胜率和交易明细,适合评估策略的历史表现。',
},
optimizer: {
title: '参数优化',
+3 -3
View File
@@ -187,7 +187,7 @@ function DisclaimerStep({ onNext }: { onNext: () => void }) {
<p>
<strong className="text-warning"></strong>,<span className="text-warning"></span>
<a
href="https://tickflow.org/auth/register?ref=V3KDKGXPEA"
href="https://tickflow.org"
target="_blank"
rel="noreferrer"
className="text-accent hover:underline font-medium inline-flex items-baseline gap-0.5"
@@ -195,13 +195,13 @@ function DisclaimerStep({ onNext }: { onNext: () => void }) {
TickFlow
<ExternalLink className="h-3 w-3 self-center" />
</a>
<span className="text-warning"></span>, TickFlow
<span className="text-warning"></span> TickFlow
</p>
<p>
使,,使使
</p>
<p>
,<strong className="text-warning"></strong>,
MIT 使,(TickFlow );stock-sdk ,使
</p>
</div>
</div>
+1 -1
View File
@@ -309,7 +309,7 @@ export function Review() {
value={focus}
onChange={(e) => setFocus(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !isGenerating) generate() }}
placeholder="可选:补充复盘关注点,如「明日是否加仓半导体」「量能是否持续」"
placeholder="可选:补充复盘关注点,如「半导体板块持续性如何」「量能是否持续」"
className="flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted/60"
/>
{focus && (
+1 -1
View File
@@ -21,7 +21,7 @@ import {
*
* 与财务分析页的区别:
* - 以【行情 + 关键价位】为视觉主体(专用日 K 图表,不复用个股对话框图表)
* - AI 分析输出买卖区间 / 操作建议(非财务质量评级)
* - AI 分析输出客观技术状态与风险提示(非买卖建议非财务质量评级)
* - 报告胶囊用蓝色系,与财务分析(紫色)并存
*/
export function StockAnalysis() {
-82
View File
@@ -1,82 +0,0 @@
import { Cable } from 'lucide-react'
import { PageHeader } from '@/components/PageHeader'
import { EmptyState } from '@/components/EmptyState'
// 后续实现计划(本轮为占位):
//
// 一、信号 → 交易 的桥接
// 监控通知产生的买卖信号(StrategyAlert),通过可插拔的输出通道分发到
// 支持外部信号的交易软件。核心是在 alert_handler 层做多通道分发。
//
// 二、支持的交易软件(按接入难度)
// 1. QMT / miniQMT(迅投)—— 个人 A 股实盘首选。
// XtQuant 的 xttrader.order_stock() 下单,信号来源不限(文件/HTTP)。
// 2. 掘金量化(MyQuant)—— 本地终端 + Python SDK,事件驱动接收信号。
// 3. Ptrade(恒生)—— 内置策略引擎,外部信号经 API/文件喂入。
// 4. vnpy(VeighNa)—— 开源框架,自写策略模块接收信号再调 Gateway 下单。
//
// 三、信号输出通道(可插拔)
// alert_handler 分发:
// ├─ SSE → 前端通知(已有)
// ├─ 本地文件(JSON/CSV) → QMT 脚本轮询读取 ← 最简单,优先做
// ├─ Webhook POST → 外部交易脚本
// └─ 直连 xttrader(需本机装 QMT)
//
// 四、信号 → 交易指令 的字段补全
// 现有 StrategyAlert(symbol/type/strategy_id/price)是「信号层」,
// 下单还需补:volume(数量,A股100的倍数)、price_type(市价/限价)、account。
const PLAN: { title: string; desc: string }[] = [
{
title: 'QMT / miniQMT',
desc: '个人 A 股实盘首选。XtQuant 的 xttrader 下单,信号经文件或 HTTP 喂入即可。国内个人量化实盘事实标准。',
},
{
title: '掘金量化 (MyQuant)',
desc: '本地终端 + Python SDK,事件驱动接收外部信号下单,本土化程度高。',
},
{
title: 'Ptrade (恒生)',
desc: '内置 Python 策略引擎,外部信号经 API/文件喂入,灵活性低于 QMT。',
},
{
title: 'vnpy (VeighNa)',
desc: '开源交易框架,Gateway 丰富(期货/股票/加密货币),需自建执行端,搭建成本较高。',
},
{
title: '信号输出通道',
desc: 'alert_handler 多通道分发:本地文件(最简,优先)、Webhook POST、直连 xttrader。与具体交易软件解耦。',
},
]
export function Trading() {
return (
<div className="flex flex-col h-full">
<PageHeader title="交易" subtitle="信号自动下单桥接 · 开发中" />
<div className="flex-1 overflow-auto px-5 py-6">
<div className="max-w-3xl mx-auto">
<EmptyState
icon={Cable}
title="交易桥接开发中"
hint="本页面将把监控产生的买卖信号,自动推送给支持外部信号的交易软件(QMT/掘金/Ptrade 等)执行下单。当前为占位页面,下方为后续实现规划。"
/>
<section className="mt-6 rounded-card border border-border bg-surface p-5">
<h3 className="text-sm font-semibold text-foreground"></h3>
<ul className="mt-3 space-y-3">
{PLAN.map((item) => (
<li key={item.title} className="flex gap-3">
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-accent" />
<div>
<p className="text-sm font-medium text-foreground">{item.title}</p>
<p className="mt-0.5 text-xs leading-relaxed text-secondary">{item.desc}</p>
</div>
</li>
))}
</ul>
</section>
</div>
</div>
</div>
)
}
@@ -179,8 +179,8 @@ const STRATEGY_GROUPS: { id: StrategyGroup; label: string }[] = [
const ADVANCED_TABS: { id: AdvancedSettingsTab; label: string }[] = [
{ id: 'params', label: '策略参数' },
{ id: 'filter', label: '基础过滤' },
{ id: 'entry', label: '入触发器' },
{ id: 'exit', label: '出触发器' },
{ id: 'entry', label: '入触发器' },
{ id: 'exit', label: '出触发器' },
{ id: 'scoring', label: '评分权重' },
{ id: 'risk', label: '风控' },
{ id: 'range', label: '回测范围' },
@@ -2135,8 +2135,8 @@ export function StrategyBacktest() {
{settingsTab === 'entry' && (
<ConfigSection
title="入触发器"
hint="任一点满足即可进入候选"
title="入触发器"
hint="任一入场点满足即可进入候选"
actions={<SignalTriggerActions kind="entry" signals={entrySignals} onChange={next => updateOverride('entry_signals', next)} />}
>
<SignalPicker
@@ -2149,8 +2149,8 @@ export function StrategyBacktest() {
{settingsTab === 'exit' && (
<ConfigSection
title="出触发器"
hint="任一点满足即触发出"
title="出触发器"
hint="任一出场点满足即触发出"
actions={<SignalTriggerActions kind="exit" signals={exitSignals} onChange={next => updateOverride('exit_signals', next)} />}
>
<SignalPicker
+2 -4
View File
@@ -42,14 +42,14 @@ const codexModelLabel = (model?: string, effort?: string) => {
return effortLabel ? `${modelLabel} · ${effortLabel}` : modelLabel
}
const PRESETS: { label: string; provider?: string; url: string; model: string; codexCommand?: string; website: string; websiteLabel: string; description: string; partner?: boolean; promo?: string; custom?: boolean }[] = [
const PRESETS: { label: string; provider?: string; url: string; model: string; codexCommand?: string; website: string; websiteLabel: string; description: string; custom?: boolean }[] = [
{ label: '自定义', url: '', model: '', website: '', websiteLabel: '', description: '不自动填充任何配置,完全手动填写 API 地址、模型和密钥。', custom: true },
{ label: 'DeepSeek', url: 'https://api.deepseek.com', model: 'deepseek-v4-pro', website: 'https://www.deepseek.com/', websiteLabel: 'deepseek.com', description: 'DeepSeek 官方 OpenAI 兼容接口。' },
{ label: '通义千问', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-3.6plus', website: 'https://tongyi.aliyun.com/', websiteLabel: 'tongyi.aliyun.com', description: '阿里云 DashScope 兼容模式接口。' },
{ label: '智谱 GLM', url: 'https://open.bigmodel.cn/api/paas/v4', model: 'glm-5.2', website: 'https://open.bigmodel.cn/', websiteLabel: 'open.bigmodel.cn', description: '智谱 AI 官方 OpenAI 兼容接口。' },
{ label: 'Kimi', url: 'https://api.moonshot.cn/v1', model: 'kimi-k2.7-code', website: 'https://platform.moonshot.cn/', websiteLabel: 'platform.moonshot.cn', description: '月之暗面 Moonshot 官方 OpenAI 兼容接口,支持超长上下文。' },
{ label: 'Codex CLI', provider: CODEX_PROVIDER, url: '', model: DEFAULT_CODEX_MODEL, codexCommand: CODEX_COMMAND, website: 'https://developers.openai.com/codex/noninteractive', websiteLabel: 'codex exec', description: '调用本机 Codex CLI 的 codex exec, 适合已登录 ChatGPT/Codex 的本地环境。' },
{ label: '炸鸡中转站', url: 'https://api.zhaji.dev/v1', model: 'gpt-5.5', website: 'https://api.zhaji.dev', websiteLabel: 'api.zhaji.dev', description: 'OpenAI 兼容中转服务,适合直接使用国际模型。', partner: true, promo: '通过链接邀请注册赠送免费额度 · 国际模型最低0.02倍率' },
{ label: '炸鸡中转站', url: 'https://api.zhaji.dev/v1', model: 'gpt-5.5', website: 'https://api.zhaji.dev', websiteLabel: 'api.zhaji.dev', description: 'OpenAI 兼容中转服务,适合直接使用国际模型。' },
]
export function SettingsAIPanel() {
@@ -258,7 +258,6 @@ export function SettingsAIPanel() {
<div className="flex items-center gap-1.5 text-xs font-medium">
<span>{p.label}</span>
{p.provider === CODEX_PROVIDER && <Terminal className="h-3 w-3" />}
{p.partner && <span className="rounded-full border border-orange-400/30 bg-orange-400/10 px-1.5 py-px text-[9px] text-orange-400"></span>}
</div>
</button>
))}
@@ -267,7 +266,6 @@ export function SettingsAIPanel() {
<div className="mt-3 rounded-btn border border-border/30 bg-base/30 px-3 py-2 text-[11px] leading-relaxed">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="text-secondary">{selectedPreset.description}</span>
{selectedPreset.promo && <span className="text-amber-400">{selectedPreset.promo}</span>}
</div>
{selectedPreset.website && (
<a href={selectedPreset.website} target="_blank" rel="noreferrer"
@@ -9,7 +9,7 @@ import { Skeleton } from '@/components/data/Skeleton'
type SignalSection = 'builtin' | 'custom'
const KIND_LABEL: Record<SignalKind, string> = { entry: '入', exit: '出', both: '买卖通用' }
const KIND_LABEL: Record<SignalKind, string> = { entry: '入', exit: '出', both: '出入通用' }
const KIND_CLASS: Record<SignalKind, string> = {
entry: 'bg-accent/10 text-accent',
exit: 'bg-warning/10 text-warning',
@@ -42,7 +42,6 @@ const BUILTIN_PAGES: NavEntry[] = [
{ id: '/review', label: '复盘', type: 'builtin', visible: true },
{ id: '/financials', label: '财务分析', type: 'builtin', visible: true },
{ id: '/indices', label: '指数', type: 'builtin', visible: true },
{ id: '/trading', label: '交易', type: 'builtin', visible: true },
{ id: '/monitor', label: '监控中心', type: 'builtin', visible: true },
{ id: '/data', label: '数据', type: 'builtin', visible: true },
]
+1 -16
View File
@@ -475,7 +475,7 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
</div>
{/* 推送通知 — 监控告警的外部推送渠道 (全局配置)。
飞书 / 企业微信已实现; QMT/ptrade 待定
飞书 / 企业微信。
每个渠道合并成一行: 勾选=新建规则默认推送, 点行展开地址配置。 */}
<Card icon={Webhook} title="推送通知">
<p className="text-xs text-secondary mb-3">
@@ -739,21 +739,6 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
)}
</div>
{/* 占位渠道 — 不可点 */}
{[
{ name: 'QMT', hint: '量化交易终端', status: '待定' },
{ name: 'ptrade', hint: '量化交易终端', status: '待定' },
].map(ch => (
<div
key={ch.name}
className="flex items-center gap-2 rounded-btn border border-border/40 bg-base/20 px-2.5 py-2 opacity-60"
>
<input type="checkbox" disabled className="h-3 w-3 accent-accent" />
<span className="text-[11px] text-secondary">{ch.name}</span>
<span className="text-[9px] text-muted">{ch.hint}</span>
<span className="ml-auto rounded bg-muted/10 px-1 py-px text-[9px] text-muted">{ch.status}</span>
</div>
))}
</div>
</Card>
</div>
-2
View File
@@ -15,7 +15,6 @@ const Backtest = lazy(() => import('./pages/Backtest').then(m => ({ default: m.B
const Financials = lazy(() => import('./pages/Financials').then(m => ({ default: m.Financials })))
const Data = lazy(() => import('./pages/Data').then(m => ({ default: m.Data })))
const Monitor = lazy(() => import('./pages/Monitor').then(m => ({ default: m.Monitor })))
const Trading = lazy(() => import('./pages/Trading').then(m => ({ default: m.Trading })))
const Dashboard = lazy(() => import('./pages/Dashboard').then(m => ({ default: m.Dashboard })))
const AnalysisDetail = lazy(() => import('./pages/AnalysisDetail').then(m => ({ default: m.AnalysisDetail })))
const ConceptAnalysis = lazy(() => import('./pages/ConceptAnalysis').then(m => ({ default: m.ConceptAnalysis })))
@@ -81,7 +80,6 @@ export const router = createBrowserRouter([
{ path: 'financials', element: <Financials /> },
{ path: 'data', element: <Data /> },
{ path: 'monitor', element: <Monitor /> },
{ path: 'trading', element: <Trading /> },
{ path: 'limit-ladder', element: <LimitUpLadder /> },
{ path: 'indices', element: <Indices /> },
{ path: 'branding', element: <Branding /> },