修复okx

This commit is contained in:
dekun
2026-05-25 07:21:29 +08:00
parent 1a21295646
commit 80cc26ef27
3 changed files with 299 additions and 172 deletions
+165 -164
View File
@@ -1,164 +1,165 @@
# ============================================================================= # =============================================================================
# 环境配置模板(可提交 Git)。程序运行时只读取同目录下的 .env。 # 环境配置模板(可提交 Git)。程序运行时只读取同目录下的 .env。
# #
# 首次部署 / 新机: # 首次部署 / 新机:
# cp .env.example .env # cp .env.example .env
# nano .env # 填入真实密钥、端口、代理等 # nano .env # 填入真实密钥、端口、代理等
# #
# 升级代码(git pull)前建议备份(.env 不在 Git 中,pull 不会覆盖): # 升级代码(git pull)前建议备份(.env 不在 Git 中,pull 不会覆盖):
# cp .env .env.backup.$(date +%Y%m%d) # cp .env .env.backup.$(date +%Y%m%d)
# #
# 从备份恢复: # 从备份恢复:
# cp .env.backup.YYYYMMDD .env # cp .env.backup.YYYYMMDD .env
# ============================================================================= # =============================================================================
APP_ENV=production APP_ENV=production
# 服务监听地址(云服务器通常用 0.0.0.0) # 服务监听地址(云服务器通常用 0.0.0.0)
APP_HOST=0.0.0.0 APP_HOST=0.0.0.0
# 服务端口 # 服务端口
APP_PORT=5004 APP_PORT=5004
# 是否开启调试模式(生产建议 false) # 是否开启调试模式(生产建议 false)
APP_DEBUG=false APP_DEBUG=false
# 登录账号 # 登录账号
APP_USERNAME=dekun APP_USERNAME=dekun
# 登录密码(请改成你自己的强密码) # 登录密码(请改成你自己的强密码)
APP_PASSWORD=ChangeMe123! APP_PASSWORD=ChangeMe123!
# 是否关闭登录校验(局域网可设 true;公网务必 false) # 是否关闭登录校验(局域网可设 true;公网务必 false)
APP_AUTH_DISABLED=true APP_AUTH_DISABLED=true
# --- 多账户交易中控 manual_trading_hub --- # --- 多账户交易中控 manual_trading_hub ---
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致 # 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true # 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true
# HUB_BRIDGE_TOKEN=your-long-random-token # HUB_BRIDGE_TOKEN=your-long-random-token
# Flask 会话密钥(必须替换为长随机字符串) # Flask 会话密钥(必须替换为长随机字符串)
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
# 企业微信机器人 Webhook(用于行情/风控推送) # 企业微信机器人 Webhook(用于行情/风控推送)
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_WITH_REAL_KEY WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_WITH_REAL_KEY
# 数据库文件路径(相对路径会自动按项目目录解析) # 数据库文件路径(相对路径会自动按项目目录解析)
DB_PATH=crypto.db DB_PATH=crypto.db
# 交易截图上传目录 # 交易截图上传目录
UPLOAD_DIR=static/images UPLOAD_DIR=static/images
# 训练总资金(U # 训练总资金(U
# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所 # TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所
# 每天起始基数(U # 每天起始基数(U
DAILY_START_CAPITAL=30 DAILY_START_CAPITAL=30
# 日内回撤后基数(U # 日内回撤后基数(U
DAILY_LOSS_CAPITAL=20 DAILY_LOSS_CAPITAL=20
# 日内盈利后基数(U # 日内盈利后基数(U
DAILY_PROFIT_CAPITAL=50 DAILY_PROFIT_CAPITAL=50
# BTC 默认杠杆倍数 # BTC 默认杠杆倍数
BTC_LEVERAGE=10 BTC_LEVERAGE=10
# 山寨币默认杠杆倍数 # 山寨币默认杠杆倍数
ALT_LEVERAGE=5 ALT_LEVERAGE=5
# 交易日重置小时(北京时间) # 交易日重置小时(北京时间)
TRADING_DAY_RESET_HOUR=8 TRADING_DAY_RESET_HOUR=8
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单) # 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true LIVE_TRADING_ENABLED=true
# OKX API Key(实盘) # OKX API Key(实盘)
OKX_API_KEY=REPLACE_WITH_OKX_API_KEY OKX_API_KEY=REPLACE_WITH_OKX_API_KEY
# OKX API Secret(实盘) # OKX API Secret(实盘)
OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET
# OKX API Passphrase(实盘) # OKX API Passphrase(实盘)
OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE
# 保证金模式:cross=全仓,isolated=逐仓 # 保证金模式:cross=全仓,isolated=逐仓
OKX_TD_MODE=cross OKX_TD_MODE=cross
# 持仓模式:hedge=双向持仓,net=单向净持仓 # 持仓模式:hedge=双向持仓,net=单向净持仓
OKX_POS_MODE=hedge OKX_POS_MODE=hedge
# 仓位查询 instTypeOKX # 仓位查询 instTypeOKX
OKX_POSITION_INST_TYPE=SWAP OKX_POSITION_INST_TYPE=SWAP
# 关键位监控:5m收线突破过滤参数 # 关键位监控:5m收线突破过滤参数
KLINE_TIMEFRAME=5m KLINE_TIMEFRAME=5m
KEY_BREAKOUT_LIMIT_PCT=1.5 KEY_BREAKOUT_LIMIT_PCT=1.5
KEY_ALERT_MAX_TIMES=3 KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5 KEY_ALERT_INTERVAL_MINUTES=5
# 资金与仓位刷新周期(秒) # 资金与仓位刷新周期(秒)
BALANCE_REFRESH_SECONDS=60 BALANCE_REFRESH_SECONDS=60
# 后台监控轮询周期(秒) # 后台监控轮询周期(秒)
MONITOR_POLL_SECONDS=3 MONITOR_POLL_SECONDS=3
# 使用可用资金时的缓冲比例(如0.98代表用98%) # 使用可用资金时的缓冲比例(如0.98代表用98%)
FULL_MARGIN_BUFFER_RATIO=0.98 FULL_MARGIN_BUFFER_RATIO=0.98
# 自动划转:将目标账户补足到 AUTO_TRANSFER_AMOUNT # 自动划转:将目标账户补足到 AUTO_TRANSFER_AMOUNT
AUTO_TRANSFER_ENABLED=false AUTO_TRANSFER_ENABLED=false
AUTO_TRANSFER_AMOUNT=30 AUTO_TRANSFER_AMOUNT=30
AUTO_TRANSFER_FROM=funding AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap AUTO_TRANSFER_TO=swap
TRANSFER_CCY=USDT TRANSFER_CCY=USDT
# 强制清仓整点(北京时间,默认 0=凌晨00点) # 强制清仓整点(北京时间,默认 0=凌晨00点)
FORCE_CLOSE_BJ_HOUR=0 FORCE_CLOSE_BJ_HOUR=0
# 是否启用强制清仓(默认关闭,true 才会在整点执行) # 是否启用强制清仓(默认关闭,true 才会在整点执行)
FORCE_CLOSE_ENABLED=false FORCE_CLOSE_ENABLED=false
# 推送与AI超时(秒) # 推送与AI超时(秒)
WECHAT_TIMEOUT_SECONDS=10 WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120 AI_TIMEOUT_SECONDS=120
# AI 复盘服务地址(本机 Ollama 默认地址) # AI 复盘服务地址(本机 Ollama 默认地址)
AI_PROVIDER=openai AI_PROVIDER=openai
OPENAI_API_BASE=https://op.bz121.com/v1 OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥 OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b OPENAI_MODEL=gemma4:e4b
OLLAMA_API=http://127.0.0.1:11434/api/generate OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# OKX 代理(可选):用于本机网络对 OKX TLS/SNI 不稳定时,通过 SSH 动态转发 SOCKS5 出口 # OKX 代理(可选):用于本机网络对 OKX TLS/SNI 不稳定时,通过 SSH 动态转发 SOCKS5 出口
# 1) 先在本机建立隧道(示例): # 1) 先在本机建立隧道(示例):
# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes # ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes
# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名): # 2) 再启用下面这一行(推荐 socks5h,让远端解析域名):
# OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080 # OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080
# #
# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用: # 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用:
# OKX_HTTP_PROXY=http://127.0.0.1:3128 # OKX_HTTP_PROXY=http://127.0.0.1:3128
# OKX_HTTPS_PROXY=http://127.0.0.1:3128 # OKX_HTTPS_PROXY=http://127.0.0.1:3128
# 开仓多周期K线图(可选) # 开仓多周期K线图(可选)
# ORDER_CHART_ENABLED=true # ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m # ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100 # ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts # ORDER_CHART_DIR=static/images/order_charts
# DAILY_OPEN_ALERT_THRESHOLD=5 # DAILY_OPEN_ALERT_THRESHOLD=5
# 关键位:标准方案止损外侧%、趋势单方案止损外侧%(默认 0.5 / 1) # 关键位:标准方案止损外侧%、趋势单方案止损外侧%(默认 0.5 / 1)
# KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5 # KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
# KEY_TREND_STOP_OUTSIDE_PCT=1 # KEY_TREND_STOP_OUTSIDE_PCT=1
# 以损定仓(按交易账户资金的百分比) # 以损定仓(按交易账户资金的百分比)
# RISK_PERCENT=2 # RISK_PERCENT=2
# 移动保本触发(达到多少R触发)与偏移(百分比) # 移动保本触发(达到多少R触发)与偏移(百分比)
# BREAKEVEN_RR_TRIGGER=1.0 # BREAKEVEN_RR_TRIGGER=1.0
# 移动保本阶梯(每多少R继续上移一次,默认1R) # 移动保本阶梯(每多少R继续上移一次,默认1R)
# BREAKEVEN_STEP_R=1.0 # BREAKEVEN_STEP_R=1.0
# BREAKEVEN_OFFSET_PCT=0.02 # BREAKEVEN_OFFSET_PCT=0.02
# 开单风格默认值:trend / swing # 开单风格默认值:trend / swing
# DEFAULT_TRADE_STYLE=trend # DEFAULT_TRADE_STYLE=trend
APP_TIMEZONE=Asia/Shanghai APP_TIMEZONE=Asia/Shanghai
AUTO_TRANSFER_BJ_HOUR=8 AUTO_TRANSFER_BJ_HOUR=8
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日/可开仓等 # TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日/可开仓等
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true # 默认启用「北京时间 TRADING_DAY_RESET_HOUR 前禁止新开仓」;网页顶栏可运行时切换(写入 DB)
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
MAX_ACTIVE_POSITIONS=1
MANUAL_MIN_PLANNED_RR=1.4 MAX_ACTIVE_POSITIONS=1
MANUAL_MIN_PLANNED_RR=1.4
KEY_CONFIRM_BREAKOUT_BAR=-2
KEY_CONFIRM_BAR=-1 KEY_CONFIRM_BREAKOUT_BAR=-2
KEY_VOLUME_MA_BARS=20 KEY_CONFIRM_BAR=-1
KEY_VOLUME_RATIO_MIN=1.3 KEY_VOLUME_MA_BARS=20
# 【箱体/收敛】突破K收盘越过关键位下限% KEY_VOLUME_RATIO_MIN=1.3
KEY_BREAKOUT_AMP_MIN_PCT=0.03 # 【箱体/收敛】突破K收盘越过关键位下限%
KEY_BREAKOUT_AMP_MAX_PCT=0.5 KEY_BREAKOUT_AMP_MIN_PCT=0.03
# 【阻力/支撑】突破后微信提醒 KEY_BREAKOUT_AMP_MAX_PCT=0.5
KEY_ALERT_MAX_TIMES=3 # 【阻力/支撑】突破后微信提醒
KEY_ALERT_INTERVAL_MINUTES=5 KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5
EXCHANGE_DISPLAY_NAME=OKX
OKX_ACCOUNT_LABEL= EXCHANGE_DISPLAY_NAME=OKX
OKX_ACCOUNT_LABEL=
BACKUP_ROOT=/root/backups
BACKUP_RETENTION_DAYS=30 BACKUP_ROOT=/root/backups
BACKUP_INSTANCE=crypto_monitor_okx BACKUP_RETENTION_DAYS=30
BACKUP_INSTANCE=crypto_monitor_okx
+91 -6
View File
@@ -143,6 +143,7 @@ TRADING_DAY_RESET_HOUR = int(os.getenv("TRADING_DAY_RESET_HOUR", "8"))
TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv( TRADING_DAY_RESET_OPEN_GUARD_ENABLED = os.getenv(
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true" "TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "true"
).lower() in ("1", "true", "yes", "on") ).lower() in ("1", "true", "yes", "on")
RUNTIME_KEY_OPEN_GUARD = "trading_day_reset_open_guard_enabled"
APP_TIMEZONE = os.getenv("APP_TIMEZONE", "Asia/Shanghai") APP_TIMEZONE = os.getenv("APP_TIMEZONE", "Asia/Shanghai")
@@ -1064,6 +1065,11 @@ def init_db():
(id INTEGER PRIMARY KEY AUTOINCREMENT, transfer_type TEXT, transfer_day TEXT, (id INTEGER PRIMARY KEY AUTOINCREMENT, transfer_type TEXT, transfer_day TEXT,
amount REAL, from_account TEXT, to_account TEXT, status TEXT, message TEXT, amount REAL, from_account TEXT, to_account TEXT, status TEXT, message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''') created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
c.execute(
"""CREATE TABLE IF NOT EXISTS app_runtime_settings
(key TEXT PRIMARY KEY, value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"""
)
c.execute('''DROP INDEX IF EXISTS idx_transfer_logs_unique_day''') c.execute('''DROP INDEX IF EXISTS idx_transfer_logs_unique_day''')
c.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_transfer_logs_auto_daily_unique c.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_transfer_logs_auto_daily_unique
ON transfer_logs(transfer_type, transfer_day) ON transfer_logs(transfer_type, transfer_day)
@@ -2233,8 +2239,45 @@ def auto_transfer_once_per_day():
) )
def trading_day_reset_allows_new_open(now): def get_trading_day_reset_open_guard_enabled(conn=None):
if not TRADING_DAY_RESET_OPEN_GUARD_ENABLED: """True=启用整点限制(默认 8:00 前禁止新开仓/登记监控)。"""
owns = conn is None
if owns:
conn = get_db()
try:
row = conn.execute(
"SELECT value FROM app_runtime_settings WHERE key=?",
(RUNTIME_KEY_OPEN_GUARD,),
).fetchone()
if row is not None:
return str(row[0]).lower() in ("1", "true", "yes", "on")
except Exception:
pass
finally:
if owns:
conn.close()
return TRADING_DAY_RESET_OPEN_GUARD_ENABLED
def set_trading_day_reset_open_guard_enabled(enabled: bool, conn=None):
owns = conn is None
if owns:
conn = get_db()
try:
conn.execute(
"INSERT INTO app_runtime_settings(key, value, updated_at) VALUES (?,?,?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
(RUNTIME_KEY_OPEN_GUARD, "1" if enabled else "0", app_now_str()),
)
if owns:
conn.commit()
finally:
if owns:
conn.close()
def trading_day_reset_allows_new_open(now, conn=None):
if not get_trading_day_reset_open_guard_enabled(conn):
return True return True
return now.hour >= TRADING_DAY_RESET_HOUR return now.hour >= TRADING_DAY_RESET_HOUR
@@ -3821,33 +3864,39 @@ def _finalize_fib_key_fill(conn, row):
live_amt = get_live_position_contracts(ex_sym, direction) live_amt = get_live_position_contracts(ex_sym, direction)
amount = float(live_amt or 0) amount = float(live_amt or 0)
if amount <= 0: if amount <= 0:
send_wechat_msg( msg = (
f"# ❌ {symbol} 斐波成交后处理失败\n" f"# ❌ {symbol} 斐波成交后处理失败\n"
f"**账户:{_wechat_account_label()}**\n" f"**账户:{_wechat_account_label()}**\n"
f"- 无法取得持仓/下单数量,未挂 TP/SL\n" f"- 无法取得持仓/下单数量,未挂 TP/SL\n"
) )
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, row, msg, "fib_fill_no_amount")
return return
ok, reason = precheck_risk(conn, symbol, direction) ok, reason = precheck_risk(conn, symbol, direction)
if not ok: if not ok:
send_wechat_msg( msg = (
f"# ❌ {symbol} 斐波成交后风控拒绝\n" f"# ❌ {symbol} 斐波成交后风控拒绝\n"
f"**账户:{_wechat_account_label()}**\n" f"**账户:{_wechat_account_label()}**\n"
f"- 类型:{typ}\n" f"- 类型:{typ}\n"
f"- 原因:{reason}\n" f"- 原因:{reason}\n"
f"- 请手动处理仓位与挂单\n" f"- 请手动处理仓位与挂单\n"
) )
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, row, msg, "fib_risk_rejected")
return return
tpsl_attached = False tpsl_attached = False
try: try:
_okx_place_tp_sl_orders(ex_sym, direction, amount, sl, tp) _okx_place_tp_sl_orders(ex_sym, direction, amount, sl, tp)
tpsl_attached = True tpsl_attached = True
except Exception as e: except Exception as e:
send_wechat_msg( msg = (
f"# ❌ {symbol} 斐波成交后挂 TP/SL 失败\n" f"# ❌ {symbol} 斐波成交后挂 TP/SL 失败\n"
f"**账户:{_wechat_account_label()}**\n" f"**账户:{_wechat_account_label()}**\n"
f"- 错误:{friendly_okx_error(e)}\n" f"- 错误:{friendly_okx_error(e)}\n"
f"- 请手动补挂止盈止损\n" f"- 请手动补挂止盈止损\n"
) )
send_wechat_msg(msg)
_finalize_key_monitor_one_shot(conn, row, msg, "fib_tpsl_failed")
return return
contract_size = get_contract_size(ex_sym) contract_size = get_contract_size(ex_sym)
base_amount = round(float(amount) * contract_size, 8) base_amount = round(float(amount) * contract_size, 8)
@@ -4624,7 +4673,9 @@ def render_main_page(page="trade"):
) )
rate = round(win/total*100,2) if total else 0 rate = round(win/total*100,2) if total else 0
active_count = len(order_list) active_count = len(order_list)
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
open_guard_blocks_now = open_guard_enabled and now.hour < TRADING_DAY_RESET_HOUR
can_trade = trading_day_reset_allows_new_open(now, conn) and active_count < MAX_ACTIVE_POSITIONS
key_gate_rule_text = ( key_gate_rule_text = (
f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 > {KEY_BREAKOUT_AMP_MIN_PCT}%" f"【箱体/收敛】{KLINE_TIMEFRAME} 两根闭合K|突破越过关键位 > {KEY_BREAKOUT_AMP_MIN_PCT}%"
f"确认K收于箱外|量能>前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}" f"确认K收于箱外|量能>前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"
@@ -4661,6 +4712,8 @@ def render_main_page(page="trade"):
btc_leverage=BTC_LEVERAGE, btc_leverage=BTC_LEVERAGE,
alt_leverage=ALT_LEVERAGE, alt_leverage=ALT_LEVERAGE,
reset_hour=TRADING_DAY_RESET_HOUR, reset_hour=TRADING_DAY_RESET_HOUR,
open_guard_enabled=open_guard_enabled,
open_guard_blocks_now=open_guard_blocks_now,
balance_refresh_seconds=BALANCE_REFRESH_SECONDS, balance_refresh_seconds=BALANCE_REFRESH_SECONDS,
auto_transfer_enabled=AUTO_TRANSFER_ENABLED, auto_transfer_enabled=AUTO_TRANSFER_ENABLED,
auto_transfer_amount=AUTO_TRANSFER_AMOUNT, auto_transfer_amount=AUTO_TRANSFER_AMOUNT,
@@ -4744,7 +4797,9 @@ def api_account_snapshot():
current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS) current_capital = round(trading_capital, FUNDS_DECIMALS) if trading_capital is not None else round(local_current_capital, FUNDS_DECIMALS)
recommended_capital = get_recommended_capital(current_capital) recommended_capital = get_recommended_capital(current_capital)
active_count = get_active_position_count(conn) active_count = get_active_position_count(conn)
open_guard_enabled = get_trading_day_reset_open_guard_enabled(conn)
conn.close() conn.close()
open_guard_blocks_now = open_guard_enabled and now.hour < TRADING_DAY_RESET_HOUR
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
available_trading_usdt = get_available_trading_usdt() available_trading_usdt = get_available_trading_usdt()
return jsonify({ return jsonify({
@@ -4755,11 +4810,41 @@ def api_account_snapshot():
"active_count": active_count, "active_count": active_count,
"max_active_positions": MAX_ACTIVE_POSITIONS, "max_active_positions": MAX_ACTIVE_POSITIONS,
"can_trade": can_trade, "can_trade": can_trade,
"open_guard_enabled": open_guard_enabled,
"open_guard_blocks_now": open_guard_blocks_now,
"reset_hour": TRADING_DAY_RESET_HOUR,
"manual_min_planned_rr": MANUAL_MIN_PLANNED_RR, "manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,
"trading_day": trading_day, "trading_day": trading_day,
}) })
@app.route("/api/settings/open_guard", methods=["POST"])
@login_required
def api_settings_open_guard():
data = request.get_json(silent=True) or {}
raw = data.get("enabled")
if raw is None:
raw = request.form.get("enabled")
if raw is None:
return jsonify({"ok": False, "msg": "缺少 enabled 参数"}), 400
enabled = str(raw).lower() in ("1", "true", "yes", "on")
set_trading_day_reset_open_guard_enabled(enabled)
now = app_now()
conn = get_db()
active_count = get_active_position_count(conn)
guard_on = get_trading_day_reset_open_guard_enabled(conn)
conn.close()
can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
return jsonify(
{
"ok": True,
"open_guard_enabled": guard_on,
"can_trade": can_trade,
"reset_hour": TRADING_DAY_RESET_HOUR,
}
)
@app.route("/api/price_snapshot") @app.route("/api/price_snapshot")
@login_required @login_required
def api_price_snapshot(): def api_price_snapshot():
+43 -2
View File
@@ -258,6 +258,15 @@
<div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div></div> <div class="stat-item"><div class="label">当日资金(交易账户)</div><div class="value" id="current-capital">{{ funds_fmt(current_capital) }}U</div></div>
</div> </div>
<div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div> <div class="rule-tip">实时价格更新时间:<span id="price-last-updated">--</span>(北京时间 UTC+8</div>
<div class="rule-tip" id="open-guard-bar" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#cfd3ef">
<input type="checkbox" id="allow-open-before-reset" {% if not open_guard_enabled %}checked{% endif %}>
允许北京时间 {{ reset_hour }}:00 前开仓(斐波成交登记、人工下单)
</label>
<span id="open-guard-status" style="color:#8892b0;font-size:.75rem">
{% if open_guard_enabled %}已限制:{{ reset_hour }}:00 前不可开仓{% else %}已放开:{{ reset_hour }}:00 前允许开仓{% endif %}
</span>
</div>
<div class="grid"> <div class="grid">
{% if page == 'key_monitor' %} {% if page == 'key_monitor' %}
@@ -379,7 +388,7 @@
</div> </div>
<div class="rule-tip" id="order-rule-tip"> <div class="rule-tip" id="order-rule-tip">
规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x 规则:最多 {{ max_active_positions }} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x
{% if can_trade %}可开仓{% else %}不可开仓(持仓已满或未到北京时间 {{ reset_hour }}:00{% endif %} {% if can_trade %}可开仓{% else %}不可开仓{% if active_count >= max_active_positions %}(持仓 {{ active_count }}/{{ max_active_positions }}{% endif %}{% if open_guard_blocks_now %}(未到北京时间 {{ reset_hour }}:00{% endif %}{% endif %}
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1 人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
</div> </div>
<div class="rule-tip"> <div class="rule-tip">
@@ -1745,15 +1754,47 @@ function refreshAccountSnapshot(){
if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) { if (typeof data.available_trading_usdt !== "undefined" && data.available_trading_usdt !== null) {
latestAvailableUsdt = Number(data.available_trading_usdt); latestAvailableUsdt = Number(data.available_trading_usdt);
} }
const canTradeText = data.can_trade ? "可开仓" : `不可开仓(持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}} 或未到北京时间 {{ reset_hour }}:00`; let canTradeText = "可开仓";
if(!data.can_trade){
const parts = [];
if((data.active_count||0) >= (data.max_active_positions||{{ max_active_positions }})) parts.push(`持仓 ${data.active_count}/${data.max_active_positions}`);
if(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
canTradeText = parts.length ? `不可开仓(${parts.join("")}` : "不可开仓";
}
const tip = document.getElementById("order-rule-tip"); const tip = document.getElementById("order-rule-tip");
const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : ""; const avail = (latestAvailableUsdt !== null && !Number.isNaN(latestAvailableUsdt)) ? `;交易账户可用约${latestAvailableUsdt.toFixed(2)}U` : "";
if(tip){ if(tip){
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`; tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${canTradeText}${avail}`;
} }
const allowEl = document.getElementById("allow-open-before-reset");
const guardStatus = document.getElementById("open-guard-status");
const resetH = data.reset_hour != null ? data.reset_hour : {{ reset_hour }};
if(allowEl && typeof data.open_guard_enabled !== "undefined"){
allowEl.checked = !data.open_guard_enabled;
}
if(guardStatus && typeof data.open_guard_enabled !== "undefined"){
guardStatus.innerText = data.open_guard_enabled
? `已限制:${resetH}:00 前不可开仓`
: `已放开:${resetH}:00 前允许开仓`;
}
}).catch(()=>{}); }).catch(()=>{});
} }
const allowOpenBeforeResetEl = document.getElementById("allow-open-before-reset");
if(allowOpenBeforeResetEl){
allowOpenBeforeResetEl.addEventListener("change", function(){
const allow = !!this.checked;
fetch("/api/settings/open_guard", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({enabled: !allow}),
}).then(r=>r.json()).then(data=>{
if(!data.ok){ alert(data.msg || "保存失败"); return; }
refreshAccountSnapshot();
}).catch(()=>alert("保存失败"));
});
}
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");