修复web前端,增加openai

This commit is contained in:
dekun
2026-05-23 11:53:36 +08:00
parent 4439bedcb7
commit ada9478713
20 changed files with 335 additions and 355 deletions
+177
View File
@@ -0,0 +1,177 @@
"""大模型调用:OpenAI 兼容接口(默认)或本机 Ollama 二选一。"""
from __future__ import annotations
import base64
import os
from typing import List, Optional, Sequence
from urllib.parse import urljoin
import requests
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
AI_PROVIDER = (os.getenv("AI_PROVIDER", "openai") or "openai").strip().lower()
OPENAI_API_BASE = (os.getenv("OPENAI_API_BASE", "https://op.bz121.com/v1") or "").strip().rstrip("/")
OPENAI_API_KEY = (os.getenv("OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY") or "").strip()
OPENAI_MODEL = (os.getenv("OPENAI_MODEL", "gemma4:e4b") or "gemma4:e4b").strip()
OLLAMA_API = os.getenv("OLLAMA_API", "http://127.0.0.1:11434/api/generate")
AI_MODEL = os.getenv("AI_MODEL", "huihui_ai/deepseek-r1-abliterated:latest")
def _use_openai() -> bool:
return AI_PROVIDER in ("openai", "openai_compatible", "gateway")
def _read_image_base64(image_path: str) -> Optional[str]:
try:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
except Exception:
return None
def _collect_images(
image_paths: Optional[Sequence[str]] = None,
images_b64: Optional[Sequence[str]] = None,
) -> List[str]:
out: List[str] = []
for p in image_paths or []:
b = _read_image_base64(p)
if b:
out.append(b)
for b in images_b64 or []:
if b:
out.append(str(b))
return out
def _openai_chat_url() -> str:
base = OPENAI_API_BASE or "https://op.bz121.com/v1"
if base.endswith("/chat/completions"):
return base
return f"{base}/chat/completions"
def _generate_openai(prompt: str, images: List[str], temperature: float) -> str:
if not OPENAI_API_KEY:
return "AI 调用失败:未配置 OPENAI_API_KEY"
headers = {
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json",
}
if images:
content: List[dict] = [{"type": "text", "text": prompt}]
for b64 in images:
content.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{b64}"},
}
)
messages = [{"role": "user", "content": content}]
else:
messages = [{"role": "user", "content": prompt}]
body = {
"model": OPENAI_MODEL,
"messages": messages,
"temperature": temperature,
"stream": False,
}
r = requests.post(
_openai_chat_url(),
headers=headers,
json=body,
timeout=AI_TIMEOUT_SECONDS,
)
r.raise_for_status()
data = r.json()
choices = data.get("choices") or []
if not choices:
return "AI 生成失败:响应无 choices"
msg = choices[0].get("message") or {}
return (msg.get("content") or "").strip() or "AI 生成失败:空内容"
def _generate_ollama(prompt: str, images: List[str], temperature: float) -> str:
payload = {
"model": AI_MODEL,
"prompt": prompt,
"stream": False,
"options": {"temperature": temperature},
}
if images:
payload["images"] = images
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
r.raise_for_status()
return (r.json().get("response") or "").strip() or "AI 生成失败"
def ai_generate(
prompt: str,
*,
image_paths: Optional[Sequence[str]] = None,
images_b64: Optional[Sequence[str]] = None,
temperature: float = 0.2,
) -> str:
"""统一文本生成;失败时返回以「AI 调用失败」开头的说明。"""
images = _collect_images(image_paths, images_b64)
try:
if _use_openai():
return _generate_openai(prompt, images, temperature)
return _generate_ollama(prompt, images, temperature)
except requests.HTTPError as e:
detail = ""
try:
detail = (e.response.text or "")[:500]
except Exception:
pass
prov = "OpenAI" if _use_openai() else "Ollama"
return f"AI 调用失败({prov} HTTP {e.response.status_code if e.response else '?'}):{detail or str(e)}"
except Exception as e:
prov = "OpenAI" if _use_openai() else "Ollama"
return f"AI 调用失败({prov}):{str(e)}"
def ai_review(trades_text: str, period_title: str, image_paths=None) -> str:
prompt = f"""
你是一位专业交易教练。下面是用户的{period_title}交易记录,请做简洁、可执行的复盘(中文)。
【硬性规则 — 必须遵守】
- 你只能根据「交易记录」里**明确出现的字段**陈述事实;禁止编造:是否触发止损、是否扛单、亏损是否扩大、图上具体结构/进出场点位等记录里**没有**的信息。
- 「平仓/离场」只是交易员自述摘要,不是客观成交明细;若记录未写明代币是否打到止损价、是否软件平仓等,不要断言执行路径,可用「在记录有限前提下,一种可能是……」或简短写「执行路径记录不足,无法判断」。
- 「提前离场」类结论必须优先依据记录中的「提前离场记录」字段;若该段全为「无」或未出现有效内容,不得写道「明显扛单」「拒不止损」「未执行硬止损」等。
- 实际RR为负只说明结果相对于预期RR不利,不等同于「风控失灵」或「止损纪律崩溃」,除非记录里另有依据。
- 禁止用语:人身攻击、夸张定性(如「致命伤」「灾难」);语气克制、对事不对人。
- 若有截图且你能辨认,再结合图讨论;看不清或无明确定位则明确说「无法从图确认」,不得虚构 K 线故事。
【输出结构】
1. 总体盈亏结构(紧扣笔数、盈亏数字与 RR,少形容词)
2. 心态与执行(每笔 1–10 分 + 一句依据;依据必须对应记录字段)
3. 行为标签(提前离场 / 乱开仓 / 扛单等):仅在有字段或自述支撑时点名;否则写「记录未勾选或未描述,不作强加」
4. 改进建议(最多 3 条,每条具体可执行)
5. 图表(若有且可读):结合价格行为简述;否则一两句说明无法看图分析
交易记录:
{trades_text}
""".strip()
return ai_generate(prompt, image_paths=image_paths, temperature=0.2)
def ai_short_advice(prompt_text: str) -> str:
prompt = f"""
你是交易风控助理。请用中文给出**最多 3 条**提醒,要求:
- 每条不超过 25 个字
- 语气克制、具体、可执行
- 不要输出 Markdown,不要编号前缀以外的废话
场景:
{prompt_text}
""".strip()
return ai_generate(prompt, temperature=0.2)
def ai_provider_label() -> str:
if _use_openai():
return f"OpenAI 兼容 · {OPENAI_MODEL} @ {OPENAI_API_BASE}"
return f"Ollama · {AI_MODEL}"
+7 -2
View File
@@ -141,9 +141,14 @@ FORCE_CLOSE_ENABLED=false
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 复盘服务地址(本机 Ollama 默认地址
# AI 提供方:openai(默认,OpenAI 兼容网关)| ollama(本机 Ollama
AI_PROVIDER=openai
# OpenAI 兼容接口(示例:https://op.bz121.com/v1 ,账号见 gateway.json
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
# 本机 OllamaAI_PROVIDER=ollama 时使用)
OLLAMA_API=http://127.0.0.1:11434/api/generate
# AI 模型名称
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# Binance 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
+19 -76
View File
@@ -34,6 +34,7 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from fib_key_monitor_lib import (
FIB_KEY_MONITOR_TYPES,
calc_fib_plan,
@@ -212,8 +213,6 @@ BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER", "1.0"))
BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT", "0.02"))
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R", "1.0"))
DEFAULT_TRADE_STYLE = (os.getenv("DEFAULT_TRADE_STYLE", "trend") or "trend").strip().lower()
OLLAMA_API = os.getenv("OLLAMA_API", "http://127.0.0.1:11434/api/generate")
AI_MODEL = os.getenv("AI_MODEL", "huihui_ai/deepseek-r1-abliterated:latest")
BINANCE_SOCKS_PROXY = (os.getenv("BINANCE_SOCKS_PROXY") or "").strip()
BINANCE_HTTP_PROXY = (os.getenv("BINANCE_HTTP_PROXY") or "").strip()
@@ -533,61 +532,6 @@ def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
return "\n".join(lines) + "\n"
def ai_review(trades_text, period_title, image_paths=None):
prompt = f"""
你是一位专业交易教练下面是用户的{period_title}交易记录请做简洁可执行的复盘中文
硬性规则 必须遵守
- 你只能根据交易记录**明确出现的字段**陈述事实禁止编造是否触发止损是否扛单亏损是否扩大图上具体结构/进出场点位等记录里**没有**的信息
- 平仓/离场只是交易员自述摘要不是客观成交明细若记录未写明代币是否打到止损价是否软件平仓等不要断言执行路径可用在记录有限前提下一种可能是或简短写执行路径记录不足无法判断
- 提前离场类结论必须优先依据记录中的提前离场记录字段若该段全为或未出现有效内容不得写道明显扛单拒不止损未执行硬止损
- 实际RR为负只说明结果相对于预期RR不利不等同于风控失灵止损纪律崩溃除非记录里另有依据
- 禁止用语人身攻击夸张定性致命伤灾难语气克制对事不对人
- 若有截图且你能辨认再结合图讨论看不清或无明确定位则明确说无法从图确认不得虚构 K 线故事
输出结构
1. 总体盈亏结构紧扣笔数盈亏数字与 RR少形容词
2. 心态与执行每笔 110 + 一句依据依据必须对应记录字段
3. 行为标签提前离场 / 乱开仓 / 扛单等仅在有字段或自述支撑时点名否则写记录未勾选或未描述不作强加
4. 改进建议最多 3 每条具体可执行
5. 图表若有且可读结合价格行为简述否则一两句说明无法看图分析
交易记录
{trades_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
images = []
for p in image_paths or []:
b64 = _read_image_base64(p)
if b64:
images.append(b64)
if images:
payload["images"] = images
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return r.json().get("response", "AI 生成失败")
except Exception as e:
return f"AI 调用失败:{str(e)}"
def ai_short_advice(prompt_text):
prompt = f"""
你是交易风控助理请用中文给出**最多 3 **提醒要求
- 每条不超过 25 个字
- 语气克制具体可执行
- 不要输出 Markdown不要编号前缀以外的废话
场景
{prompt_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return (r.json().get("response") or "").strip()
except Exception:
return ""
def _load_font(size):
if not ImageFont:
return None
@@ -1101,16 +1045,10 @@ JSON 字段:
"note": ""
}
""".strip()
payload = {
"model": AI_MODEL,
"prompt": prompt,
"images": [image_b64],
"stream": False,
"options": {"temperature": 0.1},
}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
raw = r.json().get("response", "")
raw = ai_generate(prompt, images_b64=[image_b64], temperature=0.1)
if raw.startswith("AI 调用失败"):
return {}
data = _extract_json_object(raw) or {}
if not isinstance(data, dict):
data = {}
@@ -5734,18 +5672,16 @@ def render_main_page(page="trade"):
f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"
)
strategy_extra = {}
if page in ("strategy_trend", "strategy_roll"):
if page in ("strategy", "strategy_trend", "strategy_roll"):
from strategy_ui import strategy_page_template_vars
strategy_extra = strategy_page_template_vars(
conn, page, default_risk_percent=float(RISK_PERCENT)
conn,
"strategy",
default_risk_percent=float(RISK_PERCENT),
request_obj=request,
trend_cfg=app.extensions.get("strategy_trend_cfg"),
)
if page == "strategy_trend":
cfg = app.extensions.get("strategy_trend_cfg")
if cfg:
from strategy_trend_register import load_trend_page_context
strategy_extra.update(load_trend_page_context(conn, request, cfg))
conn.close()
return render_template(
"index.html",
@@ -7739,16 +7675,23 @@ except Exception as _hub_err:
print(f"[hub_bridge] binance: {_hub_err}")
@app.route("/strategy")
@login_required
def strategy_trading_page():
return render_main_page("strategy")
@app.route("/strategy/trend")
@login_required
def strategy_trend_page():
return render_main_page("strategy_trend")
qs = request.query_string.decode()
return redirect(f"/strategy?{qs}" if qs else "/strategy")
@app.route("/strategy/roll")
@login_required
def strategy_roll_page():
return render_main_page("strategy_roll")
return redirect("/strategy")
from strategy_register import install_strategy_trading
+3 -5
View File
@@ -218,7 +218,7 @@
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
<a href="/strategy/trend" class="{% if page in ('strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
</div>
@@ -539,10 +539,8 @@
</div>
</div>
{% elif page == 'strategy_trend' %}
{% include 'strategy_trend_panel.html' %}
{% elif page == 'strategy_roll' %}
{% include 'strategy_roll_panel.html' %}
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% include 'strategy_trading_page.html' %}
{% endif %}
+5 -2
View File
@@ -141,9 +141,12 @@ FORCE_CLOSE_ENABLED=false
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 复盘服务地址(本机 Ollama 默认地址)
# AI 提供方:openai(默认)| ollama
AI_PROVIDER=openai
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
OLLAMA_API=http://127.0.0.1:11434/api/generate
# AI 模型名称
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# Gate 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
+19 -76
View File
@@ -34,6 +34,7 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from fib_key_monitor_lib import (
FIB_KEY_MONITOR_TYPES,
KEY_ENTRY_REASON_BY_SIGNAL,
@@ -207,8 +208,6 @@ BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER", "1.0"))
BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT", "0.02"))
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R", "1.0"))
DEFAULT_TRADE_STYLE = (os.getenv("DEFAULT_TRADE_STYLE", "trend") or "trend").strip().lower()
OLLAMA_API = os.getenv("OLLAMA_API", "http://127.0.0.1:11434/api/generate")
AI_MODEL = os.getenv("AI_MODEL", "huihui_ai/deepseek-r1-abliterated:latest")
GATE_SOCKS_PROXY = (os.getenv("GATE_SOCKS_PROXY") or "").strip()
GATE_HTTP_PROXY = (os.getenv("GATE_HTTP_PROXY") or "").strip()
@@ -527,61 +526,6 @@ def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
return "\n".join(lines) + "\n"
def ai_review(trades_text, period_title, image_paths=None):
prompt = f"""
你是一位专业交易教练下面是用户的{period_title}交易记录请做简洁可执行的复盘中文
硬性规则 必须遵守
- 你只能根据交易记录**明确出现的字段**陈述事实禁止编造是否触发止损是否扛单亏损是否扩大图上具体结构/进出场点位等记录里**没有**的信息
- 平仓/离场只是交易员自述摘要不是客观成交明细若记录未写明代币是否打到止损价是否软件平仓等不要断言执行路径可用在记录有限前提下一种可能是或简短写执行路径记录不足无法判断
- 提前离场类结论必须优先依据记录中的提前离场记录字段若该段全为或未出现有效内容不得写道明显扛单拒不止损未执行硬止损
- 实际RR为负只说明结果相对于预期RR不利不等同于风控失灵止损纪律崩溃除非记录里另有依据
- 禁止用语人身攻击夸张定性致命伤灾难语气克制对事不对人
- 若有截图且你能辨认再结合图讨论看不清或无明确定位则明确说无法从图确认不得虚构 K 线故事
输出结构
1. 总体盈亏结构紧扣笔数盈亏数字与 RR少形容词
2. 心态与执行每笔 110 + 一句依据依据必须对应记录字段
3. 行为标签提前离场 / 乱开仓 / 扛单等仅在有字段或自述支撑时点名否则写记录未勾选或未描述不作强加
4. 改进建议最多 3 每条具体可执行
5. 图表若有且可读结合价格行为简述否则一两句说明无法看图分析
交易记录
{trades_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
images = []
for p in image_paths or []:
b64 = _read_image_base64(p)
if b64:
images.append(b64)
if images:
payload["images"] = images
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return r.json().get("response", "AI 生成失败")
except Exception as e:
return f"AI 调用失败:{str(e)}"
def ai_short_advice(prompt_text):
prompt = f"""
你是交易风控助理请用中文给出**最多 3 **提醒要求
- 每条不超过 25 个字
- 语气克制具体可执行
- 不要输出 Markdown不要编号前缀以外的废话
场景
{prompt_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return (r.json().get("response") or "").strip()
except Exception:
return ""
def _load_font(size):
if not ImageFont:
return None
@@ -1095,16 +1039,10 @@ JSON 字段:
"note": ""
}
""".strip()
payload = {
"model": AI_MODEL,
"prompt": prompt,
"images": [image_b64],
"stream": False,
"options": {"temperature": 0.1},
}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
raw = r.json().get("response", "")
raw = ai_generate(prompt, images_b64=[image_b64], temperature=0.1)
if raw.startswith("AI 调用失败"):
return {}
data = _extract_json_object(raw) or {}
if not isinstance(data, dict):
data = {}
@@ -5739,18 +5677,16 @@ def render_main_page(page="trade"):
f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"
)
strategy_extra = {}
if page in ("strategy_trend", "strategy_roll"):
if page in ("strategy", "strategy_trend", "strategy_roll"):
from strategy_ui import strategy_page_template_vars
strategy_extra = strategy_page_template_vars(
conn, page, default_risk_percent=float(RISK_PERCENT)
conn,
"strategy",
default_risk_percent=float(RISK_PERCENT),
request_obj=request,
trend_cfg=app.extensions.get("strategy_trend_cfg"),
)
if page == "strategy_trend":
cfg = app.extensions.get("strategy_trend_cfg")
if cfg:
from strategy_trend_register import load_trend_page_context
strategy_extra.update(load_trend_page_context(conn, request, cfg))
conn.close()
return render_template(
"index.html",
@@ -7826,16 +7762,23 @@ except Exception as _hub_err:
print(f"[hub_bridge] gate: {_hub_err}")
@app.route("/strategy")
@login_required
def strategy_trading_page():
return render_main_page("strategy")
@app.route("/strategy/trend")
@login_required
def strategy_trend_page():
return render_main_page("strategy_trend")
qs = request.query_string.decode()
return redirect(f"/strategy?{qs}" if qs else "/strategy")
@app.route("/strategy/roll")
@login_required
def strategy_roll_page():
return render_main_page("strategy_roll")
return redirect("/strategy")
from strategy_register import install_strategy_trading
+3 -5
View File
@@ -218,7 +218,7 @@
<div class="top-nav">
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
<a href="/strategy/trend" class="{% if page in ('strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
</div>
@@ -539,10 +539,8 @@
</div>
</div>
{% elif page == 'strategy_trend' %}
{% include 'strategy_trend_panel.html' %}
{% elif page == 'strategy_roll' %}
{% include 'strategy_roll_panel.html' %}
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% include 'strategy_trading_page.html' %}
{% endif %}
+4 -2
View File
@@ -119,9 +119,11 @@ FORCE_CLOSE_ENABLED=false
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 复盘服务地址(本机 Ollama 默认地址)
AI_PROVIDER=openai
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
OLLAMA_API=http://127.0.0.1:11434/api/generate
# AI 模型名称
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# Gate 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
+24 -78
View File
@@ -34,6 +34,7 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from hub_auth import request_allowed as hub_request_allowed
from history_window_lib import (
PRESET_CUSTOM,
@@ -196,8 +197,6 @@ BREAKEVEN_RR_TRIGGER = float(os.getenv("BREAKEVEN_RR_TRIGGER", "1.0"))
BREAKEVEN_OFFSET_PCT = float(os.getenv("BREAKEVEN_OFFSET_PCT", "0.02"))
BREAKEVEN_STEP_R = float(os.getenv("BREAKEVEN_STEP_R", "1.0"))
DEFAULT_TRADE_STYLE = (os.getenv("DEFAULT_TRADE_STYLE", "trend") or "trend").strip().lower()
OLLAMA_API = os.getenv("OLLAMA_API", "http://127.0.0.1:11434/api/generate")
AI_MODEL = os.getenv("AI_MODEL", "huihui_ai/deepseek-r1-abliterated:latest")
GATE_SOCKS_PROXY = (os.getenv("GATE_SOCKS_PROXY") or "").strip()
GATE_HTTP_PROXY = (os.getenv("GATE_HTTP_PROXY") or "").strip()
@@ -516,61 +515,6 @@ def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
return "\n".join(lines) + "\n"
def ai_review(trades_text, period_title, image_paths=None):
prompt = f"""
你是一位专业交易教练下面是用户的{period_title}交易记录请做简洁可执行的复盘中文
硬性规则 必须遵守
- 你只能根据交易记录**明确出现的字段**陈述事实禁止编造是否触发止损是否扛单亏损是否扩大图上具体结构/进出场点位等记录里**没有**的信息
- 平仓/离场只是交易员自述摘要不是客观成交明细若记录未写明代币是否打到止损价是否软件平仓等不要断言执行路径可用在记录有限前提下一种可能是或简短写执行路径记录不足无法判断
- 提前离场类结论必须优先依据记录中的提前离场记录字段若该段全为或未出现有效内容不得写道明显扛单拒不止损未执行硬止损
- 实际RR为负只说明结果相对于预期RR不利不等同于风控失灵止损纪律崩溃除非记录里另有依据
- 禁止用语人身攻击夸张定性致命伤灾难语气克制对事不对人
- 若有截图且你能辨认再结合图讨论看不清或无明确定位则明确说无法从图确认不得虚构 K 线故事
输出结构
1. 总体盈亏结构紧扣笔数盈亏数字与 RR少形容词
2. 心态与执行每笔 110 + 一句依据依据必须对应记录字段
3. 行为标签提前离场 / 乱开仓 / 扛单等仅在有字段或自述支撑时点名否则写记录未勾选或未描述不作强加
4. 改进建议最多 3 每条具体可执行
5. 图表若有且可读结合价格行为简述否则一两句说明无法看图分析
交易记录
{trades_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
images = []
for p in image_paths or []:
b64 = _read_image_base64(p)
if b64:
images.append(b64)
if images:
payload["images"] = images
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return r.json().get("response", "AI 生成失败")
except Exception as e:
return f"AI 调用失败:{str(e)}"
def ai_short_advice(prompt_text):
prompt = f"""
你是交易风控助理请用中文给出**最多 3 **提醒要求
- 每条不超过 25 个字
- 语气克制具体可执行
- 不要输出 Markdown不要编号前缀以外的废话
场景
{prompt_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return (r.json().get("response") or "").strip()
except Exception:
return ""
def _load_font(size):
if not ImageFont:
return None
@@ -1072,16 +1016,10 @@ JSON 字段:
"note": ""
}
""".strip()
payload = {
"model": AI_MODEL,
"prompt": prompt,
"images": [image_b64],
"stream": False,
"options": {"temperature": 0.1},
}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
raw = r.json().get("response", "")
raw = ai_generate(prompt, images_b64=[image_b64], temperature=0.1)
if raw.startswith("AI 调用失败"):
return {}
data = _extract_json_object(raw) or {}
if not isinstance(data, dict):
data = {}
@@ -5291,8 +5229,9 @@ def render_main_page(page="trade"):
preview_expires_ms = None
trend_preview_expired = False
trend_preview_id_arg = ""
if page == "strategy_trend":
if page in ("strategy", "strategy_trend", "strategy_roll"):
_trend_cleanup_stale_previews(conn)
if page in ("strategy", "strategy_trend"):
trend_preview_id_arg = (request.args.get("preview_id") or "").strip()
if trend_preview_id_arg:
pr = conn.execute(
@@ -5313,7 +5252,7 @@ def render_main_page(page="trade"):
elif pr:
trend_preview_expired = True
strategy_extra = {}
if page == "strategy_roll":
if page in ("strategy", "strategy_trend", "strategy_roll"):
from strategy_ui import fetch_roll_page_data
strategy_extra = fetch_roll_page_data(
@@ -6205,17 +6144,17 @@ def preview_trend_pullback():
if not okp:
conn.close()
flash(reasonp)
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
ok_live, reason_live = ensure_exchange_live_ready()
if not ok_live:
conn.close()
flash(reason_live)
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
payload, err = parse_and_compute_trend_pullback_plan(request.form)
if err:
conn.close()
flash(err)
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
pid = str(uuid.uuid4())
exp_ms = int(time.time() * 1000) + int(TREND_PULLBACK_PREVIEW_TTL_SECONDS) * 1000
created = app_now_str()
@@ -6263,7 +6202,7 @@ def execute_trend_pullback():
pid = (request.form.get("preview_id") or "").strip()
if not pid:
flash("缺少预览 ID")
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
conn = get_db()
_trend_cleanup_stale_previews(conn)
pr = conn.execute("SELECT * FROM trend_pullback_previews WHERE id=?", (pid,)).fetchone()
@@ -6271,7 +6210,7 @@ def execute_trend_pullback():
if not pr or int(pr["expires_at_ms"] or 0) < now_ms:
conn.close()
flash("预览已过期或不存在,请重新生成预览")
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
okp, reasonp = precheck_trend_pullback_start(conn)
if not okp:
conn.close()
@@ -6294,7 +6233,7 @@ def execute_trend_pullback():
flash(
f"当前可用余额与预览快照偏差 {drift_pct:.2f}%,超过允许 {TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT}% ,请重新生成预览"
)
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
symbol = pr["symbol"]
exchange_symbol = pr["exchange_symbol"]
direction = pr["direction"] or "long"
@@ -6411,7 +6350,7 @@ def trend_pullback_breakeven(pid):
raise ValueError
except ValueError:
flash("保本偏移% 格式无效")
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
conn = get_db()
row = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (pid,)
@@ -6419,7 +6358,7 @@ def trend_pullback_breakeven(pid):
if not row:
conn.close()
flash("未找到运行中的趋势回调计划")
return redirect(url_for("strategy_trend_page"))
return redirect(url_for("strategy_trading_page"))
ok, err = apply_trend_pullback_manual_breakeven(conn, row, offset_pct=offset_pct)
conn.commit()
conn.close()
@@ -7381,16 +7320,23 @@ except Exception as _hub_err:
print(f"[hub_bridge] gate_bot: {_hub_err}")
@app.route("/strategy")
@login_required
def strategy_trading_page():
return render_main_page("strategy")
@app.route("/strategy/trend")
@login_required
def strategy_trend_page():
return render_main_page("strategy_trend")
qs = request.query_string.decode()
return redirect(f"/strategy?{qs}" if qs else "/strategy")
@app.route("/strategy/roll")
@login_required
def strategy_roll_page():
return render_main_page("strategy_roll")
return redirect("/strategy")
from strategy_register import install_strategy_trading
+6 -6
View File
@@ -85,7 +85,9 @@
.detail-modal .panel-image{margin-top:10px;max-width:min(100%,680px);border-radius:8px;cursor:pointer;border:1px solid #2a3150}
.table-wrap{overflow-x:auto}
.trade-dashboard{grid-column:1/-1;display:flex;flex-direction:column;gap:14px}
.trade-panels-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.trade-panels-row,.dual-panel-grid,.strategy-trading-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.strategy-trading-grid .card{min-height:320px;display:flex;flex-direction:column}
.strategy-trading-grid .panel-scroll{flex:1;overflow:auto;max-height:78vh}
.trade-panels-row > .card{min-height:0;height:100%;display:flex;flex-direction:column;box-sizing:border-box}
.trade-panels-row > .trend-card{gap:12px}
.trade-panels-row > .order-card .order-live-positions{margin-top:auto;flex:0 1 auto;min-height:0}
@@ -205,7 +207,7 @@
</div>
<div class="top-nav">
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
<a href="/strategy/trend" class="{% if page in ('strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
</div>
@@ -370,11 +372,9 @@
</div>
</div>
</div>
{% elif page == 'strategy_trend' %}
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% set can_trade_trend = can_trade %}
{% include 'strategy_trend_panel.html' %}
{% elif page == 'strategy_roll' %}
{% include 'strategy_roll_panel.html' %}
{% include 'strategy_trading_page.html' %}
{% endif %}
{% if page == 'records' %}
+4 -1
View File
@@ -100,8 +100,11 @@ WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 复盘服务地址(本机 Ollama 默认地址)
AI_PROVIDER=openai
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
OLLAMA_API=http://127.0.0.1:11434/api/generate
# AI 模型名称
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# OKX 代理(可选):用于本机网络对 OKX TLS/SNI 不稳定时,通过 SSH 动态转发 SOCKS5 出口
+19 -66
View File
@@ -34,6 +34,7 @@ import sys
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
from ai_client import ai_generate, ai_review, ai_short_advice
from fib_key_monitor_lib import (
FIB_KEY_MONITOR_TYPES,
calc_fib_plan,
@@ -190,8 +191,6 @@ KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_
"on",
)
DEFAULT_TRADE_STYLE = (os.getenv("DEFAULT_TRADE_STYLE", "trend") or "trend").strip().lower()
OLLAMA_API = os.getenv("OLLAMA_API", "http://127.0.0.1:11434/api/generate")
AI_MODEL = os.getenv("AI_MODEL", "huihui_ai/deepseek-r1-abliterated:latest")
OKX_SOCKS_PROXY = (os.getenv("OKX_SOCKS_PROXY") or "").strip()
OKX_HTTP_PROXY = (os.getenv("OKX_HTTP_PROXY") or "").strip()
@@ -441,51 +440,6 @@ def _extract_json_object(text):
return None
def ai_review(trades_text, period_title, image_paths=None):
prompt = f"""
你是一位专业交易教练下面是用户的{period_title}交易记录请做专业简洁可执行的复盘
1. 总体盈亏结构
2. 心态问题与执行漏洞请给每笔交易一个1-10的心态分并简短说明
3. 提前离场乱开仓扛单等行为分析
4. 给出具体改进建议最多3条
5. 若附带截图请结合图中价格行为结构进出场位置一起分析看不清时请明确说明不确定
交易记录
{trades_text}
用中文输出直接给结论与建议
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.3}}
images = []
for p in image_paths or []:
b64 = _read_image_base64(p)
if b64:
images.append(b64)
if images:
payload["images"] = images
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return r.json().get("response", "AI 生成失败")
except Exception as e:
return f"AI 调用失败:{str(e)}"
def ai_short_advice(prompt_text):
prompt = f"""
你是交易风控助理请用中文给出**最多 3 **提醒要求
- 每条不超过 25 个字
- 语气克制具体可执行
- 不要输出 Markdown不要编号前缀以外的废话
场景
{prompt_text}
""".strip()
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
return (r.json().get("response") or "").strip()
except Exception:
return ""
def _load_font(size):
if not ImageFont:
return None
@@ -985,16 +939,10 @@ JSON 字段:
"note": ""
}
""".strip()
payload = {
"model": AI_MODEL,
"prompt": prompt,
"images": [image_b64],
"stream": False,
"options": {"temperature": 0.1},
}
try:
r = requests.post(OLLAMA_API, json=payload, timeout=AI_TIMEOUT_SECONDS)
raw = r.json().get("response", "")
raw = ai_generate(prompt, images_b64=[image_b64], temperature=0.1)
if raw.startswith("AI 调用失败"):
return {}
data = _extract_json_object(raw) or {}
if not isinstance(data, dict):
data = {}
@@ -4235,18 +4183,16 @@ def render_main_page(page="trade"):
f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"
)
strategy_extra = {}
if page in ("strategy_trend", "strategy_roll"):
if page in ("strategy", "strategy_trend", "strategy_roll"):
from strategy_ui import strategy_page_template_vars
strategy_extra = strategy_page_template_vars(
conn, page, default_risk_percent=float(RISK_PERCENT)
conn,
"strategy",
default_risk_percent=float(RISK_PERCENT),
request_obj=request,
trend_cfg=app.extensions.get("strategy_trend_cfg"),
)
if page == "strategy_trend":
cfg = app.extensions.get("strategy_trend_cfg")
if cfg:
from strategy_trend_register import load_trend_page_context
strategy_extra.update(load_trend_page_context(conn, request, cfg))
conn.close()
return render_template(
"index.html",
@@ -6006,16 +5952,23 @@ except Exception as _hub_err:
print(f"[hub_bridge] okx: {_hub_err}")
@app.route("/strategy")
@login_required
def strategy_trading_page():
return render_main_page("strategy")
@app.route("/strategy/trend")
@login_required
def strategy_trend_page():
return render_main_page("strategy_trend")
qs = request.query_string.decode()
return redirect(f"/strategy?{qs}" if qs else "/strategy")
@app.route("/strategy/roll")
@login_required
def strategy_roll_page():
return render_main_page("strategy_roll")
return redirect("/strategy")
from strategy_register import install_strategy_trading
+3 -5
View File
@@ -154,7 +154,7 @@
<div class="header"><h1>加密货币|交易监控 + AI复盘一体化</h1></div>
<div class="top-nav">
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">交易执行</a>
<a href="/strategy/trend" class="{% if page in ('strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
</div>
@@ -352,10 +352,8 @@
{% endfor %}
</div>
</div>
{% elif page == 'strategy_trend' %}
{% include 'strategy_trend_panel.html' %}
{% elif page == 'strategy_roll' %}
{% include 'strategy_roll_panel.html' %}
{% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
{% include 'strategy_trading_page.html' %}
{% endif %}
{% if page == 'records' %}
+2 -2
View File
@@ -60,7 +60,7 @@ def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None:
)
else:
flash(err.get("msg") or "预览失败")
return redirect(url_for("strategy_roll_page"))
return redirect(url_for("strategy_trading_page"))
@_lr
@app.route("/strategy/roll/execute", methods=["POST"])
@@ -68,7 +68,7 @@ def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None:
data = request.form
ok, msg = _roll_execute(cfg, data)
flash(msg)
return redirect(url_for("strategy_roll_page"))
return redirect(url_for("strategy_trading_page"))
# 趋势回调:仍由各市面 app 注册原有路由;导航指向 /strategy/trend
+4 -9
View File
@@ -1,6 +1,5 @@
{% include 'strategy_subnav.html' %}
<div class="card" style="grid-column:1/-1">
<h2 style="margin:0 0 8px">顺势加仓(滚仓)</h2>
<div class="strategy-panel-inner">
<h2 style="margin:0 0 8px">顺势加仓</h2>
<div class="rule-tip">
<strong>仅人工加仓</strong>,程序不会自动触发。须先在「实盘下单」有同向持仓。<br>
做多最多滚仓 <strong>3</strong> 次;止盈<strong>锁定首仓</strong>不变;每次填写<strong>新统一止损</strong>,总风险%按「合并持仓打到新止损≈账户风险」反推张数。<br>
@@ -30,10 +29,8 @@
<button type="submit" {% if roll_trend_active %}disabled style="opacity:.5"{% endif %} onclick="return confirm('确认按预览逻辑实盘加仓并更新止损?')">执行滚仓</button>
</form>
<p class="rule-tip" style="margin-top:8px">执行前可用开发者工具 POST <code>/strategy/roll/preview</code> 查看 JSON 预览。</p>
</div>
<div class="card" style="grid-column:1/-1">
<h3 style="margin:0 0 8px">活跃滚仓组</h3>
<h3 style="margin:14px 0 8px;font-size:.95rem;color:#b8c4ff">活跃滚仓组</h3>
<div class="table-wrap">
<table>
<tr><th>ID</th><th>币种</th><th>方向</th><th>腿数</th><th>首仓TP</th><th>当前SL</th></tr>
@@ -51,10 +48,8 @@
{% endfor %}
</table>
</div>
</div>
<div class="card" style="grid-column:1/-1">
<h3 style="margin:0 0 8px">最近滚仓腿</h3>
<h3 style="margin:14px 0 8px;font-size:.95rem;color:#b8c4ff">最近滚仓腿</h3>
<div class="table-wrap">
<table>
<tr><th>#</th><th></th><th>方式</th><th>张数</th><th>新SL</th><th>状态</th></tr>
@@ -0,0 +1,12 @@
<div class="dual-panel-grid trade-panels-row strategy-trading-grid" style="grid-column:1/-1;align-items:stretch">
<div class="card strategy-panel-trend" style="display:flex;flex-direction:column;min-height:320px">
<div class="panel-scroll" style="flex:1;max-height:78vh;overflow:auto">
{% include 'strategy_trend_panel.html' %}
</div>
</div>
<div class="card strategy-panel-roll" style="display:flex;flex-direction:column;min-height:320px">
<div class="panel-scroll" style="flex:1;max-height:78vh;overflow:auto">
{% include 'strategy_roll_panel.html' %}
</div>
</div>
</div>
+2 -3
View File
@@ -1,8 +1,7 @@
{% set mf = money_fmt|default(funds_fmt) %}
{% macro amt_disp(sym, val) %}{% if amt_fmt is defined %}{{ amt_fmt(sym, val) }}{% else %}{{ val }}{% endif %}{% endmacro %}
{% include 'strategy_subnav.html' %}
<div class="card trend-card" style="grid-column:1/-1">
<h2 style="margin-bottom:8px">趋势回调策略</h2>
<div class="strategy-panel-inner trend-card">
<h2 style="margin-bottom:8px">趋势回调</h2>
<div class="rule-tip">
<strong>生成预览</strong>:读取合约 USDT <strong>可用余额快照</strong>并计算计划(不下单)。预览有效期 <strong>{{ trend_pullback_preview_ttl }} 秒</strong><br>
<strong>确认执行</strong>:市价首仓 50% + 挂交易所止损;首仓后可<strong>手动保本</strong>(默认均价+{{ trend_manual_breakeven_offset_pct }}%);剩余 50% 在止损与补仓区间之间共 {{ trend_pullback_dca_legs }} 档(做多为<strong>上沿</strong>、做空为<strong>下沿</strong>;程序可能因最小张数自动减档)市价补仓;<strong>止盈由程序监控</strong><br>
+1 -1
View File
@@ -569,7 +569,7 @@ def register_trend_routes(app: Flask, cfg: dict) -> None:
get_db = cfg["get_db"]
def _redirect_trend(**kw):
return redirect(url_for("strategy_trend_page", **kw))
return redirect(url_for("strategy_trading_page", **kw))
@app.route("/preview_trend_pullback", methods=["POST"])
@lr
+16 -11
View File
@@ -73,16 +73,21 @@ def strategy_page_template_vars(
default_risk_percent: float = 2.0,
count_active_trends: Optional[Callable] = None,
trend_disabled_note: str = "",
request_obj=None,
trend_cfg: Optional[dict] = None,
) -> dict[str, Any]:
"""render_main_page 在 conn.close() 前合并进 render_template 的变量。"""
if page == "strategy_roll":
return fetch_roll_page_data(
conn,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
)
if page == "strategy_trend":
return {
"trend_disabled_note": trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE,
}
return {}
if page not in ("strategy", "strategy_trend", "strategy_roll"):
return {}
out = fetch_roll_page_data(
conn,
default_risk_percent=default_risk_percent,
count_active_trends=count_active_trends,
)
if trend_cfg and request_obj is not None:
from strategy_trend_register import load_trend_page_context
out.update(load_trend_page_context(conn, request_obj, trend_cfg))
elif page == "strategy_trend":
out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE
return out
+5 -5
View File
@@ -29,12 +29,12 @@ strategy_templates/ # 主站内嵌 panelsubnav、roll、trend 禁用
## 二、导航与页面
顶栏一项 **「策略交易」**(高亮含 `/strategy/trend``/strategy/roll`),页内子导航切换:
顶栏 **「策略交易」**`/strategy`:页内 **左右并列** 两张卡片(趋势回调 | 顺势加仓),布局与「实盘下单」双栏一致。旧链接 `/strategy/trend``/strategy/roll` 会自动跳转到 `/strategy`
| 路由 | 子 Tab | 说明 |
|------|--------|------|
| `/strategy/trend` | 趋势回调 | **币安 / Gate / OKX / gate_bot 四所均可**(预览、执行、自动补仓、程序止盈) |
| `/strategy/roll` | 顺势加仓 | **四所均可用**(须已有同向持仓),与实盘页同一布局 |
| 区域 | 说明 |
|------|------|
| 左栏 · 趋势回调 | **四所均可**(预览、执行、自动补仓、程序止盈) |
| 右栏 · 顺势加仓 | 须已有同向持仓;滚仓组/历史表在右栏内滚动 |
| `/trade` | 实盘下单 | 首仓、以损定仓、移动保本(不变) |
各所 `app.py` 注册 `@app.route("/strategy/trend|roll")``render_main_page(...)``install_strategy_trading` 仅注册滚仓 POST API。