diff --git a/ai_client.py b/ai_client.py new file mode 100644 index 0000000..1db9584 --- /dev/null +++ b/ai_client.py @@ -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}" diff --git a/crypto_monitor_binance/.env.example b/crypto_monitor_binance/.env.example index f15daeb..d5487a4 100644 --- a/crypto_monitor_binance/.env.example +++ b/crypto_monitor_binance/.env.example @@ -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 +# 本机 Ollama(AI_PROVIDER=ollama 时使用) OLLAMA_API=http://127.0.0.1:11434/api/generate -# AI 模型名称 AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest # Binance 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口 diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 00c2a34..44e1354 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -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. 心态与执行(每笔 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 @@ -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 diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 83f0037..9ffe01c 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -218,7 +218,7 @@
@@ -539,10 +539,8 @@ - {% 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 %} diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index fdead10..fd4f819 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -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 出口 diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 3140f4d..02999e6 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -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. 心态与执行(每笔 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 @@ -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 diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 6d0cd2d..bce99a2 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -218,7 +218,7 @@ @@ -539,10 +539,8 @@ - {% 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 %} diff --git a/crypto_monitor_gate_bot/.env.example b/crypto_monitor_gate_bot/.env.example index 236b373..cf64475 100644 --- a/crypto_monitor_gate_bot/.env.example +++ b/crypto_monitor_gate_bot/.env.example @@ -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 出口 diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 455c392..5b7dd2d 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -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 diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 142ff9d..c742305 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -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 @@ @@ -370,11 +372,9 @@ - {% 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' %} diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 09bd34c..9527787 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -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 出口 diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index f03ac8c..3535c41 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -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 diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index bd7f0ee..0557b40 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -154,7 +154,7 @@执行前可用开发者工具 POST /strategy/roll/preview 查看 JSON 预览。
| ID | 币种 | 方向 | 腿数 | 首仓TP | 当前SL |
|---|
| # | 组 | 方式 | 张数 | 新SL | 状态 |
|---|