10 Commits

Author SHA1 Message Date
dekun eec57610dc fix: 注册 symbol_live_price.js 静态路由(现价不显示)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 00:46:23 +08:00
dekun 3a740235ac feat: 币种输入旁实时显示交易所现价
Shared SymbolLivePrice polls /api/order_defaults on input with debounce; wired to trade, key monitor, trend, and key focus forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 00:42:44 +08:00
dekun efe7a57e60 fix: trade_policy 宏使用 with context 修复 500
Jinja imported macros did not receive trade_policy from render context, causing Internal Server Error on all instance pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 00:36:06 +08:00
dekun c0ad50a7b5 feat: 账户方向与币种白名单 env 开关(三所)
Per-instance TRADE_DIRECTION / TRADE_SYMBOL_WHITELIST restricts UI and API for manual orders, key monitors, and strategies; includes sync script for deployment profiles.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 00:30:49 +08:00
dekun 65b911994c fix: 今日统计数字改用常规字体,去掉发光效果
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 23:16:22 +08:00
dekun 9deb58a38a feat: 监控区 2x2 布局与左上今日统计卡
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 23:10:32 +08:00
dekun eb975b0133 fix: 交易安全审计修复 — 补偿平仓、中控同步、滚仓/趋势防护
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 22:44:16 +08:00
dekun df28e6dfb8 fix: ccxt Gate 类名 gateio 改为兼容 gate
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 22:21:33 +08:00
dekun 4923b32bbe feat: 新增 Plan B 整目录重装脚本,不影响 setup_env 一键安装
添加 deploy/reinstall.sh 备份 env、克隆、调 setup_env、恢复配置并 PM2 启动;
附带 pm2_start_all.sh 与 hub_settings 清理工具。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 22:07:59 +08:00
dekun 9f67de3677 refactor: 移除 gate_bot,统一为三所架构并更新文档
删除 crypto_monitor_gate_bot 目录,中控与子代理改为 binance/okx/gate 三账户;
文档与 UI 文案「四所」改为「三所」;新增清库前一次性配置备份脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 22:00:08 +08:00
175 changed files with 29193 additions and 40274 deletions
+1
View File
@@ -23,6 +23,7 @@ manual_trading_hub/hub_ai_summaries.json
manual_trading_hub/hub_ai_chat.json manual_trading_hub/hub_ai_chat.json
manual_trading_hub/hub_ai_fund_history.json manual_trading_hub/hub_ai_fund_history.json
manual_trading_hub/data/ manual_trading_hub/data/
backups/
# 数据库与上传(运行时生成) # 数据库与上传(运行时生成)
**/*.sqlite **/*.sqlite
+2 -2
View File
@@ -1,6 +1,6 @@
# AI 复盘与模型配置说明 # AI 复盘与模型配置说明
`crypto_monitor_*` 实例共用仓库根目录 **`ai_client.py`**(通过 `PYTHONPATH=..` 导入)。用于 **交易记录与复盘** 页的 AI 点评、短评建议,以及从复盘截图提取结构化 JSON。 `crypto_monitor_*` 实例共用仓库根目录 **`ai_client.py`**(通过 `PYTHONPATH=..` 导入)。用于 **交易记录与复盘** 页的 AI 点评、短评建议,以及从复盘截图提取结构化 JSON。
--- ---
@@ -36,7 +36,7 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
### Ollama ### Ollama
- 需本机已安装并拉取对应模型;`AI_PROVIDER=ollama` 时使用 `OLLAMA_API``AI_MODEL` - 需本机已安装并拉取对应模型;`AI_PROVIDER=ollama` 时使用 `OLLAMA_API``AI_MODEL`
- `app.py` **不再** 直连 Ollama;统一走 `ai_client.ai_generate` / `ai_review` / `ai_short_advice` - `app.py` **不再** 直连 Ollama;统一走 `ai_client.ai_generate` / `ai_review` / `ai_short_advice`
--- ---
+5 -6
View File
@@ -1,6 +1,6 @@
# 复盘交易系统(crypto_monitor # 复盘交易系统(crypto_monitor
多交易所 **USDT 永续** 的下单监控、**关键位**、**策略交易**、**止盈止损 / 移动保本** 与 **AI 复盘**所独立部署 + 可选 **中控** 聚合监控。 多交易所 **USDT 永续** 的下单监控、**关键位**、**策略交易**、**止盈止损 / 移动保本** 与 **AI 复盘**所独立部署 + 可选 **中控** 聚合监控。
**远程仓库**[https://git.bz121.com/dekun/crypto_monitor.git](https://git.bz121.com/dekun/crypto_monitor.git) **远程仓库**[https://git.bz121.com/dekun/crypto_monitor.git](https://git.bz121.com/dekun/crypto_monitor.git)
@@ -35,7 +35,7 @@ bash deploy/setup_env.sh --install-system-deps
|------|------|------| |------|------|------|
| **关键位监控** | 箱体/收敛自动开仓、阻力支撑提醒、斐波限价;止盈止损方案与 **移动保本** 开关 | 各所 [关键位自动下单说明.md](./crypto_monitor_binance/关键位自动下单说明.md)(Gate/OKX 目录内同名);方案细则 **[关键位止盈止损与移动保本更新说明.md](./关键位止盈止损与移动保本更新说明.md)** | | **关键位监控** | 箱体/收敛自动开仓、阻力支撑提醒、斐波限价;止盈止损方案与 **移动保本** 开关 | 各所 [关键位自动下单说明.md](./crypto_monitor_binance/关键位自动下单说明.md)(Gate/OKX 目录内同名);方案细则 **[关键位止盈止损与移动保本更新说明.md](./关键位止盈止损与移动保本更新说明.md)** |
| **实盘下单 / 下单监控** | 首仓、以损定仓;监控内 **止盈 / 止损**、**移动保本**(步进 R、偏移%) | 各所 [使用说明.md](./crypto_monitor_binance/使用说明.md) · 顶栏「实盘下单」`/trade` | | **实盘下单 / 下单监控** | 首仓、以损定仓;监控内 **止盈 / 止损**、**移动保本**(步进 R、偏移%) | 各所 [使用说明.md](./crypto_monitor_binance/使用说明.md) · 顶栏「实盘下单」`/trade` |
| **策略交易** | **趋势回调** + **顺势加仓**`/strategy` 双栏) | **[策略交易说明.md](./策略交易说明.md)** · 趋势细则 [crypto_monitor_gate_bot/趋势回调策略说明.md](./crypto_monitor_gate_bot/趋势回调策略说明.md) | | **策略交易** | **趋势回调** + **顺势加仓**`/strategy` 双栏) | **[策略交易说明.md](./策略交易说明.md)** · 趋势细则 [docs/trend-pullback-strategy.md](./docs/trend-pullback-strategy.md) |
| **策略交易记录** | 已结束计划快照(最近 100 条)、筛选与展开详情 | [策略交易说明.md §五](./策略交易说明.md) · 顶栏 `/strategy/records` | | **策略交易记录** | 已结束计划快照(最近 100 条)、筛选与展开详情 | [策略交易说明.md §五](./策略交易说明.md) · 顶栏 `/strategy/records` |
| **交易复盘** | 平仓记录、错过机会、图表;**AI 点评** | **[AI复盘与模型配置说明.md](./AI复盘与模型配置说明.md)** · 顶栏「交易记录与复盘」`/records` | | **交易复盘** | 平仓记录、错过机会、图表;**AI 点评** | **[AI复盘与模型配置说明.md](./AI复盘与模型配置说明.md)** · 顶栏「交易记录与复盘」`/records` |
| **中控** | 多账户持仓/委托聚合、行情 K 线、紧急全平(**不在中控网页下单**) | [manual_trading_hub/使用说明.md](./manual_trading_hub/使用说明.md) · [部署文档.md](./manual_trading_hub/部署文档.md) | | **中控** | 多账户持仓/委托聚合、行情 K 线、紧急全平(**不在中控网页下单**) | [manual_trading_hub/使用说明.md](./manual_trading_hub/使用说明.md) · [部署文档.md](./manual_trading_hub/部署文档.md) |
@@ -49,8 +49,7 @@ bash deploy/setup_env.sh --install-system-deps
| 目录 | 交易所 / 角色 | 部署文档 | | 目录 | 交易所 / 角色 | 部署文档 |
|------|----------------|----------| |------|----------------|----------|
| `crypto_monitor_binance/` | Binance U 本位永续 | [部署文档.md](./crypto_monitor_binance/部署文档.md) | | `crypto_monitor_binance/` | Binance U 本位永续 | [部署文档.md](./crypto_monitor_binance/部署文档.md) |
| `crypto_monitor_gate/` | Gate 主号 | [部署文档.md](./crypto_monitor_gate/部署文档.md) | | `crypto_monitor_gate/` | Gate | [部署文档.md](./crypto_monitor_gate/部署文档.md) |
| `crypto_monitor_gate_bot/` | Gate 机器人 / 趋势户 | [部署文档.md](./crypto_monitor_gate_bot/部署文档.md) |
| `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) | | `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) |
| `manual_trading_hub/` | 中控 + 子代理 | [部署文档.md](./manual_trading_hub/部署文档.md) | | `manual_trading_hub/` | 中控 + 子代理 | [部署文档.md](./manual_trading_hub/部署文档.md) |
| `lib/` | **共用模块**(策略、关键位、交易、中控库、AI、静态与模板) | **[docs/lib-structure.md](./docs/lib-structure.md)** | | `lib/` | **共用模块**(策略、关键位、交易、中控库、AI、静态与模板) | **[docs/lib-structure.md](./docs/lib-structure.md)** |
@@ -64,7 +63,7 @@ bash deploy/setup_env.sh --install-system-deps
## 技术要点 ## 技术要点
- **Python 3.10+**、Flask、ccxt、SQLite`crypto.db` - **Python 3.10+**、Flask、ccxt、SQLite`crypto.db`
- `.env` 前缀不同(`BINANCE_*` / `GATE_*` / `OKX_*`),**不可混用** - `.env` 前缀不同(`BINANCE_*` / `GATE_*` / `OKX_*`),**不可混用**
- 实盘须 `LIVE_TRADING_ENABLED=true` 且理解 API 权限与 IP 白名单风险 - 实盘须 `LIVE_TRADING_ENABLED=true` 且理解 API 权限与 IP 白名单风险
-**SOCKS** 访问交易所时配置各所 `*_SOCKS_PROXY` 并安装 PySocks -**SOCKS** 访问交易所时配置各所 `*_SOCKS_PROXY` 并安装 PySocks
@@ -72,7 +71,7 @@ bash deploy/setup_env.sh --install-system-deps
## 推荐阅读顺序 ## 推荐阅读顺序
1. [docs/ubuntu-server.md](./docs/ubuntu-server.md) — 装 Python / Node / PM2PM2 启动所 + 中控 1. [docs/ubuntu-server.md](./docs/ubuntu-server.md) — 装 Python / Node / PM2PM2 启动所 + 中控
2. 各所 **`.env`**(从 `.env.example` 复制) 2. 各所 **`.env`**(从 `.env.example` 复制)
3. 所用功能对应上表 **功能导航** 文档 3. 所用功能对应上表 **功能导航** 文档
4. [备份与恢复.md](./备份与恢复.md) — 生产机备份习惯 4. [备份与恢复.md](./备份与恢复.md) — 生产机备份习惯
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "复盘系统中控", "name": "复盘系统中控",
"short_name": "中控", "short_name": "中控",
"description": "所交易监控与行情中控", "description": "所交易监控与行情中控",
"start_url": "/monitor", "start_url": "/monitor",
"display": "standalone", "display": "standalone",
"background_color": "#0b0e18", "background_color": "#0b0e18",
+7
View File
@@ -52,6 +52,13 @@ UPLOAD_DIR=static/images
# BINANCE_FUNDING_INCLUDE_SPOT=false # BINANCE_FUNDING_INCLUDE_SPOT=false
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启) # 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk POSITION_SIZING_MODE=risk
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数(U # 每天起始基数(U
DAILY_START_CAPITAL=30 DAILY_START_CAPITAL=30
# 日内回撤后基数(U # 日内回撤后基数(U
+157 -6
View File
@@ -130,6 +130,9 @@ from lib.key_monitor.trigger_entry_key_monitor_lib import (
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
acquire_trigger_entry_exec_lock,
is_trigger_entry_in_flight_row,
release_trigger_entry_exec_lock,
is_breakout_trigger_entry_key_monitor_type, is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
@@ -156,6 +159,14 @@ from lib.trade.position_sizing_lib import (
mode_label_zh, mode_label_zh,
risk_percent_for_storage, risk_percent_for_storage,
) )
from lib.trade.trade_policy_lib import load_trade_policy
from lib.trade.trade_policy_app_lib import (
check_direction_policy,
check_open_policy,
check_symbol_policy,
default_symbol_for_policy,
trade_policy_template_context,
)
from lib.key_monitor.key_monitor_full_margin_lib import ( from lib.key_monitor.key_monitor_full_margin_lib import (
monitor_type_disallowed_in_full_margin, monitor_type_disallowed_in_full_margin,
purge_disallowed_key_monitors, purge_disallowed_key_monitors,
@@ -339,6 +350,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
# 计仓模式:risk=以损定仓(默认);full_margin=合约可用保证金×比例全仓杠杆(仅 env 切换,须无仓) # 计仓模式:risk=以损定仓(默认);full_margin=合约可用保证金×比例全仓杠杆(仅 env 切换,须无仓)
POSITION_SIZING_MODE = load_position_sizing_mode() POSITION_SIZING_MODE = load_position_sizing_mode()
TRADE_POLICY = load_trade_policy()
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3")) MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
@@ -2032,6 +2044,12 @@ def normalize_symbol_input(symbol):
return f"{sym}/USDT" return f"{sym}/USDT"
def validate_trade_policy_open(symbol, direction):
return check_open_policy(
TRADE_POLICY, symbol, direction, normalize_symbol_input
)
def normalize_kline_limit(limit_raw, default=200): def normalize_kline_limit(limit_raw, default=200):
try: try:
n = int(limit_raw) n = int(limit_raw)
@@ -3469,6 +3487,40 @@ def ensure_markets_loaded(force=False):
MARKETS_LOADED = True MARKETS_LOADED = True
def _abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, planned_amount):
from lib.trade.compensating_close_lib import run_compensating_close
def _close():
ensure_markets_loaded()
try:
cancel_binance_futures_open_orders(exchange_symbol)
except Exception:
pass
live = get_live_position_contracts(exchange_symbol, direction)
amt = live if live is not None and live > 0 else _filled_amount_for_tpsl(order, planned_amount)
if amt is None or float(amt) <= 0:
return
side = "sell" if direction == "long" else "buy"
try:
amount = float(exchange.amount_to_precision(exchange_symbol, float(amt)))
except Exception:
amount = float(amt)
last_err = None
for params in _binance_market_close_param_candidates(direction):
try:
exchange.create_order(exchange_symbol, "market", side, amount, None, params)
return
except Exception as e:
last_err = e
if _is_binance_close_param_retryable(str(e)):
continue
raise
if last_err:
raise last_err
run_compensating_close(_close, log_prefix="binance_compensating_close")
def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss=None, take_profit=None): def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss=None, take_profit=None):
ensure_markets_loaded() ensure_markets_loaded()
mm = "cross" if BINANCE_MARGIN_MODE in ("cross", "cross_margin") else "isolated" mm = "cross" if BINANCE_MARGIN_MODE in ("cross", "cross_margin") else "isolated"
@@ -3487,8 +3539,10 @@ def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss
_binance_place_tp_sl_orders(exchange_symbol, direction, pos_amt, stop_loss, take_profit) _binance_place_tp_sl_orders(exchange_symbol, direction, pos_amt, stop_loss, take_profit)
order["tpsl_attached"] = True order["tpsl_attached"] = True
except RuntimeError: except RuntimeError:
_abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, amount)
raise raise
except Exception as e: except Exception as e:
_abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, amount)
raise RuntimeError(f"交易所未接受条件止盈/止损委托,已拒绝开仓:{str(e)}") from e raise RuntimeError(f"交易所未接受条件止盈/止损委托,已拒绝开仓:{str(e)}") from e
return order return order
@@ -4495,6 +4549,72 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
) )
def _finalize_hub_flat_monitor_binance(conn, r, *, result, pnl_amount, closed_at, miss_reason):
opened_at = get_opened_at_value(r)
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
session_date = r["session_date"] or get_trading_day(closed_at_dt)
update_session_capital(conn, session_date, pnl_amount)
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r),
direction=r["direction"],
trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"],
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
take_profit=r["take_profit"],
margin_capital=r["margin_capital"],
leverage=r["leverage"],
pnl_amount=pnl_amount,
hold_seconds=hold_seconds,
trade_style=r["trade_style"],
risk_amount=r["risk_amount"],
planned_rr=calc_rr_ratio(
r["direction"],
r["trigger_price"],
r["initial_stop_loss"] or r["stop_loss"],
r["take_profit"],
),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result,
miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at,
closed_at=closed_at,
)
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
def reconcile_hub_external_close(conn, symbol, direction):
from lib.hub.hub_reconcile_flat_lib import reconcile_hub_external_close_impl
from lib.hub.hub_symbol_lib import symbols_match
global _RECONCILE_FLAT_STREAK
return reconcile_hub_external_close_impl(
conn,
symbol,
direction,
exchange_configured=exchange_private_api_configured,
not_configured_msg="未配置 BINANCE_API_KEY / BINANCE_API_SECRET",
symbols_match=symbols_match,
get_opened_at_value=get_opened_at_value,
resolve_monitor_exchange_symbol=resolve_monitor_exchange_symbol,
get_live_position_contracts=get_live_position_contracts,
cancel_conditional_orders=cancel_binance_futures_open_orders,
resolve_synced_flat_close=resolve_synced_flat_close,
finalize_stopped_monitor=_finalize_hub_flat_monitor_binance,
sync_trade_records=None,
reconcile_flat_streak=_RECONCILE_FLAT_STREAK,
to_ms_with_fallback=_to_ms_with_fallback,
prefer_manual_resolve=False,
order_row_monitor_type=order_row_monitor_type,
)
def reconcile_external_closes(conn, days=None): def reconcile_external_closes(conn, days=None):
global _RECONCILE_FLAT_STREAK global _RECONCILE_FLAT_STREAK
if not exchange_private_api_configured(): if not exchange_private_api_configured():
@@ -5777,7 +5897,7 @@ def _market_open_for_trigger_entry(
def _execute_trigger_entry_cross(conn, row): def _execute_trigger_entry_cross(conn, row):
"""标记价触达计划入场:先删监控行防重复触发,再市价开仓""" """标记价触达计划入场:加锁防重复触发,成交成功后再删监控行"""
symbol = row["symbol"] symbol = row["symbol"]
direction = (row["direction"] or "long").lower() direction = (row["direction"] or "long").lower()
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
@@ -5788,7 +5908,8 @@ def _execute_trigger_entry_cross(conn, row):
tc_en, tc_h, _ = time_close_settings_from_row(row) tc_en, tc_h, _ = time_close_settings_from_row(row)
kid = int(row["id"]) kid = int(row["id"])
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,)) if not acquire_trigger_entry_exec_lock(conn, kid):
return False, "触价开仓进行中"
conn.commit() conn.commit()
try: try:
@@ -5806,6 +5927,8 @@ def _execute_trigger_entry_cross(conn, row):
time_close_hours=tc_h, time_close_hours=tc_h,
) )
except Exception as e: except Exception as e:
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = friendly_exchange_error(e) fail_msg = friendly_exchange_error(e)
send_wechat_msg( send_wechat_msg(
f"# ❌ {symbol} 触价开仓异常\n" f"# ❌ {symbol} 触价开仓异常\n"
@@ -5817,6 +5940,8 @@ def _execute_trigger_entry_cross(conn, row):
return False, fail_msg return False, fail_msg
if ok and det: if ok and det:
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-" rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-"
msg = ( msg = (
f"# ✅ {symbol} 触价开仓成交\n" f"# ✅ {symbol} 触价开仓成交\n"
@@ -5833,6 +5958,8 @@ def _execute_trigger_entry_cross(conn, row):
send_wechat_msg(msg) send_wechat_msg(msg)
insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED) insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED)
return True, None return True, None
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = err or "触价触发后开仓失败" fail_msg = err or "触价触发后开仓失败"
send_wechat_msg( send_wechat_msg(
f"# ❌ {symbol} 触价开仓失败\n" f"# ❌ {symbol} 触价开仓失败\n"
@@ -5860,6 +5987,8 @@ def check_trigger_entry_key_monitors():
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0)
kid = int(r["id"]) kid = int(r["id"])
if is_trigger_entry_in_flight_row(r):
continue
if entry <= 0 or sl <= 0 or tp <= 0: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
@@ -6388,7 +6517,7 @@ def check_order_monitors():
new_sl = round_price_to_exchange(ex_sym, new_sl) new_sl = round_price_to_exchange(ex_sym, new_sl)
tp_ex = float(take_profit or 0) tp_ex = float(take_profit or 0)
ok_live, _live_reason = ensure_exchange_live_ready() ok_live, _live_reason = ensure_exchange_live_ready()
synced_ex = not ok_live synced_ex = False
if ok_live and tp_ex > 0: if ok_live and tp_ex > 0:
try: try:
replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex)
@@ -6818,8 +6947,8 @@ def background_task():
from lib.strategy.strategy_trend_register import check_trend_pullback_plans from lib.strategy.strategy_trend_register import check_trend_pullback_plans
check_trend_pullback_plans(cfg) check_trend_pullback_plans(cfg)
except: except Exception as e:
pass print(f"[monitor_loop] {e}", flush=True)
time.sleep(MONITOR_POLL_SECONDS) time.sleep(MONITOR_POLL_SECONDS)
@@ -7163,6 +7292,7 @@ def render_main_page(page="trade", embed_mode=None):
risk_percent=RISK_PERCENT, risk_percent=RISK_PERCENT,
position_sizing_mode=POSITION_SIZING_MODE, position_sizing_mode=POSITION_SIZING_MODE,
position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE), position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),
trade_policy=trade_policy_template_context(TRADE_POLICY),
open_position_button_label=( open_position_button_label=(
"开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)" "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)"
), ),
@@ -7882,7 +8012,10 @@ def key_focus():
selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None) selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None)
if selected_key is None and key_list: if selected_key is None and key_list:
selected_key = key_list[0] selected_key = key_list[0]
default_symbol = symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT" default_symbol = default_symbol_for_policy(
TRADE_POLICY,
symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT",
)
return render_template( return render_template(
"key_focus_v2.html", "key_focus_v2.html",
key_list=key_list, key_list=key_list,
@@ -7892,6 +8025,7 @@ def key_focus():
default_kline_limit=200, default_kline_limit=200,
price_refresh_seconds=PRICE_REFRESH_SECONDS, price_refresh_seconds=PRICE_REFRESH_SECONDS,
exchange_display=EXCHANGE_DISPLAY_NAME, exchange_display=EXCHANGE_DISPLAY_NAME,
trade_policy=trade_policy_template_context(TRADE_POLICY),
) )
@@ -8002,6 +8136,12 @@ def add_key():
if not symbol: if not symbol:
flash("symbol 不能为空") flash("symbol 不能为空")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_sym, sym_msg = check_symbol_policy(
TRADE_POLICY, symbol, normalize_symbol_input
)
if not ok_sym:
flash(sym_msg)
return redirect("/key_monitor")
mt = (d.get("type") or "").strip() mt = (d.get("type") or "").strip()
direction_sel = (d.get("direction") or "").strip().lower() direction_sel = (d.get("direction") or "").strip().lower()
dup_msg = check_duplicate_submit( dup_msg = check_duplicate_submit(
@@ -8016,6 +8156,10 @@ def add_key():
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_dir, dir_msg = check_direction_policy(TRADE_POLICY, direction_sel)
if not ok_dir:
flash(dir_msg)
return redirect("/key_monitor")
allowed_types = ( allowed_types = (
tuple(KEY_MONITOR_AUTO_TYPES) tuple(KEY_MONITOR_AUTO_TYPES)
+ tuple(KEY_MONITOR_ALERT_ONLY_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
@@ -8261,6 +8405,11 @@ def add_order():
conn.close() conn.close()
flash("symbol 不能为空") flash("symbol 不能为空")
return redirect("/") return redirect("/")
ok_pol, pol_msg = validate_trade_policy_open(symbol, direction)
if not ok_pol:
conn.close()
flash(f"账户限制:{pol_msg}")
return redirect("/trade")
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction)) dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
if dup_msg: if dup_msg:
conn.close() conn.close()
@@ -9589,6 +9738,7 @@ def _hub_meta_bundle():
"max_active_positions": MAX_ACTIVE_POSITIONS, "max_active_positions": MAX_ACTIVE_POSITIONS,
"btc_leverage": BTC_LEVERAGE, "btc_leverage": BTC_LEVERAGE,
"alt_leverage": ALT_LEVERAGE, "alt_leverage": ALT_LEVERAGE,
"trade_policy": trade_policy_template_context(TRADE_POLICY),
} }
@@ -9667,6 +9817,7 @@ try:
ohlcv_fn=_hub_fetch_ohlcv, ohlcv_fn=_hub_fetch_ohlcv,
volume_rank_fn=_hub_fetch_volume_rank, volume_rank_fn=_hub_fetch_volume_rank,
market_fn=_hub_fetch_market, market_fn=_hub_fetch_market,
reconcile_hub_flat_fn=reconcile_hub_external_close,
risk_status_fn=hub_account_risk_status, risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close, user_close_fn=hub_user_initiated_close,
render_main_page_fn=render_main_page, render_main_page_fn=render_main_page,
+16 -7
View File
@@ -243,7 +243,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=48"> <link rel="stylesheet" href="/static/instance_theme.css?v=50">
</head> </head>
<body <body
@@ -253,6 +253,7 @@
data-btc-leverage="{{ btc_leverage }}" data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}" data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}" data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
> >
{% macro period_stats(title, s) %} {% macro period_stats(title, s) %}
<div class="stats-period-block"> <div class="stats-period-block">
@@ -278,6 +279,9 @@
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row"> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div> <div class="exchange-tag">{{ exchange_display }}</div>
{% if trade_policy.badge_text %}
<span class="trade-policy-badge" title="账户交易限制(.env">{{ trade_policy.badge_text }}</span>
{% endif %}
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span> <span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题"> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题"> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
@@ -374,10 +378,9 @@
<button type="submit">手动划转</button> <button type="submit">手动划转</button>
</form> </form>
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}"> <form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required> {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
<select id="order-direction" name="direction" required> {{ trade_policy_symbol('symbol', 'order-symbol') }}
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> {{ trade_policy_direction('direction', 'order-direction') }}
</select>
<select id="sltp-mode" name="sltp_mode"> <select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option> <option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option> <option value="price">止盈止损:价格模式</option>
@@ -406,7 +409,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef"> <label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记) <input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label> </label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span> {% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required> <input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比"> <input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span> <span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
@@ -846,6 +851,7 @@
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script> <script src="/static/strategy_roll.js?v=6"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
@@ -2020,7 +2026,10 @@ function refreshAccountSnapshot(){
const orderSymbolEl = document.getElementById("order-symbol"); const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction"); const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin"); const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults); if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults); if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
+1 -1
View File
@@ -140,7 +140,7 @@
## 升级步骤 ## 升级步骤
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env` 1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。 2. 在 VPS 上为 Binance / Gate / **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
3. 重启 Binance 实例(如 `pm2 restart crypto_binance`);SQLite 会自动 `ALTER` 缺列(斐波、交易所盈亏、`entry_reason` 等)。 3. 重启 Binance 实例(如 `pm2 restart crypto_binance`);SQLite 会自动 `ALTER` 缺列(斐波、交易所盈亏、`entry_reason` 等)。
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。 4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**`/stats` 可见分品类统计与「北京 8:00 切日」说明。 5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**`/stats` 可见分品类统计与「北京 8:00 切日」说明。
+3 -3
View File
@@ -149,7 +149,7 @@ cp .env .env.backup.$(date +%Y%m%d)
### 5.3 AI 复盘与模型(可选) ### 5.3 AI 复盘与模型(可选)
所共用仓库根目录 **`ai_client.py`**PM2 的 **`PYTHONPATH=..`** 须包含仓库根)。在 `.env` 中配置 **`AI_PROVIDER`** 所共用仓库根目录 **`ai_client.py`**PM2 的 **`PYTHONPATH=..`** 须包含仓库根)。在 `.env` 中配置 **`AI_PROVIDER`**
| 模式 | 主要变量 | | 模式 | 主要变量 |
|------|----------| |------|----------|
@@ -191,10 +191,10 @@ cd /opt/crypto_monitor/crypto_monitor_gate
bash scripts/install_backup_cron.sh bash scripts/install_backup_cron.sh
``` ```
Gate Bot 实例(趋势回调等): 实例(趋势回调等):
```bash ```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot cd /opt/crypto_monitor/crypto_monitor_gate
bash scripts/install_backup_cron.sh bash scripts/install_backup_cron.sh
``` ```
+7
View File
@@ -50,6 +50,13 @@ UPLOAD_DIR=static/images
# TOTAL_CAPITAL=100 # TOTAL_CAPITAL=100
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启) # 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk POSITION_SIZING_MODE=risk
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数(U # 每天起始基数(U
DAILY_START_CAPITAL=30 DAILY_START_CAPITAL=30
# 日内回撤后基数(U # 日内回撤后基数(U
+144 -65
View File
@@ -131,6 +131,9 @@ from lib.key_monitor.trigger_entry_key_monitor_lib import (
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
acquire_trigger_entry_exec_lock,
is_trigger_entry_in_flight_row,
release_trigger_entry_exec_lock,
is_breakout_trigger_entry_key_monitor_type, is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
@@ -155,6 +158,14 @@ from lib.trade.position_sizing_lib import (
mode_label_zh, mode_label_zh,
risk_percent_for_storage, risk_percent_for_storage,
) )
from lib.trade.trade_policy_lib import load_trade_policy
from lib.trade.trade_policy_app_lib import (
check_direction_policy,
check_open_policy,
check_symbol_policy,
default_symbol_for_policy,
trade_policy_template_context,
)
from lib.key_monitor.key_monitor_full_margin_lib import ( from lib.key_monitor.key_monitor_full_margin_lib import (
monitor_type_disallowed_in_full_margin, monitor_type_disallowed_in_full_margin,
purge_disallowed_key_monitors, purge_disallowed_key_monitors,
@@ -329,6 +340,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账 # 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
POSITION_SIZING_MODE = load_position_sizing_mode() POSITION_SIZING_MODE = load_position_sizing_mode()
TRADE_POLICY = load_trade_policy()
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3")) MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
@@ -394,8 +406,10 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(ORDER_CHART_DIR, exist_ok=True) os.makedirs(ORDER_CHART_DIR, exist_ok=True)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
from lib.exchange.gate_ccxt_lib import gate_ccxt_class
# Gate.io USDT 永续(swap # Gate.io USDT 永续(swap
exchange = ccxt.gateio({ exchange = gate_ccxt_class()({
"enableRateLimit": True, "enableRateLimit": True,
"options": { "options": {
"defaultType": "swap", "defaultType": "swap",
@@ -1984,6 +1998,12 @@ def normalize_symbol_input(symbol):
return f"{sym}/USDT" return f"{sym}/USDT"
def validate_trade_policy_open(symbol, direction):
return check_open_policy(
TRADE_POLICY, symbol, direction, normalize_symbol_input
)
def normalize_kline_limit(limit_raw, default=200): def normalize_kline_limit(limit_raw, default=200):
try: try:
n = int(limit_raw) n = int(limit_raw)
@@ -3133,7 +3153,7 @@ def _gate_place_tp_sl_orders_position_price_orders(exchange_symbol, direction, s
try: try:
exchange.privateFuturesPostSettlePriceOrders(_payload(tp_s, tp_rule)) exchange.privateFuturesPostSettlePriceOrders(_payload(tp_s, tp_rule))
except Exception: except Exception:
cancel_gate_swap_trigger_orders(exchange_symbol) # 保留已挂止损,仅放弃本次 TP;上层可补偿平仓或重试
raise raise
return return
except Exception as e: except Exception as e:
@@ -3250,6 +3270,27 @@ def ensure_markets_loaded(force=False):
MARKETS_LOADED = True MARKETS_LOADED = True
def _abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, planned_amount):
"""TP/SL 挂失败时市价平掉刚开的仓并撤残留条件单。"""
from lib.trade.compensating_close_lib import run_compensating_close
def _close():
ensure_markets_loaded()
try:
cancel_gate_swap_trigger_orders(exchange_symbol)
except Exception:
pass
live = get_live_position_contracts(exchange_symbol, direction)
amt = live if live is not None and live > 0 else _gate_contracts_amount_for_tpsl(order, planned_amount)
if amt is None or float(amt) <= 0:
return
side = "sell" if direction == "long" else "buy"
params = build_gate_order_params(direction, reduce_only=True)
exchange.create_order(exchange_symbol, "market", side, float(amt), None, params)
run_compensating_close(_close, log_prefix="gate_compensating_close")
def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss=None, take_profit=None): def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss=None, take_profit=None):
ensure_markets_loaded() ensure_markets_loaded()
exchange.set_leverage(leverage, exchange_symbol) exchange.set_leverage(leverage, exchange_symbol)
@@ -3263,22 +3304,48 @@ def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss
_gate_place_tp_sl_orders(exchange_symbol, direction, contracts_amt, stop_loss, take_profit) _gate_place_tp_sl_orders(exchange_symbol, direction, contracts_amt, stop_loss, take_profit)
order["tpsl_attached"] = True order["tpsl_attached"] = True
except RuntimeError: except RuntimeError:
_abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, amount)
raise raise
except Exception as e: except Exception as e:
_abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, amount)
raise RuntimeError(f"交易所未接受条件止盈/止损委托,已拒绝开仓:{str(e)}") from e raise RuntimeError(f"交易所未接受条件止盈/止损委托,已拒绝开仓:{str(e)}") from e
return order return order
def close_exchange_order(order_row): def close_exchange_order(order_row):
"""
市价全平数量优先取交易所当前持仓张数避免仅用入库 order_amount 导致平不干净
"""
ensure_markets_loaded() ensure_markets_loaded()
exchange_symbol = order_row["exchange_symbol"] or normalize_exchange_symbol(order_row["symbol"]) exchange_symbol = order_row["exchange_symbol"] or normalize_exchange_symbol(order_row["symbol"])
amount = float(order_row["order_amount"] or 0)
if amount <= 0:
raise ValueError("平仓失败:缺少有效下单数量")
direction = order_row["direction"] direction = order_row["direction"]
db_amt = float(order_row["order_amount"] or 0)
side = "sell" if direction == "long" else "buy" side = "sell" if direction == "long" else "buy"
last_resp = None
for _ in range(3):
live = get_live_position_contracts(exchange_symbol, direction)
if live is not None and live > 0:
raw_amt = live
else:
raw_amt = db_amt
if raw_amt <= 0:
if last_resp is not None:
return last_resp
raise ValueError("平仓失败:缺少有效下单数量")
try:
amount = float(exchange.amount_to_precision(exchange_symbol, raw_amt))
except Exception:
amount = float(raw_amt)
if amount <= 0:
if last_resp is not None:
return last_resp
raise ValueError("平仓失败:数量经精度舍入后为 0")
params = build_gate_order_params(direction, reduce_only=True) params = build_gate_order_params(direction, reduce_only=True)
return exchange.create_order(exchange_symbol, "market", side, amount, None, params) last_resp = exchange.create_order(exchange_symbol, "market", side, amount, None, params)
live_after = get_live_position_contracts(exchange_symbol, direction)
if live_after is None or live_after <= 0:
return last_resp
return last_resp
def _gate_swap_trigger_order_params(): def _gate_swap_trigger_order_params():
@@ -4111,53 +4178,8 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None, *, prefer_m
) )
def reconcile_hub_external_close(conn, symbol, direction): def _finalize_hub_flat_monitor(conn, r, *, result, pnl_amount, closed_at, miss_reason):
"""中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
if not exchange_private_api_configured():
return {"ok": False, "msg": "未配置 GATE_API_KEY / GATE_API_SECRET", "synced": 0}
from lib.exchange.gate_position_history_lib import unified_symbol_for_match
sym_u = unified_symbol_for_match(symbol)
dir_l = (direction or "").strip().lower()
if dir_l not in ("long", "short"):
return {"ok": False, "msg": "side 须为 long 或 short", "synced": 0}
synced = 0
rows = conn.execute(
"SELECT * FROM order_monitors WHERE status IN ('active', 'error')"
).fetchall()
for r in rows:
if unified_symbol_for_match(r["symbol"]) != sym_u:
continue
if (r["direction"] or "").strip().lower() != dir_l:
continue
oid = int(r["id"])
if r["status"] == "error":
opened_at_chk = get_opened_at_value(r)
existing = conn.execute(
"SELECT id FROM trade_records WHERE symbol=? AND opened_at=? AND monitor_type=? LIMIT 1",
(r["symbol"], opened_at_chk, order_row_monitor_type(r)),
).fetchone()
if existing:
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
synced += 1
continue
exchange_symbol = resolve_monitor_exchange_symbol(r)
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
if live_contracts is None:
continue
if live_contracts > 0:
time.sleep(0.6)
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
if live_contracts is None or live_contracts > 0:
continue
global _RECONCILE_FLAT_STREAK
_RECONCILE_FLAT_STREAK.pop(oid, None)
cancel_gate_swap_trigger_orders(exchange_symbol)
opened_at = get_opened_at_value(r) opened_at = get_opened_at_value(r)
opened_at_ms = _to_ms_with_fallback(r["opened_at_ms"] if "opened_at_ms" in r.keys() else None, opened_at)
result, pnl_amount, closed_at, miss_reason = resolve_synced_flat_close(
r, opened_at, opened_at_ms=opened_at_ms, prefer_manual=True
)
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now() closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt) hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
session_date = r["session_date"] or get_trading_day(closed_at_dt) session_date = r["session_date"] or get_trading_day(closed_at_dt)
@@ -4179,7 +4201,12 @@ def reconcile_hub_external_close(conn, symbol, direction):
hold_seconds=hold_seconds, hold_seconds=hold_seconds,
trade_style=r["trade_style"], trade_style=r["trade_style"],
risk_amount=r["risk_amount"], risk_amount=r["risk_amount"],
planned_rr=calc_rr_ratio(r["direction"], r["trigger_price"], r["initial_stop_loss"] or r["stop_loss"], r["take_profit"]), planned_rr=calc_rr_ratio(
r["direction"],
r["trigger_price"],
r["initial_stop_loss"] or r["stop_loss"],
r["take_profit"],
),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]), actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result, result=result,
miss_reason=handoff_trade_miss_reason(miss_reason, r), miss_reason=handoff_trade_miss_reason(miss_reason, r),
@@ -4188,12 +4215,34 @@ def reconcile_hub_external_close(conn, symbol, direction):
) )
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],)) conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day()) clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
synced += 1
try:
sync_trade_records_from_exchange(conn, force=True) def reconcile_hub_external_close(conn, symbol, direction):
except Exception: """中控市价全平后:立即同步匹配 order_monitor,并读 Gate 平仓历史。"""
pass from lib.hub.hub_reconcile_flat_lib import reconcile_hub_external_close_impl
return {"ok": True, "synced": synced} from lib.hub.hub_symbol_lib import symbols_match
global _RECONCILE_FLAT_STREAK
return reconcile_hub_external_close_impl(
conn,
symbol,
direction,
exchange_configured=exchange_private_api_configured,
not_configured_msg="未配置 GATE_API_KEY / GATE_API_SECRET",
symbols_match=symbols_match,
get_opened_at_value=get_opened_at_value,
resolve_monitor_exchange_symbol=resolve_monitor_exchange_symbol,
get_live_position_contracts=get_live_position_contracts,
cancel_conditional_orders=cancel_gate_swap_trigger_orders,
resolve_synced_flat_close=resolve_synced_flat_close,
finalize_stopped_monitor=_finalize_hub_flat_monitor,
sync_trade_records=sync_trade_records_from_exchange,
reconcile_flat_streak=_RECONCILE_FLAT_STREAK,
to_ms_with_fallback=_to_ms_with_fallback,
prefer_manual_resolve=True,
order_row_monitor_type=order_row_monitor_type,
)
def reconcile_external_closes(conn, days=None): def reconcile_external_closes(conn, days=None):
@@ -5489,7 +5538,7 @@ def _market_open_for_trigger_entry(
def _execute_trigger_entry_cross(conn, row): def _execute_trigger_entry_cross(conn, row):
"""标记价触达计划入场:先删监控行防重复触发,再市价开仓""" """标记价触达计划入场:加锁防重复触发,成交成功后再删监控行"""
symbol = row["symbol"] symbol = row["symbol"]
direction = (row["direction"] or "long").lower() direction = (row["direction"] or "long").lower()
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
@@ -5500,7 +5549,8 @@ def _execute_trigger_entry_cross(conn, row):
tc_en, tc_h, _ = time_close_settings_from_row(row) tc_en, tc_h, _ = time_close_settings_from_row(row)
kid = int(row["id"]) kid = int(row["id"])
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,)) if not acquire_trigger_entry_exec_lock(conn, kid):
return False, "触价开仓进行中"
conn.commit() conn.commit()
try: try:
@@ -5518,6 +5568,8 @@ def _execute_trigger_entry_cross(conn, row):
time_close_hours=tc_h, time_close_hours=tc_h,
) )
except Exception as e: except Exception as e:
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = friendly_exchange_error(e) fail_msg = friendly_exchange_error(e)
send_wechat_msg( send_wechat_msg(
f"# ❌ {symbol} 触价开仓异常\n" f"# ❌ {symbol} 触价开仓异常\n"
@@ -5529,6 +5581,8 @@ def _execute_trigger_entry_cross(conn, row):
return False, fail_msg return False, fail_msg
if ok and det: if ok and det:
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-" rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-"
msg = ( msg = (
f"# ✅ {symbol} 触价开仓成交\n" f"# ✅ {symbol} 触价开仓成交\n"
@@ -5545,6 +5599,8 @@ def _execute_trigger_entry_cross(conn, row):
send_wechat_msg(msg) send_wechat_msg(msg)
insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED) insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED)
return True, None return True, None
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = err or "触价触发后开仓失败" fail_msg = err or "触价触发后开仓失败"
send_wechat_msg( send_wechat_msg(
f"# ❌ {symbol} 触价开仓失败\n" f"# ❌ {symbol} 触价开仓失败\n"
@@ -5572,6 +5628,8 @@ def check_trigger_entry_key_monitors():
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0)
kid = int(r["id"]) kid = int(r["id"])
if is_trigger_entry_in_flight_row(r):
continue
if entry <= 0 or sl <= 0 or tp <= 0: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
@@ -6115,7 +6173,7 @@ def check_order_monitors():
new_sl = round_price_to_exchange(ex_sym, new_sl) new_sl = round_price_to_exchange(ex_sym, new_sl)
tp_ex = float(take_profit or 0) tp_ex = float(take_profit or 0)
ok_live, _live_reason = ensure_exchange_live_ready() ok_live, _live_reason = ensure_exchange_live_ready()
synced_ex = not ok_live synced_ex = False
if ok_live and tp_ex > 0: if ok_live and tp_ex > 0:
try: try:
replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex)
@@ -6524,8 +6582,8 @@ def background_task():
from lib.strategy.strategy_trend_register import check_trend_pullback_plans from lib.strategy.strategy_trend_register import check_trend_pullback_plans
check_trend_pullback_plans(cfg) check_trend_pullback_plans(cfg)
except: except Exception as e:
pass print(f"[monitor_loop] {e}", flush=True)
time.sleep(MONITOR_POLL_SECONDS) time.sleep(MONITOR_POLL_SECONDS)
@@ -7011,6 +7069,7 @@ def render_main_page(page="trade", embed_mode=None):
risk_percent=RISK_PERCENT, risk_percent=RISK_PERCENT,
position_sizing_mode=POSITION_SIZING_MODE, position_sizing_mode=POSITION_SIZING_MODE,
position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE), position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),
trade_policy=trade_policy_template_context(TRADE_POLICY),
open_position_button_label=( open_position_button_label=(
"开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)" "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)"
), ),
@@ -7747,7 +7806,10 @@ def key_focus():
selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None) selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None)
if selected_key is None and key_list: if selected_key is None and key_list:
selected_key = key_list[0] selected_key = key_list[0]
default_symbol = symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT" default_symbol = default_symbol_for_policy(
TRADE_POLICY,
symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT",
)
return render_template( return render_template(
"key_focus_v2.html", "key_focus_v2.html",
key_list=key_list, key_list=key_list,
@@ -7757,6 +7819,7 @@ def key_focus():
default_kline_limit=200, default_kline_limit=200,
price_refresh_seconds=PRICE_REFRESH_SECONDS, price_refresh_seconds=PRICE_REFRESH_SECONDS,
exchange_display=EXCHANGE_DISPLAY_NAME, exchange_display=EXCHANGE_DISPLAY_NAME,
trade_policy=trade_policy_template_context(TRADE_POLICY),
) )
@@ -7869,6 +7932,12 @@ def add_key():
if not symbol: if not symbol:
flash("symbol 不能为空") flash("symbol 不能为空")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_sym, sym_msg = check_symbol_policy(
TRADE_POLICY, symbol, normalize_symbol_input
)
if not ok_sym:
flash(sym_msg)
return redirect("/key_monitor")
mt = (d.get("type") or "").strip() mt = (d.get("type") or "").strip()
direction_pre = (d.get("direction") or "").strip().lower() direction_pre = (d.get("direction") or "").strip().lower()
dup_msg = check_duplicate_submit( dup_msg = check_duplicate_submit(
@@ -7884,6 +7953,10 @@ def add_key():
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_dir, dir_msg = check_direction_policy(TRADE_POLICY, direction_sel)
if not ok_dir:
flash(dir_msg)
return redirect("/key_monitor")
allowed_types = ( allowed_types = (
tuple(KEY_MONITOR_AUTO_TYPES) tuple(KEY_MONITOR_AUTO_TYPES)
+ tuple(KEY_MONITOR_ALERT_ONLY_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
@@ -8166,6 +8239,11 @@ def add_order():
conn.close() conn.close()
flash("symbol 不能为空") flash("symbol 不能为空")
return redirect("/") return redirect("/")
ok_pol, pol_msg = validate_trade_policy_open(symbol, direction)
if not ok_pol:
conn.close()
flash(f"账户限制:{pol_msg}")
return redirect("/trade")
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction)) dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
if dup_msg: if dup_msg:
conn.close() conn.close()
@@ -9509,6 +9587,7 @@ def _hub_meta_bundle():
"max_active_positions": MAX_ACTIVE_POSITIONS, "max_active_positions": MAX_ACTIVE_POSITIONS,
"btc_leverage": BTC_LEVERAGE, "btc_leverage": BTC_LEVERAGE,
"alt_leverage": ALT_LEVERAGE, "alt_leverage": ALT_LEVERAGE,
"trade_policy": trade_policy_template_context(TRADE_POLICY),
} }
+16 -7
View File
@@ -243,7 +243,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=48"> <link rel="stylesheet" href="/static/instance_theme.css?v=50">
</head> </head>
<body <body
@@ -253,6 +253,7 @@
data-btc-leverage="{{ btc_leverage }}" data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}" data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}" data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
> >
{% macro period_stats(title, s) %} {% macro period_stats(title, s) %}
<div class="stats-period-block"> <div class="stats-period-block">
@@ -278,6 +279,9 @@
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row"> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div> <div class="exchange-tag">{{ exchange_display }}</div>
{% if trade_policy.badge_text %}
<span class="trade-policy-badge" title="账户交易限制(.env">{{ trade_policy.badge_text }}</span>
{% endif %}
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span> <span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题"> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题"> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
@@ -354,10 +358,9 @@
</div> </div>
{% include 'order_monitor_rule_tips_gate.html' %} {% include 'order_monitor_rule_tips_gate.html' %}
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}"> <form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required> {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
<select id="order-direction" name="direction" required> {{ trade_policy_symbol('symbol', 'order-symbol') }}
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> {{ trade_policy_direction('direction', 'order-direction') }}
</select>
<select id="sltp-mode" name="sltp_mode"> <select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option> <option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option> <option value="price">止盈止损:价格模式</option>
@@ -386,7 +389,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef"> <label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记) <input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label> </label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span> {% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required> <input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比"> <input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span> <span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
@@ -813,6 +818,7 @@
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script> <script src="/static/strategy_roll.js?v=6"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
@@ -1946,7 +1952,10 @@ function refreshAccountSnapshot(){
const orderSymbolEl = document.getElementById("order-symbol"); const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction"); const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin"); const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults); if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults); if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
+1 -1
View File
@@ -141,7 +141,7 @@
## 升级步骤 ## 升级步骤
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env` 1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。 2. 在 VPS 上为 Binance / Gate / **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
3. 重启 Gate 实例服务(如 `pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 缺列(斐波、交易所盈亏、`entry_reason` 等)。 3. 重启 Gate 实例服务(如 `pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 缺列(斐波、交易所盈亏、`entry_reason` 等)。
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。 4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**`/stats` 可见分品类统计与「北京 8:00 切日」说明。 5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**`/stats` 可见分品类统计与「北京 8:00 切日」说明。
+1 -1
View File
@@ -157,7 +157,7 @@ bash scripts/backup_data.sh # 试跑
备份目录:`/root/backups/crypto_monitor_gate/YYYY-MM-DD/`。详见 Binance 项目 `部署文档.md` 第 5.4 节(恢复步骤、可选 `.env` 变量相同)。 备份目录:`/root/backups/crypto_monitor_gate/YYYY-MM-DD/`。详见 Binance 项目 `部署文档.md` 第 5.4 节(恢复步骤、可选 `.env` 变量相同)。
若还部署了 **`crypto_monitor_gate_bot`**,请在该目录同样执行 `bash scripts/install_backup_cron.sh` 若还部署了 **`crypto_monitor_okx`**,请在该目录同样执行 `bash scripts/install_backup_cron.sh`
### 5.5 必填项检查(Gate + 代理) ### 5.5 必填项检查(Gate + 代理)
-210
View File
@@ -1,210 +0,0 @@
# =============================================================================
# 环境配置模板(可提交 Git)。程序运行时只读取同目录下的 .env。
#
# 首次部署 / 新机:
# cp .env.example .env
# nano .env # 填入真实密钥、端口、代理等
#
# 升级代码(git pull)前建议备份(.env 不在 Git 中,pull 不会覆盖):
# cp .env .env.backup.$(date +%Y%m%d)
#
# 从备份恢复:
# cp .env.backup.YYYYMMDD .env
# =============================================================================
APP_ENV=production
# 服务监听地址(云服务器通常用 0.0.0.0)
APP_HOST=0.0.0.0
# 服务端口
APP_PORT=5002
# 是否开启调试模式(生产建议 false)
APP_DEBUG=false
# 登录账号
APP_USERNAME=admin
# 登录密码(请改成你自己的强密码)
APP_PASSWORD=admin123
# 是否关闭登录校验(局域网可设 true;公网务必 false)
APP_AUTH_DISABLED=true
# --- 多账户交易中控 manual_trading_hub ---
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true
# HUB_BRIDGE_TOKEN=your-long-random-token
# Flask 会话密钥(必须替换为长随机字符串)
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
# 企业微信机器人 Webhook(用于行情/风控推送)
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_WITH_REAL_KEY
# 数据库文件路径(相对路径会自动按项目目录解析)
DB_PATH=crypto.db
# 交易截图上传目录
UPLOAD_DIR=static/images
# 自动备份(scripts/backup_data.sh + cron,可选;默认即可)
# BACKUP_ROOT=/root/backups
# BACKUP_RETENTION_DAYS=30
# BACKUP_INSTANCE=crypto_monitor_gate_bot
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
# TOTAL_CAPITAL=100
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk
# 每天起始基数(U
DAILY_START_CAPITAL=30
# 日内回撤后基数(U
DAILY_LOSS_CAPITAL=20
# 日内盈利后基数(U
DAILY_PROFIT_CAPITAL=50
# BTC 默认杠杆倍数
BTC_LEVERAGE=10
# 山寨币默认杠杆倍数
ALT_LEVERAGE=5
# 交易日重置小时(北京时间)
TRADING_DAY_RESET_HOUR=8
# 整点前禁止新开仓:true=启用(默认),false=关闭(仍可保留 8 点作为交易日划分)
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true
# Gate API Key(实盘)
GATE_API_KEY=REPLACE_WITH_GATE_API_KEY
# Gate API Secret(实盘)
GATE_API_SECRET=REPLACE_WITH_GATE_API_SECRET
# 保证金模式:cross=全仓,isolated=逐仓
GATE_TD_MODE=cross
# 持仓筛选:hedge=双向持仓下按多空腿过滤;其它值(如 single)不按腿过滤
GATE_POS_MODE=hedge
# 永续止盈止损:是否优先用官方仓位类触发单(POST price_ordersclose-*-position);false=仅用旧版两张 ccxt 条件单
GATE_TPSL_USE_POSITION_ORDER=true
# 触发单超时(秒),默认 604800=7 天;设为 0 或负数则不向 API 传 expiration
GATE_TPSL_TRIGGER_EXPIRATION=604800
# 触发参考价:0=最新成交 1=标记价 2=指数价(非法值按 0)
GATE_TPSL_PRICE_TYPE=0
# 仓位类 TP/SL 相对现价的最小间距(%),避免 Gate 1026「触发价须高于/低于现价」
GATE_TPSL_LAST_PRICE_GAP_PCT=0.05
# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Gate·模拟)
# EXCHANGE_DISPLAY_NAME=Gate.io
# =============================================================================
# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用)
# =============================================================================
# 【周期】门控 K 线周期,如 5m、15m
KLINE_TIMEFRAME=5m
# 【确认K】闭合 K 序列中的棒偏移:突破棒默认 -2,确认棒默认 -1
KEY_CONFIRM_BREAKOUT_BAR=-2
KEY_CONFIRM_BAR=-1
# 【量能】突破棒成交量 > 前 N 根均量 × 倍数
KEY_VOLUME_MA_BARS=20
KEY_VOLUME_RATIO_MIN=1.3
# 【突破K实体幅度】占开盘价百分比区间
# 【箱体/收敛】突破K收盘越过关键位下限%;无上限(过猛由计划RR过滤)
KEY_BREAKOUT_AMP_MIN_PCT=0.03
KEY_BREAKOUT_AMP_MAX_PCT=0.5
# 【阻力/支撑】突破后微信提醒
KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5
# 【日成交量排名】品种须在该排名前 N 名
KEY_DAILY_VOLUME_RANK_MAX=30
# 【关键位自动开仓盈亏比】严格大于该值才市价开仓
KEY_AUTO_MIN_PLANNED_RR=1.5
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
# 趋势单方案:止损在突破 K 极值外侧的百分比(默认 1 即 1%)
KEY_TREND_STOP_OUTSIDE_PCT=1
KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5
# =============================================================================
# 交易执行 / 人工风控(页面「实盘下单」)
# =============================================================================
# 【最大同时持仓】默认 1=单仓
MAX_ACTIVE_POSITIONS=1
# 【人工下单最低盈亏比】低于该值前后端均拒绝(默认 1.4,即须 >=1.4:1
MANUAL_MIN_PLANNED_RR=1.4
# 【关键位连开计仓】已有持仓时按无仓时资金快照算基数
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒(不拦单)
DAILY_OPEN_ALERT_THRESHOLD=5
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日(北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
DAILY_OPEN_HARD_LIMIT=0
# =============================================================================
# 账户冷静期 / 日冻结风控(手动平仓、外部平仓、复盘情绪标签)
# 详见 docs/account-risk-cooldown.md
# =============================================================================
# RISK_CONTROL_ENABLED=true
# RISK_COOLING_HOURS_MANUAL=4
# RISK_COOLING_HOURS_MANUAL_JOURNAL=1
# RISK_MANUAL_CLOSE_DAILY_LIMIT=2
# RISK_MOOD_ISSUES_DAILY_FREEZE=true
# 资金与仓位刷新周期(秒)
BALANCE_REFRESH_SECONDS=60
# 前端价格快照轮询(秒)
PRICE_REFRESH_SECONDS=5
# 后台监控轮询周期(秒)
MONITOR_POLL_SECONDS=3
# 重启后多少秒内不做「外部平仓」同步(避免 API 未就绪误判)
RECONCILE_STARTUP_GRACE_SEC=90
# 连续多少次轮询确认交易所空仓后,才记为外部平仓(默认 3 次 ≈ 9 秒)
RECONCILE_FLAT_CONFIRM_POLLS=3
# 使用可用资金时的缓冲比例(如0.98代表用98%)
FULL_MARGIN_BUFFER_RATIO=0.98
# =============================================================================
# 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值)
# =============================================================================
AUTO_TRANSFER_ENABLED=false
# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转
AUTO_TRANSFER_AMOUNT=30
AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap
TRANSFER_CCY=USDT
# 北京时间该整点小时内尝试;账簿按 UTC 自然日去重
AUTO_TRANSFER_BJ_HOUR=8
# 强制清仓整点(北京时间,默认 0=凌晨00点)
FORCE_CLOSE_BJ_HOUR=0
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
FORCE_CLOSE_ENABLED=false
# 推送与AI超时(秒)
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# 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_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# Gate 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
# 1) 先在本机建立隧道(示例):
# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes
# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名):
# GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080
#
# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用:
# GATE_HTTP_PROXY=http://127.0.0.1:3128
# GATE_HTTPS_PROXY=http://127.0.0.1:3128
# 开仓多周期K线图(可选)
# ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts
# 详见 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT;说明文档 docs/daily-open-limit.md
# 以损定仓(按交易账户资金的百分比)
# RISK_PERCENT=2
# 移动保本触发(达到多少R触发)与偏移(百分比)
# BREAKEVEN_RR_TRIGGER=1.0
# 移动保本阶梯(每多少R继续上移一次,默认1R)
# BREAKEVEN_STEP_R=1.0
# BREAKEVEN_OFFSET_PCT=0.02
# 开单风格默认值:trend / swing
# DEFAULT_TRADE_STYLE=trend
APP_TIMEZONE=Asia/Shanghai
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
-90
View File
@@ -1,90 +0,0 @@
# crypto_monitor_gate
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Gate.io USDT 永续**,通过 **ccxt** 访问。
## 文档导航
| 文档 | 说明 |
|------|------|
| **[使用说明.md](./使用说明.md)** | 日常怎么用:登录、关键位四类、手工开仓、单仓与微信等 |
| **[关键位自动下单说明.md](./关键位自动下单说明.md)** | 关键位自动开仓的 RR、止盈止损、结案原因与 `.env` |
| **[部署文档.md](./部署文档.md)** | Ubuntu、PM2、**SSH SOCKS** 访问 Gate API 等 |
另:**Binance U 本位** 对等实现见同级的 **`crypto_monitor_binance`** 仓库。
---
## 功能概要
- **关键位监控**:5m 收线硬条件、企业微信推送;**箱体 / 收敛** 在 RR 达标时可 **自动市价开仓**(见专门文档);**阻力 / 支撑** 仅单次提醒结案
- **下单监控**:本地风控(含移动保本)、止盈/止损触达后轮询尝试平仓并记账
- **实盘(可选)**`LIVE_TRADING_ENABLED=true` 且配置 **`GATE_API_KEY` / `GATE_API_SECRET`** 时,支持开仓、挂单 TP/SL、余额与划转(权限依账户而定)
- **止盈止损(Gate**:市价成交后经 **`_gate_place_tp_sl_orders`** 挂单;优先 **仓位类 `price_orders`**(受 `GATE_TPSL_USE_POSITION_ORDER``GATE_TPSL_PRICE_TYPE``GATE_POS_MODE` 等影响)
---
## 环境要求
- Python 3.10+(建议)
- 依赖:`flask``requests``ccxt``werkzeug``PySocks`(经 SOCKS 代理时);`Pillow`K 线导出等可选用)
安装示例:
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
source .venv/bin/activate
pip install -r ../requirements.txt
```
## 配置(`.env.example``.env`
- **`.env.example`**:模板(可提交 Git);首次:`cp .env.example .env` 后编辑。
- **`.env`**:本机真实配置(勿提交);`git pull` 不覆盖;升级前建议备份(见《部署文档》§5.2)。
项目启动时加载**仓库根目录**下的 `.env`。常用项:
| 变量 | 说明 |
|------|------|
| `GATE_API_KEY` / `GATE_API_SECRET` | Gate API(需合约与对应权限) |
| `LIVE_TRADING_ENABLED` | `true` 允许真实下单;`false` 仅本地与推送逻辑 |
| `GATE_MARGIN_MODE` / `GATE_POS_MODE` | 保证金与持仓模式 |
| `GATE_TPSL_USE_POSITION_ORDER` / `GATE_TPSL_PRICE_TYPE` 等 | 条件止盈止损行为 |
| `GATE_SOCKS_PROXY` | 可选;直连不稳时 SSH 动态转发(详见部署文档) |
| `APP_PASSWORD` / `FLASK_SECRET_KEY` | Web 登录与 Session |
| `WECHAT_WEBHOOK` | 企业微信机器人 |
| `EXCHANGE_DISPLAY_NAME` / `GATE_ACCOUNT_LABEL` | 页面与推送展示的账户文案 |
其余见 **`.env.example` 内注释** 或 **`app.py` 顶部默认值**。
## 运行
生产使用 **PM2**`ecosystem.config.cjs`)。调试:
```bash
source .venv/bin/activate && python app.py
```
见 [docs/ubuntu-server.md](../docs/ubuntu-server.md)。
端口由 **`APP_PORT`** 控制(未设置默认 **5000**)。浏览器登录 **`/login`**,口令为 **`APP_PASSWORD`**。
## 部署(Linux / PM2 / SSH SOCKS
**[部署文档.md](./部署文档.md)**。
## 自检脚本
```bash
python scripts/verify_gate_funding.py
```
用于核对密钥前缀(不落 Secret)、资金/合约可读性等(需网络与权限)。
## 数据与脚本
- 默认 SQLite:由 **`DB_PATH`** 指定(常见为项目下 `crypto.db`
- `scripts/fix_breakeven_labels.py`:修正「止损」但盈亏为正的记录标签(参见部署文档说明)
## 风险与合规
实盘有亏损风险。请确认 API 权限、IP 白名单、杠杆与保证金模式与 **Gate.io** 后台一致,并遵守当地法律法规与交易所用户协议。
File diff suppressed because it is too large Load Diff
@@ -1,34 +0,0 @@
/**
* PM2 进程定义Ubuntu / Linux
*
* 仅托管 Flask 应用**SSH SOCKS 隧道** `ssh -D` 常驻可用 tmux / autossh勿交给 PM2
* `.env` `GATE_SOCKS_PROXY` 端口一致即可不必交给 PM2
*
* 使用前项目根目录存在 `.venv`且已安装依赖 SOCKS 时需 PySocks
*
* 启动
* pm2 start ecosystem.config.cjs
* 保存开机列表
* pm2 save && pm2 startup
*/
const path = require("path");
const ROOT = __dirname;
const REPO_ROOT = path.join(ROOT, "..");
const PY = path.join(ROOT, ".venv", "bin", "python");
module.exports = {
apps: [
{
name: "crypto_gate_bot",
cwd: ROOT,
script: path.join(ROOT, "app.py"),
interpreter: PY,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "800M",
env: { PYTHONPATH: REPO_ROOT },
},
],
};
@@ -1,109 +0,0 @@
#!/usr/bin/env bash
# Daily backup: SQLite DB + static/images → /root/backups/<instance>/<YYYY-MM-DD>/
# Prune backup folders older than RETENTION_DAYS (default 30).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_DIR"
BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
INSTANCE_NAME="${BACKUP_INSTANCE:-$(basename "$PROJECT_DIR")}"
TZ_NAME="${BACKUP_TZ:-Asia/Shanghai}"
log() {
printf '[%s] %s\n' "$(TZ="$TZ_NAME" date '+%Y-%m-%d %H:%M:%S %Z')" "$*"
}
read_env_var() {
local key="$1"
local default="$2"
local line
if [[ ! -f .env ]]; then
printf '%s' "$default"
return
fi
line="$(grep -E "^${key}=" .env 2>/dev/null | tail -1 || true)"
if [[ -z "$line" ]]; then
printf '%s' "$default"
return
fi
printf '%s' "${line#*=}" | tr -d '\r'
}
resolve_project_path() {
local p="$1"
if [[ "$p" == /* ]]; then
printf '%s' "$p"
else
printf '%s' "$PROJECT_DIR/$p"
fi
}
prune_old_backups() {
local base="$BACKUP_ROOT/$INSTANCE_NAME"
[[ -d "$base" ]] || return 0
local cutoff
cutoff="$(TZ="$TZ_NAME" date -d "-${RETENTION_DAYS} days" +%Y-%m-%d 2>/dev/null || true)"
if [[ -z "$cutoff" ]]; then
find "$base" -mindepth 1 -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" -print0 |
xargs -r -0 rm -rf
return 0
fi
local dir name
for dir in "$base"/*/; do
[[ -d "$dir" ]] || continue
name="$(basename "$dir")"
[[ "$name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || continue
if [[ "$name" < "$cutoff" ]]; then
log "prune: remove $dir (older than ${RETENTION_DAYS} days)"
rm -rf "$dir"
fi
done
}
DB_REL="$(read_env_var DB_PATH crypto.db)"
UPLOAD_REL="$(read_env_var UPLOAD_DIR static/images)"
BACKUP_ROOT="$(read_env_var BACKUP_ROOT "$BACKUP_ROOT")"
RETENTION_DAYS="$(read_env_var BACKUP_RETENTION_DAYS "$RETENTION_DAYS")"
INSTANCE_NAME="$(read_env_var BACKUP_INSTANCE "$INSTANCE_NAME")"
DB_PATH="$(resolve_project_path "$DB_REL")"
UPLOAD_DIR="$(resolve_project_path "$UPLOAD_REL")"
DATE_TAG="$(TZ="$TZ_NAME" date +%Y-%m-%d)"
DEST="$BACKUP_ROOT/$INSTANCE_NAME/$DATE_TAG"
if [[ ! -f "$DB_PATH" ]]; then
log "error: database not found: $DB_PATH"
exit 1
fi
mkdir -p "$DEST"
log "start backup instance=$INSTANCE_NAME dest=$DEST"
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$DB_PATH" ".backup '$DEST/crypto.db'"
log "db: sqlite3 backup -> $DEST/crypto.db"
else
cp -a "$DB_PATH" "$DEST/crypto.db"
log "db: cp -> $DEST/crypto.db (sqlite3 not installed)"
fi
if [[ -d "$UPLOAD_DIR" ]]; then
tar -czf "$DEST/static_images.tar.gz" -C "$(dirname "$UPLOAD_DIR")" "$(basename "$UPLOAD_DIR")"
log "images: $UPLOAD_DIR -> $DEST/static_images.tar.gz"
else
log "warn: upload dir missing, skip images: $UPLOAD_DIR"
fi
{
echo "instance=$INSTANCE_NAME"
echo "project_dir=$PROJECT_DIR"
echo "backup_date=$DATE_TAG"
echo "db_path=$DB_PATH"
echo "upload_dir=$UPLOAD_DIR"
} >"$DEST/manifest.txt"
prune_old_backups
log "done"
@@ -1,69 +0,0 @@
#!/usr/bin/env python3
"""One-shot SQLite backup before code deploy. Reads DB_PATH from .env (default crypto.db)."""
from __future__ import annotations
import os
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path
PROJECT_DIR = Path(__file__).resolve().parent.parent
def _read_env_db_path() -> Path:
env_file = PROJECT_DIR / ".env"
default = PROJECT_DIR / "crypto.db"
if not env_file.is_file():
return default
for line in env_file.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, val = line.split("=", 1)
if key.strip() != "DB_PATH":
continue
val = val.strip().strip('"').strip("'")
p = Path(val)
return p if p.is_absolute() else PROJECT_DIR / p
return default
def main() -> int:
db_path = _read_env_db_path()
if not db_path.is_file():
print(f"error: database not found: {db_path}")
return 1
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
dest_dir = PROJECT_DIR / "backups" / stamp
dest_dir.mkdir(parents=True, exist_ok=True)
dest = dest_dir / db_path.name
try:
src = sqlite3.connect(str(db_path))
dst = sqlite3.connect(str(dest))
src.backup(dst)
dst.close()
src.close()
method = "sqlite3 backup"
except Exception:
shutil.copy2(db_path, dest)
method = "file copy"
manifest = dest_dir / "manifest.txt"
manifest.write_text(
"\n".join(
[
f"project_dir={PROJECT_DIR}",
f"source_db={db_path}",
f"backup_file={dest}",
f"method={method}",
f"created_at={stamp}",
]
),
encoding="utf-8",
)
print(f"ok: {dest} ({method})")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,108 +0,0 @@
#!/usr/bin/env python3
"""
一次性修复历史交易记录标签
trade_records 止损但实际盈利的记录改为保本止盈
默认条件可通过参数修改
- monitor_type = 下单监控
- result = 止损
- pnl_amount > 0
用法示例
1) 仅预览不落库
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
2) 执行修复
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
"""
from __future__ import annotations
import argparse
import sqlite3
import sys
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Fix historical stop-loss records with positive pnl.")
parser.add_argument("--db", required=True, help="Path to sqlite db file, e.g. ./crypto.db")
parser.add_argument("--monitor-type", default="下单监控", help="Filter by monitor_type (default: 下单监控)")
parser.add_argument("--from-result", default="止损", help="Source result label (default: 止损)")
parser.add_argument("--to-result", default="保本止盈", help="Target result label (default: 保本止盈)")
parser.add_argument("--dry-run", action="store_true", help="Preview only, no write")
parser.add_argument("--apply", action="store_true", help="Execute update")
return parser.parse_args()
def main() -> int:
args = parse_args()
db_path = Path(args.db).expanduser().resolve()
if not db_path.exists():
print(f"[ERR] DB not found: {db_path}")
return 1
if args.dry_run and args.apply:
print("[ERR] --dry-run and --apply are mutually exclusive.")
return 1
if not args.dry_run and not args.apply:
print("[INFO] No mode provided, defaulting to --dry-run.")
args.dry_run = True
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
cur = conn.cursor()
where_sql = """
monitor_type = ?
AND result = ?
AND CAST(COALESCE(pnl_amount, 0) AS REAL) > 0
"""
params = (args.monitor_type, args.from_result)
cur.execute(f"SELECT COUNT(*) AS c FROM trade_records WHERE {where_sql}", params)
will_change = int(cur.fetchone()["c"])
print(f"[INFO] Candidate rows: {will_change}")
if will_change == 0:
print("[INFO] Nothing to update.")
conn.close()
return 0
cur.execute(
f"""
SELECT id, symbol, result, pnl_amount, closed_at
FROM trade_records
WHERE {where_sql}
ORDER BY id DESC
LIMIT 10
""",
params,
)
sample = cur.fetchall()
print("[INFO] Sample (latest 10):")
for r in sample:
print(
f" id={r['id']} symbol={r['symbol']} result={r['result']} "
f"pnl={r['pnl_amount']} closed_at={r['closed_at']}"
)
if args.dry_run:
print("[DRY-RUN] No write executed.")
conn.close()
return 0
cur.execute(
f"UPDATE trade_records SET result=? WHERE {where_sql}",
(args.to_result, *params),
)
changed = int(cur.rowcount)
conn.commit()
conn.close()
print(f"[DONE] Updated rows: {changed}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -1,38 +0,0 @@
#!/usr/bin/env bash
# Install daily backup cron: Beijing 00:00 (CRON_TZ=Asia/Shanghai).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BACKUP_SCRIPT="$SCRIPT_DIR/backup_data.sh"
INSTANCE_NAME="${BACKUP_INSTANCE:-$(basename "$PROJECT_DIR")}"
LOG_FILE="${BACKUP_CRON_LOG:-/var/log/crypto-monitor-backup-${INSTANCE_NAME}.log}"
if [[ ! -x "$BACKUP_SCRIPT" ]]; then
chmod +x "$BACKUP_SCRIPT"
fi
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
{
crontab -l 2>/dev/null | grep -vF "$BACKUP_SCRIPT" || true
echo "CRON_TZ=Asia/Shanghai"
echo "0 0 * * * $BACKUP_SCRIPT >> $LOG_FILE 2>&1"
} >"$TMP"
awk '
BEGIN { tz = 0 }
/^CRON_TZ=Asia\/Shanghai$/ {
if (tz++) next
}
{ print }
' "$TMP" >"${TMP}.2"
mv "${TMP}.2" "$TMP"
crontab "$TMP"
echo "Installed cron for $INSTANCE_NAME"
echo " Schedule : daily 00:00 Asia/Shanghai"
echo " Script : $BACKUP_SCRIPT"
echo " Log : $LOG_FILE"
crontab -l | grep -F "$BACKUP_SCRIPT" || true
@@ -1,93 +0,0 @@
"""
在项目根目录执行会加载根目录 .env
python scripts/verify_gate_funding.py
依次探测[0] swap 余额 App交易账户同源[1][3] 现货 / 统一账户资金路径
打印 GATE_API_KEY 8 位便于与 Gate 控制台核对不含 Secret用于服务器自检
"""
from __future__ import annotations
import importlib.util
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
def _load_app():
path = os.path.join(ROOT, "app.py")
spec = importlib.util.spec_from_file_location("crypto_app", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def main():
os.chdir(ROOT)
mod = _load_app()
print("LIVE_TRADING_ENABLED =", os.getenv("LIVE_TRADING_ENABLED"))
ok, reason = mod.ensure_exchange_live_ready()
print("ensure_exchange_live_ready =", ok, repr(reason))
if not ok:
print("跳过私有接口探测")
return 1
mod.ensure_markets_loaded()
k = (os.getenv("GATE_API_KEY") or "").strip()
s = (os.getenv("GATE_API_SECRET") or "").strip()
if not k or "REPLACE" in k.upper():
print("WARN: GATE_API_KEY 为空或仍像占位符,请核对 .env")
if not s or "REPLACE" in s.upper():
print("WARN: GATE_API_SECRET 为空或仍像占位符,请核对 .env")
print("GATE_API_KEY prefix (8 chars):", (k[:8] + "") if len(k) > 8 else "(short)")
# 0) swap — 与 App「交易账户」余额同源(优先看此项是否与网页一致)
try:
bal = mod.exchange.fetch_balance({"type": "swap"})
v0 = mod._extract_usdt_total(bal)
print("[0] fetch_balance(swap) USDT total =", v0)
except Exception as e:
print("[0] fetch_balance(swap) FAILED:", type(e).__name__, e)
# 1) fetch_balance spot + marginMode spot
try:
bal = mod.exchange.fetch_balance({"type": "spot", "marginMode": "spot"})
v = mod._extract_usdt_total(bal)
print("[1] fetch_balance(spot,marginMode=spot) USDT total =", v)
except Exception as e:
print("[1] fetch_balance(spot) FAILED:", type(e).__name__, e)
# 2) raw spot accounts
try:
resp = mod.exchange.privateSpotGetAccounts({})
v2 = mod._parse_gate_spot_accounts_response_usdt(resp)
print("[2] privateSpotGetAccounts USDT =", v2)
except Exception as e:
print("[2] privateSpotGetAccounts FAILED:", type(e).__name__, e)
# 3) unified accounts raw
try:
raw = mod.exchange.privateUnifiedGetAccounts({})
body = raw
if isinstance(body, dict) and isinstance(body.get("result"), dict):
body = body["result"]
if isinstance(body, dict):
keys = sorted(body.keys())
print("[3] unified top-level keys (sample):", keys[:25], "..." if len(keys) > 25 else "")
v3 = mod._parse_usdt_from_gate_unified_accounts_body(body) if isinstance(body, dict) else None
print("[3] parsed unified USDT =", v3)
except Exception as e:
print("[3] privateUnifiedGetAccounts FAILED:", type(e).__name__, e)
fu = mod._fetch_gate_funding_usdt()
print(">>> _fetch_gate_funding_usdt() =", fu)
f, t = mod.get_exchange_capitals(force=True)
print(">>> get_exchange_capitals(force=True) funding, trading =", f, t)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

@@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#22d3ee"/>
<stop offset="100%" stop-color="#34d399"/>
</linearGradient>
</defs>
<rect width="512" height="512" rx="108" fill="#0c1019"/>
<rect x="36" y="36" width="440" height="440" rx="88" fill="#141b2d"/>
<rect x="36" y="36" width="440" height="440" rx="88" fill="none" stroke="url(#g)" stroke-width="12"/>
<path d="M120 320 L200 248 L280 272 L392 168" fill="none" stroke="url(#g)" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="392" cy="168" r="18" fill="#34d399"/>
<rect x="168" y="268" width="28" height="64" rx="6" fill="#f87171"/>
<line x1="182" y1="248" x2="182" y2="340" stroke="#f87171" stroke-width="10" stroke-linecap="round"/>
<rect x="268" y="220" width="28" height="96" rx="6" fill="#34d399"/>
<line x1="282" y1="200" x2="282" y2="340" stroke="#34d399" stroke-width="10" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,23 +0,0 @@
{
"name": "交易监控复盘",
"short_name": "监控",
"description": "加密货币永续交易监控与复盘",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0d14",
"theme_color": "#0b0d14",
"icons": [
{
"src": "/static/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
ok2
@@ -1,136 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="UTF-8">
<script src="/static/instance_theme.js?v=4"></script>
<title>登录 · {{ exchange_display }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a10;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
color: #fff;
}
.login-box {
background: #12121a;
padding: 2.5rem;
border-radius: 16px;
width: 100%;
max-width: 400px;
border: 1px solid #242435;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
}
.login-box h2 {
margin-bottom: 2rem;
text-align: center;
font-size: 1.5rem;
background: linear-gradient(90deg, #4cc2ff, #7b42ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #a9a9ff;
}
.form-group input {
width: 100%;
padding: 0.85rem 1rem;
border-radius: 10px;
border: 1px solid #2e2e45;
background: #1a1a29;
color: #fff;
font-size: 0.95rem;
outline: none;
}
.form-group input:focus {
border-color: #4cc2ff;
}
button {
width: 100%;
padding: 0.9rem;
border-radius: 10px;
border: none;
background: linear-gradient(90deg, #4285f4, #7b42ff);
color: #fff;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: 0.2s;
}
button:hover {
opacity: 0.9;
}
.flash {
padding: 0.8rem;
margin-bottom: 1rem;
background: #331e24;
color: #ff6666;
border-radius: 8px;
text-align: center;
font-size: 0.85rem;
}
.exchange-line {
text-align: center;
font-size: 0.82rem;
color: #8892b0;
margin: -0.5rem 0 1.25rem;
}
.exchange-line strong {
color: #b8f5d0;
font-weight: 600;
}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=4">
</head>
<div class="login-theme-bar">
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
<body>
<div class="login-box">
<h2>交易监控系统登录</h2>
<p class="exchange-line">交易所:<strong>{{ exchange_display }}</strong></p>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
<form method="POST" autocomplete="off">
<div class="form-group">
<label>账号</label>
<input type="text" name="username" required placeholder="请输入账号" autocomplete="off" autocapitalize="off" spellcheck="false">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required placeholder="请输入密码" autocomplete="new-password">
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
@@ -1,194 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>实盘下单放大 | 100根K线</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px}
.container{width:min(98vw,1900px);margin:0 auto}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150;margin-bottom:12px}
.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.btn{padding:7px 10px;border-radius:8px;text-decoration:none;border:1px solid #304164;background:#151a2a;color:#8fc8ff;cursor:pointer}
.btn:hover{background:#1f2740}
select,button{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff}
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;margin-top:10px}
.meta-item{background:#141b2f;border:1px solid #27324e;border-radius:8px;padding:8px}
.meta-item .k{font-size:.76rem;color:#9fb0d8}
.meta-item .v{font-size:1rem;margin-top:4px;word-break:break-all}
.status{font-size:.84rem;color:#95a2c2}
.status.err{color:#ff8080}
#chart-wrap{height:560px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
#chart{width:100%;height:100%}
.empty{padding:18px;color:#95a2c2}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="row" style="justify-content:space-between">
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
</div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
{% if orders %}
<div class="row" style="margin-top:10px">
<label>订单</label>
<select id="order-id">
{% for o in orders %}
<option value="{{ o.id }}" {% if selected_order and o.id == selected_order.id %}selected{% endif %}>
#{{ o.id }} {{ o.symbol }} {{ '做多' if o.direction == 'long' else '做空' }}
</option>
{% endfor %}
</select>
<label>周期</label>
<select id="timeframe">
{% for tf in ['1m','3m','5m','15m','30m','1h','4h','1d'] %}
<option value="{{ tf }}" {% if tf == default_timeframe %}selected{% endif %}>{{ tf }}</option>
{% endfor %}
</select>
<button id="manual-refresh" type="button">刷新</button>
<span id="load-status" class="status"></span>
</div>
{% else %}
<div class="empty">当前没有激活订单,无法展示放大K线。</div>
{% endif %}
</div>
{% if orders %}
<div class="card">
<div class="meta">
<div class="meta-item"><div class="k">交易对</div><div class="v" id="m-symbol">-</div></div>
<div class="meta-item"><div class="k">方向</div><div class="v" id="m-direction">-</div></div>
<div class="meta-item"><div class="k">成交价</div><div class="v" id="m-entry">-</div></div>
<div class="meta-item"><div class="k">止损</div><div class="v" id="m-sl">-</div></div>
<div class="meta-item"><div class="k">止盈</div><div class="v" id="m-tp">-</div></div>
<div class="meta-item"><div class="k">盈亏比</div><div class="v" id="m-rr">-</div></div>
<div class="meta-item"><div class="k">现价</div><div class="v" id="m-price">-</div></div>
<div class="meta-item"><div class="k">浮盈亏</div><div class="v" id="m-pnl">-</div></div>
</div>
</div>
<div class="card">
<div id="chart-wrap"><div id="chart"></div></div>
</div>
{% endif %}
</div>
{% if orders %}
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
<script>
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
const orderSelect = document.getElementById("order-id");
const tfSelect = document.getElementById("timeframe");
const statusEl = document.getElementById("load-status");
const updatedAtEl = document.getElementById("updated-at");
const chartHost = document.getElementById("chart");
const fmt = (v, d=6) => (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) ? "-" : Number(v).toFixed(d);
let chart = null;
let candleSeries = null;
let priceLines = [];
function ensureChart(){
if(chart){ return true; }
if(!window.LightweightCharts){
statusEl.className = "status err";
statusEl.innerText = "图表库加载失败";
return false;
}
chart = LightweightCharts.createChart(chartHost, {
layout: { background: { color: "#0f1320" }, textColor: "#d6deff" },
grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } },
rightPriceScale: { borderColor: "#2a3150" },
timeScale: { borderColor: "#2a3150", timeVisible: true, secondsVisible: false },
crosshair: { mode: 0 }
});
candleSeries = chart.addCandlestickSeries({
upColor: "#4cd97f",
downColor: "#ff6666",
borderVisible: false,
wickUpColor: "#4cd97f",
wickDownColor: "#ff6666"
});
window.addEventListener("resize", () => {
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
});
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
return true;
}
function resetPriceLines(){
if(!candleSeries){ return; }
priceLines.forEach(line => {
try { candleSeries.removePriceLine(line); } catch (_) {}
});
priceLines = [];
}
function addLine(price, title, color){
if(!candleSeries || typeof price === "undefined" || price === null){ return; }
const p = Number(price);
if(Number.isNaN(p) || p <= 0){ return; }
priceLines.push(candleSeries.createPriceLine({
price: p, color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title
}));
}
function paintOrder(order){
document.getElementById("m-symbol").innerText = order.symbol || "-";
document.getElementById("m-direction").innerText = (order.direction === "short") ? "做空" : "做多";
document.getElementById("m-entry").innerText = order.trigger_price_display || fmt(order.trigger_price, 8);
document.getElementById("m-sl").innerText = order.stop_loss_display || fmt(order.stop_loss, 8);
document.getElementById("m-tp").innerText = order.take_profit_display || fmt(order.take_profit, 8);
document.getElementById("m-rr").innerText = (order.rr_ratio === null || typeof order.rr_ratio === "undefined") ? "-" : `1:${Number(order.rr_ratio).toFixed(2)}`;
document.getElementById("m-price").innerText = order.current_price_display || fmt(order.current_price, 8);
const pnlEl = document.getElementById("m-pnl");
pnlEl.innerText = `${fmt(order.float_pnl, 2)}U (${fmt(order.float_pct, 2)}%)`;
pnlEl.style.color = Number(order.float_pnl || 0) > 0 ? "#4cd97f" : (Number(order.float_pnl || 0) < 0 ? "#ff6666" : "#d6deff");
}
async function loadOrderKline(){
if(!ensureChart()){ return; }
const orderId = orderSelect.value;
const timeframe = tfSelect.value;
if(!orderId){ return; }
statusEl.className = "status";
statusEl.innerText = "加载中...";
try{
const resp = await fetch(`/api/order_kline?order_id=${encodeURIComponent(orderId)}&timeframe=${encodeURIComponent(timeframe)}`);
const data = await resp.json();
if(!resp.ok || !data.ok){ throw new Error(data.msg || "请求失败"); }
const candles = Array.isArray(data.candles) ? data.candles : [];
if(!candles.length){
statusEl.className = "status err";
statusEl.innerText = "暂无K线数据";
return;
}
candleSeries.setData(candles);
resetPriceLines();
addLine(data.order.trigger_price, "成交价", "#42a5f5");
addLine(data.order.stop_loss, "止损", "#ff6666");
addLine(data.order.take_profit, "止盈", "#4cd97f");
chart.timeScale().fitContent();
paintOrder(data.order || {});
updatedAtEl.innerText = data.updated_at || "--";
statusEl.className = "status";
statusEl.innerText = `已加载 ${candles.length} 根K线`;
}catch(err){
statusEl.className = "status err";
statusEl.innerText = err && err.message ? err.message : "加载失败";
}
}
document.getElementById("manual-refresh").addEventListener("click", loadOrderKline);
orderSelect.addEventListener("change", loadOrderKline);
tfSelect.addEventListener("change", loadOrderKline);
loadOrderKline();
setInterval(loadOrderKline, refreshMs);
</script>
{% endif %}
</body>
</html>
-147
View File
@@ -1,147 +0,0 @@
# 使用说明
**本文件对应仓库:`crypto_monitor_gate`Gate.io USDT 永续)。**
功能、界面与 **Binance U 本位版**(目录 `crypto_monitor_binance`)基本一致,差异主要在 **`.env` 里交易所密钥与部分参数名**`GATE_*` / `BINANCE_*`),文末有对照。
**更细的部署(SSH 代理、PM2、依赖安装)** 见同目录 **`部署文档.md`**。
**关键位自动开仓的规则、RR、结案原因** 见 **`关键位自动下单说明.md`**。
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**,主要能力包括:
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型,按 **5m 收线** 做硬条件过滤;符合条件后 **企业微信** 提醒,部分类型可 **自动市价开仓**(见第 4 节与专门文档)。 |
| **实盘下单监控** | 手工填止损/止盈,**以损定仓** 市价开单,挂上条件止盈止损,并在页面跟踪浮盈亏、保本逻辑等。 |
| **交易记录 / 复盘** | 平仓结果、盈亏、错过的单等归档与导出;可选 **AI 复盘**(见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md))。 |
| **策略交易** | 顶栏 `/strategy`:趋势回调 + 顺势加仓双栏;见 [策略交易说明.md](../策略交易说明.md)。 |
后台按 **`MONITOR_POLL_SECONDS`**(默认几秒)轮询行情与监控逻辑。**切勿**在未理解规则时同时运行两套程序共用一个实盘账户。
---
## 2. 运行前必须配置(`.env`
首次在本目录执行 **`cp .env.example .env`**,再编辑 `.env``.env` 勿提交 Git`git pull` 不会改你的 `.env`,升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`)。
至少检查以下项(具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`:打开站点后的登录口令。`FLASK_SECRET_KEY`:Session 密钥,请勿使用默认值。 |
| **企业微信** | `WECHAT_WEBHOOK`:告警与关键位推送机器人的 Webhook。 |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`:**不会**向交易所发送开仓指令(适合测试流程)。改为 `true` 且密钥正确才会实盘。 |
| **交易所 API** | **本仓库:** `GATE_API_KEY``GATE_API_SECRET`;合约相关见 `GATE_MARGIN_MODE``GATE_POS_MODE``GATE_TPSL_*` 等。**勿**把 `.env` 提交到 Git。 |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR``KEY_STOP_OUTSIDE_BREAKOUT_PCT`(详见 `关键位自动下单说明.md`)。 |
| **AI 复盘** | `AI_PROVIDER=openai`(默认)或 `ollama`;变量见 `.env.example` 与 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)。 |
网络不稳定时可为 Gate 配置 **`GATE_SOCKS_PROXY`** 等(见 **`部署文档.md`**)。
---
## 3. 如何启动与登录
1. 按 **`部署文档.md`** 建好虚拟环境、安装依赖(如 `flask``requests``ccxt`、按需 `Pillow``PySocks` 等),配置好 `.env`
2. 启动 Flask 应用(本仓库可用 **`ecosystem.config.cjs`** 交给 PM2,或本地 `python app.py` / `flask run`,以你当前脚本为准)。
3. 浏览器访问站点,打开 **`/login`**,使用 **`.env` 里的 `APP_PASSWORD`** 登录。
登录后顶栏:**关键位监控** | **实盘下单** | **策略交易**`/strategy`| **策略交易记录**`/strategy/records`| **交易记录与复盘** | **统计分析**
---
## 4. 关键位监控(顶栏「关键位监控」→ `/key_monitor`
### 4.1 添加一条关键位
1. **币种**:如 `BTC``BTC/USDT`(会规范成内部符号)。
2. **类型**(必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**(需 `LIVE_TRADING_ENABLED=true` 且无其他持仓占位)。结案后本条从列表消失并记入历史。 |
| **收敛突破** | 同上(自动开仓类)。 |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**。 |
| **关键支撑位** | 同上(仅提醒)。 |
| **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选)。
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**
**限制:**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**」;仍可添加「**关键阻力位 / 支撑位**」。
**4h EMA55** 与你的方向逆势,页面会 **额外 Flash 提示****不阻挡**提交。
### 4.2 触发后会发生什么(简版)
- **箱体 / 收敛**:门控通过后计算计划 SL/TP 与 RR;不达标则 **微信说明 + `rr_insufficient` 结案**;达标则尝试 **市价开仓**,成功 **`auto_opened`**,失败 **`exchange_failed`**——均 **不重试同一关键位**
- **阻力 / 支撑**:仅 **单次推送****`key_level_alert_only`** 结案。
详细公式、结案字段、与企业微信文案口径见 **`关键位自动下单说明.md`**。
### 4.3 列表与历史
- 当前条目可 **删除**(会按规则记入历史的情形见页面说明)。
- **关键位历史**:已结案记录;可配合导出链接(若有)做备份。
---
## 5. 实盘下单(顶栏「实盘下单」→ `/trade`
用于 **自己点按钮** 开单:
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制(默认 1,与关键位自动单共用)。
- **人工开仓**时计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**(默认 1.4:1),否则页面弹窗且后端拒绝。
- 填写币种、方向、杠杆(可选)、止损/止盈(价格或百分比按表单说明)。
- 勾选是否启用 **移动保本** 等行为以 `.env`/页面默认值为准。
平仓通过页面 **平仓**(或等价入口),会从交易所市价处理并更新记录。**删除/误操作可能造成真实盈亏**,请先确认环境与方向。
开仓成功后持仓卡片上会显示 **「来源」**:手工单一般为 **下单监控**;来自关键位自动单的为 **关键位监控**
---
## 6. 企业微信会看到什么
- 关键位:按类型与结案结果推送(RR 不足、下单失败、自动开仓成功、仅阻力支撑提醒等),**每条关键位结案路径原则上一条主推送**(详见 `关键位自动下单说明.md`)。
- 手工开仓、平仓、部分异常也会在规则满足时推送(以代码与配置为准)。
若未配置 **`WECHAT_WEBHOOK`** 或网络失败,可能只是看不到推送,不代表逻辑未执行;要紧操作请以 **交易所端持仓与挂单** 为准核对。
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 验证页面、录入、推送,再开小资金开实盘。
2. **API 权限**:仅开所需合约权限;勿泄露密钥;定期轮换。
3. **单进程控盘**:同一账户避免本程序与其他机器人 **重复开仓**
4. **自动备份**:服务器上执行 `bash scripts/install_backup_cron.sh`(每天北京时间 0:00 → `/root/backups`,保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次。
5. **升级代码后**:启动时会跑 **数据库迁移**(如新列 `order_monitors.monitor_type`);首次启动关注一下日志或无报错页面。
---
## 8. 常见问题(简要)
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 5m 门控是否全通过(页面门控摘要)、币种日成交量是否在规则内、`KLINE_TIMEFRAME`。 |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED``KEY_AUTO_MIN_PLANNED_RR`、计划 RR、是否已有持仓、API/余额报错(微信或日志)。 |
| 加不了箱体/收敛 | 是否已有活跃持仓;先平仓或改用「阻力/支撑位」仅提醒。 |
| 推送收不到 | `WECHAT_WEBHOOK`、企业微信机器人配额与网络。 |
---
## 9. Binance 版(`crypto_monitor_binance`)差异速查
| 项目 | Gate 本仓库 | Binance 版 |
|------|-------------|------------|
| API 变量 | `GATE_API_KEY``GATE_API_SECRET``GATE_*` | `BINANCE_API_KEY``BINANCE_API_SECRET``BINANCE_*` |
| 实盘开关 | `LIVE_TRADING_ENABLED`(通用) | 同上 |
| 止盈止损挂载路径 | `_gate_place_tp_sl_orders``GATE_TPSL_*` | `_binance_place_tp_sl_orders`U 本位条件单) |
| 资金显示舍入 | 以本仓库为准 | 与 **`FUNDS_DECIMALS`** 等一致 |
| 专门文档 | **`关键位自动下单说明.md`**(各仓库有一份,开头标明交易所) | 同左 |
操作流程(登录、关键位四类、手工单、单仓)**两份程序一致**:换目录、换 `.env` 即可对照使用。
@@ -1,143 +0,0 @@
# 关键位监控说明(自动开仓 + 人工盯盘)
**适用:`crypto_monitor_gate`Gate U 本位永续)**
Binance / OKX 见各自目录下同名文档;共享逻辑在仓库根目录 `key_monitor_lib.py`
本文档与 `.env``check_key_monitors``add_key``_key_hard_checks``_process_key_rs_level_alert` 一致。
---
## 一、监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**(门控 + RR) | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**(同上) | 同上 |
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
**添加时(所有类型):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30**;上沿 **>** 下沿。
---
## 二、关键阻力位 / 关键支撑位(人工盯盘)
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**(程序同时监控两侧,**无法预先判定**做多还是做空)。
- 页面 **不显示、不要求** 方向;库中 `direction` 初始为 `watch`**首次突破后** 写入 `long`(向上突破上沿)或 `short`(向下突破下沿)。
### 2.2 触发(极简)
- 周期:**`KLINE_TIMEFRAME`(默认 5m)最近一根已闭合 K** 的 **收盘价**(非影线)。
- **向上突破上沿:** `收盘 > upper` → 推断方向 **多 / 向上**,本次监控任务开始按节奏提醒。
- **向下突破下沿:** `收盘 < lower` → 推断方向 **空 / 向下**,本次任务同样开始提醒。
- **任一侧突破即结束本条监控周期**(不会在突破后再等待另一侧;上沿、下沿谁先满足用谁,同根 K 仅可能满足一侧)。
**不参与:** 量能、二确 K、越过幅度下限、日成交排名(运行时)、计划 RR、自动开仓。
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次:首次检测到突破的当次轮询(若已闭合 5m 满足条件)。
- 第 2、3 次:仅按间隔推送(**不要求**价格仍在箱外)。
- 第 3 次推送后:写入 `key_monitor_history``close_reason=**key_level_alert_done**`,从 `key_monitors` **删除**
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根(突破 K + 确认 K |
| 提醒次数 | 3 次后结案 | 自动单:触发后 1 次业务推送并结案 |
---
## 三、箱体突破 / 收敛突破(自动开仓)
### 3.1 K 线结构(默认索引)
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控(须全部通过)
1. **有效突破(收盘越界)**
- 多:`突破 K 收盘 > upper`
- 空:`突破 K 收盘 < lower`
2. **突破越过幅度(仅下限)**
- 多:`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`(默认 **0.03%**
- 空:`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**;突破过猛由 **计划 RR** 过滤。
- **不再**使用 K 线实体占开盘价比例;`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**
3. **确认 K 不进箱体**
- 多:确认 K 收盘 **`> upper`**(不得在 `[lower, upper]` 内)
- 空:确认 K 收盘 **`< lower`**
4. **量能:** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`(默认 20)根均量 × `KEY_VOLUME_RATIO_MIN`(默认 1.3
5. **日成交量排名:** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30
6. **计划 RR(最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后,`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)才市价开仓
### 3.3 止损 / 止盈(确认 K 收盘为 E)
箱体高 **H = |upper lower|**。止损锚在 **突破 K 极值** 外侧:
| 方向 | 止损(标准/趋势方案) |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表(与改版前一致):
| 方案 | `sl_tp_mode` | 多:SL / TP | 空:SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案(`close_reason`
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿) |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四、环境与参数(`.env` 摘要)
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限(默认 0.03) | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用(默认 3 次 / 5 分钟) |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 五、相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
-148
View File
@@ -1,148 +0,0 @@
# 界面与风控更新说明(Gate 实例)
## 顶栏导航(4 项)
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加、实时门控、历史 |
| 2 | 实盘下单 | `/trade` | 人工开仓、划转、实时持仓(**默认首页** `/``/trade` |
| 3 | 交易记录与复盘 | `/records` | 交易记录、复盘表单、AI 历史(受顶栏 UTC 时间窗筛选) |
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 关键位监控页
- 标题去掉「5m」;规则条从 `.env` 读取(周期、确认K、量能、自动开仓盈亏比、日成交量排名)。
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控。
- 右列:关键位历史(失效/结案),与左列等高滚动;**受顶栏 UTC 列表时间窗筛选**(默认 UTC 当日)。
- 监控类型新增:**斐波回调0.618**、**斐波回调0.786**(与 Binance 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
### 斐波关键位监控(方案 A:交易所限价)
| 项 | 说明 |
|----|------|
| 同币互斥 | 每个币种只能有一条斐波监控(0.618 与 0.786 不可并存) |
| 上下沿 | 上沿 **H**、下沿 **L**(须 H > L |
| 挂单价 E | **做多** `E = H ratio × (H L)`(自 H 向下回撤);**做空** `E = L + ratio × (H L)`(自 L 向上反弹) |
| 做多 | 限价 @ E,止损 L,止盈 H |
| 做空 | 限价 @ E,止损 H,止盈 L |
| 添加后 | **立即**在 Gate 挂限价单;卡片显示 **挂E**、限价单 ID |
| 失效 | 以**标记价**判断:做多且标记价 ≥ H、做空且标记价 ≤ L,且限价**未成交** → 撤销该限价单并结案(不写历史开仓) |
| 成交后 | 按仓位挂交易所 TP/SL → 写入 **实盘下单监控**`monitor_type=关键位监控``key_signal_type=斐波回调0.618/0.786`)→ 从关键位列表移除 |
| 撤单 | 仅撤本条斐波的 `fib_limit_order_id`**不会** `cancel_all`,避免误伤其他委托 |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`(与箱体/收敛一致);0.618 理论约 1.6:10.786 约 3.7:1 |
| 日成交量 | 与箱体/收敛相同,须在前 `KEY_DAILY_VOLUME_RANK_MAX` 名内方可添加 |
后台轮询:`check_fib_key_monitors()`(标记价失效 / 成交检测);箱体/收敛仍走 `check_key_monitors()`,互不干扰。
手动删除关键位时,若斐波限价尚未成交,会先撤交易所限价再删库记录。
### 箱体 / 收敛自动开仓(来源标注)
- 自动开仓写入 `order_monitors.key_signal_type``箱体突破``收敛突破`
- 持仓卡片、交易记录列表会显示「来源 · 信号类型」。
## 列表时间窗(UTC,全站顶栏)
共用模块:仓库根目录 `history_window_lib.py`Gate / Binance 主站一致)。
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**`win_preset=utc_today`,从 UTC 0:00 至当前时刻) |
| 可选 | 近 24 小时、近 7 天、自定义起止(UTC,`datetime-local` |
| 作用范围 | 关键位历史、交易记录列表、复盘记录 API、AI 历史 API、导出「交易记录」「关键位历史」 |
| 与统计的关系 | **仅影响列表/导出****统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切交易日** |
| 库内时间 | DB 存北京时间字符串;后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
| 切换方式 | 顶栏「列表筛选(UTC)」→ 选预设 → **应用**(保留当前路由,如 `/records?win_preset=…` |
查询参数示例:
- `?win_preset=utc_today`
- `?win_preset=utc_last24h` / `utc_last7d`
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
## 交易记录与复盘
- 平仓记录可同步交易所已实现盈亏(Gate 仓位历史等);列表盈亏列优先显示交易所数据,标注 **所** / **估**
- 记录页提供 **立即同步**`POST /api/sync_exchange_pnl`),用于补全或刷新 `exchange_realized_pnl` 等字段。
- 未做人工复盘时,展示以交易所盈亏为准(有同步数据时)。
- **列表默认只显示当前 UTC 时间窗内**的记录(见上节);导出 CSV 同步该时间窗。
- 表头 **「止损(开仓)」**:展示开仓快照 `initial_stop_loss`(无则回退 `stop_loss`);核对/复盘仍可用有效止损字段。
- 平仓写入 `trade_records` 时:`stop_loss``initial_stop_loss` 均写入**开仓时止损快照**`key_signal_type` 保留箱体/收敛/斐波来源(`fib_key_monitor_lib.key_signal_type_for_trade_record`)。
- **开仓类型**`entry_reason`):机器单平仓入库时,若未手填,按 `key_signal_type` 自动映射(见下表);列表/导出「开仓类型」列 = 复盘核对值优先,否则入库值,否则按信号映射。
| `key_signal_type` | 自动写入的 `entry_reason` |
|-------------------|---------------------------|
| 箱体突破 | 关键位箱体突破 |
| 收敛突破 | 关键位收敛突破 |
| 斐波回调0.618 | 关键位斐波0.618 |
| 斐波回调0.786 | 关键位斐波0.786 |
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案(与趋势/波段类并列)。
- 复盘 **离场触发** 新增 **「止盈」**;从交易记录「填入复盘」时,若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项,并按 `key_signal_type` 预填开仓类型。
- 勾选「保存时自动生成多周期 K 线图」时:以 **平仓时间** 为锚点,各周期向前约 `ORDER_CHART_LIMIT`(默认 100)根 K 线(`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」。
- `/api/journals``/api/reviews` 支持同一时间窗 query,与列表一致。
### 导出(交易记录 v3
- 文件名:`trade_records_v3_YYYYMMDD.csv`
- 相对 v2 增加:`key_signal_type``initial_stop_loss`(及开仓快照列)、`planned_rr``actual_rr``risk_amount`、交易所盈亏与时间字段等;末列「开仓类型」为有效展示文案。
- 「关键位历史」导出同样受 UTC 时间窗限制。
## 实盘下单页
- 左列:实盘下单监控(表单、划转、规则)。
- 右列:实时持仓(独立模块)。
- **人工开仓门控**:计划盈亏比 &lt; `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。
- **移动保本**(勾选启用):监控轮询达到触发 RR 后,止损阶梯上移时**同步交易所**——调用与页面「挂止盈止损」相同的 **先撤后挂**`replace_active_monitor_tpsl_on_exchange`:撤该合约全部 TP/SL 条件单 → 按新止损 + 原止盈重挂)。仅交易所成功后才写库;失败发企业微信告警,本地止损不变。未配置实盘 API 时仍只更新本地(与旧行为一致)。
## 统计分析页(`/stats`
| 项 | 说明 |
|----|------|
| 切日 | **北京时间**;交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00``.env` 默认 **8** |
| 品类下拉 | 页顶 **「统计品类」** 下拉切换(默认「全部交易」):全部交易、下单监控、关键位箱体突破、关键位收敛结构、关键位斐波0.618、关键位斐波0.786;一次只显示所选品类的日/周/月 |
| URL | 切换后写入 `stats_segment=`(如 `all``manual``key_box``key_conv``key_fib618``key_fib786`),刷新 `/stats` 可保持选项 |
| 每块指标 | 日 / 周 / 月:开单次数、平仓笔数、胜率、净盈亏、回撤、连续亏损等(与原口径一致) |
| 开单次数 | 人工块:`monitor_type=下单监控` 且无 `key_signal_type`;关键位块:按 `order_monitors.key_signal_type` 计数 |
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录,按北京交易日归类,**不**随顶栏 UTC 列表窗切换 |
## 持仓与计仓
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大)。
- 关键位自动开仓:在已有持仓时,若 `KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true`,按**首笔开仓前**交易账户资金快照计仓(`trading_sessions.key_sizing_capital_snapshot`)。
## 配置
详见 `.env.example` 中「关键位门控」「交易执行 / 人工风控」注释段。Gate 专用项(`GATE_*`、止盈止损触发等)保持原有段落不变。
## 自动备份(服务器)
- 脚本:`scripts/backup_data.sh``crypto.db` + `static/images`
- 定时:`scripts/install_backup_cron.sh` → 每天 **北京时间 0:00**,目录 **`/root/backups/<实例名>/YYYY-MM-DD/`**,保留 **30**
- 详见 `部署文档.md` 第 5.4 节(自动备份)
## 数据库(启动时自动迁移)
`key_monitors` 新增斐波字段(示例):`fib_limit_order_id``fib_entry_price``fib_stop_loss``fib_take_profit``fib_order_amount``fib_margin_capital``fib_leverage`
`trade_records` / `order_monitors` 新增或沿用:`key_signal_type``exchange_realized_pnl``exchange_opened_at``exchange_closed_at``exchange_sync_key``entry_reason``reviewed_entry_reason``initial_stop_loss`
**历史数据**:本次**不做**旧记录的批量回填(`entry_reason` / `initial_stop_loss` / `key_signal_type` 等);仅**新产生**的平仓与复盘按新逻辑写入。旧行展示可回退已有字段。
## 涉及文件(便于排查)
| 路径 | 说明 |
|------|------|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
| `fib_key_monitor_lib.py` | 斐波计算、`KEY_ENTRY_REASON_BY_SIGNAL``entry_reason_from_key_signal` |
| `crypto_monitor_gate/app.py` | 列表筛选、统计分块、导出 v3、复盘 K 线锚点、入库逻辑 |
| `crypto_monitor_gate/templates/index.html` | 顶栏时间窗、统计分块 UI、止损(开仓)列、复盘预填 |
## 升级步骤
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`
2. 在 VPS 上为 Binance / Gate / Gate Bot **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装)。
3. 重启 Gate 实例服务(如 `pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 缺列(斐波、交易所盈亏、`entry_reason` 等)。
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存。
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**`/stats` 可见分品类统计与「北京 8:00 切日」说明。
6. 建议在测试币上先添加一条斐波监控,确认:限价已挂出、标记价失效会撤单、成交后出现持仓监控且 TP/SL 已挂上;平仓后交易记录止损(开仓)与开仓类型是否正确。
-339
View File
@@ -1,339 +0,0 @@
# `crypto_monitor_gate_bot` 部署指南:SSH SOCKS + Gate.io + PM2Ubuntu
Ubuntu 环境总览见 **[docs/ubuntu-server.md](../docs/ubuntu-server.md)**。
本文面向:**在本机运行本项目**,但 **直连 Gate.io API 不稳定或被重置** 的场景。思路是:
- 本机用 `ssh -D` 做动态转发,把 **SOCKS5 出口**放到能正常访问 Gate 的机器(常见为一台境外 VPS)
- 项目在 `.env` 中设置 **`GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080`**(或你实际端口),`ccxt` 经 SOCKS 访问交易所
- **SSH 隧道**:用 `ssh -D` 在本机常驻(可用 **tmux****autossh** 保持连接),**不要** 把 `ssh` 交给 PM2
- 使用 **PM2** 仅托管 **Flask 应用**;仓库根目录 **`ecosystem.config.cjs`** 只定义 `crypto-monitor-gate`
> 安全提醒:不要把 `.env`、私钥 `.pem`、Gate API Key 提交到 Git;下文只用占位符。
---
## 0. 你需要准备的东西
- 一台 **Ubuntu**(或同类 Linux)运行项目的机器(下文称「本机」)
- 一台可 SSH 登录、且 **能正常访问 Gate.io API** 的 VPS(示例:`HostName` 填你的服务器 IP,用户如 `root`
- SSH:**私钥登录**(推荐,便于隧道脚本无人值守)
- 本机已安装:`python3``python3-venv``pip``curl``ssh``git`(可选)、`node` + `npm`(安装 PM2
---
## 1. 获取代码与目录
将包含 `app.py` 的项目放到固定目录,例如:
```bash
mkdir -p /opt/crypto_monitor
cd /opt/crypto_monitor
git clone https://git.bz121.com/dekun/crypto_monitor.git
cd crypto_monitor/crypto_monitor_gate_bot
```
下文用 **`/opt/crypto_monitor/crypto_monitor_gate_bot`** 仅为示例,请换成你的实际绝对路径。
拉取代码后,若目录下尚无 `.env`
```bash
cp -n .env.example .env
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 私钥示例:~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`(示例别名 **`gate-vps`**,与你手工启动 `ssh -D ... gate-vps` 一致即可):
```sshconfig
Host gate-vps
HostName 你的_VPS_IP
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试:
```bash
ssh gate-vps true
```
> 若尚未完全改为密钥登录,可暂时注释 `BatchMode yes`,调试完成后再打开。
---
## 3. 手工验证:SSH SOCKS + Gate API
### 3.1 本地 SOCKS(示例端口 1080
```bash
ssh -N -D 127.0.0.1:1080 gate-vps
```
保持运行,另开终端继续。
### 3.2 验证经 SOCKS 可访问 Gate
```bash
curl -4 -sS --max-time 15 --proxy socks5h://127.0.0.1:1080 https://api.gateio.ws/api/v4/spot/time
```
应返回 JSON(含服务器时间字段)。若此处失败,**不要先启动应用**:先修隧道或 VPS 出站。
---
## 4. Python 虚拟环境
```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
走 SOCKS 时 **必须** 安装 **`PySocks`**,否则易出现代理相关报错。
可选:
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量(`.env.example``.env`
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释,可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置;`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot
cp -n .env.example .env
nano .env
```
### 5.2 备份与 `git pull`
- **`.env` 不在 Git 中**`git pull` **不会**覆盖本地 `.env`
- 远端若更新 **`.env.example`**,pull 后请**手动**把新增变量补进你的 `.env`
- **升级前备份**`cp .env .env.backup.$(date +%Y%m%d)`;恢复:`cp .env.backup.YYYYMMDD .env`
- **换机**`scp` 复制 `.env`,或新机 `cp .env.example .env` 后重填。
### 5.3 AI 复盘与模型(可选)
共用根目录 **`ai_client.py`**`PYTHONPATH=..`)。`.env` 默认 **`AI_PROVIDER=openai`** + `OPENAI_API_BASE` / `OPENAI_API_KEY` / `OPENAI_MODEL`;或 **`ollama`** + `OLLAMA_API` / `AI_MODEL`。详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**。
### 5.4 自动备份(数据库 + 复盘图片)
每天 **北京时间 0:00** 备份到 **`/root/backups`**,保留 **30 天**`crypto.db` + `static/images`)。
```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot
chmod +x scripts/backup_data.sh scripts/install_backup_cron.sh
bash scripts/install_backup_cron.sh
bash scripts/backup_data.sh # 试跑
```
备份目录:`/root/backups/crypto_monitor_gate_bot/YYYY-MM-DD/`。与 Binance / Gate 实例规则相同,详见 `crypto_monitor_binance/部署文档.md` 第 5.4 节(恢复步骤、可选 `.env` 变量)。
若服务器同时跑 **binance、gate、gate_bot** 三个实例,请在**各自项目目录**各执行一次 `install_backup_cron.sh`
### 5.4 必填项检查(Gate + 代理)
与交易所相关的变量必须是 **Gate** 前缀(**不要**再写 OKX 变量,否则代理不会生效、密钥也不会被识别)。至少确认:
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# 实盘(按需)
LIVE_TRADING_ENABLED=false
GATE_API_KEY=你的_Key
GATE_API_SECRET=你的_Secret
# 经本机 SSH 动态转发访问 Gate(端口与隧道一致)
GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 若不用 SOCKS,可改用 HTTP 代理(一般二选一)
# GATE_HTTP_PROXY=http://127.0.0.1:7890
# GATE_HTTPS_PROXY=http://127.0.0.1:7890
```
说明:**推荐 `socks5h://`**,由 SOCKS 端解析域名,与 `curl --proxy socks5h://...` 行为一致。
### 5.4 趋势回调策略(可选)
若使用「交易执行」页的 **趋势回调** 计划:
- 详细规则见项目根目录 **`趋势回调策略说明.md`**。
- **两阶段**:先「生成预览」(默认 **120 秒**内有效),再「确认执行」;执行时若可用余额与预览快照偏差超过 **5%** 会拒绝(可调 `.env`)。
- 补仓档位数默认 **5**,预览有效期与余额偏差阈值可在 `.env` 覆盖:
```env
TREND_PULLBACK_DCA_LEGS=5
TREND_PULLBACK_PREVIEW_TTL_SECONDS=120
TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT=5
```
- **生成预览**与**确认执行**时都会读取 **Gate 永续账户 USDT 可用余额**;请尽量使用 **单独子账户** 承载策略资金。
**界面与对账(与策略说明 3.4–3.5 节一致)**
- 页顶 **计划历史**:仅 **已结束** 的趋势计划(不含未执行预览);可 **删除** 计划行,并删除 `trend_plan_id` 关联的「趋势回调」`trade_records`(新数据;旧行无 `trend_plan_id` 不级联)。
- **运行中计划**展示交易所 **未实现盈亏**(浮盈亏)。
- **交易记录**:趋势单在配置 API Key 后,打开「交易执行 / 交易记录」页会按节流(约 **25 秒**内同进程最多一次)拉取 Gate **平仓历史**,回填 **`exchange_realized_pnl`** 等;列表展示优先用交易所口径(见策略说明)。
**与交易所对齐的可选环境变量**
```env
# 平仓历史同步起点:北京日期 YYYY-MM-DD 的 0 点(与 APP_TIMEZONE 一致);留空则从近 90 天拉取
# EXCHANGE_POSITION_SYNC_FROM_BJ=2026-05-14
# EXCHANGE_POSITION_HISTORY_LIMIT=200
```
说明:同步 **只读** 交易所接口,**不要求** `LIVE_TRADING_ENABLED=true`;无 Key 时不拉取,界面仍可用(浮盈亏可能为「—」、交易记录仍为本地「估」)。
**交易记录 CSV**:导出为 **v3**,含 `trend_plan_id` 与交易所对齐列(详见策略说明数据库一节)。
---
## 6. 手工启动 Flask(验证)
1. SOCKS 已监听 `127.0.0.1:1080`
2. 已 `source .venv/bin/activate`
3. `.env` 已含 `GATE_SOCKS_PROXY`
```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot
source .venv/bin/activate
python app.py
```
浏览器访问:`http://127.0.0.1:5000`(或你在 `.env` 中的端口)。
---
## 7. 安装 PM2
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 8. PM2:使用仓库内 `ecosystem.config.cjs`(推荐)
在项目根目录:
```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot
pm2 start ecosystem.config.cjs
pm2 status
pm2 logs --lines 200
```
默认只启动 **`crypto-monitor-gate`**`.venv/bin/python app.py`)。
### 本机已可直连 Gate、不需要隧道时
`.env` 里应 **去掉或留空** `GATE_SOCKS_PROXY`(除非仍要走别的代理),再 `pm2 start ecosystem.config.cjs`
### 开机自启
```bash
pm2 save
pm2 startup
# 按屏幕提示执行一条 sudo 命令
```
---
## 9. 等价手工命令(不使用 ecosystem 文件时)
### 9.1 SSH SOCKS(自行后台常驻,不推荐用 PM2)
示例(前台调试;生产请用 **PM2**,见本文与 [docs/ubuntu-server.md](../docs/ubuntu-server.md)):
```bash
ssh -N -D 127.0.0.1:1080 gate-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes
```
### 9.2 Flask
```bash
cd /opt/crypto_monitor/crypto_monitor_gate_bot
pm2 start /opt/crypto_monitor/crypto_monitor_gate_bot/.venv/bin/python --name crypto-monitor-gate -- \
/opt/crypto_monitor/crypto_monitor_gate_bot/app.py
```
---
## 10. 交易所「连接不上」排查清单
1. **`.env` 是否为 Gate 变量**:必须是 `GATE_SOCKS_PROXY` / `GATE_API_KEY` / `GATE_API_SECRET`,不是 OKX。
2. **隧道是否在本机端口监听**(若配置了 `GATE_SOCKS_PROXY`):
```bash
ss -lntp | grep 1080 || true
```
3. **curl 复测 Gate**(与第 3.2 节相同);curl 不通则应用也不会通。
4. **PySocks**`pip show PySocks`,缺失则 `pip install PySocks`
5. **SSH 隧道连不上**:检查私钥权限、`~/.ssh/config`、VPS 出站与端口是否与 `.env` 一致。
6. **启动顺序**:先保证 SOCKS 已监听,再 `pm2 start` 应用(或重启应用)。
---
## 11. 推荐启动顺序(习惯)
1. 若走代理:先启动并确认 SSH SOCKS 已监听,再 `curl --proxy socks5h://127.0.0.1:1080 https://api.gateio.ws/api/v4/spot/time` 成功
2. `pm2 start ecosystem.config.cjs`
3. 再确认页面与余额等接口正常
---
## 12. 免责声明
交易所有合规与地区政策要求。请确保使用方式符合当地法律法规与交易所条款。本文仅描述网络与工程部署路径。
---
## 附录:数据库标签修复脚本 `scripts/fix_breakeven_labels.py`
在 Ubuntu 上:
1)预览(不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2)确认后执行:
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件:`monitor_type='下单监控'``result='止损'``pnl_amount > 0` → 改为 `result='保本止盈'`
+7
View File
@@ -50,6 +50,13 @@ UPLOAD_DIR=static/images
# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所 # TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启) # 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk POSITION_SIZING_MODE=risk
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数(U # 每天起始基数(U
DAILY_START_CAPITAL=30 DAILY_START_CAPITAL=30
# 日内回撤后基数(U # 日内回撤后基数(U
+149 -10
View File
@@ -131,6 +131,9 @@ from lib.key_monitor.trigger_entry_key_monitor_lib import (
TRIGGER_ENTRY_VALIDITY_HOURS, TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit, check_trigger_entry_intent_limit,
count_pending_trigger_entries, count_pending_trigger_entries,
acquire_trigger_entry_exec_lock,
is_trigger_entry_in_flight_row,
release_trigger_entry_exec_lock,
is_breakout_trigger_entry_key_monitor_type, is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired, is_trigger_entry_expired,
is_trigger_entry_key_monitor_type, is_trigger_entry_key_monitor_type,
@@ -154,6 +157,14 @@ from lib.trade.position_sizing_lib import (
mode_label_zh, mode_label_zh,
risk_percent_for_storage, risk_percent_for_storage,
) )
from lib.trade.trade_policy_lib import load_trade_policy
from lib.trade.trade_policy_app_lib import (
check_direction_policy,
check_open_policy,
check_symbol_policy,
default_symbol_for_policy,
trade_policy_template_context,
)
from lib.key_monitor.key_monitor_full_margin_lib import ( from lib.key_monitor.key_monitor_full_margin_lib import (
monitor_type_disallowed_in_full_margin, monitor_type_disallowed_in_full_margin,
purge_disallowed_key_monitors, purge_disallowed_key_monitors,
@@ -301,6 +312,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日(与 OKX 日界一致便于对账) # 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日(与 OKX 日界一致便于对账)
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
POSITION_SIZING_MODE = load_position_sizing_mode() POSITION_SIZING_MODE = load_position_sizing_mode()
TRADE_POLICY = load_trade_policy()
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3")) MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
@@ -1933,6 +1945,12 @@ def normalize_symbol_input(symbol):
return f"{sym}/USDT" return f"{sym}/USDT"
def validate_trade_policy_open(symbol, direction):
return check_open_policy(
TRADE_POLICY, symbol, direction, normalize_symbol_input
)
def normalize_kline_limit(limit_raw, default=200): def normalize_kline_limit(limit_raw, default=200):
try: try:
n = int(limit_raw) n = int(limit_raw)
@@ -2754,15 +2772,39 @@ def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss
def close_exchange_order(order_row): def close_exchange_order(order_row):
"""
市价全平数量优先取交易所当前持仓张数避免仅用入库 order_amount 导致平不干净
"""
ensure_markets_loaded() ensure_markets_loaded()
exchange_symbol = order_row["exchange_symbol"] or normalize_okx_symbol(order_row["symbol"]) exchange_symbol = order_row["exchange_symbol"] or normalize_okx_symbol(order_row["symbol"])
amount = float(order_row["order_amount"] or 0)
if amount <= 0:
raise ValueError("平仓失败:缺少有效下单数量")
direction = order_row["direction"] direction = order_row["direction"]
db_amt = float(order_row["order_amount"] or 0)
side = "sell" if direction == "long" else "buy" side = "sell" if direction == "long" else "buy"
last_resp = None
for _ in range(3):
live = get_live_position_contracts(exchange_symbol, direction)
if live is not None and live > 0:
raw_amt = live
else:
raw_amt = db_amt
if raw_amt <= 0:
if last_resp is not None:
return last_resp
raise ValueError("平仓失败:缺少有效下单数量")
try:
amount = float(exchange.amount_to_precision(exchange_symbol, raw_amt))
except Exception:
amount = float(raw_amt)
if amount <= 0:
if last_resp is not None:
return last_resp
raise ValueError("平仓失败:数量经精度舍入后为 0")
params = build_okx_order_params(direction, reduce_only=True) params = build_okx_order_params(direction, reduce_only=True)
return exchange.create_order(exchange_symbol, "market", side, amount, None, params) last_resp = exchange.create_order(exchange_symbol, "market", side, amount, None, params)
live_after = get_live_position_contracts(exchange_symbol, direction)
if live_after is None or live_after <= 0:
return last_resp
return last_resp
def cancel_okx_swap_open_orders(exchange_symbol): def cancel_okx_swap_open_orders(exchange_symbol):
@@ -3557,6 +3599,71 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
) )
def _finalize_hub_flat_monitor_okx(conn, r, *, result, pnl_amount, closed_at, miss_reason):
opened_at = get_opened_at_value(r)
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
session_date = r["session_date"] or get_trading_day(closed_at_dt)
update_session_capital(conn, session_date, pnl_amount)
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r),
direction=r["direction"],
trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"],
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
take_profit=r["take_profit"],
margin_capital=r["margin_capital"],
leverage=r["leverage"],
pnl_amount=pnl_amount,
hold_seconds=hold_seconds,
trade_style=r["trade_style"],
risk_amount=r["risk_amount"],
planned_rr=calc_rr_ratio(
r["direction"],
r["trigger_price"],
r["initial_stop_loss"] or r["stop_loss"],
r["take_profit"],
),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result,
miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at,
closed_at=closed_at,
)
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
def reconcile_hub_external_close(conn, symbol, direction):
from lib.hub.hub_reconcile_flat_lib import reconcile_hub_external_close_impl
from lib.hub.hub_symbol_lib import symbols_match
global _RECONCILE_FLAT_STREAK
return reconcile_hub_external_close_impl(
conn,
symbol,
direction,
exchange_configured=exchange_private_api_configured,
not_configured_msg="未配置 OKX_API_KEY / OKX_API_SECRET",
symbols_match=symbols_match,
get_opened_at_value=get_opened_at_value,
resolve_monitor_exchange_symbol=resolve_monitor_exchange_symbol,
get_live_position_contracts=get_live_position_contracts,
cancel_conditional_orders=cancel_okx_swap_open_orders,
resolve_synced_flat_close=resolve_synced_flat_close,
finalize_stopped_monitor=_finalize_hub_flat_monitor_okx,
sync_trade_records=sync_trade_records_from_exchange,
reconcile_flat_streak=_RECONCILE_FLAT_STREAK,
to_ms_with_fallback=_to_ms_with_fallback,
prefer_manual_resolve=False,
order_row_monitor_type=order_row_monitor_type,
)
def reconcile_external_closes(conn, days=None): def reconcile_external_closes(conn, days=None):
global _RECONCILE_FLAT_STREAK global _RECONCILE_FLAT_STREAK
if not exchange_private_api_configured(): if not exchange_private_api_configured():
@@ -5006,7 +5113,7 @@ def _market_open_for_trigger_entry(
def _execute_trigger_entry_cross(conn, row): def _execute_trigger_entry_cross(conn, row):
"""标记价触达计划入场:先删监控行防重复触发,再市价开仓""" """标记价触达计划入场:加锁防重复触发,成交成功后再删监控行"""
symbol = row["symbol"] symbol = row["symbol"]
direction = (row["direction"] or "long").lower() direction = (row["direction"] or "long").lower()
ex_sym = normalize_exchange_symbol(symbol) ex_sym = normalize_exchange_symbol(symbol)
@@ -5017,7 +5124,8 @@ def _execute_trigger_entry_cross(conn, row):
tc_en, tc_h, _ = time_close_settings_from_row(row) tc_en, tc_h, _ = time_close_settings_from_row(row)
kid = int(row["id"]) kid = int(row["id"])
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,)) if not acquire_trigger_entry_exec_lock(conn, kid):
return False, "触价开仓进行中"
conn.commit() conn.commit()
try: try:
@@ -5035,6 +5143,8 @@ def _execute_trigger_entry_cross(conn, row):
time_close_hours=tc_h, time_close_hours=tc_h,
) )
except Exception as e: except Exception as e:
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = friendly_exchange_error(e) fail_msg = friendly_exchange_error(e)
send_wechat_msg( send_wechat_msg(
f"# ❌ {symbol} 触价开仓异常\n" f"# ❌ {symbol} 触价开仓异常\n"
@@ -5046,6 +5156,8 @@ def _execute_trigger_entry_cross(conn, row):
return False, fail_msg return False, fail_msg
if ok and det: if ok and det:
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-" rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-"
msg = ( msg = (
f"# ✅ {symbol} 触价开仓成交\n" f"# ✅ {symbol} 触价开仓成交\n"
@@ -5062,6 +5174,8 @@ def _execute_trigger_entry_cross(conn, row):
send_wechat_msg(msg) send_wechat_msg(msg)
insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED) insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED)
return True, None return True, None
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = err or "触价触发后开仓失败" fail_msg = err or "触价触发后开仓失败"
send_wechat_msg( send_wechat_msg(
f"# ❌ {symbol} 触价开仓失败\n" f"# ❌ {symbol} 触价开仓失败\n"
@@ -5089,6 +5203,8 @@ def check_trigger_entry_key_monitors():
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0) sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") or 0) tp = float(_sqlite_row_val(r, "fib_take_profit") or 0)
kid = int(r["id"]) kid = int(r["id"])
if is_trigger_entry_in_flight_row(r):
continue
if entry <= 0 or sl <= 0 or tp <= 0: if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid") _finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue continue
@@ -5856,7 +5972,7 @@ def check_order_monitors():
new_sl = round_price_to_exchange(ex_sym, new_sl) new_sl = round_price_to_exchange(ex_sym, new_sl)
tp_ex = float(take_profit or 0) tp_ex = float(take_profit or 0)
ok_live, _live_reason = ensure_okx_live_ready() ok_live, _live_reason = ensure_okx_live_ready()
synced_ex = not ok_live synced_ex = False
last_ex_sync = float(_BREAKEVEN_LAST_EX_SYNC.get(pid, 0)) last_ex_sync = float(_BREAKEVEN_LAST_EX_SYNC.get(pid, 0))
interval_ok = ( interval_ok = (
time.time() - last_ex_sync time.time() - last_ex_sync
@@ -6265,8 +6381,8 @@ def background_task():
from lib.strategy.strategy_trend_register import check_trend_pullback_plans from lib.strategy.strategy_trend_register import check_trend_pullback_plans
check_trend_pullback_plans(cfg) check_trend_pullback_plans(cfg)
except: except Exception as e:
pass print(f"[monitor_loop] {e}", flush=True)
time.sleep(MONITOR_POLL_SECONDS) time.sleep(MONITOR_POLL_SECONDS)
@@ -6514,6 +6630,7 @@ def render_main_page(page="trade", embed_mode=None):
risk_percent=RISK_PERCENT, risk_percent=RISK_PERCENT,
position_sizing_mode=POSITION_SIZING_MODE, position_sizing_mode=POSITION_SIZING_MODE,
position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE), position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),
trade_policy=trade_policy_template_context(TRADE_POLICY),
open_position_button_label=( open_position_button_label=(
"开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)" "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)"
), ),
@@ -7184,7 +7301,10 @@ def key_focus():
selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None) selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None)
if selected_key is None and key_list: if selected_key is None and key_list:
selected_key = key_list[0] selected_key = key_list[0]
default_symbol = symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT" default_symbol = default_symbol_for_policy(
TRADE_POLICY,
symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT",
)
return render_template( return render_template(
"key_focus_v2.html", "key_focus_v2.html",
key_list=key_list, key_list=key_list,
@@ -7193,6 +7313,8 @@ def key_focus():
default_timeframe=KLINE_TIMEFRAME, default_timeframe=KLINE_TIMEFRAME,
default_kline_limit=200, default_kline_limit=200,
price_refresh_seconds=PRICE_REFRESH_SECONDS, price_refresh_seconds=PRICE_REFRESH_SECONDS,
exchange_display=EXCHANGE_DISPLAY_NAME,
trade_policy=trade_policy_template_context(TRADE_POLICY),
) )
@@ -7418,6 +7540,12 @@ def add_key():
if not symbol: if not symbol:
flash("symbol 不能为空") flash("symbol 不能为空")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_sym, sym_msg = check_symbol_policy(
TRADE_POLICY, symbol, normalize_symbol_input
)
if not ok_sym:
flash(sym_msg)
return redirect("/key_monitor")
mt = (d.get("type") or "").strip() mt = (d.get("type") or "").strip()
direction_sel = (d.get("direction") or "").strip().lower() direction_sel = (d.get("direction") or "").strip().lower()
dup_msg = check_duplicate_submit( dup_msg = check_duplicate_submit(
@@ -7432,6 +7560,10 @@ def add_key():
elif direction_sel not in ("long", "short"): elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空") flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor") return redirect("/key_monitor")
ok_dir, dir_msg = check_direction_policy(TRADE_POLICY, direction_sel)
if not ok_dir:
flash(dir_msg)
return redirect("/key_monitor")
allowed_types = ( allowed_types = (
tuple(KEY_MONITOR_AUTO_TYPES) tuple(KEY_MONITOR_AUTO_TYPES)
+ tuple(KEY_MONITOR_ALERT_ONLY_TYPES) + tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
@@ -7683,6 +7815,11 @@ def add_order():
conn.close() conn.close()
flash("symbol 不能为空") flash("symbol 不能为空")
return redirect("/trade") return redirect("/trade")
ok_pol, pol_msg = validate_trade_policy_open(symbol, direction)
if not ok_pol:
conn.close()
flash(f"账户限制:{pol_msg}")
return redirect("/trade")
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction)) dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
if dup_msg: if dup_msg:
conn.close() conn.close()
@@ -8973,6 +9110,7 @@ def _hub_meta_bundle():
"max_active_positions": MAX_ACTIVE_POSITIONS, "max_active_positions": MAX_ACTIVE_POSITIONS,
"btc_leverage": BTC_LEVERAGE, "btc_leverage": BTC_LEVERAGE,
"alt_leverage": ALT_LEVERAGE, "alt_leverage": ALT_LEVERAGE,
"trade_policy": trade_policy_template_context(TRADE_POLICY),
} }
@@ -9051,6 +9189,7 @@ try:
ohlcv_fn=_hub_fetch_ohlcv, ohlcv_fn=_hub_fetch_ohlcv,
volume_rank_fn=_hub_fetch_volume_rank, volume_rank_fn=_hub_fetch_volume_rank,
market_fn=_hub_fetch_market, market_fn=_hub_fetch_market,
reconcile_hub_flat_fn=reconcile_hub_external_close,
risk_status_fn=hub_account_risk_status, risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close, user_close_fn=hub_user_initiated_close,
render_main_page_fn=render_main_page, render_main_page_fn=render_main_page,
+16 -7
View File
@@ -243,7 +243,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px} .stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4} .stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style> </style>
<link rel="stylesheet" href="/static/instance_theme.css?v=48"> <link rel="stylesheet" href="/static/instance_theme.css?v=50">
</head> </head>
<body <body
@@ -253,6 +253,7 @@
data-btc-leverage="{{ btc_leverage }}" data-btc-leverage="{{ btc_leverage }}"
data-alt-leverage="{{ alt_leverage }}" data-alt-leverage="{{ alt_leverage }}"
data-full-margin-buffer="{{ full_margin_buffer_ratio }}" data-full-margin-buffer="{{ full_margin_buffer_ratio }}"
data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}"
> >
{% macro period_stats(title, s) %} {% macro period_stats(title, s) %}
<div class="stats-period-block"> <div class="stats-period-block">
@@ -278,6 +279,9 @@
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row"> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div> <div class="exchange-tag">{{ exchange_display }}</div>
{% if trade_policy.badge_text %}
<span class="trade-policy-badge" title="账户交易限制(.env">{{ trade_policy.badge_text }}</span>
{% endif %}
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span> <span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题"> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题"> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
@@ -383,10 +387,9 @@
<button type="submit">手动划转</button> <button type="submit">手动划转</button>
</form> </form>
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}"> <form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required> {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
<select id="order-direction" name="direction" required> {{ trade_policy_symbol('symbol', 'order-symbol') }}
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> {{ trade_policy_direction('direction', 'order-direction') }}
</select>
<select id="sltp-mode" name="sltp_mode"> <select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option> <option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option> <option value="price">止盈止损:价格模式</option>
@@ -415,7 +418,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef"> <label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记) <input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label> </label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span> {% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required> <input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比"> <input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span> <span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
@@ -842,6 +847,7 @@
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script> <script src="/static/strategy_roll.js?v=6"></script>
<script> <script>
const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }}; const JOURNAL_ENTRY_REASON_OPTIONS = {{ entry_reason_options | tojson }};
@@ -1998,7 +2004,10 @@ if(allowOpenBeforeResetEl){
const orderSymbolEl = document.getElementById("order-symbol"); const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction"); const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin"); const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults); if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults); if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
+1 -1
View File
@@ -157,7 +157,7 @@ nano .env
- **升级前备份**`cp .env .env.backup.$(date +%Y%m%d)`;恢复:`cp .env.backup.YYYYMMDD .env` - **升级前备份**`cp .env .env.backup.$(date +%Y%m%d)`;恢复:`cp .env.backup.YYYYMMDD .env`
- **换机**`scp` 复制 `.env`,或新机 `cp .env.example .env` 后重填。 - **换机**`scp` 复制 `.env`,或新机 `cp .env.example .env` 后重填。
**AI 复盘**所共用根目录 **`ai_client.py`**。默认 **`AI_PROVIDER=openai`**,网关 `https://op.bz121.com/v1`,模型 `gemma4:e4b`;或改 **`ollama`** 走本机 Ollama。PM2 须 **`PYTHONPATH=..`**。详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**。 **AI 复盘**所共用根目录 **`ai_client.py`**。默认 **`AI_PROVIDER=openai`**,网关 `https://op.bz121.com/v1`,模型 `gemma4:e4b`;或改 **`ollama`** 走本机 Ollama。PM2 须 **`PYTHONPATH=..`**。详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**。
### 5.3 必填项检查(OKX + 代理) ### 5.3 必填项检查(OKX + 代理)
+7 -3
View File
@@ -29,12 +29,14 @@ bash deploy/setup_env.sh --install-system-deps
常用参数: 常用参数:
```bash ```bash
bash deploy/setup_env.sh --only binance,gate_bot # 仅部分子项目 bash deploy/setup_env.sh --only binance,gate # 仅部分子项目
bash deploy/setup_env.sh --recreate-venv # 重建虚拟环境 bash deploy/setup_env.sh --recreate-venv # 重建虚拟环境
bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2 bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2
bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example
``` ```
**整目录重装**(保留 `.env`、清库、去脏 PM2)见 **[reinstall-plan-b.md](./reinstall-plan-b.md)**,执行 `bash deploy/reinstall.sh`。与 `setup_env.sh` 独立,不影响首次一键安装。
若在其它环境编辑过脚本后报 `pipefail` 错误,先转 LF 若在其它环境编辑过脚本后报 `pipefail` 错误,先转 LF
```bash ```bash
@@ -68,11 +70,13 @@ sed -i 's/\r$//' deploy/setup_env.sh
pm2 save pm2 save
``` ```
3. 四所 `.env` 同步脚本见 **[docs/env-sync-scripts.md](../docs/env-sync-scripts.md)**。 或一条命令:`bash deploy/pm2_start_all.sh`
3. 三所 `.env` 同步脚本见 **[docs/env-sync-scripts.md](../docs/env-sync-scripts.md)**。
--- ---
## 依赖说明 ## 依赖说明
- 个监控子项目共用根目录 **[requirements.txt](../requirements.txt)**。 - 个监控子项目共用根目录 **[requirements.txt](../requirements.txt)**。
- 走 SOCKS 须 **PySocks**(已包含在 requirements 中)。 - 走 SOCKS 须 **PySocks**(已包含在 requirements 中)。
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# 按推荐顺序启动三所 Flask + 中控 hub/三 agentPM2)。
# 用法(仓库根或任意目录):
# bash deploy/pm2_start_all.sh
#
# 与 deploy/setup_env.sh 独立:setup_env 只建 venv;本脚本负责 PM2 启动。
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
set -o pipefail
fi
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"
start_one() {
local dir_name="$1"
local proj="${REPO_ROOT}/${dir_name}"
local eco="${proj}/ecosystem.config.cjs"
if [[ ! -f "${eco}" ]]; then
echo "skip (no ecosystem): ${dir_name}" >&2
return 0
fi
echo "==> pm2 start ${dir_name}"
(cd "${proj}" && pm2 start ecosystem.config.cjs)
}
if ! command -v pm2 >/dev/null 2>&1; then
echo "未找到 pm2,请先安装 Node.js 与 pm2(见 docs/ubuntu-server.md" >&2
exit 1
fi
start_one crypto_monitor_binance
start_one crypto_monitor_gate
start_one crypto_monitor_okx
start_one manual_trading_hub
pm2 save 2>/dev/null || true
echo ""
echo "PM2 进程:"
pm2 list
+112
View File
@@ -0,0 +1,112 @@
# Plan B:整目录重装(生产清库)
适用于:**保留三所 `.env` 与中控配置,丢弃旧代码、旧 SQLite、脏 PM2 名单**(例如移除 `gate_bot` 后偶发重启)。
**[setup_env.sh](./setup_env.sh)** 的关系:
| 脚本 | 用途 |
|------|------|
| `setup_env.sh` | **首次安装 / 日常**:建 venv、装依赖、从 `.env.example` 复制(**不变** |
| `reinstall.sh` | **整目录重装**:备份 → 移走旧目录 → `git clone` → 调 `setup_env.sh` → 恢复配置 → PM2 |
---
## 一键执行(推荐)
在现有服务器安装上以 **root** 执行:
```bash
cd /opt/crypto_monitor
bash deploy/reinstall.sh --yes
```
交互确认(不加 `--yes`):
```bash
bash deploy/reinstall.sh
```
仅预览步骤:
```bash
bash deploy/reinstall.sh --dry-run
```
---
## 脚本会做什么
1. 备份到 **`/root/backups/pre-reinstall-YYYYMMDD-HHMMSS/`**
- 三所 `crypto_monitor_*/.env`
- `manual_trading_hub/.env`
- `manual_trading_hub/hub_settings.json`(若有)
- 可选:仓库内 `one_shot` 备份目录
2. **`pm2 stop all` + `pm2 delete all`**
3. **`mv /opt/crypto_monitor /opt/crypto_monitor.old.时间戳`**
4. **`git clone`** 到 `/opt/crypto_monitor`(默认 `main`
5. **`bash deploy/setup_env.sh --skip-env-copy --recreate-venv --skip-pm2`**
6. 从备份 **恢复 `.env` / `hub_settings.json`**
7. **`deploy/sanitize_hub_settings.py`** 去掉 `gate_bot` / 第四账户
8. **`deploy/pm2_start_all.sh`** + `pm2 save`
9. 为三所重装 **每日 0 点备份 cron**(可用 `--no-backup-cron` 跳过)
**不会备份/恢复**`crypto.db`、hub `data/*.db``static/images`(符合「全新启动」)。
**不会动**:宝塔/Nginx 反代、SSH SOCKS 隧道(tmux 内)。
---
## 环境变量
```bash
export INSTALL_ROOT=/opt/crypto_monitor
export GIT_URL=https://git.bz121.com/dekun/crypto_monitor.git
export GIT_BRANCH=main
export BACKUP_ROOT=/root/backups
bash deploy/reinstall.sh --yes
```
---
## 验收
```bash
pm2 list
# 应有 7 个: crypto_binance crypto_gate crypto_okx manual-trading-hub manual-agent-*
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5100/
```
浏览器:中控 `/monitor` 登录,三所 LINK 绿,监控区为空库。
---
## 回滚
旧目录默认保留为 `/opt/crypto_monitor.old.时间戳`,配置在 `/root/backups/pre-reinstall-*`
```bash
pm2 delete all
rm -rf /opt/crypto_monitor
mv /opt/crypto_monitor.old.XXXXXXXX /opt/crypto_monitor
bash /opt/crypto_monitor/deploy/pm2_start_all.sh
```
确认新环境稳定后再删 `.old.*` 目录。
---
## 辅助脚本
| 文件 | 说明 |
|------|------|
| [pm2_start_all.sh](./pm2_start_all.sh) | 按顺序 PM2 启动三所 + hubsetup_env 之后手动用) |
| [sanitize_hub_settings.py](./sanitize_hub_settings.py) | 清理 `hub_settings.json` 中 gate_bot 条目 |
---
## 相关文档
- [deploy/README.md](./README.md) — 首次一键安装
- [docs/ubuntu-server.md](../docs/ubuntu-server.md) — Python / PM2 版本
- [备份与恢复.md](../备份与恢复.md) — 日常 DB 备份 cron
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env bash
# Plan B:整目录重装 /opt/crypto_monitor(备份 .env → 移走旧目录 → git clone → setup_env → 恢复配置 → PM2
#
# 与 deploy/setup_env.sh 分工:
# setup_env.sh — 首次 / 日常:建 venv、装依赖、复制 .env.example(一键安装,不变)
# reinstall.sh — 生产清库重装:保留密钥与 hub 配置,丢弃旧代码/旧库/脏 PM2
#
# 用法(在现有安装目录以 root 执行):
# cd /opt/crypto_monitor
# bash deploy/reinstall.sh # 交互确认
# bash deploy/reinstall.sh --yes # 跳过确认
# bash deploy/reinstall.sh --dry-run # 仅打印步骤
#
# 可选环境变量:
# INSTALL_ROOT=/opt/crypto_monitor
# GIT_URL=https://git.bz121.com/dekun/crypto_monitor.git
# GIT_BRANCH=main
# BACKUP_ROOT=/root/backups
#
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
set -o pipefail
fi
DEPLOY_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_SOURCE="${DEPLOY_DIR}/reinstall.sh"
REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"
INSTALL_ROOT="${INSTALL_ROOT:-/opt/crypto_monitor}"
GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/crypto_monitor.git}"
GIT_BRANCH="${GIT_BRANCH:-main}"
BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}"
TZ_NAME="${REINSTALL_TZ:-Asia/Shanghai}"
ASSUME_YES=0
DRY_RUN=0
INSTALL_BACKUP_CRON=1
CONFIG_PATHS=(
"crypto_monitor_binance/.env"
"crypto_monitor_okx/.env"
"crypto_monitor_gate/.env"
"manual_trading_hub/.env"
"manual_trading_hub/hub_settings.json"
)
usage() {
sed -n '2,18p' "$0" | sed 's/^# \?//'
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--yes|-y) ASSUME_YES=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--no-backup-cron) INSTALL_BACKUP_CRON=0; shift ;;
-h|--help) usage 0 ;;
*) echo "未知参数: $1" >&2; usage 1 ;;
esac
done
log() { printf '[%s] %s\n' "$(TZ="${TZ_NAME}" date '+%Y-%m-%d %H:%M:%S')" "$*"; }
step() { echo ""; log "==> $*"; }
run() {
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] $*"
return 0
fi
log "+ $*"
"$@"
}
confirm() {
if [[ "${ASSUME_YES}" -eq 1 || "${DRY_RUN}" -eq 1 ]]; then
return 0
fi
local msg="$1"
read -r -p "${msg} [y/N] " ans
[[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]]
}
resolve_path() {
local base="$1"
local rel="$2"
printf '%s/%s' "${base}" "${rel}"
}
backup_configs() {
local src_root="$1"
local dest="$2"
mkdir -p "${dest}"
local rel copied=0
for rel in "${CONFIG_PATHS[@]}"; do
local src
src="$(resolve_path "${src_root}" "${rel}")"
if [[ -f "${src}" ]]; then
mkdir -p "${dest}/$(dirname "${rel}")"
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] backup ${src} -> ${dest}/${rel}"
else
cp -a "${src}" "${dest}/${rel}"
log "backup ${rel}"
fi
copied=$((copied + 1))
else
log "skip (missing): ${rel}"
fi
done
if [[ "${copied}" -eq 0 ]]; then
echo "错误: 未备份到任何配置文件,请检查 ${src_root}" >&2
exit 1
fi
if [[ -f "${src_root}/scripts/one_shot_backup_config_before_cleanup.py" ]]; then
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] python3 scripts/one_shot_backup_config_before_cleanup.py (in ${src_root})"
else
(cd "${src_root}" && python3 scripts/one_shot_backup_config_before_cleanup.py) || true
if compgen -G "${src_root}/backups/one-shot-*" >/dev/null; then
cp -a "${src_root}"/backups/one-shot-* "${dest}/" 2>/dev/null || true
fi
fi
fi
if [[ "${DRY_RUN}" -eq 0 ]]; then
{
echo "created_at=${STAMP}"
echo "install_root=${INSTALL_ROOT}"
echo "old_dir=${OLD_DIR}"
echo "git_url=${GIT_URL}"
echo "git_branch=${GIT_BRANCH}"
echo "script=${SCRIPT_SOURCE}"
} >"${dest}/reinstall.manifest"
fi
}
restore_configs() {
local backup_dir="$1"
local dest_root="$2"
local rel
for rel in "${CONFIG_PATHS[@]}"; do
local src dest
src="${backup_dir}/${rel}"
dest="$(resolve_path "${dest_root}" "${rel}")"
if [[ -f "${src}" ]]; then
mkdir -p "$(dirname "${dest}")"
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] restore ${src} -> ${dest}"
else
cp -a "${src}" "${dest}"
log "restore ${rel}"
fi
fi
done
local hub_settings
hub_settings="$(resolve_path "${dest_root}" "manual_trading_hub/hub_settings.json")"
if [[ -f "${hub_settings}" && "${DRY_RUN}" -eq 0 ]]; then
python3 "${dest_root}/deploy/sanitize_hub_settings.py" "${hub_settings}" || true
fi
}
install_instance_backup_cron() {
local dest_root="$1"
local dir
for dir in crypto_monitor_binance crypto_monitor_gate crypto_monitor_okx; do
local proj="${dest_root}/${dir}"
local inst="${proj}/scripts/install_backup_cron.sh"
local data="${proj}/scripts/backup_data.sh"
if [[ -f "${inst}" && -f "${data}" ]]; then
chmod +x "${inst}" "${data}"
run bash "${inst}"
fi
done
}
verify_pm2() {
log "预期 PM2 进程(7 个): crypto_binance crypto_gate crypto_okx manual-trading-hub manual-agent-*"
if [[ "${DRY_RUN}" -eq 1 ]]; then
return 0
fi
pm2 list || true
if pm2 list 2>/dev/null | grep -qiE 'gate_bot|15203'; then
log "警告: PM2 列表仍含 gate_bot 相关进程,请 pm2 delete 后 pm2 save"
fi
}
# --- 前置检查 ---
if [[ "$(id -u)" -ne 0 ]]; then
echo "请使用 root 执行(推荐路径 ${INSTALL_ROOT}" >&2
exit 1
fi
if [[ ! -f "${REPO_ROOT}/deploy/setup_env.sh" ]]; then
echo "当前脚本不在有效仓库内: ${REPO_ROOT}" >&2
exit 1
fi
if [[ "${REPO_ROOT}" != "${INSTALL_ROOT}" ]]; then
log "提示: 当前仓库 ${REPO_ROOT} 与 INSTALL_ROOT=${INSTALL_ROOT} 不一致;将备份当前仓库并克隆到 INSTALL_ROOT"
fi
STAMP="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)"
BACKUP_DIR="${BACKUP_ROOT}/pre-reinstall-${STAMP}"
OLD_DIR="${INSTALL_ROOT}.old.${STAMP}"
SRC_ROOT="${REPO_ROOT}"
if [[ -d "${INSTALL_ROOT}" && "${REPO_ROOT}" != "${INSTALL_ROOT}" ]]; then
SRC_ROOT="${INSTALL_ROOT}"
fi
step "计划"
echo " 备份目录: ${BACKUP_DIR}"
echo " 配置来源: ${SRC_ROOT}"
echo " 旧目录移走: ${OLD_DIR}"
echo " 新克隆: ${GIT_URL} (${GIT_BRANCH}) -> ${INSTALL_ROOT}"
echo " 环境: deploy/setup_env.sh --skip-env-copy --recreate-venv --skip-pm2"
echo ""
echo " 将停止并 delete 全部 PM2 进程;不备份 crypto.db / hub data / 图片。"
if ! confirm "确认执行 Plan B 整目录重装?"; then
log "已取消"
exit 0
fi
# --- 1. 备份 ---
step "备份配置到 ${BACKUP_DIR}"
backup_configs "${SRC_ROOT}" "${BACKUP_DIR}"
# --- 2. 停 PM2 ---
step "停止并清空 PM2"
if command -v pm2 >/dev/null 2>&1; then
run pm2 stop all || true
run pm2 delete all || true
else
log "未安装 pm2,跳过"
fi
# --- 3. 移走旧目录 ---
step "移走旧安装 ${INSTALL_ROOT} -> ${OLD_DIR}"
if [[ -d "${INSTALL_ROOT}" ]]; then
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] mv ${INSTALL_ROOT} ${OLD_DIR}"
else
mv "${INSTALL_ROOT}" "${OLD_DIR}"
fi
else
log "目标目录不存在,跳过 mv"
fi
# --- 4. 克隆 ---
step "git clone"
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] git clone -b ${GIT_BRANCH} ${GIT_URL} ${INSTALL_ROOT}"
else
git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}"
fi
# --- 5. setup_env(一键安装逻辑,不复制 .env)---
step "重建 Python 虚拟环境 (setup_env.sh)"
if [[ "${DRY_RUN}" -eq 1 ]]; then
log "[dry-run] bash ${INSTALL_ROOT}/deploy/setup_env.sh --skip-env-copy --recreate-venv --skip-pm2"
else
bash "${INSTALL_ROOT}/deploy/setup_env.sh" --skip-env-copy --recreate-venv --skip-pm2
fi
# --- 6. 恢复配置 ---
step "恢复 .env 与 hub_settings.json"
restore_configs "${BACKUP_DIR}" "${INSTALL_ROOT}"
# --- 7. PM2 启动 ---
step "PM2 启动全部进程"
if command -v pm2 >/dev/null 2>&1; then
run bash "${INSTALL_ROOT}/deploy/pm2_start_all.sh"
run pm2 save
else
log "未安装 pm2;请手动: bash ${INSTALL_ROOT}/deploy/pm2_start_all.sh"
fi
# --- 8. 定时备份 cron(可选)---
if [[ "${INSTALL_BACKUP_CRON}" -eq 1 ]]; then
step "安装三所每日备份 cron"
install_instance_backup_cron "${INSTALL_ROOT}"
fi
# --- 完成 ---
step "完成"
verify_pm2
echo ""
echo "备份: ${BACKUP_DIR}"
echo "旧目录(确认无误后可删): ${OLD_DIR}"
echo ""
echo "验收建议:"
echo " pm2 list"
echo " curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5100/"
echo " 浏览器打开中控 /monitor,确认三所 LINK 正常"
echo ""
echo "回滚(未删旧目录时):"
echo " pm2 delete all"
echo " rm -rf ${INSTALL_ROOT}"
echo " mv ${OLD_DIR} ${INSTALL_ROOT}"
echo " cp -a ${BACKUP_DIR}/*/ ${INSTALL_ROOT}/ # 若需恢复配置"
echo " bash ${INSTALL_ROOT}/deploy/pm2_start_all.sh"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""重装后清理 hub_settings.json 中已废弃的 gate_bot / 第四账户条目。"""
from __future__ import annotations
import json
import sys
from pathlib import Path
DROP_KEYS = frozenset({"gate_bot", "gate-bot"})
DROP_MARKERS = (
"gate_bot",
"crypto_monitor_gate_bot",
"15203",
":5002",
)
def _text(*parts: object) -> str:
return " ".join(str(p) for p in parts if p is not None).lower()
def should_drop(ex: dict) -> bool:
key = str(ex.get("key") or "").strip().lower()
if key in DROP_KEYS:
return True
blob = _text(
ex.get("name"),
ex.get("flask_url"),
ex.get("agent_url"),
ex.get("review_url"),
)
if any(m in blob for m in DROP_MARKERS):
return True
ex_id = str(ex.get("id") or "").strip()
if ex_id == "3" and key not in ("gate", ""):
return True
return False
def sanitize_settings(data: dict) -> tuple[dict, list[str]]:
removed: list[str] = []
exchanges = data.get("exchanges")
if not isinstance(exchanges, list):
return data, removed
kept: list[dict] = []
seen_keys: set[str] = set()
for ex in exchanges:
if not isinstance(ex, dict):
continue
key = str(ex.get("key") or "").strip().lower()
label = f"id={ex.get('id')} key={key} name={ex.get('name')}"
if should_drop(ex):
removed.append(label)
continue
if key and key in seen_keys:
removed.append(f"duplicate {label}")
continue
if key:
seen_keys.add(key)
kept.append(ex)
out = dict(data)
out["exchanges"] = kept
return out, removed
def main(argv: list[str] | None = None) -> int:
args = argv if argv is not None else sys.argv[1:]
if len(args) != 1:
print("用法: python deploy/sanitize_hub_settings.py <hub_settings.json>", file=sys.stderr)
return 2
path = Path(args[0])
if not path.is_file():
print(f"文件不存在: {path}", file=sys.stderr)
return 1
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as e:
print(f"JSON 解析失败: {e}", file=sys.stderr)
return 1
if not isinstance(data, dict):
print("hub_settings.json 根节点必须是 object", file=sys.stderr)
return 1
cleaned, removed = sanitize_settings(data)
if removed:
path.write_text(json.dumps(cleaned, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print("已移除条目:")
for line in removed:
print(f" - {line}")
else:
print("无需修改(未发现 gate_bot / 第四账户)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1 -2
View File
@@ -3,7 +3,7 @@
# #
# 用法: # 用法:
# bash deploy/setup_env.sh # bash deploy/setup_env.sh
# bash deploy/setup_env.sh --only binance,gate_bot # bash deploy/setup_env.sh --only binance,gate
# bash deploy/setup_env.sh --skip-pm2 # bash deploy/setup_env.sh --skip-pm2
# bash deploy/setup_env.sh --recreate-venv # bash deploy/setup_env.sh --recreate-venv
# bash deploy/setup_env.sh --install-system-deps # root + apt 时安装 python*-venv # bash deploy/setup_env.sh --install-system-deps # root + apt 时安装 python*-venv
@@ -244,7 +244,6 @@ ensure_venv_prereqs "${PY}"
should_include binance && setup_monitor crypto_monitor_binance should_include binance && setup_monitor crypto_monitor_binance
should_include gate && setup_monitor crypto_monitor_gate should_include gate && setup_monitor crypto_monitor_gate
should_include gate_bot && setup_monitor crypto_monitor_gate_bot
should_include okx && setup_monitor crypto_monitor_okx should_include okx && setup_monitor crypto_monitor_okx
should_include hub && setup_hub should_include hub && setup_hub
+1 -1
View File
@@ -1,6 +1,6 @@
# 账户冷静期 / 日冻结风控 # 账户冷静期 / 日冻结风控
所实例(币安 / OKX / Gate / Gate 趋势)共用 `account_risk_lib.py` 所实例(币安 / OKX / Gate / Gate)共用 `account_risk_lib.py`
**仅用户主动平仓**计入风控;交易所止盈/止损、空仓同步、改保本/改委托等**不触发**冷静期。 **仅用户主动平仓**计入风控;交易所止盈/止损、空仓同步、改保本/改委托等**不触发**冷静期。
## 状态展示 ## 状态展示
+4 -4
View File
@@ -1,4 +1,4 @@
# 每日自动划转(所统一) # 每日自动划转(所统一)
## 行为 ## 行为
@@ -9,7 +9,7 @@
| 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`(默认 funding)划入差额 | | 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`(默认 funding)划入差额 |
| 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` | | 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` |
| 与目标相差 &lt; 0.01U | 跳过,不写划转 | | 与目标相差 &lt; 0.01U | 跳过,不写划转 |
| 存在 **active** 持仓(`order_monitors`,或 Gate 趋势回调已开仓计划) | **不划转**,写账簿 `skipped`,并**企业微信**说明「持仓中,本次资金无划转」 | | 存在 **active** 持仓(`order_monitors`,或 Gate回调已开仓计划) | **不划转**,写账簿 `skipped`,并**企业微信**说明「持仓中,本次资金无划转」 |
## 配置示例(目标 50U ## 配置示例(目标 50U
@@ -25,7 +25,7 @@ AUTO_TRANSFER_BJ_HOUR=8
API Key 须具备万向划转权限(与手动划转相同)。 API Key 须具备万向划转权限(与手动划转相同)。
## 用脚本更新`.env` ## 用脚本更新`.env`
详见 **[env-sync-scripts.md](./env-sync-scripts.md)**。常用命令: 详见 **[env-sync-scripts.md](./env-sync-scripts.md)**。常用命令:
@@ -41,5 +41,5 @@ python scripts/sync_four_exchange_transfer_env.py --set-amount 50 --enable-auto-
# 计仓 + 划转一并补全 # 计仓 + 划转一并补全
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate crypto-monitor-gate-bot pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
``` ```
+5 -5
View File
@@ -1,6 +1,6 @@
# 单日开仓次数限制(所统一) # 单日开仓次数限制(所统一)
各交易实例(Binance / OKX / Gate / Gate_bot)在 `.env` 中独立配置,互不影响。 各交易实例(Binance / OKX / Gate)在 `.env` 中独立配置,互不影响。
## 交易日口径 ## 交易日口径
@@ -59,7 +59,7 @@ DAILY_OPEN_HARD_LIMIT=3
- `TRADING_DAY_RESET_OPEN_GUARD_ENABLED`:切日前禁止新开 - `TRADING_DAY_RESET_OPEN_GUARD_ENABLED`:切日前禁止新开
- `MAX_ACTIVE_POSITIONS`:同时持仓上限 - `MAX_ACTIVE_POSITIONS`:同时持仓上限
- Gate_bot`precheck_trend_pullback_start` 同样校验单日硬上限 - Gate`precheck_trend_pullback_start` 同样校验单日硬上限
## 页面与接口 ## 页面与接口
@@ -71,11 +71,11 @@ DAILY_OPEN_HARD_LIMIT=3
修改各实例 `.env` 后重启对应 pm2 进程,例如: 修改各实例 `.env` 后重启对应 pm2 进程,例如:
```bash ```bash
pm2 restart crypto_binance crypto_okx crypto_gate crypto_gate_bot pm2 restart crypto_binance crypto_okx crypto_gate
``` ```
## 实现位置 ## 实现位置
- 共享逻辑:`daily_open_limit_lib.py` - 共享逻辑:`daily_open_limit_lib.py`
- `app.py``precheck_risk``can_trade``api/account_snapshot`、开仓成功后的 AI 提醒文案 - `app.py``precheck_risk``can_trade``api/account_snapshot`、开仓成功后的 AI 提醒文案
- 单元测试:`tests/test_daily_open_limit_lib.py` - 单元测试:`tests/test_daily_open_limit_lib.py`
+7 -7
View File
@@ -1,13 +1,13 @@
# `.env` 同步脚本说明 # `.env` 同步脚本说明
在**仓库根目录**执行。仅处理所实例目录下的 `.env`,**不覆盖** API 密钥与已存在的自定义值;若某目录无 `.env``SKIP`(需先 `cp .env.example .env`)。 在**仓库根目录**执行。仅处理所实例目录下的 `.env`,**不覆盖** API 密钥与已存在的自定义值;若某目录无 `.env``SKIP`(需先 `cp .env.example .env`)。
| 目录 | | 目录 |
|------| |------|
| `crypto_monitor_binance` | | `crypto_monitor_binance` |
| `crypto_monitor_okx` | | `crypto_monitor_okx` |
| `crypto_monitor_gate` | | `crypto_monitor_gate` |
| `crypto_monitor_gate_bot` | | `crypto_monitor_gate` |
修改 `.env` 后须 **`pm2 restart`** 对应实例后生效。 修改 `.env` 后须 **`pm2 restart`** 对应实例后生效。
@@ -37,9 +37,9 @@ python scripts/sync_four_exchange_env.py --set-mode full_margin
| 参数 | 说明 | | 参数 | 说明 |
|------|------| |------|------|
| `--dry-run` | 只打印将做的变更,不写 `.env` | | `--dry-run` | 只打印将做的变更,不写 `.env` |
| `--set-mode risk\|full_margin` | 强制`POSITION_SIZING_MODE` | | `--set-mode risk\|full_margin` | 强制`POSITION_SIZING_MODE` |
| `--set-transfer-amount U` | 强制`AUTO_TRANSFER_AMOUNT` | | `--set-transfer-amount U` | 强制`AUTO_TRANSFER_AMOUNT` |
| `--enable-auto-transfer` | 强制`AUTO_TRANSFER_ENABLED=true` | | `--enable-auto-transfer` | 强制`AUTO_TRANSFER_ENABLED=true` |
--- ---
@@ -106,7 +106,7 @@ python scripts/sync_four_exchange_position_sizing_env.py --set-buffer 0.98
## 部署后重启 ## 部署后重启
```bash ```bash
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate crypto-monitor-gate-bot pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
``` ```
## 相关文档 ## 相关文档
+2 -2
View File
@@ -48,7 +48,7 @@
| 项 | 约定 | | 项 | 约定 |
|----|------| |----|------|
| 交易来源 | `trade_records` + 未落库的 `strategy_trade_snapshots`,经 `/api/hub/trades/archive` 拉取 | | 交易来源 | `trade_records` + 未落库的 `strategy_trade_snapshots`,经 `/api/hub/trades/archive` 拉取 |
| 犯病标签 | 中控 `trade_overlay.behavior_tag = sick` | | 犯病标签 | 中控 `trade_overlay.behavior_tag = sick` |
| K 线真源 | 仅 **5m** 写入 `hub_symbol_archive.db` | | K 线真源 | 仅 **5m** 写入 `hub_symbol_archive.db` |
| 建档种子 | 该币 **最早开仓** 向前 **30 天** 5m | | 建档种子 | 该币 **最早开仓** 向前 **30 天** 5m |
@@ -80,7 +80,7 @@
| DELETE | `/api/archive/quotes/{id}` | 删除语录 | | DELETE | `/api/archive/quotes/{id}` | 删除语录 |
| GET | `/api/archive/ohlcv` | K 线视窗(`timeframe` / `mode` / `anchor_ms` / `at` | | GET | `/api/archive/ohlcv` | K 线视窗(`timeframe` / `mode` / `anchor_ms` / `at` |
| PATCH | `/api/archive/trade/{exchange_key}/{trade_id}` | 更新标签/备注 | | PATCH | `/api/archive/trade/{exchange_key}/{trade_id}` | 更新标签/备注 |
| POST | `/api/archive/sync` | 立即同步所交易 + K 线 | | POST | `/api/archive/sync` | 立即同步所交易 + K 线 |
`GET /api/archive/daily-trades` 主要 query `GET /api/archive/daily-trades` 主要 query
+8 -8
View File
@@ -1,8 +1,9 @@
# lib/ 共用模块结构 # lib/ 共用模块结构
所实例与中控共用的 Python 库、模板与静态资源统一放在仓库根目录的 **`lib/`** 下。部署单元(`crypto_monitor_*``manual_trading_hub`)仍保持独立目录与 PM2 配置不变。 所实例与中控共用的 Python 库、模板与静态资源统一放在仓库根目录的 **`lib/`** 下。部署单元(`crypto_monitor_*``manual_trading_hub`)仍保持独立目录与 PM2 配置不变。
**重构前快照 Git 标签**`pre-lib-modularization`(可用 `git checkout pre-lib-modularization` 查看旧布局)。 **重构前快照 Git 标签**`pre-lib-modularization`(可用 `git checkout pre-lib-modularization` 查看旧布局)。
**移除 gate_bot 前快照 Git 标签**`pre-remove-gate-bot`
--- ---
@@ -10,9 +11,8 @@
``` ```
crypto_monitor/ crypto_monitor/
├── crypto_monitor_binance/ # 所:各自 app + .env + PM2 ├── crypto_monitor_binance/ # 所:各自 app + .env + PM2
├── crypto_monitor_gate/ ├── crypto_monitor_gate/
├── crypto_monitor_gate_bot/
├── crypto_monitor_okx/ ├── crypto_monitor_okx/
├── manual_trading_hub/ # 中控 + 子代理 agent ├── manual_trading_hub/ # 中控 + 子代理 agent
@@ -52,9 +52,9 @@ crypto_monitor/
| **`lib/instance/templates/`** | 嵌入页片段(原 `embed_templates/` | `embed_page_fragment.html` | | **`lib/instance/templates/`** | 嵌入页片段(原 `embed_templates/` | `embed_page_fragment.html` |
| **`lib/exchange/`** | 特定交易所工具 | `gate_transfer_lib.py``okx_orders_lib.py` 等 | | **`lib/exchange/`** | 特定交易所工具 | `gate_transfer_lib.py``okx_orders_lib.py` 等 |
| **`lib/common/`** | 跨功能小工具 | `form_submit_lib.py``wechat_notify_lib.py` 等 | | **`lib/common/`** | 跨功能小工具 | `form_submit_lib.py``wechat_notify_lib.py` 等 |
| **`lib/common/static/`** | 所与中控共用的 JS/CSS(原根目录 `static/` | `instance_theme.js``strategy_roll.js` 等 | | **`lib/common/static/`** | 所与中控共用的 JS/CSS(原根目录 `static/` | `instance_theme.js``strategy_roll.js` 等 |
> **说明**`hub_*` 命名表示「中控侧能力或行情聚合」,但部分模块(如 `hub_volume_rank_lib``hub_market_info_lib``app.py` 也会调用,并非中控独占。 > **说明**`hub_*` 命名表示「中控侧能力或行情聚合」,但部分模块(如 `hub_volume_rank_lib``hub_market_info_lib``app.py` 也会调用,并非中控独占。
--- ---
@@ -107,9 +107,9 @@ install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
## 静态资源与 URL ## 静态资源与 URL
- 所页面仍通过 **`/static/...`** 访问共用脚本;`hub_bridge.install_instance_theme_static``lib/common/static/` 提供部分根级静态路由。 - 所页面仍通过 **`/static/...`** 访问共用脚本;`hub_bridge.install_instance_theme_static``lib/common/static/` 提供部分根级静态路由。
- 各所目录下 **`static/`**(图标、上传图片等)仍为实例私有,未迁入 `lib/` - 各所目录下 **`static/`**(图标、上传图片等)仍为实例私有,未迁入 `lib/`
- 中控 `manual_trading_hub/hub.py` 通过 `_REPO_ROOT / "lib" / "common" / "static"` 挂载与所共用的 badge、复盘 JS 等。 - 中控 `manual_trading_hub/hub.py` 通过 `_REPO_ROOT / "lib" / "common" / "static"` 挂载与所共用的 badge、复盘 JS 等。
--- ---
@@ -134,7 +134,7 @@ python -m unittest discover -s tests -p "test_*.py"
## 后续可选整理 ## 后续可选整理
- `app.py` 体量接近,可逐步抽取公共 `exchange_app` 基座(改动面大,单独规划)。 - `app.py` 体量接近,可逐步抽取公共 `exchange_app` 基座(改动面大,单独规划)。
- `manual_trading_hub/okx_orders_lib.py` 为 agent 本地副本,可与 `lib/exchange/okx_orders_lib.py` 合并去重。 - `manual_trading_hub/okx_orders_lib.py` 为 agent 本地副本,可与 `lib/exchange/okx_orders_lib.py` 合并去重。
- 可引入 `pyproject.toml` + `pip install -e .`,替代 `sys.path.insert`(长期维护更规范)。 - 可引入 `pyproject.toml` + `pip install -e .`,替代 `sys.path.insert`(长期维护更规范)。
+2 -2
View File
@@ -2,7 +2,7 @@
## 功能 ## 功能
所(Binance / OKX / Gate / Gate趋势)**实盘下单监控**表单中,在「开仓」按钮前显示 **预估盈亏比** 所(Binance / OKX / Gate**实盘下单监控**表单中,在「开仓」按钮前显示 **预估盈亏比**
- **价格模式**:填完币种、方向、止损价、止盈价后,调用 `GET /api/order_defaults` 取标记价,按几何距离计算 RR。 - **价格模式**:填完币种、方向、止损价、止盈价后,调用 `GET /api/order_defaults` 取标记价,按几何距离计算 RR。
- **百分比模式**:填完币种、方向、止损%、止盈% 后拉快照校验币种,再显示 RR(`止盈% / 止损%`)。 - **百分比模式**:填完币种、方向、止损%、止盈% 后拉快照校验币种,再显示 RR(`止盈% / 止损%`)。
@@ -28,4 +28,4 @@
## 校验记录 ## 校验记录
- `node --check static/manual_order_rr_preview.js` - `node --check static/manual_order_rr_preview.js`
- `tests/test_manual_order_rr_preview.py`RR 公式与`calc_rr_ratio` 口径一致 - `tests/test_manual_order_rr_preview.py`RR 公式与`calc_rr_ratio` 口径一致
+3 -3
View File
@@ -1,4 +1,4 @@
# 计仓模式(所统一) # 计仓模式(所统一)
## 配置 ## 配置
@@ -34,7 +34,7 @@ FULL_MARGIN_BUFFER_RATIO=0.98
**允许:** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价、触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。 **允许:** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价、触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发)。
## 用脚本更新`.env` ## 用脚本更新`.env`
详见 **[env-sync-scripts.md](./env-sync-scripts.md)**。常用命令: 详见 **[env-sync-scripts.md](./env-sync-scripts.md)**。常用命令:
@@ -53,5 +53,5 @@ python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
# 计仓 + 划转一并补全 # 计仓 + 划转一并补全
python scripts/sync_four_exchange_env.py python scripts/sync_four_exchange_env.py
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate crypto-monitor-gate-bot pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
``` ```
+2 -2
View File
@@ -9,7 +9,7 @@
3. `link rel="icon"` / `favicon.ico` 3. `link rel="icon"` / `favicon.ico`
4. 若都没有 → 灰色地球或网页标题首字 4. 若都没有 → 灰色地球或网页标题首字
本仓库已在 **中控****所监控页** 配置统一品牌图标(深色圆角底 + 青绿趋势线 + 简化的 K 线),与页面 UI 一致。PNG/ICO 由 **Pillow** 生成,避免损坏的 favicon 出现花屏。 本仓库已在 **中控****所监控页** 配置统一品牌图标(深色圆角底 + 青绿趋势线 + 简化的 K 线),与页面 UI 一致。PNG/ICO 由 **Pillow** 生成,避免损坏的 favicon 出现花屏。
## 文件位置 ## 文件位置
@@ -17,7 +17,7 @@
|------|----------| |------|----------|
| 源稿 | `brand/icon.svg``brand/icons/*.png` | | 源稿 | `brand/icon.svg``brand/icons/*.png` |
| 中控 | `manual_trading_hub/static/icons/``/assets/icons/...` | | 中控 | `manual_trading_hub/static/icons/``/assets/icons/...` |
| 所 | `crypto_monitor_*/static/icons/``/static/icons/...` | | 所 | `crypto_monitor_*/static/icons/``/static/icons/...` |
## 重新生成 / 同步 ## 重新生成 / 同步
+16 -16
View File
@@ -1,8 +1,8 @@
# 趋势回调:中控平仓与交易记录(检阅备忘) # 趋势回调:中控平仓与交易记录(检阅备忘)
本文档汇总 **中控手动结束趋势计划**、**交易记录 / 策略记录** 写入规则,以及 **所展示统一**、**补仓表计价** 相关修复,便于自行检阅与排错。 本文档汇总 **中控手动结束趋势计划**、**交易记录 / 策略记录** 写入规则,以及 **所展示统一**、**补仓表计价** 相关修复,便于自行检阅与排错。
适用仓库:`crypto_monitor`Binance / OKX / Gate / Gate Bot + `manual_trading_hub`)。 适用仓库:`crypto_monitor`Binance / OKX / + `manual_trading_hub`)。
--- ---
@@ -21,7 +21,7 @@
--- ---
## 2. 调用链(所统一) ## 2. 调用链(所统一)
``` ```
manual_trading_hub manual_trading_hub
@@ -32,7 +32,7 @@ manual_trading_hub
→ _finalize_plan(cfg, conn, row, "手动平仓", exit_price) → _finalize_plan(cfg, conn, row, "手动平仓", exit_price)
``` ```
共用实现:`strategy_trend_register.py`所同一套,Gate Bot `stop_trend_pullback` 也调用 `_finalize_plan`)。 共用实现:`strategy_trend_register.py`所同一套,各所`stop_trend_pullback` 也调用 `_finalize_plan`)。
--- ---
@@ -54,7 +54,7 @@ manual_trading_hub
**现象**:策略记录有(止损 -2.71U),**交易记录没有**。 **现象**:策略记录有(止损 -2.71U),**交易记录没有**。
**原因**Gate Bot `insert_trade_record`**缺少 `entry_reason` 参数**,而 `_finalize_plan` 固定传入 `entry_reason="趋势回调"`,触发: **原因**各所`insert_trade_record`**缺少 `entry_reason` 参数**,而 `_finalize_plan` 固定传入 `entry_reason="趋势回调"`,触发:
```text ```text
TypeError: insert_trade_record() got an unexpected keyword argument 'entry_reason' TypeError: insert_trade_record() got an unexpected keyword argument 'entry_reason'
@@ -64,7 +64,7 @@ TypeError: insert_trade_record() got an unexpected keyword argument 'entry_reaso
**修复提交**`80226ee` **修复提交**`80226ee`
- Gate Bot `insert_trade_record` 增加 `entry_reason` - `insert_trade_record` 增加 `entry_reason`
- `_call_insert_trade_record`:按各所函数 **签名过滤** 参数,避免未知字段导致失败 - `_call_insert_trade_record`:按各所函数 **签名过滤** 参数,避免未知字段导致失败
- 调整写入顺序:交易记录 → 计划结束 → commit - 调整写入顺序:交易记录 → 计划结束 → commit
@@ -79,11 +79,11 @@ cd /opt/crypto_monitor # 或本机仓库根目录
# 先预览 # 先预览
python scripts/backfill_trend_trade_records.py \ python scripts/backfill_trend_trade_records.py \
--db crypto_monitor_gate_bot/crypto.db --dry-run --db crypto_monitor_gate/crypto.db --dry-run
# 确认后写入 # 确认后写入
python scripts/backfill_trend_trade_records.py \ python scripts/backfill_trend_trade_records.py \
--db crypto_monitor_gate_bot/crypto.db --apply --db crypto_monitor_gate/crypto.db --apply
``` ```
其它所将 `--db` 换成对应 `crypto.db` 路径即可。 其它所将 `--db` 换成对应 `crypto.db` 路径即可。
@@ -99,7 +99,7 @@ python scripts/backfill_trend_trade_records.py \
--- ---
## 7. 所展示统一(中控 ↔ 实例) ## 7. 所展示统一(中控 ↔ 实例)
### 7.1 数据 enrich 入口 ### 7.1 数据 enrich 入口
@@ -109,13 +109,13 @@ python scripts/backfill_trend_trade_records.py \
| 中控 `/api/hub/monitor` | `enrich_trend_plan_for_hub` → 同上 | | 中控 `/api/hub/monitor` | `enrich_trend_plan_for_hub` → 同上 |
| 补仓明细表 | `attach_trend_dca_levels``enrich_trend_dca_levels_with_tp` | | 补仓明细表 | `attach_trend_dca_levels``enrich_trend_dca_levels_with_tp` |
Gate Bot `hub_bridge` 安装后调用 `patch_trend_hub_enrich`,与另外三所 `install_strategy_trend` 行为一致。 `hub_bridge` 安装后调用 `patch_trend_hub_enrich`,与另外三所 `install_strategy_trend` 行为一致。
### 7.2 补仓表「触发价 / 加仓后均价」 ### 7.2 补仓表「触发价 / 加仓后均价」
**禁止**为凑均价 **反推虚构成交价**(曾错误出现做多补仓触发价 0.3941 等离谱数值)。 **禁止**为凑均价 **反推虚构成交价**(曾错误出现做多补仓触发价 0.3941 等离谱数值)。
**`trend_leg_display_price`所唯一口径)** **`trend_leg_display_price`所唯一口径)**
| 列 | 规则 | | 列 | 规则 |
|----|------| |----|------|
@@ -138,7 +138,7 @@ Gate Bot 在 `hub_bridge` 安装后调用 `patch_trend_hub_enrich`,与另外
```bash ```bash
cd /opt/crypto_monitor cd /opt/crypto_monitor
git pull # 需含 80226ee、08082eb git pull # 需含 80226ee、08082eb
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate crypto-monitor-gate-bot manual-trading-hub pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate manual-trading-hub
pm2 save pm2 save
``` ```
@@ -157,16 +157,16 @@ pm2 save
| `strategy_trend_lib.py` | `trend_leg_display_price``enrich_trend_dca_levels_with_tp` | | `strategy_trend_lib.py` | `trend_leg_display_price``enrich_trend_dca_levels_with_tp` |
| `strategy_snapshot_lib.py` | 策略快照写入 | | `strategy_snapshot_lib.py` | 策略快照写入 |
| `hub_bridge.py` | `/api/hub/trend/stop/<pid>` | | `hub_bridge.py` | `/api/hub/trend/stop/<pid>` |
| `crypto_monitor_gate_bot/app.py` | `insert_trade_record`(含 `entry_reason` | | `crypto_monitor_gate/app.py` | `insert_trade_record`(含 `entry_reason` |
| `scripts/backfill_trend_trade_records.py` | 漏记交易记录补录 | | `scripts/backfill_trend_trade_records.py` | 漏记交易记录补录 |
### 8.4 相关提交 ### 8.4 相关提交
| 提交 | 说明 | | 提交 | 说明 |
|------|------| |------|------|
| `6a4ec69` | 中控与所趋势展示 enrich 统一 | | `6a4ec69` | 中控与所趋势展示 enrich 统一 |
| `08082eb` | 移除补仓表反推虚构成交价 | | `08082eb` | 移除补仓表反推虚构成交价 |
| `80226ee` | 修复 Gate Bot 中控平仓漏写 `trade_records` | | `80226ee` | 修复 中控平仓漏写 `trade_records` |
--- ---
@@ -175,7 +175,7 @@ pm2 save
| 文档 | 内容 | | 文档 | 内容 |
|------|------| |------|------|
| [策略交易说明.md](../策略交易说明.md) | 策略总览、策略交易记录页 | | [策略交易说明.md](../策略交易说明.md) | 策略总览、策略交易记录页 |
| [crypto_monitor_gate_bot/趋势回调策略说明.md](../crypto_monitor_gate_bot/趋势回调策略说明.md) | 趋势回调业务细则 | | [crypto_monitor_gate/趋势回调策略说明.md](../crypto_monitor_gate/趋势回调策略说明.md) | 趋势回调业务细则 |
| [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md) | 中控监控与趋势卡布局 | | [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md) | 中控监控与趋势卡布局 |
| [hub-symbol-archive-kline.md](./hub-symbol-archive-kline.md) | 币种档案、永久 5m K 线、交易 overlay | | [hub-symbol-archive-kline.md](./hub-symbol-archive-kline.md) | 币种档案、永久 5m K 线、交易 overlay |
@@ -1,16 +1,16 @@
# 趋势回调策略(机器人)说明 # 趋势回调策略说明
本文描述 **「趋势回调」** 自动交易计划的业务规则与实现口径。 本文描述 **「趋势回调」** 自动交易计划的业务规则与实现口径。
**所主站**Binance / Gate / OKX / 本目录 `crypto_monitor_gate_bot`)均在顶栏 **策略交易 → `/strategy`** 左栏提供同一套逻辑(共用 `strategy_trend_register.py`);本目录侧重 **Gate 子账户 / 机器人** 实例,可与主 Gate 账户隔离部署 **所主站**Binance / Gate / OKX)均在顶栏 **策略交易 → `/strategy`** 左栏提供同一套逻辑(共用 `strategy_trend_register.py`);各所使用各自 API 与 `crypto.db`
**检阅备忘**(中控平仓、交易记录、补仓展示、漏记补录):[docs/trend-hub-close-and-trade-records.md](../docs/trend-hub-close-and-trade-records.md) **检阅备忘**(中控平仓、交易记录、补仓展示、漏记补录):[trend-hub-close-and-trade-records.md](./trend-hub-close-and-trade-records.md)
--- ---
## 1. 适用场景 ## 1. 适用场景
- 单独用于跑策略的 **Gate.io USDT 永续** 子账户(建议与主资金隔离);其它交易所实例同理,使用各自 API 与 `crypto.db` - **USDT 永续** 实例独立部署,使用各自 API 与 `crypto.db`
- 你已明确:**方向、止损价、补仓区间边界价、止盈价、杠杆**,并接受程序按风险预算拆分 **首仓 50% + 多档补仓 50%** - 你已明确:**方向、止损价、补仓区间边界价、止盈价、杠杆**,并接受程序按风险预算拆分 **首仓 50% + 多档补仓 50%**
--- ---
+17 -8
View File
@@ -22,7 +22,7 @@
|----|------| |----|------|
| **版本** | **Python 3.10 或 3.11**`python3 --version` ≥ 3.10);脚本会拒绝 3.9 及以下 | | **版本** | **Python 3.10 或 3.11**`python3 --version` ≥ 3.10);脚本会拒绝 3.9 及以下 |
| **虚拟环境** | 每个子项目独立 **`.venv`**`deploy/setup_env.sh` 自动创建) | | **虚拟环境** | 每个子项目独立 **`.venv`**`deploy/setup_env.sh` 自动创建) |
| **依赖文件** | 所监控共用仓库根目录 **`requirements.txt`**;中控用 **`manual_trading_hub/requirements.txt`** | | **依赖文件** | 所监控共用仓库根目录 **`requirements.txt`**;中控用 **`manual_trading_hub/requirements.txt`** |
| **SOCKS** | 走代理时必须安装 **PySocks**(已写入 requirements | | **SOCKS** | 走代理时必须安装 **PySocks**(已写入 requirements |
### 2.1 系统包(root ### 2.1 系统包(root
@@ -75,13 +75,12 @@ pm2 startup # 按提示执行,保证重启后 PM2 自启
### 3.2 PM2 启动顺序(推荐) ### 3.2 PM2 启动顺序(推荐)
```bash ```bash
# 1) 所 Flask(在各子目录执行,或分别 start) # 1) 所 Flask(在各子目录执行,或分别 start)
cd /opt/crypto_monitor/crypto_monitor_binance && pm2 start ecosystem.config.cjs cd /opt/crypto_monitor/crypto_monitor_binance && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_gate && pm2 start ecosystem.config.cjs cd /opt/crypto_monitor/crypto_monitor_gate && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_gate_bot && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_okx && pm2 start ecosystem.config.cjs cd /opt/crypto_monitor/crypto_monitor_okx && pm2 start ecosystem.config.cjs
# 2) 中控 + 子代理(一条配置 5 进程) # 2) 中控 + 子代理(一条配置 4 进程hub + 3 agent
cd /opt/crypto_monitor/manual_trading_hub cd /opt/crypto_monitor/manual_trading_hub
pm2 start ecosystem.config.cjs pm2 start ecosystem.config.cjs
@@ -103,14 +102,24 @@ pm2 restart all # 或按进程名 restart
| 目录 | ecosystem 内典型名称 | | 目录 | ecosystem 内典型名称 |
|------|---------------------| |------|---------------------|
| `crypto_monitor_binance` | `crypto-monitor-binance` | | `crypto_monitor_binance` | `crypto_binance` |
| `crypto_monitor_gate` | `crypto-monitor-gate` | | `crypto_monitor_gate` | `crypto_gate` |
| `crypto_monitor_gate_bot` | `crypto-monitor-gate-bot` | | `crypto_monitor_okx` | `crypto_okx` |
| `crypto_monitor_okx` | `crypto-monitor-okx` |
| `manual_trading_hub` | `manual-trading-hub``manual-agent-*` | | `manual_trading_hub` | `manual-trading-hub``manual-agent-*` |
以各目录 **`ecosystem.config.cjs`** 为准。 以各目录 **`ecosystem.config.cjs`** 为准。
### 3.4 整目录重装(清库 / 去脏 PM2)
保留 `.env`、丢弃旧库与旧 PM2 名单时,见 **[deploy/reinstall-plan-b.md](../deploy/reinstall-plan-b.md)**
```bash
cd /opt/crypto_monitor
bash deploy/reinstall.sh --yes
```
首次安装仍只用 `deploy/setup_env.sh`,二者互不影响。
--- ---
## 4. 目录与权限 ## 4. 目录与权限
+2 -2
View File
@@ -1,4 +1,4 @@
"""AI 日复盘 / 周复盘:附图收集与 journal 文本格式化(所共用)。""" """AI 日复盘 / 周复盘:附图收集与 journal 文本格式化(所共用)。"""
from __future__ import annotations from __future__ import annotations
import os import os
@@ -46,7 +46,7 @@ def journal_row_lines_for_ai(
*, *,
include_hold_duration: bool = True, include_hold_duration: bool = True,
) -> str: ) -> str:
"""把 journal 字段拼成给 AI 的文本;所日复盘/周复盘共用。""" """把 journal 字段拼成给 AI 的文本;所日复盘/周复盘共用。"""
lines = [ lines = [
( (
f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} " f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} "
+1 -1
View File
@@ -1,4 +1,4 @@
/* 账户风控状态徽章 — 所实例 + 中控共用;兼容 data-theme light/dark */ /* 账户风控状态徽章 — 所实例 + 中控共用;兼容 data-theme light/dark */
:root, :root,
html[data-theme="dark"] { html[data-theme="dark"] {
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* 账户风控徽章倒计时 所实例 + 中控共用 * 账户风控徽章倒计时 所实例 + 中控共用
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
+4
View File
@@ -85,6 +85,10 @@
if (typeof global.refreshPriceSnapshotConditional === "function") { if (typeof global.refreshPriceSnapshotConditional === "function") {
global.refreshPriceSnapshotConditional(); global.refreshPriceSnapshotConditional();
} }
if (global.SymbolLivePrice && typeof global.SymbolLivePrice.init === "function") {
const root = document.getElementById("embed-page-root") || document;
global.SymbolLivePrice.init(root);
}
} }
function injectFragment(html) { function injectFragment(html) {
+89 -2
View File
@@ -975,7 +975,7 @@ html[data-theme="light"] .ai-result.is-loading {
50% { opacity: 0.55; } 50% { opacity: 0.55; }
} }
/* AI 日复盘 / 周复盘 Markdown(弹窗 + 内联结果区,所共用) */ /* AI 日复盘 / 周复盘 Markdown(弹窗 + 内联结果区,所共用) */
html[data-theme="light"] .ai-result-md, html[data-theme="light"] .ai-result-md,
html[data-theme="light"] .detail-modal .panel-body.md-review { html[data-theme="light"] .detail-modal .panel-body.md-review {
color: #1a2838 !important; color: #1a2838 !important;
@@ -1028,7 +1028,7 @@ html[data-theme="light"] .detail-modal .panel-body.md-review .md-raw-block-title
border-top-color: #d0dae4 !important; border-top-color: #d0dae4 !important;
} }
/* ── Gate Bot 统计分栏(机器人 / 趋势回调)── */ /* ── 统计分栏(机器人 / 趋势回调)── */
html[data-theme="light"] .stats-split-col { html[data-theme="light"] .stats-split-col {
background: #fff !important; background: #fff !important;
border-color: #b8c8d8 !important; border-color: #b8c8d8 !important;
@@ -1572,3 +1572,90 @@ html[data-theme="light"] .order-preview-profit strong {
color: #087a50 !important; color: #087a50 !important;
} }
/* ── 账户交易限制(方向 / 币种白名单)── */
.trade-policy-badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: #8fc8ff;
background: rgba(31, 58, 90, 0.55);
border: 1px solid rgba(143, 200, 255, 0.35);
line-height: 1.4;
}
.trade-policy-dir-lock {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 600;
color: #4cd97f;
background: rgba(76, 217, 127, 0.1);
border: 1px solid rgba(76, 217, 127, 0.28);
white-space: nowrap;
}
html[data-theme="light"] .trade-policy-badge {
color: #1a4a7a;
background: #e8f2fb;
border-color: #9ec5e8;
}
html[data-theme="light"] .trade-policy-dir-lock {
color: #087a50;
background: #e8f8f0;
border-color: #9ed4b8;
}
/* ── 币种输入实时现价 ── */
.symbol-live-price {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 600;
color: #8fc8ff;
background: rgba(31, 58, 90, 0.35);
border: 1px solid rgba(143, 200, 255, 0.22);
white-space: nowrap;
line-height: 1.35;
}
.symbol-live-price--ok {
color: #4cd97f;
border-color: rgba(76, 217, 127, 0.35);
background: rgba(76, 217, 127, 0.08);
}
.symbol-live-price--loading {
opacity: 0.75;
}
.symbol-live-price--err {
color: #e8a090;
border-color: rgba(232, 160, 144, 0.35);
}
.symbol-live-price-note {
font-size: 0.72rem;
color: #8892b0;
white-space: nowrap;
}
html[data-theme="light"] .symbol-live-price {
color: #1a4a7a;
background: #eef4fb;
border-color: #b8cfe8;
}
html[data-theme="light"] .symbol-live-price--ok {
color: #087a50;
background: #e8f8f0;
border-color: #9ed4b8;
}
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* 所实例主题默认暗色单独登录用 instance-theme中控 iframe/SSO hub-theme 联动 * 所实例主题默认暗色单独登录用 instance-theme中控 iframe/SSO hub-theme 联动
*/ */
(function (global) { (function (global) {
const STANDALONE_KEY = "instance-theme"; const STANDALONE_KEY = "instance-theme";
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* 所实例共用 UI复盘详情盈亏着色等 * 所实例共用 UI复盘详情盈亏着色等
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* 关键位监控添加表单类型切换显隐成交量排名校验所实例共用 * 关键位监控添加表单类型切换显隐成交量排名校验所实例共用
*/ */
(function (global) { (function (global) {
const RS_TYPES = new Set([ const RS_TYPES = new Set([
+169
View File
@@ -0,0 +1,169 @@
/**
* 表单币种输入防抖 + 定时刷新展示交易所最新价/api/order_defaults
*/
(function (global) {
"use strict";
const DEFAULT_DEBOUNCE_MS = 350;
const DEFAULT_POLL_MS = 5000;
const bound = new WeakSet();
function $(id) {
return id ? document.getElementById(id) : null;
}
function symbolValue(el) {
if (!el) return "";
return (el.value || "").trim();
}
function directionValue(dirId) {
const el = dirId ? $(dirId) : null;
const v = (el && el.value ? el.value : "long").trim().toLowerCase();
return v === "short" ? "short" : "long";
}
function formatPrice(px, sym) {
const n = Number(px);
if (!Number.isFinite(n)) return "—";
const u = (sym || "").trim().toUpperCase();
let digits = 4;
if (u.startsWith("BTC") || u.startsWith("ETH") || n >= 1000) digits = 2;
else if (n >= 10) digits = 3;
else if (n >= 1) digits = 4;
else if (n >= 0.01) digits = 5;
else digits = 6;
return n.toFixed(digits);
}
function pollMs() {
const raw =
(document.body && document.body.getAttribute("data-price-refresh-ms")) || "";
const n = Number(raw);
return Number.isFinite(n) && n >= 2000 ? n : DEFAULT_POLL_MS;
}
function paint(el, sym, px, err) {
if (!el) return;
if (err) {
el.textContent = "现价:—";
el.classList.add("symbol-live-price--err");
el.classList.remove("symbol-live-price--ok");
el.title = err;
return;
}
if (px === null || typeof px === "undefined") {
el.textContent = "现价:—";
el.classList.remove("symbol-live-price--ok", "symbol-live-price--err");
el.title = sym ? "无法读取交易所价格" : "";
return;
}
const label = sym ? sym.toUpperCase().replace(/\/USDT.*/, "") : "";
el.textContent = label ? label + " 现价 " + formatPrice(px, sym) : "现价 " + formatPrice(px, sym);
el.classList.add("symbol-live-price--ok");
el.classList.remove("symbol-live-price--err");
el.title = "交易所最新价(约 " + pollMs() / 1000 + "s 刷新)";
}
function bindOne(el) {
if (!el || bound.has(el)) return;
bound.add(el);
const symId = el.getAttribute("data-symbol-input");
const dirId = el.getAttribute("data-direction-input") || "";
let debounceTimer = null;
let pollTimer = null;
let fetchSeq = 0;
function clearPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPoll() {
clearPoll();
pollTimer = setInterval(refresh, pollMs());
}
function refresh() {
const symEl = $(symId);
const sym = symbolValue(symEl);
if (!sym) {
paint(el, "", null, "");
clearPoll();
return;
}
const dir = directionValue(dirId);
const seq = ++fetchSeq;
el.classList.add("symbol-live-price--loading");
fetch(
"/api/order_defaults?symbol=" +
encodeURIComponent(sym) +
"&direction=" +
encodeURIComponent(dir)
)
.then(function (r) {
return r.json().then(function (d) {
return { status: r.status, data: d };
}).catch(function () {
return { status: r.status, data: null };
});
})
.then(function (res) {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
const data = res.data || {};
if (res.status >= 400 || !data || !data.ok) {
paint(el, sym, null, (data && data.msg) || "读取失败");
return;
}
const px = data.last_price != null ? data.last_price : data.price;
if (px === null || typeof px === "undefined") {
paint(el, data.symbol || sym, null, "无法读取交易所价格");
return;
}
paint(el, data.symbol || sym, px, "");
if (!pollTimer) startPoll();
})
.catch(function () {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
paint(el, sym, null, "网络错误");
});
}
function schedule() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(refresh, DEFAULT_DEBOUNCE_MS);
}
const symEl = $(symId);
if (symEl) {
symEl.addEventListener("input", schedule);
symEl.addEventListener("change", schedule);
}
const dirEl = dirId ? $(dirId) : null;
if (dirEl) {
dirEl.addEventListener("change", schedule);
}
schedule();
}
function init(root) {
const scope = root || document;
scope.querySelectorAll(".symbol-live-price").forEach(bindOne);
}
global.SymbolLivePrice = { init: init, bind: bindOne };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
init(document);
});
} else {
init(document);
}
})(typeof window !== "undefined" ? window : globalThis);
+1 -1
View File
@@ -1,4 +1,4 @@
/* 交易日历:内照明心 + 所统计分析共用,随 data-theme 浅/深切换 */ /* 交易日历:内照明心 + 所统计分析共用,随 data-theme 浅/深切换 */
.trade-cal-wrap { .trade-cal-wrap {
--trade-cal-wrap-bg: var(--inset-surface, rgba(0, 0, 0, 0.22)); --trade-cal-wrap-bg: var(--inset-surface, rgba(0, 0, 0, 0.22));
--trade-cal-cell-bg: var(--section-surface, var(--inset-surface, rgba(0, 0, 0, 0.32))); --trade-cal-cell-bg: var(--section-surface, var(--inset-surface, rgba(0, 0, 0, 0.32)));
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* 交易日历组件内照明心档案 + 所统计分析共用 * 交易日历组件内照明心档案 + 所统计分析共用
*/ */
(function (global) { (function (global) {
"use strict"; "use strict";
+9
View File
@@ -0,0 +1,9 @@
"""Gate.io ccxt 构造(ccxt 4.x 起类名由 gateio 改为 gate)。"""
from __future__ import annotations
import ccxt
def gate_ccxt_class():
"""返回 ccxt Gate 交易所类(兼容旧版 gateio 名称)。"""
return getattr(ccxt, "gate", None) or ccxt.gateio
+1 -1
View File
@@ -1,4 +1,4 @@
"""Gate.io 资金划转(crypto_monitor_gate / crypto_monitor_gate_bot 共用)。""" """Gate.io 资金划转(crypto_monitor_gate 共用)。"""
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
+1 -2
View File
@@ -1,4 +1,4 @@
"""中控备份与恢复:所 SQLite、K 线库、env、hub JSON。""" """中控备份与恢复:所 SQLite、K 线库、env、hub JSON。"""
from __future__ import annotations from __future__ import annotations
import json import json
@@ -22,7 +22,6 @@ EXCHANGE_DIRS: list[tuple[str, str]] = [
("binance", "crypto_monitor_binance"), ("binance", "crypto_monitor_binance"),
("okx", "crypto_monitor_okx"), ("okx", "crypto_monitor_okx"),
("gate", "crypto_monitor_gate"), ("gate", "crypto_monitor_gate"),
("gate_bot", "crypto_monitor_gate_bot"),
] ]
HUB_JSON_FILES = ( HUB_JSON_FILES = (
+6 -2
View File
@@ -42,7 +42,7 @@ def _merge_query_into_path(path: str, **params: str) -> str:
def install_instance_theme_static(app) -> None: def install_instance_theme_static(app) -> None:
"""仓库 lib/common/static 下 instance_theme.* 等供所页面共用。""" """仓库 lib/common/static 下 instance_theme.* 等供所页面共用。"""
import os import os
from flask import Response, send_file from flask import Response, send_file
@@ -63,6 +63,7 @@ def install_instance_theme_static(app) -> None:
"key_monitor_form.js": "application/javascript; charset=utf-8", "key_monitor_form.js": "application/javascript; charset=utf-8",
"time_close_ui.js": "application/javascript; charset=utf-8", "time_close_ui.js": "application/javascript; charset=utf-8",
"manual_order_rr_preview.js": "application/javascript; charset=utf-8", "manual_order_rr_preview.js": "application/javascript; charset=utf-8",
"symbol_live_price.js": "application/javascript; charset=utf-8",
"strategy_roll.js": "application/javascript; charset=utf-8", "strategy_roll.js": "application/javascript; charset=utf-8",
"instance_page.css": "text/css; charset=utf-8", "instance_page.css": "text/css; charset=utf-8",
"instance_embed.js": "application/javascript; charset=utf-8", "instance_embed.js": "application/javascript; charset=utf-8",
@@ -96,7 +97,7 @@ def register_trade_stats_calendar_route(
reset_hour: int, reset_hour: int,
get_db_fn=None, get_db_fn=None,
): ):
"""所统计分析页:按月返回各交易日盈亏/笔数。""" """所统计分析页:按月返回各交易日盈亏/笔数。"""
from flask import jsonify, request from flask import jsonify, request
from lib.trade.trade_stats_calendar_lib import build_trade_stats_calendar from lib.trade.trade_stats_calendar_lib import build_trade_stats_calendar
@@ -630,6 +631,7 @@ def register_hub_routes(app):
fetch_trades_for_trading_day, fetch_trades_for_trading_day,
summarize_trades, summarize_trades,
) )
from lib.trade.daily_open_limit_lib import count_opens_for_trading_day
c = _ctx() c = _ctx()
get_db = c.get("get_db") get_db = c.get("get_db")
@@ -651,6 +653,7 @@ def register_hub_routes(app):
row_to_dict_fn=c.get("row_to_dict"), row_to_dict_fn=c.get("row_to_dict"),
reset_hour=reset_hour, reset_hour=reset_hour,
) )
opens_today = count_opens_for_trading_day(conn, trading_day)
finally: finally:
conn.close() conn.close()
stats = summarize_trades(trades) stats = summarize_trades(trades)
@@ -659,6 +662,7 @@ def register_hub_routes(app):
"ok": True, "ok": True,
"trading_day": trading_day, "trading_day": trading_day,
"trading_day_reset_hour": reset_hour, "trading_day_reset_hour": reset_hour,
"opens_today": opens_today,
"trades": trades, "trades": trades,
"stats": stats, "stats": stats,
} }
+93
View File
@@ -0,0 +1,93 @@
"""监控区看板:三所当日统计聚合。"""
from __future__ import annotations
from typing import Any
def _coerce_float(value: Any) -> float | None:
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def position_unrealized_pnl(pos: dict[str, Any]) -> float:
for key in ("unrealized_pnl", "unrealizedPnl", "upnl"):
v = _coerce_float(pos.get(key))
if v is not None:
return v
return 0.0
def _open_positions(agent: dict[str, Any] | None) -> list[dict[str, Any]]:
if not isinstance(agent, dict):
return []
positions = agent.get("positions")
if not isinstance(positions, list):
return []
out: list[dict[str, Any]] = []
for p in positions:
if not isinstance(p, dict):
continue
try:
c = abs(float(p.get("contracts") or 0))
except (TypeError, ValueError):
c = 0.0
if c > 1e-12:
out.append(p)
return out
def aggregate_monitor_board_totals(
rows: list[dict[str, Any]],
*,
trading_day: str,
reset_hour: int = 8,
) -> dict[str, Any]:
"""汇总监控 board 各行 → 左上统计卡数据。"""
open_count = 0
closed_count = 0
win_count = 0
loss_count = 0
win_pnl_u = 0.0
loss_pnl_u = 0.0
open_position_count = 0
float_pnl_u = 0.0
for row in rows or []:
if not isinstance(row, dict):
continue
day_stats = row.get("day_stats") if isinstance(row.get("day_stats"), dict) else {}
if day_stats.get("ok"):
open_count += int(day_stats.get("opens_today") or 0)
st = day_stats.get("trade_stats") if isinstance(day_stats.get("trade_stats"), dict) else {}
closed_count += int(st.get("closed_count") or 0)
win_count += int(st.get("win_count") or 0)
loss_count += int(st.get("loss_count") or 0)
win_pnl_u += float(st.get("win_pnl_u") or 0)
loss_pnl_u += float(st.get("loss_pnl_u") or 0)
ag = row.get("agent") if isinstance(row.get("agent"), dict) else {}
open_pos = _open_positions(ag)
open_position_count += len(open_pos)
agent_upnl = _coerce_float(ag.get("total_unrealized_pnl"))
if agent_upnl is not None:
float_pnl_u += agent_upnl
else:
float_pnl_u += sum(position_unrealized_pnl(p) for p in open_pos)
return {
"trading_day": trading_day,
"reset_hour": int(reset_hour),
"open_count": open_count,
"closed_count": closed_count,
"win_count": win_count,
"loss_count": loss_count,
"win_pnl_u": round(win_pnl_u, 4),
"loss_pnl_u": round(loss_pnl_u, 4),
"realized_pnl_u": round(win_pnl_u + loss_pnl_u, 4),
"open_position_count": open_position_count,
"float_pnl_u": round(float_pnl_u, 4),
}
+1 -1
View File
@@ -68,7 +68,7 @@ def normalize_chart_timeframe(raw: str | None, default: str = "5m") -> str:
def normalize_perpetual_symbol(symbol: str) -> str: def normalize_perpetual_symbol(symbol: str) -> str:
"""BTC/USDT → BTC/USDT:USDT(与所 ccxt swap 行情一致)。""" """BTC/USDT → BTC/USDT:USDT(与所 ccxt swap 行情一致)。"""
sym = (symbol or "").strip().upper() sym = (symbol or "").strip().upper()
if not sym: if not sym:
return "" return ""
+4 -4
View File
@@ -60,7 +60,7 @@ def position_side_from_ccxt(p: dict[str, Any], contracts: float | None = None) -
def parse_position_entry_price(p: dict[str, Any]) -> float | None: def parse_position_entry_price(p: dict[str, Any]) -> float | None:
"""所 ccxt 持仓开仓均价。""" """所 ccxt 持仓开仓均价。"""
if not isinstance(p, dict): if not isinstance(p, dict):
return None return None
info = p.get("info") or {} info = p.get("info") or {}
@@ -134,7 +134,7 @@ def _coerce_signed(*values: Any) -> float | None:
def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None: def parse_position_unrealized_pnl(p: dict[str, Any]) -> float | None:
"""所 ccxt 持仓统一解析未实现盈亏(Gate/OKX/Binance 字段名不一致)。""" """所 ccxt 持仓统一解析未实现盈亏(Gate/OKX/Binance 字段名不一致)。"""
if not isinstance(p, dict): if not isinstance(p, dict):
return None return None
info = p.get("info") or {} info = p.get("info") or {}
@@ -162,7 +162,7 @@ def enrich_ccxt_position_metrics_out(
funds_decimals: int = 2, funds_decimals: int = 2,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
parse_ccxt_position_metrics 产出后统一 parse_ccxt_position_metrics 产出后统一
- 标记价用 hub 兜底 - 标记价用 hub 兜底
- 未实现盈亏 = resolve(交易所值, entry/mark/张数/contractSize 推算) - 未实现盈亏 = resolve(交易所值, entry/mark/张数/contractSize 推算)
""" """
@@ -194,7 +194,7 @@ def enrich_ccxt_position_metrics_out(
def parse_position_mark_price(p: dict[str, Any]) -> float | None: def parse_position_mark_price(p: dict[str, Any]) -> float | None:
"""所 ccxt 持仓统一解析标记价(与 crypto_monitor_* parse_ccxt_position_metrics 口径一致)。""" """所 ccxt 持仓统一解析标记价(与 crypto_monitor_* parse_ccxt_position_metrics 口径一致)。"""
if not isinstance(p, dict): if not isinstance(p, dict):
return None return None
info = p.get("info") or {} info = p.get("info") or {}
+95
View File
@@ -0,0 +1,95 @@
"""Hub 中控市价全平后立即同步 order_monitors(三所共用)。"""
from __future__ import annotations
import time
from typing import Any, Callable
def reconcile_hub_external_close_impl(
conn,
symbol: str,
direction: str,
*,
exchange_configured: Callable[[], bool],
not_configured_msg: str,
symbols_match: Callable[[str, str], bool],
get_opened_at_value: Callable[[Any], str],
resolve_monitor_exchange_symbol: Callable[[Any], str],
get_live_position_contracts: Callable[[str, str], float | None],
cancel_conditional_orders: Callable[[str], None],
resolve_synced_flat_close: Callable[..., tuple],
finalize_stopped_monitor: Callable[..., None],
sync_trade_records: Callable[..., None] | None = None,
reconcile_flat_streak: dict | None = None,
to_ms_with_fallback: Callable[..., int | None] | None = None,
prefer_manual_resolve: bool = False,
order_row_monitor_type: Callable[[Any], str] | None = None,
) -> dict[str, Any]:
if not exchange_configured():
return {"ok": False, "msg": not_configured_msg, "synced": 0}
sym_req = (symbol or "").strip()
dir_l = (direction or "").strip().lower()
if dir_l not in ("long", "short"):
return {"ok": False, "msg": "side 须为 long 或 short", "synced": 0}
synced = 0
streak = reconcile_flat_streak if reconcile_flat_streak is not None else {}
rows = conn.execute(
"SELECT * FROM order_monitors WHERE status IN ('active', 'error')"
).fetchall()
for r in rows:
if not symbols_match(str(r["symbol"] or ""), sym_req):
continue
if (r["direction"] or "").strip().lower() != dir_l:
continue
oid = int(r["id"])
if r["status"] == "error":
opened_at_chk = get_opened_at_value(r)
mtype = order_row_monitor_type(r) if order_row_monitor_type else r["monitor_type"]
existing = conn.execute(
"SELECT id FROM trade_records WHERE symbol=? AND opened_at=? AND monitor_type=? LIMIT 1",
(r["symbol"], opened_at_chk, mtype),
).fetchone()
if existing:
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (oid,))
synced += 1
continue
exchange_symbol = resolve_monitor_exchange_symbol(r)
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
if live_contracts is None:
continue
if live_contracts > 0:
time.sleep(0.6)
live_contracts = get_live_position_contracts(exchange_symbol, r["direction"])
if live_contracts is None or live_contracts > 0:
continue
streak.pop(oid, None)
cancel_conditional_orders(exchange_symbol)
opened_at = get_opened_at_value(r)
opened_at_ms = None
if to_ms_with_fallback is not None:
keys = r.keys() if hasattr(r, "keys") else ()
opened_at_ms = to_ms_with_fallback(
r["opened_at_ms"] if "opened_at_ms" in keys else None,
opened_at,
)
resolve_kw = {"opened_at_ms": opened_at_ms}
if prefer_manual_resolve:
resolve_kw["prefer_manual"] = True
result, pnl_amount, closed_at, miss_reason = resolve_synced_flat_close(
r, opened_at, **resolve_kw
)
finalize_stopped_monitor(
conn,
r,
result=result,
pnl_amount=pnl_amount,
closed_at=closed_at,
miss_reason=miss_reason,
)
synced += 1
if sync_trade_records is not None:
try:
sync_trade_records(conn, force=True)
except Exception:
pass
return {"ok": True, "synced": synced}
+6
View File
@@ -616,6 +616,8 @@ def fetch_trades_for_archive(
def summarize_trades(trades: list[dict]) -> dict[str, Any]: def summarize_trades(trades: list[dict]) -> dict[str, Any]:
"""单笔列表 → 笔数 / 盈亏 / 胜败统计。""" """单笔列表 → 笔数 / 盈亏 / 胜败统计。"""
total_pnl = 0.0 total_pnl = 0.0
win_pnl = 0.0
loss_pnl = 0.0
win = loss = flat = 0 win = loss = flat = 0
for t in trades or []: for t in trades or []:
try: try:
@@ -625,8 +627,10 @@ def summarize_trades(trades: list[dict]) -> dict[str, Any]:
total_pnl += pnl total_pnl += pnl
if pnl > 1e-9: if pnl > 1e-9:
win += 1 win += 1
win_pnl += pnl
elif pnl < -1e-9: elif pnl < -1e-9:
loss += 1 loss += 1
loss_pnl += pnl
else: else:
flat += 1 flat += 1
return { return {
@@ -635,4 +639,6 @@ def summarize_trades(trades: list[dict]) -> dict[str, Any]:
"loss_count": loss, "loss_count": loss,
"flat_count": flat, "flat_count": flat,
"total_pnl_u": round(total_pnl, 4), "total_pnl_u": round(total_pnl, 4),
"win_pnl_u": round(win_pnl, 4),
"loss_pnl_u": round(loss_pnl, 4),
} }
+2 -2
View File
@@ -365,7 +365,7 @@ def _collect_scores(exchange, exchange_id: str) -> list[tuple[str, str, float]]:
return _scores_from_okx(exchange) return _scores_from_okx(exchange)
if ex_id == "binance": if ex_id == "binance":
return _scores_from_binance(exchange) return _scores_from_binance(exchange)
if ex_id in ("gateio", "gate", "gate_bot"): if ex_id in ("gateio", "gate"):
return _scores_from_gate(exchange) return _scores_from_gate(exchange)
tickers = exchange.fetch_tickers() tickers = exchange.fetch_tickers()
return _scores_from_markets(exchange, tickers or {}, ex_id) return _scores_from_markets(exchange, tickers or {}, ex_id)
@@ -373,7 +373,7 @@ def _collect_scores(exchange, exchange_id: str) -> list[tuple[str, str, float]]:
def _uses_lightweight_volume_scores(exchange_id: str) -> bool: def _uses_lightweight_volume_scores(exchange_id: str) -> bool:
ex_id = str(exchange_id or "").lower() ex_id = str(exchange_id or "").lower()
return ex_id in ("okx", "binance", "gateio", "gate", "gate_bot") return ex_id in ("okx", "binance", "gateio", "gate")
def build_usdt_swap_volume_ranks( def build_usdt_swap_volume_ranks(
+1 -2
View File
@@ -33,7 +33,6 @@ PATH_TO_EMBED_TAB: dict[str, str] = {
ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = { ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = {
"gate": "order_monitor_rule_tips_gate.html", "gate": "order_monitor_rule_tips_gate.html",
"gate_bot": "order_monitor_rule_tips_gate.html",
"binance": "order_monitor_rule_tips_binance.html", "binance": "order_monitor_rule_tips_binance.html",
"okx": "order_monitor_rule_tips_okx.html", "okx": "order_monitor_rule_tips_okx.html",
} }
@@ -45,7 +44,7 @@ def order_rule_tips_template(exchange_key: str) -> str:
def include_transfer_block(exchange_key: str) -> bool: def include_transfer_block(exchange_key: str) -> bool:
return (exchange_key or "").strip().lower() in ("gate", "gate_bot") return (exchange_key or "").strip().lower() == "gate"
def path_to_embed_tab(path: str) -> str | None: def path_to_embed_tab(path: str) -> str | None:
@@ -1155,7 +1155,10 @@ function refreshAccountSnapshot(){
const orderSymbolEl = document.getElementById("order-symbol"); const orderSymbolEl = document.getElementById("order-symbol");
const orderDirectionEl = document.getElementById("order-direction"); const orderDirectionEl = document.getElementById("order-direction");
const fullMarginEl = document.getElementById("use-full-margin"); const fullMarginEl = document.getElementById("use-full-margin");
if(orderSymbolEl) orderSymbolEl.addEventListener("change", refreshOrderDefaults); if(orderSymbolEl) {
orderSymbolEl.addEventListener("change", refreshOrderDefaults);
orderSymbolEl.addEventListener("input", refreshOrderDefaults);
}
if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults); if(orderDirectionEl) orderDirectionEl.addEventListener("change", refreshOrderDefaults);
if(fullMarginEl){ if(fullMarginEl){
fullMarginEl.addEventListener("change", function(){ fullMarginEl.addEventListener("change", function(){
@@ -34,10 +34,9 @@
</div> </div>
{% include order_rule_tips_tpl %} {% include order_rule_tips_tpl %}
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}"> <form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required> {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
<select id="order-direction" name="direction" required> {{ trade_policy_symbol('symbol', 'order-symbol') }}
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> {{ trade_policy_direction('direction', 'order-direction') }}
</select>
<select id="sltp-mode" name="sltp_mode"> <select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option> <option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option> <option value="price">止盈止损:价格模式</option>
@@ -66,7 +65,9 @@
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef"> <label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记) <input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label> </label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span> {% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('order-symbol-live-price', 'order-symbol', 'order-direction') }}
<span class="symbol-live-price-note">下单成交价以交易所成交回报为准</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required> <input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比"> <input id="order-fixed-rr" name="fixed_rr" type="number" min="0.01" step="0.01" placeholder="盈亏比(默认1.5)" value="1.5" title="止盈距离=止损距离×盈亏比">
<span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span> <span id="order-tp-preview" style="display:none;font-size:.8rem;color:#8fc8ff;align-self:center">预估止盈:—</span>
+5 -1
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4"> <link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4"> <link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
<link rel="stylesheet" href="/static/instance_page.css?v=2"> <link rel="stylesheet" href="/static/instance_page.css?v=2">
<link rel="stylesheet" href="/static/instance_theme.css?v=48"> <link rel="stylesheet" href="/static/instance_theme.css?v=50">
<script src="/static/account_risk_badge.js?v=4"></script> <script src="/static/account_risk_badge.js?v=4"></script>
<meta name="theme-color" content="#0b0d14"> <meta name="theme-color" content="#0b0d14">
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title> <title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
@@ -28,6 +28,9 @@
<h1>加密货币|交易监控 + AI复盘一体化</h1> <h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row"> <div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div> <div class="exchange-tag">{{ exchange_display }}</div>
{% if trade_policy.badge_text %}
<span class="trade-policy-badge" title="账户交易限制(.env">{{ trade_policy.badge_text }}</span>
{% endif %}
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span> <span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题"> <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题"> <button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
@@ -118,6 +121,7 @@
<script src="/static/ai_review_render.js?v=2"></script> <script src="/static/ai_review_render.js?v=2"></script>
<script src="/static/form_submit_guard.js?v=2"></script> <script src="/static/form_submit_guard.js?v=2"></script>
<script src="/static/manual_order_rr_preview.js?v=5"></script> <script src="/static/manual_order_rr_preview.js?v=5"></script>
<script src="/static/symbol_live_price.js?v=2"></script>
<script src="/static/strategy_roll.js?v=6"></script> <script src="/static/strategy_roll.js?v=6"></script>
<script src="/static/key_monitor_form.js?v=2"></script> <script src="/static/key_monitor_form.js?v=2"></script>
{% include 'embed_boot_scripts.html' %} {% include 'embed_boot_scripts.html' %}
+1 -1
View File
@@ -1,4 +1,4 @@
"""关键位监控表结构迁移(所共用)。""" """关键位监控表结构迁移(所共用)。"""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@@ -1,4 +1,4 @@
"""回调/突破触价开仓关键位监控:程序盯价、触达计划入场后市价成交(所共用逻辑)。""" """回调/突破触价开仓关键位监控:程序盯价、触达计划入场后市价成交(所共用逻辑)。"""
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
@@ -37,6 +37,34 @@ KEY_ENTRY_REASON_CALLBACK = "关键位回调触价开仓"
KEY_ENTRY_REASON_BREAKOUT = "关键位突破触价开仓" KEY_ENTRY_REASON_BREAKOUT = "关键位突破触价开仓"
KEY_ENTRY_REASON_TRIGGER_LEGACY = "关键位触价开仓" KEY_ENTRY_REASON_TRIGGER_LEGACY = "关键位触价开仓"
TRIGGER_ENTRY_IN_FLIGHT_OID = "__trigger_entry_in_flight__"
def is_trigger_entry_in_flight_row(row: Any) -> bool:
if row is None:
return False
try:
v = row["fib_limit_order_id"]
except (KeyError, IndexError, TypeError):
v = getattr(row, "fib_limit_order_id", None)
return (v or "").strip() == TRIGGER_ENTRY_IN_FLIGHT_OID
def acquire_trigger_entry_exec_lock(conn: Any, monitor_id: int) -> bool:
cur = conn.execute(
"UPDATE key_monitors SET fib_limit_order_id=? WHERE id=? "
"AND (fib_limit_order_id IS NULL OR fib_limit_order_id='')",
(TRIGGER_ENTRY_IN_FLIGHT_OID, int(monitor_id)),
)
return int(cur.rowcount or 0) == 1
def release_trigger_entry_exec_lock(conn: Any, monitor_id: int) -> None:
conn.execute(
"UPDATE key_monitors SET fib_limit_order_id=NULL WHERE id=? AND fib_limit_order_id=?",
(int(monitor_id), TRIGGER_ENTRY_IN_FLIGHT_OID),
)
def normalize_trigger_entry_monitor_type(monitor_type: Optional[str]) -> str: def normalize_trigger_entry_monitor_type(monitor_type: Optional[str]) -> str:
mt = (monitor_type or "").strip() mt = (monitor_type or "").strip()
+1 -1
View File
@@ -193,7 +193,7 @@ def build_strategy_config(
fn(content) fn(content)
note = trend_disabled_note or ( note = trend_disabled_note or (
"趋势回调(自动补仓)请在 Gate 趋势机器人实例使用:/strategy/trend" "趋势回调(自动补仓)请在 Gate机器人实例使用:/strategy/trend"
) )
return { return {
"app_module": m, "app_module": m,
+1 -1
View File
@@ -2,7 +2,7 @@
Gate.io USDT 永续 策略交易交易所侧能力 Gate.io USDT 永续 策略交易交易所侧能力
实现方式 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入 实现方式 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入
ccxt 下单精度 TP/SL本文件为文档与类型锚点避免在四个 app 重复实现滚仓公式 ccxt 下单精度 TP/SL本文件为文档与类型锚点避免在 app 重复实现滚仓公式
""" """
from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter
+1 -1
View File
@@ -1,4 +1,4 @@
"""策略交易记录页:已结束趋势 / 顺势加仓快照(所统一)。""" """策略交易记录页:已结束趋势 / 顺势加仓快照(所统一)。"""
from __future__ import annotations from __future__ import annotations
import json import json
+5
View File
@@ -277,6 +277,11 @@ def _roll_context(cfg: dict, data: dict) -> tuple[Optional[dict], Optional[str]]
if not symbol: if not symbol:
return None, "请选择或填写币种" return None, "请选择或填写币种"
direction = (data.get("direction") or "long").strip().lower() direction = (data.get("direction") or "long").strip().lower()
validate_fn = getattr(m, "validate_trade_policy_open", None) if m is not None else None
if callable(validate_fn):
ok_pol, pol_msg = validate_fn(symbol, direction)
if not ok_pol:
return None, pol_msg
ex_sym = cfg["normalize_exchange_symbol"](symbol) ex_sym = cfg["normalize_exchange_symbol"](symbol)
conn = get_db() conn = get_db()
init_strategy_tables(conn) init_strategy_tables(conn)
+14 -1
View File
@@ -40,7 +40,8 @@ def check_roll_monitors(cfg: dict[str, Any]) -> None:
_reconcile_roll_groups(conn, cfg) _reconcile_roll_groups(conn, cfg)
_check_pending_roll_legs(conn, cfg) _check_pending_roll_legs(conn, cfg)
conn.commit() conn.commit()
except Exception: except Exception as e:
print(f"[roll_monitor] {e}", flush=True)
try: try:
conn.rollback() conn.rollback()
except Exception: except Exception:
@@ -408,7 +409,19 @@ def _execute_pending_roll_leg(
return return
oid = str(order.get("id") or "") if isinstance(order, dict) else "" oid = str(order.get("id") or "") if isinstance(order, dict) else ""
try:
cfg["replace_tpsl"](ex_sym, direction, sl, tp0, mon) cfg["replace_tpsl"](ex_sym, direction, sl, tp0, mon)
except Exception as tpsl_err:
fe = cfg.get("friendly_error")
msg = fe(tpsl_err) if callable(fe) else str(tpsl_err)
conn.execute(
"""UPDATE roll_legs SET status='error', exchange_order_id=?, fill_price=?, amount=?
WHERE id=? AND status='pending'""",
(oid, fill, float(amount), leg_id),
)
_notify_roll_fail(cfg, group, leg, mark, f"加仓成交但止盈止损更新失败: {msg}")
return
conn.execute( conn.execute(
"""UPDATE roll_legs SET status='filled', fill_price=?, amount=?, exchange_order_id=?, """UPDATE roll_legs SET status='filled', fill_price=?, amount=?, exchange_order_id=?,
new_stop_loss=? WHERE id=? AND status='pending'""", new_stop_loss=? WHERE id=? AND status='pending'""",
+1 -1
View File
@@ -1,4 +1,4 @@
"""策略结束快照:趋势回调 / 顺势加仓(所共用)。""" """策略结束快照:趋势回调 / 顺势加仓(所共用)。"""
from __future__ import annotations from __future__ import annotations
import json import json
+4 -4
View File
@@ -230,7 +230,7 @@ def compute_trend_plan_core(
def calc_planned_reward_risk_ratio( def calc_planned_reward_risk_ratio(
direction: str, entry_price: float, stop_loss: float, take_profit: float direction: str, entry_price: float, stop_loss: float, take_profit: float
) -> Optional[float]: ) -> Optional[float]:
"""盈亏比(reward/risk),与所 calc_rr_ratio 口径一致。""" """盈亏比(reward/risk),与所 calc_rr_ratio 口径一致。"""
try: try:
entry = float(entry_price) entry = float(entry_price)
sl = float(stop_loss) sl = float(stop_loss)
@@ -375,7 +375,7 @@ def trend_leg_grid_price(plan: dict, leg_idx: int) -> Optional[float]:
def trend_leg_display_price(plan: dict, leg_idx: int) -> Optional[float]: def trend_leg_display_price(plan: dict, leg_idx: int) -> Optional[float]:
""" """
所统一单档展示价 = leg_fill_prices_json 实际记录否则计划网格首仓用均价/参考价 所统一单档展示价 = leg_fill_prices_json 实际记录否则计划网格首仓用均价/参考价
禁止为凑均价反推虚构成交价 禁止为凑均价反推虚构成交价
""" """
p = plan or {} p = plan or {}
@@ -398,7 +398,7 @@ def trend_leg_display_price(plan: dict, leg_idx: int) -> Optional[float]:
def reconcile_trend_leg_fill_prices(plan: dict) -> list[float]: def reconcile_trend_leg_fill_prices(plan: dict) -> list[float]:
"""首仓(0)+已补仓(1..legs_done) 展示价列表(所共用 trend_leg_display_price)。""" """首仓(0)+已补仓(1..legs_done) 展示价列表(所共用 trend_leg_display_price)。"""
p = plan or {} p = plan or {}
if int(p.get("first_order_done") or 0) == 0: if int(p.get("first_order_done") or 0) == 0:
return [] return []
@@ -563,7 +563,7 @@ def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]: def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]:
""" """
所统一补仓表 enrich实例策略页 + 中控 monitor 共用 所统一补仓表 enrich实例策略页 + 中控 monitor 共用
触发价实际成交价或计划网格末档加仓后均价用持仓均价禁止反推虚构成交价 触发价实际成交价或计划网格末档加仓后均价用持仓均价禁止反推虚构成交价
""" """
if not levels: if not levels:
+74 -16
View File
@@ -1,4 +1,4 @@
"""趋势回调:路由、轮询、页面数据(所共用,依赖各 app 模块交易所能力)。""" """趋势回调:路由、轮询、页面数据(所共用,依赖各 app 模块交易所能力)。"""
from __future__ import annotations from __future__ import annotations
import inspect import inspect
@@ -138,8 +138,8 @@ def summarize_trend_dca_probe(cfg: dict, row) -> dict:
out["block_reason"] = "交易所无持仓" out["block_reason"] = "交易所无持仓"
else: else:
out["block_reason"] = ( out["block_reason"] = (
"标记价已触达,轮询应自动下单;若仍未补请确认 PM2 进程 crypto_gate_bot " "标记价已触达,轮询应自动下单;若仍未补请确认 PM2 进程 crypto_gate "
"非 manual-agent-gate-bot)在运行,并查看 pm2 logs crypto_gate_bot" "或对应所 Flask 进程)在运行,并查看 pm2 logs"
) )
elif not reached: elif not reached:
out["block_reason"] = f"标记价 {pf} 未触达下一档 {level}" out["block_reason"] = f"标记价 {pf} 未触达下一档 {level}"
@@ -244,7 +244,7 @@ def _row(cfg, row) -> dict:
return cfg["row_to_dict"](row) return cfg["row_to_dict"](row)
def precheck_trend_start(cfg: dict, conn) -> tuple[bool, str]: def precheck_trend_start(cfg: dict, conn, *, symbol: str = "", direction: str = "long") -> tuple[bool, str]:
m = _m(cfg) m = _m(cfg)
mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk" mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk"
try: try:
@@ -255,9 +255,46 @@ def precheck_trend_start(cfg: dict, conn) -> tuple[bool, str]:
return False, src_msg return False, src_msg
except Exception: except Exception:
pass pass
sym = (symbol or "").strip()
dir_l = (direction or "long").strip().lower()
validate_fn = getattr(m, "validate_trade_policy_open", None)
if callable(validate_fn) and sym:
ok_pol, pol_msg = validate_fn(sym, dir_l)
if not ok_pol:
return False, pol_msg
if sym and dir_l in ("long", "short") and hasattr(m, "precheck_risk"):
ok_risk, risk_msg = m.precheck_risk(conn, sym, dir_l)
if not ok_risk:
return False, risk_msg
else:
now = m.app_now() now = m.app_now()
if not m.trading_day_reset_allows_new_open(now): if not m.trading_day_reset_allows_new_open(now):
return False, f"北京时间 {cfg['reset_hour']}:00 前不允许持仓" return False, f"北京时间 {cfg['reset_hour']}:00 前不允许持仓"
from lib.trade.account_risk_lib import account_risk_blocks_trading, position_limit_reached
ok_risk, risk_reason = account_risk_blocks_trading(
conn,
trading_day=m.get_trading_day(now),
now=now,
fmt_local_ms=getattr(m, "ms_to_app_local_str", lambda _x: ""),
)
if not ok_risk:
return False, risk_reason
reached, active_count, mx = position_limit_reached(
conn, max_active_positions=cfg["max_active_positions"]
)
if reached:
return False, f"已达最大持仓数({active_count}/{mx}"
from lib.trade.daily_open_limit_lib import check_daily_open_hard_limit
ok_daily, daily_reason, _opens = check_daily_open_hard_limit(
conn,
m.get_trading_day(now),
getattr(m, "DAILY_OPEN_HARD_LIMIT", 0),
cfg["reset_hour"],
)
if not ok_daily:
return False, daily_reason
active = m.get_active_position_count(conn) active = m.get_active_position_count(conn)
if active >= cfg["max_active_positions"]: if active >= cfg["max_active_positions"]:
return ( return (
@@ -520,7 +557,7 @@ def _patch_hub_trend_views(app: Flask) -> None:
def patch_trend_hub_enrich(app: Flask, cfg: dict) -> None: def patch_trend_hub_enrich(app: Flask, cfg: dict) -> None:
"""hub_bridge install 之后调用:所 /api/hub/monitor 趋势字段与策略页一致。""" """hub_bridge install 之后调用:所 /api/hub/monitor 趋势字段与策略页一致。"""
_patch_hub_monitor_enrich(app, cfg) _patch_hub_monitor_enrich(app, cfg)
@@ -1604,22 +1641,27 @@ def register_trend_routes(app: Flask, cfg: dict) -> None:
def preview_trend_pullback(): def preview_trend_pullback():
conn = get_db() conn = get_db()
init_strategy_tables(conn) init_strategy_tables(conn)
okp, msg = precheck_trend_start(cfg, conn)
if not okp:
conn.close()
flash(msg)
return _redirect_trend()
m = _m(cfg) m = _m(cfg)
ok_live, reason = m.ensure_exchange_live_ready()
if not ok_live:
conn.close()
flash(reason)
return _redirect_trend()
payload, err = parse_trend_plan(cfg, request.form) payload, err = parse_trend_plan(cfg, request.form)
if err: if err:
conn.close() conn.close()
flash(err) flash(err)
return _redirect_trend() return _redirect_trend()
okp, msg = precheck_trend_start(
cfg,
conn,
symbol=str(payload.get("symbol") or ""),
direction=str(payload.get("direction") or "long"),
)
if not okp:
conn.close()
flash(msg)
return _redirect_trend()
ok_live, reason = m.ensure_exchange_live_ready()
if not ok_live:
conn.close()
flash(reason)
return _redirect_trend()
pid = str(uuid.uuid4()) pid = str(uuid.uuid4())
exp_ms = int(time.time() * 1000) + cfg["preview_ttl"] * 1000 exp_ms = int(time.time() * 1000) + cfg["preview_ttl"] * 1000
created = m.app_now_str() created = m.app_now_str()
@@ -1678,7 +1720,12 @@ def register_trend_routes(app: Flask, cfg: dict) -> None:
conn.close() conn.close()
flash("预览已过期或不存在,请重新生成预览") flash("预览已过期或不存在,请重新生成预览")
return _redirect_trend() return _redirect_trend()
okp, msg = precheck_trend_start(cfg, conn) okp, msg = precheck_trend_start(
cfg,
conn,
symbol=str(pr["symbol"] or ""),
direction=str(pr["direction"] or "long"),
)
if not okp: if not okp:
conn.close() conn.close()
flash(msg) flash(msg)
@@ -1718,7 +1765,18 @@ def register_trend_routes(app: Flask, cfg: dict) -> None:
exchange_symbol, direction, first_amt, leverage, stop_loss=None, take_profit=None exchange_symbol, direction, first_amt, leverage, stop_loss=None, take_profit=None
) )
fill1 = m.resolve_order_entry_price(o1, exchange_symbol, live_price) fill1 = m.resolve_order_entry_price(o1, exchange_symbol, live_price)
try:
trend_refresh_stop_only(cfg, exchange_symbol, direction, stop_loss) trend_refresh_stop_only(cfg, exchange_symbol, direction, stop_loss)
except Exception as sl_err:
from lib.strategy.strategy_trend_exchange import cancel_symbol_orders, trend_market_close
try:
pos_qty = m.get_live_position_contracts(exchange_symbol, direction) or first_amt
trend_market_close(cfg, exchange_symbol, direction, float(pos_qty), leverage)
cancel_symbol_orders(cfg, exchange_symbol)
except Exception as close_err:
print(f"[trend_start] compensating close failed: {close_err}", flush=True)
raise sl_err
except Exception as e: except Exception as e:
conn.close() conn.close()
fe = getattr(m, "friendly_exchange_error", lambda x, **k: str(x)) fe = getattr(m, "friendly_exchange_error", lambda x, **k: str(x))
+2 -3
View File
@@ -77,9 +77,8 @@ def fetch_roll_page_data(
DEFAULT_TREND_DISABLED_NOTE = ( DEFAULT_TREND_DISABLED_NOTE = (
"趋势回调(预览、自动补仓、程序止盈)仅在 Gate 趋势机器人实例 " "趋势回调(预览、自动补仓、程序止盈)须在本实例 .env 设置 "
"crypto_monitor_gate_bot,常见端口 5002)中启用" "`LIVE_TRADING_ENABLED=true` 并重启对应 PM2 进程(如 crypto_gate / crypto_okx / crypto_binance"
"币安 / Gate 主站 / OKX 可使用本页「顺势加仓」;完整趋势回调请打开该实例。"
) )
+1 -1
View File
@@ -1,4 +1,4 @@
"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(所共用)。""" """策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(所共用)。"""
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional from typing import Any, Optional
+11 -4
View File
@@ -4,10 +4,14 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<script src="/static/instance_theme.js?v=5"></script> <script src="/static/instance_theme.js?v=5"></script>
<title>{{ exchange_display }} | 关键位放大</title> <title>{{ exchange_display }} | 关键位放大</title>
<link rel="stylesheet" href="/static/instance_theme.css?v=5"> <link rel="stylesheet" href="/static/instance_theme.css?v=50">
<script src="/static/symbol_live_price.js?v=2"></script>
<link rel="stylesheet" href="/static/focus_chart_page.css?v=1"> <link rel="stylesheet" href="/static/focus_chart_page.css?v=1">
</head> </head>
<body class="focus-page"> <body class="focus-page" data-price-refresh-ms="{{ price_refresh_seconds * 1000 }}">
{% if trade_policy is not defined %}
{% set trade_policy = {'symbol_restrict_enabled': false, 'direction_restrict_enabled': false, 'symbol_whitelist': [], 'allows_long': true, 'allows_short': true, 'badge_text': ''} %}
{% endif %}
<div class="container"> <div class="container">
<div class="card"> <div class="card">
<div class="row" style="justify-content:space-between"> <div class="row" style="justify-content:space-between">
@@ -25,13 +29,16 @@
</div> </div>
<div class="row"> <div class="row">
<a class="btn" href="/">返回首页</a> <a class="btn" href="/">返回首页</a>
<strong class="focus-title">关键位放大(可输入币种)</strong><span class="exchange-tag">{{ exchange_display }}</span> <strong class="focus-title">关键位放大{% if trade_policy.symbol_restrict_enabled %}(选择币种){% else %}(可输入币种){% endif %}</strong><span class="exchange-tag">{{ exchange_display }}</span>
</div> </div>
<div class="status">最近刷新:<span id="updated-at">--</span></div> <div class="status">最近刷新:<span id="updated-at">--</span></div>
</div> </div>
<div class="row" style="margin-top:10px"> <div class="row" style="margin-top:10px">
<label>币种</label> <label>币种</label>
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT"> {% from 'trade_policy_fields.html' import trade_policy_symbol with context %}
{{ trade_policy_symbol('symbol', 'symbol-input', default_symbol, placeholder='BTC/USDT') }}
{% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
{{ symbol_live_price_hint('key-focus-symbol-live-price', 'symbol-input') }}
<label>关键位</label> <label>关键位</label>
<select id="key-id"> <select id="key-id">
<option value="">无(仅看K线)</option> <option value="">无(仅看K线)</option>
@@ -142,7 +142,8 @@
{% endif %} {% endif %}
</div> </div>
<form id="key-form" action="/add_key" method="post" class="form-row"> <form id="key-form" action="/add_key" method="post" class="form-row">
<input name="symbol" placeholder="BTC 或 BTC/USDT" required> {% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction with context %}
{{ trade_policy_symbol('symbol', 'key-symbol') }}
<select name="type" id="key-type-select" required> <select name="type" id="key-type-select" required>
{% if position_sizing_mode != 'full_margin' %} {% if position_sizing_mode != 'full_margin' %}
<option value="箱体突破">箱体突破</option> <option value="箱体突破">箱体突破</option>
@@ -155,9 +156,9 @@
<option value="突破触价开仓">突破触价开仓</option> <option value="突破触价开仓">突破触价开仓</option>
<option value="关键支撑阻力">关键支撑阻力</option> <option value="关键支撑阻力">关键支撑阻力</option>
</select> </select>
<select name="direction" id="key-direction" required> {{ trade_policy_direction('direction', 'key-direction') }}
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option> {% from 'symbol_live_price_snippet.html' import symbol_live_price_hint %}
</select> {{ symbol_live_price_hint('key-symbol-live-price', 'key-symbol', 'key-direction') }}
<input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none"> <input name="key_price" id="key-fb-price" step="0.0001" placeholder="做空填高点/做多填低点" style="display:none">
<input name="trigger_entry" id="key-trigger-entry" step="0.0001" placeholder="计划入场价" style="display:none"> <input name="trigger_entry" id="key-trigger-entry" step="0.0001" placeholder="计划入场价" style="display:none">
<input name="trigger_sl" id="key-trigger-sl" step="0.0001" placeholder="止损价" style="display:none"> <input name="trigger_sl" id="key-trigger-sl" step="0.0001" placeholder="止损价" style="display:none">

Some files were not shown because too many files have changed in this diff Show More