修复web前端,增加openai
This commit is contained in:
@@ -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 出口
|
||||
|
||||
@@ -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. 心态与执行(每笔 1–10 分 + 一句依据;依据必须对应记录字段)
|
||||
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
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
Reference in New Issue
Block a user