diff --git a/.env.example b/.env.example index 5681359..c846076 100644 --- a/.env.example +++ b/.env.example @@ -1,338 +1,339 @@ -# ============================================================================= -# 环境配置模板(可提交 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=5010 -# 是否开启调试模式(生产建议 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 -# 允许复盘中控 iframe 内嵌本实例(与 hub 域名一致;默认已开启) -# APP_ALLOW_HUB_EMBED=true -# HUB_EMBED_PARENT_ORIGINS=https://hub.example.com -# HTTPS 且经 iframe 打开时建议 true;不设则 hub-sso 在 HTTPS 下也会自动尝试 SameSite=None -# APP_COOKIE_SECURE=true -# 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=coinox - -# 训练总资金(U) -# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所 -# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启) -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) -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 - -# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单) -LIVE_TRADING_ENABLED=true -# ============================================================================= -# OKX 账户 API(永续+期权共用;仅服务器 .env 手改,前端不展示) -# 新机保持为空;填真钥后 pm2 restart --update-env(含子代理) -# 旧键 OKX_OPTIONS_API_* 已废弃:若 OKX_API_* 为空,启动时会从 OPTIONS 键回填 -# ============================================================================= -OKX_API_KEY= -OKX_API_SECRET= -OKX_API_PASSPHRASE= -# 保证金模式:cross=全仓,isolated=逐仓 -OKX_TD_MODE=cross -# 持仓模式:hedge=双向持仓,net=单向净持仓 -OKX_POS_MODE=hedge -# 仓位查询 instType(OKX) -OKX_POSITION_INST_TYPE=SWAP -# 从 OKX 历史仓位同步已实现盈亏(北京时间起点,空=近 90 天 0 点起) -# EXCHANGE_POSITION_SYNC_FROM_BJ=2026-01-01 -# 单次拉取历史仓位条数上限(OKX 每页最多 100,程序会分页) -# EXCHANGE_POSITION_HISTORY_LIMIT=200 -# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 OKX·测试网) -EXCHANGE_DISPLAY_NAME=CoinOX -# 企业微信推送里展示的账户备注 -# OKX_ACCOUNT_LABEL= -# 顶栏是否显示 USDT 资金/交易账户(热更);false 时总资金仅计期权 USDC 侧 -OKX_SHOW_PERP_FUNDS=true - -# ============================================================================= -# 期权模块(与上方 OKX_API_* 同源;修改启用开关后须重启 PM2) -# 详见 docs/期权方案.md 与 docs/期权用法.md -# ============================================================================= -OKX_OPTIONS_ENABLED=false -# 以下 OKX_OPTIONS_API_* 已废弃,请勿再配置(仅兼容旧部署回填) -# OKX_OPTIONS_API_KEY= -# OKX_OPTIONS_API_SECRET= -# OKX_OPTIONS_API_PASSPHRASE= -OKX_OPTIONS_ACCOUNT_LABEL=账户·期权 -# 单笔期权本位: coin(默认,币本位+USDT买币桥) | usdc(权利金USDC;对冲仍仅USDC) -OKX_OPTIONS_MARGIN_MODE=coin -OKX_OPTIONS_TRADE_BUDGET_USDC=10 -OKX_OPTIONS_BUDGET_BUFFER=0.95 -# 币本位:按交易户USDT×缓冲复利;上限开关默认关(靠人工转走) -OKX_OPTIONS_COIN_COMPOUND=true -OKX_OPTIONS_COIN_BUDGET_USDT=10 -OKX_OPTIONS_COIN_MAX_USDT_ENABLED=false -OKX_OPTIONS_COIN_MAX_USDT=50 -# 现货买入相对权利金缓冲:1.10=多买10%;也可写 0.10。按最大可开张数×权利金×缓冲买币,不全额兑换 -OKX_OPTIONS_COIN_SPOT_BUY_BUFFER=1.10 -# 全仓复利:开启时隐藏单笔预算且不可用打满;关闭后恢复单笔预算 -OKX_OPTIONS_COMPOUND_FULL_ENABLED=true -OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED=false -OKX_OPTIONS_COMPOUND_FULL_CAP_USDC=300 -# 交易模式三选一(热更):options=单独期权 / perp_options=永期对冲 / options_options=期期对冲 -# 选单独期权时隐藏对冲导航与对冲配置;选对冲时不可单独开期权,仓位按「对冲组数上限」 -OKX_TRADE_MODE=perp_options -# 仅单独期权模式:期权同时持仓上限(笔);0=不限制;同合约加仓不占新笔数;热更 -OKX_OPTIONS_MAX_ACTIVE_POSITIONS=0 -OKX_OPTIONS_DEFAULT_UNDERLY=ETH -# 期权链仅显示卖一深度≥1张的合约(估算卖一/无深度不显示);false 则显示全部 -OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED=true -OKX_OPTIONS_MAX_DTE_DAYS=2 -OKX_OPTIONS_CHAIN_MAX_DTE_DAYS=14 -OKX_OPTIONS_ITM_MAX_DIST_USD=30 -OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0 -OKX_OPTIONS_POLL_SECONDS=15 -OKX_OPTIONS_TD_MODE=isolated -OKX_OPTIONS_ALLOW_MARKET_CLOSE=false -# 目标平仓门控(目标触达后自动平才校验;比较口径均为 USDT 估值) -OKX_OPTIONS_CLOSE_GATE_MODE=premium -# 全局倍数可选;留空则按本位用下方 COIN/USDC -OKX_OPTIONS_CLOSE_RECYCLE_MULT= -OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN=1.05 -OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC=2 -OKX_OPTIONS_CLOSE_NET_PNL_MIN_U=0 -OKX_OPTIONS_CLOSE_HOLD_SECONDS=120 -# 对冲买期权等成交超时(秒);超时撤未成交部分,未完全成交则开仓失败 -OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12 - -# ============================================================================= -# 对冲计划(仅 OKX;由 OKX_TRADE_MODE 控制是否启用;详见 docs/对冲计划开发方案.md) -# ============================================================================= -# 以下三项已由 OKX_TRADE_MODE 取代,保留兼容旧部署(未配置 TRADE_MODE 时仍可读) -HEDGE_PLAN_ENABLED=false -HEDGE_PLAN_SHOW_PERP_OPTIONS=true -HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true -HEDGE_PLAN_LIVE_ORDER=false -# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换) -HEDGE_PLAN_OPTION_PRIMARY=true -HEDGE_PLAN_OPEN_ORDER=options_first -HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true -HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false -HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true -# 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平 -HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true -# 期期「做多/做空」拆分口径:budget=按权利金预算(默认);sheets=先算同张数总张数(2n)再按比例拆 -HEDGE_PLAN_OO_BIAS_SPLIT_BY=budget -# 期期「做多/做空」主腿占比(0~1,默认 0.7=7:3);做多主腿=Call,做空主腿=Put -HEDGE_PLAN_OO_BIAS_RATIO=0.7 -# 对冲与单独期权互斥(默认 true):有对冲计划不可单独开期权;有单独期权不可启动对冲;false=可同时开 -HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true -# 半腿失败改手动补开(默认 true):不自动平已成腿,计划挂 partial,页面补开;开启时下方自动平强制无效 -HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL=true -# 对冲组数上限(默认 1;opening/active/partial 计入);仅永期/期期模式生效;热更 -MAX_ACTIVE_HEDGE_PLANS=1 -HEDGE_PLAN_MONITOR_POLL_SECONDS=15 -# 半腿失败自动平期权;若 MANUAL_COMPLETE_ON_PARTIAL=true 则运行时强制无效(建议一并写成 false) -HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION=false - -# ============================================================================= -# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2) -# ============================================================================= -# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价) -# -# POSITION_SIZING_MODE=risk(以损定仓) -# false → 不执行任何关键位自动单;支撑/阻力提醒,人工下单,顺势加仓不受影响 -# true → 允许关键位全套自动(含触价) -# -# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换) -# false → 不执行触价自动单 -# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止 -# -# 顺势加仓,趋势回调不受本开关控制;全仓模式下策略自动仍禁止. -KEY_AUTO_ORDER_ENABLED=false - -# ============================================================================= -# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用) -# ============================================================================= -# 【周期】门控 K 线周期,如 5m,15m;仅影响关键位硬条件,不改变顶栏分区 -KLINE_TIMEFRAME=5m -# OKX 遗留:突破过滤百分比(与 KEY_BREAKOUT_AMP_* 并存,程序仍读取) -KEY_BREAKOUT_LIMIT_PCT=1.5 -# 【确认K】闭合 K 序列中的棒偏移:突破棒默认 -2(倒数第2根),确认棒默认 -1(倒数第1根) -KEY_CONFIRM_BREAKOUT_BAR=-2 -KEY_CONFIRM_BAR=-1 -# 【量能】突破棒成交量 > 前 N 根均量 × 倍数(默认 N=20,倍数=1.3 即放大 30%) -KEY_VOLUME_MA_BARS=20 -KEY_VOLUME_RATIO_MIN=1.3 -# 【箱体/收敛】突破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 -# 【关键位自动开仓盈亏比】按确认K收盘 E 计算,严格大于该值才市价开仓(如 1.5 表示须 >1.5:1) -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 - -# ============================================================================= -# 交易执行 / 人工风控(页面「实盘下单」) -# ============================================================================= -# 【最大同时持仓】active 订单数达到该值后禁止人工与关键位自动再加仓(默认 1=单仓) -MAX_ACTIVE_POSITIONS=1 -# 【人工下单最低盈亏比】按当前价与 SL/TP 计算,低于该值前后端均拒绝(默认 1.4,即须 >=1.4:1) -MANUAL_MIN_PLANNED_RR=1.4 -# 【关键位连开计仓】true=已有持仓时关键位自动单仍按「无仓时」资金快照算保证金基数 -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 -# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用 -RISK_DAILY_LOSS_LIMIT=2 -RISK_MOOD_ISSUES_DAILY_FREEZE=true - -# 资金与仓位刷新周期(秒) -BALANCE_REFRESH_SECONDS=60 -# 前端价格快照轮询(秒) -PRICE_REFRESH_SECONDS=5 -# 后台监控轮询周期(秒) -MONITOR_POLL_SECONDS=3 -# 移动保本同步交易所止盈止损的最小间隔(秒),避免频繁撤挂叠单 -BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC=60 -# 重启后多少秒内不做「外部平仓」同步(避免 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 -# 强制清仓执行窗口(分钟,默认 5;该窗口内禁止开仓) -FORCE_CLOSE_GRACE_MINUTES=5 - -# 推送与AI超时(秒) -WECHAT_TIMEOUT_SECONDS=10 -AI_TIMEOUT_SECONDS=120 - -# AI 复盘服务地址(本机 Ollama 默认地址) -AI_PROVIDER=openai -OPENAI_API_BASE=https://op.bz121.com/v1 -OPENAI_API_KEY=你的密钥 -OPENAI_MODEL=gemma4:e4b -OLLAMA_API=http://127.0.0.1:11434/api/generate -AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest - -# OKX 代理(可选,仅本地开发网络受限时用;云服务器部署请留空,直连 OKX 即可) -# 1) 先在本机建立隧道(示例): -# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes -# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名): -# OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080 -# -# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用: -# OKX_HTTP_PROXY=http://127.0.0.1:3128 -# OKX_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 - -# --- CoinOX 模拟资金 --- -# sim=本地模拟资金撮合; live=实盘账户 -SIM_DEFAULT_MODE=sim -SIM_INITIAL_EQUITY_USDT=10000 -SIM_INITIAL_USDC=0 -SIM_FEE_RATE=0.0005 +# ============================================================================= +# 环境配置模板(可提交 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=5010 +# 是否开启调试模式(生产建议 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 +# 允许复盘中控 iframe 内嵌本实例(与 hub 域名一致;默认已开启) +# APP_ALLOW_HUB_EMBED=true +# HUB_EMBED_PARENT_ORIGINS=https://hub.example.com +# HTTPS 且经 iframe 打开时建议 true;不设则 hub-sso 在 HTTPS 下也会自动尝试 SameSite=None +# APP_COOKIE_SECURE=true +# 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=coinox + +# 训练总资金(U) +# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所 +# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启) +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) +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 + +# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单) +LIVE_TRADING_ENABLED=false +# ============================================================================= +# OKX 账户 API(永续+期权共用;仅服务器 .env 手改,前端不展示) +# 新机保持为空;填真钥后 pm2 restart --update-env(含子代理) +# 旧键 OKX_OPTIONS_API_* 已废弃:若 OKX_API_* 为空,启动时会从 OPTIONS 键回填 +# ============================================================================= +OKX_API_KEY= +OKX_API_SECRET= +OKX_API_PASSPHRASE= +# 保证金模式:cross=全仓,isolated=逐仓 +OKX_TD_MODE=cross +# 持仓模式:hedge=双向持仓,net=单向净持仓 +OKX_POS_MODE=hedge +# 仓位查询 instType(OKX) +OKX_POSITION_INST_TYPE=SWAP +# 从 OKX 历史仓位同步已实现盈亏(北京时间起点,空=近 90 天 0 点起) +# EXCHANGE_POSITION_SYNC_FROM_BJ=2026-01-01 +# 单次拉取历史仓位条数上限(OKX 每页最多 100,程序会分页) +# EXCHANGE_POSITION_HISTORY_LIMIT=200 +# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 OKX·测试网) +EXCHANGE_DISPLAY_NAME=CoinOX +# 企业微信推送里展示的账户备注 +# OKX_ACCOUNT_LABEL= +# 顶栏是否显示 USDT 资金/交易账户(热更);false 时总资金仅计期权 USDC 侧 +OKX_SHOW_PERP_FUNDS=true + +# ============================================================================= +# 期权模块(与上方 OKX_API_* 同源;修改启用开关后须重启 PM2) +# 详见 docs/期权方案.md 与 docs/期权用法.md +# ============================================================================= +OKX_OPTIONS_ENABLED=false +# 以下 OKX_OPTIONS_API_* 已废弃,请勿再配置(仅兼容旧部署回填) +# OKX_OPTIONS_API_KEY= +# OKX_OPTIONS_API_SECRET= +# OKX_OPTIONS_API_PASSPHRASE= +OKX_OPTIONS_ACCOUNT_LABEL=账户·期权 +# 单笔期权本位: coin(默认,币本位+USDT买币桥) | usdc(权利金USDC;对冲仍仅USDC) +OKX_OPTIONS_MARGIN_MODE=coin +OKX_OPTIONS_TRADE_BUDGET_USDC=10 +OKX_OPTIONS_BUDGET_BUFFER=0.95 +# 币本位:按交易户USDT×缓冲复利;上限开关默认关(靠人工转走) +OKX_OPTIONS_COIN_COMPOUND=true +OKX_OPTIONS_COIN_BUDGET_USDT=10 +OKX_OPTIONS_COIN_MAX_USDT_ENABLED=false +OKX_OPTIONS_COIN_MAX_USDT=50 +# 现货买入相对权利金缓冲:1.10=多买10%;也可写 0.10。按最大可开张数×权利金×缓冲买币,不全额兑换 +OKX_OPTIONS_COIN_SPOT_BUY_BUFFER=1.10 +# 全仓复利:开启时隐藏单笔预算且不可用打满;关闭后恢复单笔预算 +OKX_OPTIONS_COMPOUND_FULL_ENABLED=true +OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED=false +OKX_OPTIONS_COMPOUND_FULL_CAP_USDC=300 +# 交易模式三选一(热更):options=单独期权 / perp_options=永期对冲 / options_options=期期对冲 +# 选单独期权时隐藏对冲导航与对冲配置;选对冲时不可单独开期权,仓位按「对冲组数上限」 +OKX_TRADE_MODE=perp_options +# 仅单独期权模式:期权同时持仓上限(笔);0=不限制;同合约加仓不占新笔数;热更 +OKX_OPTIONS_MAX_ACTIVE_POSITIONS=0 +OKX_OPTIONS_DEFAULT_UNDERLY=ETH +# 期权链仅显示卖一深度≥1张的合约(估算卖一/无深度不显示);false 则显示全部 +OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED=true +OKX_OPTIONS_MAX_DTE_DAYS=2 +OKX_OPTIONS_CHAIN_MAX_DTE_DAYS=14 +OKX_OPTIONS_ITM_MAX_DIST_USD=30 +OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0 +OKX_OPTIONS_POLL_SECONDS=15 +OKX_OPTIONS_TD_MODE=isolated +OKX_OPTIONS_ALLOW_MARKET_CLOSE=false +# 目标平仓门控(目标触达后自动平才校验;比较口径均为 USDT 估值) +OKX_OPTIONS_CLOSE_GATE_MODE=premium +# 全局倍数可选;留空则按本位用下方 COIN/USDC +OKX_OPTIONS_CLOSE_RECYCLE_MULT= +OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN=1.05 +OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC=2 +OKX_OPTIONS_CLOSE_NET_PNL_MIN_U=0 +OKX_OPTIONS_CLOSE_HOLD_SECONDS=120 +# 对冲买期权等成交超时(秒);超时撤未成交部分,未完全成交则开仓失败 +OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC=12 + +# ============================================================================= +# 对冲计划(仅 OKX;由 OKX_TRADE_MODE 控制是否启用;详见 docs/对冲计划开发方案.md) +# ============================================================================= +# 以下三项已由 OKX_TRADE_MODE 取代,保留兼容旧部署(未配置 TRADE_MODE 时仍可读) +HEDGE_PLAN_ENABLED=false +HEDGE_PLAN_SHOW_PERP_OPTIONS=true +HEDGE_PLAN_SHOW_OPTIONS_OPTIONS=true +HEDGE_PLAN_LIVE_ORDER=false +# 永期子模式:true=以期权为主;false=保险模式(页面标题前标识,不可页内切换) +HEDGE_PLAN_OPTION_PRIMARY=true +HEDGE_PLAN_OPEN_ORDER=options_first +HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true +HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false +HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true +# 方案C:期期页面显示「平仓模式」(到期平/全平);关则固定到期平.默认开启,页面默认选全平 +HEDGE_PLAN_OO_CLOSE_MODE_ENABLED=true +# 期期「做多/做空」拆分口径:budget=按权利金预算(默认);sheets=先算同张数总张数(2n)再按比例拆 +HEDGE_PLAN_OO_BIAS_SPLIT_BY=budget +# 期期「做多/做空」主腿占比(0~1,默认 0.7=7:3);做多主腿=Call,做空主腿=Put +HEDGE_PLAN_OO_BIAS_RATIO=0.7 +# 对冲与单独期权互斥(默认 true):有对冲计划不可单独开期权;有单独期权不可启动对冲;false=可同时开 +HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE=true +# 半腿失败改手动补开(默认 true):不自动平已成腿,计划挂 partial,页面补开;开启时下方自动平强制无效 +HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL=true +# 对冲组数上限(默认 1;opening/active/partial 计入);仅永期/期期模式生效;热更 +MAX_ACTIVE_HEDGE_PLANS=1 +HEDGE_PLAN_MONITOR_POLL_SECONDS=15 +# 半腿失败自动平期权;若 MANUAL_COMPLETE_ON_PARTIAL=true 则运行时强制无效(建议一并写成 false) +HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION=false + +# ============================================================================= +# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2) +# ============================================================================= +# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价) +# +# POSITION_SIZING_MODE=risk(以损定仓) +# false → 不执行任何关键位自动单;支撑/阻力提醒,人工下单,顺势加仓不受影响 +# true → 允许关键位全套自动(含触价) +# +# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换) +# false → 不执行触价自动单 +# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止 +# +# 顺势加仓,趋势回调不受本开关控制;全仓模式下策略自动仍禁止. +KEY_AUTO_ORDER_ENABLED=false + +# ============================================================================= +# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用) +# ============================================================================= +# 【周期】门控 K 线周期,如 5m,15m;仅影响关键位硬条件,不改变顶栏分区 +KLINE_TIMEFRAME=5m +# OKX 遗留:突破过滤百分比(与 KEY_BREAKOUT_AMP_* 并存,程序仍读取) +KEY_BREAKOUT_LIMIT_PCT=1.5 +# 【确认K】闭合 K 序列中的棒偏移:突破棒默认 -2(倒数第2根),确认棒默认 -1(倒数第1根) +KEY_CONFIRM_BREAKOUT_BAR=-2 +KEY_CONFIRM_BAR=-1 +# 【量能】突破棒成交量 > 前 N 根均量 × 倍数(默认 N=20,倍数=1.3 即放大 30%) +KEY_VOLUME_MA_BARS=20 +KEY_VOLUME_RATIO_MIN=1.3 +# 【箱体/收敛】突破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 +# 【关键位自动开仓盈亏比】按确认K收盘 E 计算,严格大于该值才市价开仓(如 1.5 表示须 >1.5:1) +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 + +# ============================================================================= +# 交易执行 / 人工风控(页面「实盘下单」) +# ============================================================================= +# 【最大同时持仓】active 订单数达到该值后禁止人工与关键位自动再加仓(默认 1=单仓) +MAX_ACTIVE_POSITIONS=1 +# 【人工下单最低盈亏比】按当前价与 SL/TP 计算,低于该值前后端均拒绝(默认 1.4,即须 >=1.4:1) +MANUAL_MIN_PLANNED_RR=1.4 +# 【关键位连开计仓】true=已有持仓时关键位自动单仍按「无仓时」资金快照算保证金基数 +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 +# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用 +RISK_DAILY_LOSS_LIMIT=2 +RISK_MOOD_ISSUES_DAILY_FREEZE=true + +# 资金与仓位刷新周期(秒) +BALANCE_REFRESH_SECONDS=60 +# 前端价格快照轮询(秒) +PRICE_REFRESH_SECONDS=5 +# 后台监控轮询周期(秒) +MONITOR_POLL_SECONDS=3 +# 移动保本同步交易所止盈止损的最小间隔(秒),避免频繁撤挂叠单 +BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC=60 +# 重启后多少秒内不做「外部平仓」同步(避免 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 +# 强制清仓执行窗口(分钟,默认 5;该窗口内禁止开仓) +FORCE_CLOSE_GRACE_MINUTES=5 + +# 推送与AI超时(秒) +WECHAT_TIMEOUT_SECONDS=10 +AI_TIMEOUT_SECONDS=120 + +# AI 复盘服务地址(本机 Ollama 默认地址) +AI_PROVIDER=openai +OPENAI_API_BASE=https://op.bz121.com/v1 +OPENAI_API_KEY=你的密钥 +OPENAI_MODEL=gemma4:e4b +OLLAMA_API=http://127.0.0.1:11434/api/generate +AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest + +# OKX 代理(可选,仅本地开发网络受限时用;云服务器部署请留空,直连 OKX 即可) +# 1) 先在本机建立隧道(示例): +# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes +# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名): +# OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080 +# +# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用: +# OKX_HTTP_PROXY=http://127.0.0.1:3128 +# OKX_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 + +# --- CoinOX 模拟资金 --- +# sim=本地模拟资金撮合; live=实盘账户 +# 默认模拟盘(本地钱包); 切实盘请改为 live 并配置 API +SIM_DEFAULT_MODE=sim +SIM_INITIAL_EQUITY_USDT=10000 +SIM_INITIAL_USDC=0 +SIM_FEE_RATE=0.0005 diff --git a/README.md b/README.md index 2f7b2ea..0aecf8e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ OKX **币本位期权** + **U 本位永续对冲** 独立实例。 | 单独期权 | 仅币本位(USDT→买币→开期权) | | 永期对冲 | 币本位期权腿 + USDT 永续腿 | | 期期对冲 | 双腿币本位期权 | -| 模拟资金 | 默认 `sim`,本地钱包撮合 | +| 模拟资金 | **默认 `sim`**,本地钱包撮合 | | 不提供 | 说明 | |--------|------| @@ -19,29 +19,48 @@ OKX **币本位期权** + **U 本位永续对冲** 独立实例。 | 关键位自动开仓 | `/key_monitor` 已重定向 | | 策略趋势/滚仓 | 未安装 | | USDC 期权 | 已固定币本位 | +| Windows 部署 | **仅 Ubuntu 一键部署** | 源仓 `crypto_monitor` **只读复制**,不回改源仓。中控二期再做。 -## 快速开始 +## Ubuntu 一键部署(推荐) + +服务器 **root** 执行: ```bash -cd /path/to/CoinOX # 或 E:\仓库\okx对冲 -python -m venv .venv -# Windows: .venv\Scripts\activate +curl -fsSL https://git.bz121.com/dekun/CoinOX/raw/branch/main/deploy/manage.sh | bash +``` + +交互菜单:部署 / 卸载 / 更新 / 仅检测。 +安装到 `/opt/coinox`,PM2 进程 `coinox`,默认端口 `5010`,**默认模拟盘**。 + +详情见 [部署文档.md](./部署文档.md) 与 [deploy/README.md](./deploy/README.md)。 + +已安装后管理: + +```bash +bash /opt/coinox/deploy/manage.sh +``` + +## 本机开发(Ubuntu) + +```bash +cd /opt/coinox # 或你的克隆目录 +python3 -m venv .venv +source .venv/bin/activate pip install -r requirements.txt -copy .env.example .env # 编辑密钥;默认 SIM_DEFAULT_MODE=sim +cp -n .env.example .env python app.py ``` -默认端口见 `.env` 的 `APP_PORT`(模板为 `5010`,避免与旧 OKX 实例冲突)。 - ## 环境要点 +- `SIM_DEFAULT_MODE=sim`(默认模拟盘) - `OKX_OPTIONS_MARGIN_MODE=coin`(固定) - `OKX_TRADE_MODE=options|perp_options|options_options` -- `SIM_DEFAULT_MODE=sim` / `live` - `SIM_INITIAL_EQUITY_USDT=10000` +- `APP_PORT=5010` ## 风险 -实盘须自行确认 API 权限与资金;默认模拟盘,切 `live` 前请核对。 +实盘风险自负。默认模拟盘;切 `live` 前请核对 API 权限与资金。 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..c6b0a5a --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,101 @@ +# CoinOX 环境一键部署(Ubuntu / root /opt) + +在 **`/opt/coinox`** 下以 **root** 部署CoinOX 币本位期权 + U 本位对冲,使用 **PM2** 常驻. + +仓库: [https://git.bz121.com/dekun/CoinOX.git](https://git.bz121.com/dekun/CoinOX.git) + +完整步骤见根目录 **[部署文档.md](../部署文档.md)**. + +--- + +## 一键部署管理器(推荐) + +新服务器**无需先 clone**,一条命令进入菜单: + +```bash +curl -fsSL https://git.bz121.com/dekun/CoinOX/raw/branch/main/deploy/manage.sh | bash +``` + +已安装机器: + +```bash +bash /opt/coinox/deploy/manage.sh +``` + +### 菜单 + +| 选项 | 功能 | +|------|------| +| **1) 一键部署** | 环境识别 + 系统依赖 + Node/PM2 + venv + 密钥 + 启动 `coinox` | +| **2) 一键卸载** | 备份 `.env` → 停 PM2 → 移走目录 | +| **3) 更新** | 快速更新 / 依赖更新 | +| **4) 仅检测** | 识别 OS / Python / Node / PM2 / 项目状态(不安装) | +| **0) 退出** | | + +### 部署完成后 + +- 访问: `http://<服务器IP>:5010` (端口以 `.env` 的 `APP_PORT` 为准) +- 默认入口: `/hedge-plan`(对冲)/ `/options`(期权);默认模拟盘 sim +- 登录账号: **admin**(密码见 `.env` 的 `APP_PASSWORD`,首次脚本可写成 `admin123`) + +--- + +## 脚本结构 + +``` +deploy/ +├── manage.sh # 入口(自举 + 菜单) +├── lib/ +│ ├── common.sh # 公共函数 / 环境识别 / 验收 +│ ├── install.sh # 一键部署 +│ ├── uninstall.sh # 一键卸载 +│ ├── update.sh # 更新子菜单 +│ └── doctor.sh # 仅检测 +├── setup_env.sh # venv + pip(被 install 调用) +└── pull_and_restart.sh # 快速更新 +``` + +--- + +## 前置条件 + +- **Ubuntu 22.04 / 24.04**,用户 **root** +- 能 `git clone` [coinox](https://git.bz121.com/dekun/CoinOX.git) 到 `/opt/coinox` + +--- + +## 分步安装(仍可用) + +```bash +cd /opt +git clone https://git.bz121.com/dekun/CoinOX.git coinox +cd /opt/coinox +bash deploy/setup_env.sh --install-system-deps +pm2 start ecosystem.config.cjs +pm2 save && pm2 startup +``` + +`setup_env.sh` 常用参数: + +```bash +bash deploy/setup_env.sh --recreate-venv # 重建虚拟环境 +bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2 +bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example +``` + +若在 Windows 编辑过脚本后报 `pipefail` 错误,先转 LF: + +```bash +sed -i 's/\r$//' deploy/manage.sh deploy/lib/*.sh deploy/*.sh +``` + +--- + +## 环境变量(可选) + +```bash +INSTALL_ROOT=/opt/coinox +GIT_URL=https://git.bz121.com/dekun/CoinOX.git +GIT_BRANCH=main +BACKUP_ROOT=/root/backups +``` diff --git a/deploy/lib/common.sh b/deploy/lib/common.sh new file mode 100644 index 0000000..57f45cf --- /dev/null +++ b/deploy/lib/common.sh @@ -0,0 +1,513 @@ +#!/usr/bin/env bash +# deploy/lib/common.sh — CoinOX 部署公共函数 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +INSTALL_ROOT="${INSTALL_ROOT:-/opt/coinox}" +GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/CoinOX.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" +BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}" +TZ_NAME="${CM_TZ:-Asia/Shanghai}" +NODE_MAJOR="${NODE_MAJOR:-20}" +APP_PORT="${APP_PORT:-5010}" +PM2_APP_NAME="${PM2_APP_NAME:-coinox}" + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_DIR="$(cd "${LIB_DIR}/.." && pwd)" +REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" + +PM2_APPS=( + coinox +) + +# 历史进程名(仅 stop/delete 时兼容) +PM2_APPS_LEGACY=( + crypto-monitor-okx + crypto-monitor +) + +CONFIG_PATHS=( + .env +) + +log() { printf '[%s] %s\n' "$(TZ="${TZ_NAME}" date '+%Y-%m-%d %H:%M:%S')" "$*"; } +step() { echo ""; log "==> $*"; } + +die() { + echo "错误: $*" >&2 + exit 1 +} + +require_root() { + if [[ "$(id -u)" -ne 0 ]]; then + die "请使用 root 执行(推荐: sudo -i 后运行)" + fi +} + +require_ubuntu() { + if [[ ! -f /etc/os-release ]]; then + log "警告: 未检测到 /etc/os-release,跳过 Ubuntu 版本检查" + return 0 + fi + # shellcheck source=/dev/null + source /etc/os-release + if [[ "${ID:-}" != "ubuntu" ]]; then + log "警告: 当前系统为 ${ID:-unknown},官方仅测试 Ubuntu 22.04/24.04" + return 0 + fi + local ver="${VERSION_ID:-}" + if [[ "${ver}" != "22.04" && "${ver}" != "24.04" ]]; then + log "警告: Ubuntu ${ver} 未在文档中明确测试,继续执行" + fi +} + +detect_server_ip() { + local ip="" + if command -v hostname >/dev/null 2>&1; then + ip="$(hostname -I 2>/dev/null | awk '{print $1}')" + fi + if [[ -z "${ip}" ]]; then + ip="127.0.0.1" + fi + echo "${ip}" +} + +confirm_yes() { + local msg="$1" + local ans="" + cm_read ans "${msg} [y/N] " + [[ "${ans}" == [yY] || "${ans}" == [yY][eE][sS] ]] +} + +confirm_uninstall() { + local ans="" + echo "此操作将停止 CoinOX PM2 进程并移走安装目录." + cm_read ans "输入 UNINSTALL 确认卸载: " + [[ "${ans}" == "UNINSTALL" ]] +} + +# curl | bash 时 stdin 是管道,须从 /dev/tty 读取用户输入 +cm_read() { + local __var="$1" + local __prompt="$2" + local __val="" + if [[ -r /dev/tty ]]; then + IFS= read -r -p "${__prompt}" __val /dev/null 2>&1; then + die "未检测到 apt-get,请手动安装 python3-venv git curl" + fi + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y python3 python3-pip python3-venv curl git ca-certificates + local pyver="" + if command -v python3 >/dev/null 2>&1; then + pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv + fi +} + +install_node_pm2() { + step "检查 Node.js 与 PM2" + if command -v pm2 >/dev/null 2>&1; then + log "PM2 已安装: $(pm2 -v)" + return 0 + fi + if ! command -v node >/dev/null 2>&1; then + log "安装 Node.js ${NODE_MAJOR}.x ..." + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - + apt-get install -y nodejs + fi + log "安装 PM2 ..." + npm install -g pm2 + log "PM2: $(pm2 -v)" +} + +pip_progress_bar_arg() { + echo "on" +} + +pip_upgrade_tools() { + local pip_bin="$1" + local bar + bar="$(pip_progress_bar_arg)" + echo " 升级 pip ..." + "${pip_bin}" install -U pip setuptools wheel \ + --disable-pip-version-check \ + --progress-bar "${bar}" +} + +pip_install_requirements() { + local pip_bin="$1" + local req_file="$2" + local label="${3:-依赖}" + local bar + bar="$(pip_progress_bar_arg)" + echo " 安装${label} (下方为 pip 进度) ..." + [[ -f "${req_file}" ]] || die "缺少依赖文件: ${req_file}" + "${pip_bin}" install -r "${req_file}" \ + --disable-pip-version-check \ + --retries 5 \ + --progress-bar "${bar}" + echo " ${label}安装完成" +} + +install_backup_cron() { + step "安装每日备份 cron" + local inst="${REPO_ROOT}/scripts/install_backup_cron.sh" + if [[ -f "${inst}" ]]; then + chmod +x "${inst}" + bash "${inst}" || log "警告: cron 安装失败" + else + log "未找到 scripts/install_backup_cron.sh,跳过" + fi +} + +remove_backup_cron() { + step "移除备份 cron" + local tmp removed=0 + tmp="$(mktemp)" + if ! crontab -l 2>/dev/null >"${tmp}"; then + rm -f "${tmp}" + return 0 + fi + local filtered + filtered="$(grep -vF "backup_data.sh" "${tmp}" || true)" + if [[ "${filtered}" != "$(cat "${tmp}")" ]]; then + printf '%s\n' "${filtered}" | awk ' + BEGIN { tz = 0 } + /^CRON_TZ=Asia\/Shanghai$/ { + if (tz++) next + } + { print } + ' | crontab - + removed=1 + fi + rm -f "${tmp}" + if [[ "${removed}" -eq 1 ]]; then + log "已移除 backup_data.sh 相关 cron" + fi +} + +pm2_app_exists() { + local name="$1" + # pm2 pid 在部分版本对不存在的进程仍返回 0;改用 jlist 精确匹配 name + if ! command -v pm2 >/dev/null 2>&1; then + return 1 + fi + if ! command -v python3 >/dev/null 2>&1; then + pm2 describe "${name}" 2>/dev/null | grep -q "status" + return $? + fi + pm2 jlist 2>/dev/null | python3 -c ' +import json,sys +name=sys.argv[1] +try: + apps=json.load(sys.stdin) +except Exception: + sys.exit(1) +sys.exit(0 if any(isinstance(a,dict) and a.get("name")==name for a in (apps or [])) else 1) +' "${name}" +} + +pm2_stop_project_apps() { + if ! command -v pm2 >/dev/null 2>&1; then + log "未安装 pm2,跳过" + return 0 + fi + local name stopped=0 + for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do + if pm2_app_exists "${name}"; then + log "pm2 stop ${name}" + pm2 stop "${name}" 2>/dev/null || true + stopped=$((stopped + 1)) + fi + done + if [[ "${stopped}" -eq 0 ]]; then + log "未发现本项目 PM2 进程(其它 PM2 不受影响)" + fi +} + +pm2_delete_project_apps() { + if ! command -v pm2 >/dev/null 2>&1; then + return 0 + fi + local name deleted=0 + for name in "${PM2_APPS[@]}" "${PM2_APPS_LEGACY[@]}"; do + if pm2_app_exists "${name}"; then + log "pm2 delete ${name}" + pm2 delete "${name}" 2>/dev/null || true + deleted=$((deleted + 1)) + fi + done + if [[ "${deleted}" -gt 0 ]]; then + pm2 save 2>/dev/null || true + fi +} + +pm2_start_app() { + step "启动 PM2: ${PM2_APP_NAME}" + cd "${REPO_ROOT}" + [[ -f "${REPO_ROOT}/ecosystem.config.cjs" ]] || die "缺少 ecosystem.config.cjs" + [[ -x "${REPO_ROOT}/.venv/bin/python" ]] || die "缺少 .venv,请先运行 setup_env.sh" + if pm2_app_exists "${PM2_APP_NAME}"; then + log "已存在,执行 restart --update-env" + if ! pm2 restart "${PM2_APP_NAME}" --update-env; then + log "restart 失败,改为 start ecosystem.config.cjs" + pm2 start ecosystem.config.cjs + fi + else + log "首次启动: pm2 start ecosystem.config.cjs" + pm2 start ecosystem.config.cjs + fi +} + +pm2_save_startup() { + step "PM2 save & startup" + pm2 save 2>/dev/null || true + if pm2 startup systemd -u root --hp /root 2>/dev/null | grep -q "sudo"; then + pm2 startup systemd -u root --hp /root 2>/dev/null | grep "^sudo" | bash || true + else + pm2 startup 2>/dev/null || true + fi +} + +is_deployed() { + local root="$1" + [[ -x "${root}/.venv/bin/python" ]] +} + +read_app_port() { + local env_file="${REPO_ROOT}/.env" + local port="${APP_PORT}" + if [[ -f "${env_file}" ]]; then + local line + line="$(grep -E '^[[:space:]]*APP_PORT=' "${env_file}" | tail -n1 || true)" + if [[ -n "${line}" ]]; then + port="${line#*=}" + port="$(echo "${port}" | tr -d '"' | tr -d "'" | xargs)" + fi + fi + echo "${port:-5010}" +} + +check_http() { + local url="$1" + local code + code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 8 "${url}" 2>/dev/null || echo "000")" + [[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" || "${code}" == "401" || "${code}" == "403" ]] +} + +verify_deployment() { + local ip="${1:-$(detect_server_ip)}" + local port + local ok=1 + local attempt max_attempts=20 + port="$(read_app_port)" + + step "部署验收" + if command -v pm2 >/dev/null 2>&1 && pm2_app_exists "${PM2_APP_NAME}"; then + echo " [✓] PM2 ${PM2_APP_NAME} 已注册" + else + echo " [✗] PM2 缺少进程 ${PM2_APP_NAME}" + ok=0 + pm2 list 2>/dev/null || true + fi + + # Flask 冷启动常需数秒;立刻 curl 易误报 + echo " ... 等待页面就绪 (最多约 ${max_attempts}s,端口 ${port})" + local http_ok=0 + for attempt in $(seq 1 "${max_attempts}"); do + if check_http "http://127.0.0.1:${port}/"; then + http_ok=1 + break + fi + # 进程已挂掉则提前失败 + if command -v pm2 >/dev/null 2>&1 && ! pm2_app_exists "${PM2_APP_NAME}"; then + echo " [✗] 等待期间 PM2 进程消失" + ok=0 + break + fi + local status="" + status="$(pm2 jlist 2>/dev/null | python3 -c ' +import json,sys +name="'"${PM2_APP_NAME}"'" +try: + apps=json.load(sys.stdin) +except Exception: + print(""); sys.exit(0) +for a in apps or []: + if isinstance(a,dict) and a.get("name")==name: + print((a.get("pm2_env") or {}).get("status") or "") + break +' 2>/dev/null || true)" + if [[ "${status}" == "errored" || "${status}" == "stopped" ]]; then + echo " [✗] PM2 状态为 ${status}" + ok=0 + break + fi + sleep 1 + done + + if [[ "${http_ok}" -eq 1 ]]; then + echo " [✓] 页面可访问 (http://127.0.0.1:${port}/)" + else + echo " [✗] 页面不可访问 (http://127.0.0.1:${port}/)" + ok=0 + echo " 排查: pm2 logs ${PM2_APP_NAME} --lines 50" + echo " 确认端口: grep APP_PORT ${REPO_ROOT}/.env" + fi + + if [[ "${ok}" -eq 1 ]]; then + echo "" + log "验收通过: 进程在线 + 页面可打开" + return 0 + fi + echo "" + log "验收未完全通过,可执行: pm2 logs ${PM2_APP_NAME} --lines 30" + return 1 +} + +print_post_install_guide() { + local ip="${1:-$(detect_server_ip)}" + local port + port="$(read_app_port)" + cat </dev/null || echo '?')" + echo " IP: $(detect_server_ip)" + echo " 安装根: ${INSTALL_ROOT}" + echo " 仓库: ${GIT_URL} (${GIT_BRANCH})" + + step "依赖检测" + local item cmd + for item in "python3:python3" "git:git" "curl:curl" "node:node" "npm:npm" "pm2:pm2"; do + cmd="${item#*:}" + if command -v "${cmd}" >/dev/null 2>&1; then + case "${cmd}" in + python3) echo " [✓] python3: $(python3 --version 2>&1)" ;; + node) echo " [✓] node: $(node -v 2>&1)" ;; + npm) echo " [✓] npm: $(npm -v 2>&1)" ;; + pm2) echo " [✓] pm2: $(pm2 -v 2>&1)" ;; + *) echo " [✓] ${cmd}: $(command -v "${cmd}")" ;; + esac + else + echo " [✗] ${cmd}: 未安装" + fi + done + + if command -v python3 >/dev/null 2>&1; then + local ver major minor + ver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + major="${ver%%.*}" + minor="${ver#*.}" + if [[ "${major}" -gt 3 ]] || [[ "${major}" -eq 3 && "${minor}" -ge 10 ]]; then + echo " [✓] Python 版本满足 3.10+ (${ver})" + else + echo " [✗] Python 需要 3.10+,当前 ${ver}" + fi + fi + + step "项目状态" + local root="" + if root="$(resolve_repo_root 2>/dev/null)"; then + echo " [✓] 仓库就绪: ${root}" + [[ -f "${root}/requirements.txt" ]] && echo " [✓] requirements.txt" || echo " [✗] requirements.txt" + [[ -f "${root}/ecosystem.config.cjs" ]] && echo " [✓] ecosystem.config.cjs" || echo " [✗] ecosystem.config.cjs" + [[ -f "${root}/.env" ]] && echo " [✓] .env 已存在" || echo " [!] .env 尚未生成" + if [[ -x "${root}/.venv/bin/python" ]]; then + echo " [✓] .venv: $("${root}/.venv/bin/python" --version 2>&1)" + else + echo " [!] .venv 未创建" + fi + else + echo " [!] 尚未安装到 ${INSTALL_ROOT}" + fi + + if command -v pm2 >/dev/null 2>&1; then + step "PM2" + pm2 list 2>/dev/null | head -n 40 || true + fi +} diff --git a/deploy/lib/doctor.sh b/deploy/lib/doctor.sh new file mode 100644 index 0000000..139b3b0 --- /dev/null +++ b/deploy/lib/doctor.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# deploy/lib/doctor.sh — 仅检测环境/依赖,不安装 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_doctor() { + if root="$(resolve_repo_root 2>/dev/null)"; then + REPO_ROOT="${root}" + fi + doctor_report + echo "" + log "检测完成(未修改系统).需要安装请返回菜单选 1." +} + +main_doctor "$@" diff --git a/deploy/lib/install.sh b/deploy/lib/install.sh new file mode 100644 index 0000000..1c0cdb7 --- /dev/null +++ b/deploy/lib/install.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# deploy/lib/install.sh — CoinOX 一键部署 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +install_fresh() { + step "克隆仓库" + if [[ -d "${INSTALL_ROOT}" ]]; then + die "目录已存在: ${INSTALL_ROOT},请选修复环境" + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" +} + +install_repair() { + step "修复环境(保留数据与配置)" + bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps --recreate-venv +} + +run_install_pipeline() { + step "环境部署 setup_env.sh" + bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps + + pm2_start_app + pm2_save_startup + install_backup_cron + + verify_deployment "$(detect_server_ip)" || true + print_post_install_guide "$(detect_server_ip)" +} + +handle_existing_install() { + echo "" + echo "检测到已部署安装: ${INSTALL_ROOT}" + echo " a) 取消" + echo " b) 修复环境(重建 venv,保留 .env 与数据库)" + echo " c) 重新启动 PM2(不改依赖)" + local choice="" + cm_read choice "请选择 [a/b/c]: " + case "${choice}" in + b|B) + install_repair + if command -v pm2 >/dev/null 2>&1; then + pm2_start_app + pm2_save_startup + fi + verify_deployment "$(detect_server_ip)" || true + print_post_install_guide "$(detect_server_ip)" + ;; + c|C) + pm2_start_app + pm2_save_startup + verify_deployment "$(detect_server_ip)" || true + print_post_install_guide "$(detect_server_ip)" + ;; + *) + log "已取消" + ;; + esac +} + +ensure_repo_ready() { + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + elif [[ -n "${REPO_ROOT:-}" ]] && repo_ready "${REPO_ROOT}"; then + : + else + REPO_ROOT="" + fi + + if [[ -z "${REPO_ROOT}" ]]; then + install_system_packages + install_node_pm2 + install_fresh + REPO_ROOT="${INSTALL_ROOT}" + else + install_system_packages + install_node_pm2 + fi +} + +main_install() { + require_root + require_ubuntu + + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + elif repo_ready "${REPO_ROOT}"; then + : + else + REPO_ROOT="" + fi + + if [[ -n "${REPO_ROOT}" ]] && is_deployed "${REPO_ROOT}"; then + handle_existing_install + return 0 + fi + + ensure_repo_ready + run_install_pipeline +} + +main_install "$@" diff --git a/deploy/lib/uninstall.sh b/deploy/lib/uninstall.sh new file mode 100644 index 0000000..02d79c8 --- /dev/null +++ b/deploy/lib/uninstall.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# deploy/lib/uninstall.sh — CoinOX 一键卸载 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +main_uninstall() { + require_root + + local root="" + if ! root="$(resolve_repo_root)"; then + if [[ -d "${INSTALL_ROOT}" ]]; then + root="${INSTALL_ROOT}" + else + die "未找到安装目录 ${INSTALL_ROOT}" + fi + fi + REPO_ROOT="${root}" + + if ! confirm_uninstall; then + log "已取消卸载" + return 0 + fi + + local stamp backup_dir removed_dir + stamp="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)" + backup_dir="${BACKUP_ROOT}/coinox-pre-uninstall-${stamp}" + removed_dir="${INSTALL_ROOT}.removed.${stamp}" + + step "备份配置到 ${backup_dir}" + backup_configs_to "${REPO_ROOT}" "${backup_dir}" + { + echo "created_at=${stamp}" + echo "install_root=${INSTALL_ROOT}" + echo "removed_dir=${removed_dir}" + } >"${backup_dir}/uninstall.manifest" + + step "停止并移除本项目 PM2 进程(不影响其它 PM2)" + pm2_stop_project_apps + pm2_delete_project_apps + + remove_backup_cron + + step "移走安装目录" + if [[ -d "${INSTALL_ROOT}" ]]; then + mv "${INSTALL_ROOT}" "${removed_dir}" + log "已移动: ${INSTALL_ROOT} -> ${removed_dir}" + else + log "安装目录不存在,跳过" + fi + + echo "" + echo "卸载完成." + echo " 配置备份: ${backup_dir}" + echo " 旧目录: ${removed_dir} (确认无误后可手动删除)" + echo "" + echo "回滚示例:" + echo " mv ${removed_dir} ${INSTALL_ROOT}" + echo " bash ${INSTALL_ROOT}/deploy/manage.sh # 选 1 修复环境" +} + +main_uninstall "$@" diff --git a/deploy/lib/update.sh b/deploy/lib/update.sh new file mode 100644 index 0000000..43f4da9 --- /dev/null +++ b/deploy/lib/update.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# deploy/lib/update.sh — CoinOX 更新 +set -e +set -u +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "${LIB_DIR}/common.sh" + +require_installed() { + if ! repo_ready "${REPO_ROOT}"; then + die "未找到安装,请先执行「1) 一键部署」" + fi +} + +update_quick() { + step "快速更新" + bash "${REPO_ROOT}/deploy/pull_and_restart.sh" + # git pull 后重新加载,避免菜单进程仍用旧的 verify_deployment + # shellcheck source=common.sh + source "${LIB_DIR}/common.sh" + verify_deployment "$(detect_server_ip)" || true +} + +update_deps() { + step "依赖更新" + if [[ -x "${REPO_ROOT}/.venv/bin/pip" ]]; then + pip_install_requirements "${REPO_ROOT}/.venv/bin/pip" "${REPO_ROOT}/requirements.txt" "CoinOX 依赖" + else + bash "${REPO_ROOT}/deploy/setup_env.sh" --install-system-deps + fi + update_quick +} + +show_update_menu() { + while true; do + echo "" + echo " 更新选项:" + echo " 3-1) 快速更新(git pull + pm2 restart)" + echo " 3-2) 依赖更新(含 pip install)" + echo " 0) 返回主菜单" + local choice="" + cm_read choice "请选择 [0/3-1/3-2]: " + case "${choice}" in + 3-1|31|1) update_quick; break ;; + 3-2|32|2) update_deps; break ;; + 0) break ;; + *) echo "无效选项" ;; + esac + done +} + +main_update() { + require_root + if ! REPO_ROOT="$(resolve_repo_root)"; then + die "未找到安装目录 ${INSTALL_ROOT},请先执行「1) 一键部署」" + fi + require_installed + show_update_menu +} + +main_update "$@" diff --git a/deploy/manage.sh b/deploy/manage.sh new file mode 100644 index 0000000..4664070 --- /dev/null +++ b/deploy/manage.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# CoinOX 部署管理器 — 一键部署 / 卸载 / 更新 +# +# 新服务器(免克隆): +# curl -fsSL https://git.bz121.com/dekun/CoinOX/raw/branch/main/deploy/manage.sh | bash +# +# 已安装: +# bash /opt/coinox/deploy/manage.sh +# +set -e +if [ -n "${BASH_VERSION:-}" ]; then + set -o pipefail +fi + +INSTALL_ROOT="${INSTALL_ROOT:-/opt/coinox}" +GIT_URL="${GIT_URL:-https://git.bz121.com/dekun/CoinOX.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" + +_script_src="${BASH_SOURCE[0]:-}" +if [[ -n "${_script_src}" && -f "${_script_src}" ]]; then + DEPLOY_DIR="$(cd "$(dirname "${_script_src}")" && pwd)" + REPO_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)" + LIB_DIR="${DEPLOY_DIR}/lib" +else + DEPLOY_DIR="" + REPO_ROOT="" + LIB_DIR="" +fi +unset _script_src + +set -u + +repo_ready() { + [[ -f "${1}/deploy/setup_env.sh" && -f "${1}/deploy/manage.sh" && -f "${1}/app.py" ]] +} + +sync_repo_if_present() { + local root="$1" + if [[ -d "${root}/.git" ]] && command -v git >/dev/null 2>&1; then + git -C "${root}" pull -q --ff-only 2>/dev/null || true + fi +} + +bootstrap_repo() { + if repo_ready "${INSTALL_ROOT}"; then + REPO_ROOT="${INSTALL_ROOT}" + DEPLOY_DIR="${REPO_ROOT}/deploy" + LIB_DIR="${DEPLOY_DIR}/lib" + sync_repo_if_present "${REPO_ROOT}" + return 0 + fi + if [[ -n "${REPO_ROOT}" ]] && repo_ready "${REPO_ROOT}"; then + DEPLOY_DIR="${REPO_ROOT}/deploy" + LIB_DIR="${DEPLOY_DIR}/lib" + return 0 + fi + + echo "CoinOX 部署管理器 — 首次自举" + echo "将克隆到: ${INSTALL_ROOT}" + if [[ "$(id -u)" -ne 0 ]]; then + echo "错误: 请使用 root 执行" >&2 + exit 1 + fi + if ! command -v git >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y git ca-certificates curl + else + echo "错误: 未找到 git" >&2 + exit 1 + fi + fi + if [[ -d "${INSTALL_ROOT}" ]]; then + echo "错误: ${INSTALL_ROOT} 已存在但不是有效仓库" >&2 + echo "请手动处理后再运行,或设置 INSTALL_ROOT 指向其它路径" >&2 + exit 1 + fi + mkdir -p "$(dirname "${INSTALL_ROOT}")" + git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}" + exec bash "${INSTALL_ROOT}/deploy/manage.sh" "$@" /dev/null 2>&1; then + return 1 + fi + if command -v python3 >/dev/null 2>&1; then + pm2 jlist 2>/dev/null | python3 -c ' +import json,sys +name=sys.argv[1] +try: + apps=json.load(sys.stdin) +except Exception: + sys.exit(1) +sys.exit(0 if any(isinstance(a,dict) and a.get("name")==name for a in (apps or [])) else 1) +' "${name}" + return $? + fi + pm2 describe "${name}" 2>/dev/null | grep -qE 'status[[:space:]]*[|:]' +} + +read_port() { + local env_file="${REPO}/.env" + local port="5010" + if [[ -f "${env_file}" ]]; then + local line + line="$(grep -E '^[[:space:]]*APP_PORT=' "${env_file}" | tail -n1 || true)" + if [[ -n "${line}" ]]; then + port="${line#*=}" + port="$(echo "${port}" | tr -d '"' | tr -d "'" | xargs)" + fi + fi + echo "${port:-5010}" +} + +wait_http_ready() { + local port="$1" + local url="http://127.0.0.1:${port}/" + local i code + echo ">>> 等待 HTTP 就绪 ${url} (最多 30s)" + for i in $(seq 1 30); do + code="$(curl -sS -o /dev/null -w '%{http_code}' --connect-timeout 2 --max-time 3 "${url}" 2>/dev/null || echo "000")" + if [[ "${code}" == "200" || "${code}" == "302" || "${code}" == "301" || "${code}" == "401" || "${code}" == "403" ]]; then + echo ">>> HTTP OK (${code}) after ${i}s" + return 0 + fi + # 进程挂了就别空等 + if ! pm2_has_app "${PM2_APP_NAME}"; then + echo ">>> PM2 进程已消失" >&2 + return 1 + fi + sleep 1 + done + echo ">>> HTTP 仍未就绪 (最后 code=${code:-000})" >&2 + echo ">>> 请查看: pm2 logs ${PM2_APP_NAME} --lines 50" >&2 + return 1 +} + +cd "${REPO}" +echo ">>> git pull" +git pull + +if [[ "${DRY}" -eq 1 ]]; then + echo "(dry-run, skip pm2 restart)" + exit 0 +fi + +if ! command -v pm2 >/dev/null 2>&1; then + echo "warn: 未安装 pm2" >&2 + exit 1 +fi + +if [[ ! -f ecosystem.config.cjs ]]; then + echo "error: 缺少 ecosystem.config.cjs" >&2 + exit 1 +fi + +if pm2_has_app "${PM2_APP_NAME}"; then + echo ">>> pm2 restart ${PM2_APP_NAME} --update-env" + pm2 restart "${PM2_APP_NAME}" --update-env +else + echo ">>> 未注册 ${PM2_APP_NAME},执行 pm2 start ecosystem.config.cjs" + pm2 start ecosystem.config.cjs +fi + +port="$(read_port)" +wait_http_ready "${port}" || true + +echo "done" diff --git a/deploy/setup_env.sh b/deploy/setup_env.sh new file mode 100644 index 0000000..dfe3c5c --- /dev/null +++ b/deploy/setup_env.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# coinox 一键环境部署(Ubuntu / root /opt/coinox) +# +# 用法: +# bash deploy/setup_env.sh +# bash deploy/setup_env.sh --skip-pm2 +# bash deploy/setup_env.sh --recreate-venv +# bash deploy/setup_env.sh --install-system-deps +# +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)" +REQ_FILE="${REPO_ROOT}/requirements.txt" +# shellcheck source=lib/common.sh +source "${DEPLOY_DIR}/lib/common.sh" + +SKIP_PM2=0 +SKIP_ENV_COPY=0 +RECREATE_VENV=0 +INSTALL_APT_DEPS=0 +PY="" + +usage() { + sed -n '2,10p' "$0" | sed 's/^# \?//' + exit "${1:-0}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --skip-pm2) SKIP_PM2=1; shift ;; + --skip-env-copy) SKIP_ENV_COPY=1; shift ;; + --recreate-venv) RECREATE_VENV=1; shift ;; + --install-system-deps) INSTALL_APT_DEPS=1; shift ;; + -h|--help) usage 0 ;; + *) echo "未知参数: $1" >&2; usage 1 ;; + esac +done + +find_python() { + if command -v python3 >/dev/null 2>&1; then + echo python3 + return + fi + if command -v python >/dev/null 2>&1; then + echo python + return + fi + echo "未找到 python3/python,请先安装 Python 3.10+" >&2 + exit 1 +} + +check_python_version() { + local py="$1" + local ver + ver="$("${py}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" + local major minor + major="${ver%%.*}" + minor="${ver#*.}" + if [[ "${major}" -lt 3 ]] || [[ "${major}" -eq 3 && "${minor}" -lt 10 ]]; then + echo "需要 Python 3.10+,当前: ${ver}" >&2 + exit 1 + fi + echo "Python: $("${py}" --version 2>&1)" +} + +python_minor_version() { + local py="$1" + "${py}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' +} + +check_venv_available() { + local py="$1" + local tmp + tmp="$(mktemp -d 2>/dev/null || mktemp -d -t cmvenv)" + if "${py}" -m venv "${tmp}" >/dev/null 2>&1 && [[ -x "${tmp}/bin/python" ]]; then + rm -rf "${tmp}" + return 0 + fi + rm -rf "${tmp}" 2>/dev/null || true + return 1 +} + +install_debian_venv_packages() { + local py="$1" + local ver + ver="$(python_minor_version "${py}")" + if ! command -v apt-get >/dev/null 2>&1; then + echo " 未检测到 apt-get,请手动安装 python${ver}-venv 与 python3-pip" >&2 + return 1 + fi + if [[ "$(id -u)" -ne 0 ]]; then + echo " 需要 root 安装系统包,请执行:" >&2 + echo " sudo apt update && sudo apt install -y python${ver}-venv python3-pip curl" >&2 + echo " 或: sudo bash deploy/setup_env.sh --install-system-deps" >&2 + return 1 + fi + step "安装系统依赖 (python${ver}-venv) ..." + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + if ! apt-get install -y "python${ver}-venv" python3-pip curl ca-certificates git; then + apt-get install -y python3-venv python3-pip curl ca-certificates git + fi +} + +ensure_venv_prereqs() { + local py="$1" + if check_venv_available "${py}"; then + return 0 + fi + echo " 当前 Python 无法创建 venv(缺少 ensurepip,常见于未安装 python*-venv)" >&2 + if [[ "${INSTALL_APT_DEPS}" -eq 1 ]] || [[ "$(id -u)" -eq 0 ]]; then + install_debian_venv_packages "${py}" || exit 1 + if check_venv_available "${py}"; then + return 0 + fi + fi + local ver + ver="$(python_minor_version "${py}")" + echo "请安装后重试:" >&2 + echo " apt update && apt install -y python${ver}-venv python3-pip" >&2 + echo " bash deploy/setup_env.sh" >&2 + exit 1 +} + +create_project_venv() { + local py="$1" + if [[ "${RECREATE_VENV}" -eq 1 && -d .venv ]]; then + echo " 删除旧 venv ..." + rm -rf .venv + fi + if [[ -d .venv && ! -x .venv/bin/python ]]; then + echo " 清理未完成的 venv ..." + rm -rf .venv + fi + if [[ -x .venv/bin/python ]]; then + return 0 + fi + echo " 创建 venv ..." + if ! "${py}" -m venv .venv; then + rm -rf .venv 2>/dev/null || true + echo " venv 创建失败" >&2 + exit 1 + fi +} + +setup_app() { + step "CoinOX" + cd "${REPO_ROOT}" + create_project_venv "${PY}" + pip_upgrade_tools ".venv/bin/pip" + pip_install_requirements ".venv/bin/pip" "${REQ_FILE}" "CoinOX 依赖" + if [[ "${SKIP_ENV_COPY}" -eq 0 ]]; then + if [[ -f .env.example && ! -f .env ]]; then + cp -n .env.example .env 2>/dev/null || cp .env.example .env + echo " 已复制 .env.example -> .env" + elif [[ -f .env ]]; then + echo " 保留已有 .env" + else + echo " 无 .env.example,请手动配置 .env" + fi + fi + mkdir -p static/images/order_charts + echo " 完成: ${REPO_ROOT}/.venv/bin/python" +} + +install_pm2_optional() { + if [[ "${SKIP_PM2}" -eq 1 ]]; then + return + fi + step "PM2(可选)" + if ! command -v node >/dev/null 2>&1; then + echo " 未检测到 Node.js,跳过.安装后执行: npm install -g pm2" + return + fi + if command -v pm2 >/dev/null 2>&1; then + echo " PM2 已安装: $(pm2 -v)" + return + fi + echo " 正在安装 pm2 ..." + npm install -g pm2 +} + +echo "CoinOX 环境部署" +echo "仓库根目录: ${REPO_ROOT}" +echo "远端仓库: ${GIT_URL}" + +[[ -f "${REQ_FILE}" ]] || { echo "缺少 ${REQ_FILE}" >&2; exit 1; } + +PY="$(find_python)" +check_python_version "${PY}" +ensure_venv_prereqs "${PY}" + +setup_app +install_pm2_optional + +step "部署密钥(首次自动生成,不覆盖已有)" +if command -v python3 >/dev/null 2>&1; then + python3 "${REPO_ROOT}/scripts/bootstrap_deploy_secrets.py" || true +else + echo " 跳过 bootstrap_deploy_secrets(未找到 python3)" +fi + +echo "" +echo "部署完成.下一步:" +echo " 1. 编辑 ${REPO_ROOT}/.env(默认已是模拟盘 sim;实盘再填 OKX API / 代理 / 密码)" +echo " 2. pm2 start ecosystem.config.cjs" +echo " 或: bash deploy/manage.sh # 选 1" +echo "" diff --git a/scripts/bootstrap_deploy_secrets.py b/scripts/bootstrap_deploy_secrets.py new file mode 100644 index 0000000..7a02c74 --- /dev/null +++ b/scripts/bootstrap_deploy_secrets.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""首次部署:自动生成登录会话密钥与初始账号(仅空/占位时写入,不覆盖已有). + +- FLASK_SECRET_KEY +- APP_USERNAME=admin / APP_PASSWORD=admin123(仅空时) +- HUB_BRIDGE_TOKEN(仅空时,供后期中控) +- 强制默认模拟盘相关键(仅空/占位时) +""" +from __future__ import annotations + +import argparse +import os +import secrets +import sys + +_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _REPO not in sys.path: + sys.path.insert(0, _REPO) + +from lib.env.env_file_lib import apply_env_updates, env_get, read_env_lines + +FLASK_PLACEHOLDERS = frozenset( + { + "", + "CHANGE_TO_LONG_RANDOM_SECRET", + "crypto_monitor_2026_secret_key", + "coinox_2026_secret_key", + } +) +PASSWORD_PLACEHOLDERS = frozenset({"", "CHANGE_ME_STRONG_PASSWORD"}) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Bootstrap deploy secrets for CoinOX") + parser.add_argument("--dry-run", action="store_true", help="只打印将写入的项,不改文件") + args = parser.parse_args() + + env_path = os.path.join(_REPO, ".env") + if not os.path.isfile(env_path): + print(f" 跳过: 未找到 {env_path}") + return 0 + + lines = read_env_lines(env_path) + updates: dict[str, str] = {} + + cur_secret = env_get(lines, "FLASK_SECRET_KEY") + if (cur_secret or "").strip() in FLASK_PLACEHOLDERS: + updates["FLASK_SECRET_KEY"] = secrets.token_urlsafe(48) + + if not (env_get(lines, "APP_USERNAME") or "").strip(): + updates["APP_USERNAME"] = "admin" + + cur_pass = env_get(lines, "APP_PASSWORD") + if (cur_pass or "").strip() in PASSWORD_PLACEHOLDERS: + updates["APP_PASSWORD"] = "admin123" + + if not (env_get(lines, "HUB_BRIDGE_TOKEN") or "").strip(): + updates["HUB_BRIDGE_TOKEN"] = secrets.token_urlsafe(32) + + # CoinOX: 默认模拟盘(仅当未配置或空时写入) + if not (env_get(lines, "SIM_DEFAULT_MODE") or "").strip(): + updates["SIM_DEFAULT_MODE"] = "sim" + if not (env_get(lines, "OKX_OPTIONS_MARGIN_MODE") or "").strip(): + updates["OKX_OPTIONS_MARGIN_MODE"] = "coin" + if not (env_get(lines, "EXCHANGE_DISPLAY_NAME") or "").strip(): + updates["EXCHANGE_DISPLAY_NAME"] = "CoinOX" + live = (env_get(lines, "LIVE_TRADING_ENABLED") or "").strip().lower() + if live in ("",): + updates["LIVE_TRADING_ENABLED"] = "false" + + if not updates: + print(" 密钥已存在,无需写入") + return 0 + + print(f" 将更新 {env_path}: {', '.join(updates.keys())}") + if args.dry_run: + for k, v in updates.items(): + shown = ( + "***" + if k in ("APP_PASSWORD", "HUB_BRIDGE_TOKEN") or "SECRET" in k + else v + ) + print(f" {k}={shown}") + return 0 + + apply_env_updates(env_path, updates) + print(" 已写入初始密钥/账号与模拟盘默认(请尽快修改 APP_PASSWORD)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/部署文档.md b/部署文档.md index caab727..eb13273 100644 --- a/部署文档.md +++ b/部署文档.md @@ -1,367 +1,95 @@ -# `crypto_monitor_okx` 部署文档(Ubuntu) +# CoinOX 部署文档(仅 Ubuntu) -**功能与页面操作** 见同目录 **[使用说明.md](./使用说明.md)**.Ubuntu 环境(Python / Node / PM2)见 **[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.策略与 AI 见 **[策略交易说明.md](../策略交易说明.md)**,**[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**. +仓库: +安装路径:`/opt/coinox` +进程名:`coinox`(PM2) +默认端口:`5010` +默认模式:**模拟盘**(`SIM_DEFAULT_MODE=sim`) + +本项目**仅支持 Ubuntu 22.04 / 24.04 + root 一键部署**,不提供 Windows 部署步骤。 --- -# 本地部署 + SSH SOCKS 转发 + PM2 启动指南(Ubuntu) +## 一键交互式部署(推荐) -本文面向:**本地 Ubuntu 机器运行项目**,但 **本机直连 OKX 会被 TLS/SNI reset** 的场景.解决思路是: - -- 本机启动 `ssh -D` 动态转发,把 **SOCKS5 出口**放到你可正常访问 OKX 的 VPS 上 -- 项目通过环境变量 `OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080` 让 `ccxt` 走 SOCKS -- **SSH 隧道**用 `ssh -D` 常驻(可用 tmux / autossh);**Flask 应用** 仅用 **PM2** 托管(见 [docs/ubuntu-server.md](../docs/ubuntu-server.md)) - -> 安全提醒:不要把 `.env`,私钥 `.pem`,OKX API Key 提交到 Git;文档里只用占位符. - ---- - -## 0. 你需要准备的东西 - -- 一台 **Ubuntu** 本地机器(下文称“本机”) -- 一台可 SSH 登录,且 **能正常访问 OKX** 的 VPS(示例公网 IP:`47.76.87.111`,用户:`root`) -- VPS 登录方式:**SSH 私钥**(推荐)或密码(不推荐用于无人值守) -- 本机已安装: - - `python3`,`python3-venv`,`pip`(或 `python3-pip`) - - `git`(可选) - - `curl`,`ssh` - - `node` + `npm`(用于安装 `pm2`) - ---- - -## 1. 从云服务器把项目同步到本地(推荐:打包下载) - -在云服务器项目目录(包含 `app.py` 的目录)执行: +新服务器无需先 clone,root 执行: ```bash -cd /opt/crypto_monitor/crypto_monitor_okx - -# 可选:清理 Python 缓存,减少小文件传输 -find . -type d -name __pycache__ -prune -exec rm -rf {} + -find . -type f -name "*.pyc" -delete - -tar -czf crypto_monitor.tgz . +curl -fsSL https://git.bz121.com/dekun/CoinOX/raw/branch/main/deploy/manage.sh | bash ``` -下载 `crypto_monitor.tgz` 到本机后解压: +已安装机器: ```bash -mkdir -p /opt/crypto_monitor/crypto_monitor_okx -cd /opt/crypto_monitor -tar -xzf crypto_monitor.tgz -C crypto_monitor_okx -cd crypto_monitor_okx -cp -n .env.example .env # 若尚无 .env +bash /opt/coinox/deploy/manage.sh +``` + +### 菜单 + +| 选项 | 功能 | +|------|------| +| **1) 一键部署** | 检测环境 + 系统依赖 + Node/PM2 + venv + `.env` + 启动 PM2 | +| **2) 一键卸载** | 备份 `.env` → 停 PM2 → 移走目录 | +| **3) 更新** | 快速更新 / 依赖更新 | +| **4) 仅检测** | 识别 OS / Python / Node / PM2 / 项目状态(不安装) | +| **0) 退出** | | + +### 部署完成后 + +- 访问:`http://<服务器IP>:5010` +- 默认入口:`/hedge-plan`(对冲)、`/options`(期权) +- 登录:`admin` / `.env` 中 `APP_PASSWORD`(首次多为 `admin123`) +- **默认模拟盘**:本地钱包撮合,不打真实私有下单 + +切实盘前请在 env / 系统设置中配置 `OKX_API_*`,并将交易模式切为 `live`,同时确认 `LIVE_TRADING_ENABLED`。 + +--- + +## 脚本结构 + +``` +deploy/ +├── manage.sh # 入口(自举 + 交互菜单) +├── lib/ +│ ├── common.sh +│ ├── install.sh +│ ├── uninstall.sh +│ ├── update.sh +│ └── doctor.sh +├── setup_env.sh +└── pull_and_restart.sh +``` + +详见 [deploy/README.md](./deploy/README.md)。 + +--- + +## 分步安装(可选) + +```bash +cd /opt +git clone -b main https://git.bz121.com/dekun/CoinOX.git coinox +cd /opt/coinox +bash deploy/setup_env.sh --install-system-deps +pm2 start ecosystem.config.cjs +pm2 save && pm2 startup ``` --- -## 2. 配置 SSH 私钥与 `~/.ssh/config`(推荐) - -把私钥放到本机(示例:`~/.ssh/vps1.pem`),并设置权限: +## 常用运维 ```bash -mkdir -p ~/.ssh -chmod 700 ~/.ssh -mv ~/Downloads/vps1.pem ~/.ssh/vps1.pem -chmod 600 ~/.ssh/vps1.pem -``` - -编辑 `~/.ssh/config`(没有就创建),添加: - -```sshconfig -Host okx-vps - HostName 47.76.87.111 - User root - IdentityFile ~/.ssh/vps1.pem - IdentitiesOnly yes - ServerAliveInterval 30 - ServerAliveCountMax 3 - ExitOnForwardFailure yes - BatchMode yes -``` - -测试: - -```bash -ssh okx-vps true -``` - -> 如果你还没完全切到密钥登录(还会交互要密码),先把 `BatchMode yes` 注释掉,等密钥登录稳定后再打开. - ---- - -## 3. 先手工验证:SSH SOCKS + OKX API - -### 3.1 开一个本地 SOCKS(1080) - -```bash -ssh -N -D 127.0.0.1:1080 okx-vps -``` - -保持该进程运行(另开终端继续下面步骤). - -### 3.2 验证 OKX 走 SOCKS 可用 - -```bash -curl -4 -Iv --max-time 15 --proxy socks5h://127.0.0.1:1080 https://www.okx.com/api/v5/public/time -``` - -看到 `HTTP/2 200`(或至少 TLS 握手成功且返回 JSON)即 OK. - ---- - -## 4. Python 虚拟环境(venv) - -在本机项目目录: - -```bash -cd /opt/crypto_monitor/crypto_monitor_okx - -python3 -m venv .venv -source .venv/bin/activate - -python -m pip install -U pip -pip install flask requests ccxt werkzeug PySocks Pillow -``` - -> 说明:本仓库当前没有 `requirements.txt`.如果你希望“完全复刻云服务器依赖”,可以在云服务器项目环境里执行 `pip freeze > requirements.txt` 带回本机再 `pip install -r requirements.txt`(记得删掉明显无关/体积巨大的包). - -建议减少 `.pyc` 垃圾文件(可选): - -```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_okx - -cp -n .env.example .env # 已存在 .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` 后重填. - -**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 + 代理) - -至少确认/填写这些关键项(示例): - -```env -APP_HOST=127.0.0.1 -APP_PORT=5000 - -# OKX(如需实盘) -LIVE_TRADING_ENABLED=false -OKX_API_KEY=... -OKX_API_SECRET=... -OKX_API_PASSPHRASE=... - -# OKX 出口:走本机 SSH 动态转发 SOCKS -OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080 - -# 开仓多周期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=5 -# DAILY_OPEN_HARD_LIMIT=0 -# 说明见仓库 docs/daily-open-limit.md - -# AI 复盘(默认 OpenAI 兼容网关;与 Ollama 二选一) -AI_PROVIDER=openai -AI_TIMEOUT_SECONDS=120 -OPENAI_API_BASE=https://op.bz121.com/v1 -OPENAI_API_KEY=你的密钥 -OPENAI_MODEL=gemma4:e4b -# 本机 Ollama(仅 AI_PROVIDER=ollama) -OLLAMA_API=http://127.0.0.1:11434/api/generate -AI_MODEL=你的模型名 -``` - -> 完整说明见仓库根 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**.`OPENAI_API_KEY` 在 [op.bz121.com](https://op.bz121.com/) 的 `gateway.json` 获取. - -> `OKX_SOCKS_PROXY` 使用 `socks5h`:让 SOCKS 侧做域名解析(更贴近你 `curl --proxy socks5h://...` 的成功路径). - ---- - -## 6. 本机手工启动(验证 Flask) - -确保: - -1. SOCKS 隧道已运行(127.0.0.1:1080) -2. 虚拟环境已 `activate` -3. `.env` 已配置 - -启动: - -```bash -cd /opt/crypto_monitor/crypto_monitor_okx -source .venv/bin/activate -python app.py -``` - -浏览器访问:`http://127.0.0.1:5000`(或你在 `.env` 配的端口). - ---- - -## 7. 安装 PM2(Node) - -```bash -sudo npm i -g pm2 -pm2 -v -``` - ---- - -## 8. 用 PM2 启动 SSH SOCKS 隧道(推荐:密钥免交互) - -### 8.1 启动隧道进程 - -```bash -pm2 start "ssh" --name okx-socks-tunnel -- \ - -N -D 127.0.0.1:1080 okx-vps \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \ - -o ExitOnForwardFailure=yes -o BatchMode=yes -``` - -查看日志: - -```bash -pm2 logs okx-socks-tunnel --lines 200 -``` - -### 8.2 仍然验证 OKX - -```bash -curl -4 -Iv --max-time 15 --proxy socks5h://127.0.0.1:1080 https://www.okx.com/api/v5/public/time -``` - -### 8.3 开机自启(可选) - -```bash -pm2 save -pm2 startup -``` - ---- - -## 9. 用 PM2 启动 Flask(`app.py`) - -`pm2` 管理 Python 的常用方式是直接启动解释器: - -```bash -cd /opt/crypto_monitor/crypto_monitor_okx - -pm2 start /opt/crypto_monitor/crypto_monitor_okx/.venv/bin/python --name crypto-monitor -- \ - /opt/crypto_monitor/crypto_monitor_okx/app.py -``` - -> 若项目目录与上文不一致,请替换为实际绝对路径;或用 `readlink -f app.py` 得到绝对路径. - -查看日志: - -```bash -pm2 logs crypto-monitor --lines 200 -``` - -保存进程列表: - -```bash -pm2 save -``` - ---- - -## 10. 常见问题排查(高频) - -### 10.1 OKX 仍然失败:先看隧道是否在 - -```bash -ss -lntp | grep 1080 || true pm2 status -``` - -### 10.2 `pm2` 里的 `ssh` 立刻退出 - -常见原因: - -- 私钥权限不对(`chmod 600`) -- `~/.ssh/config` 写错 `HostName/User/IdentityFile` -- 开了 `BatchMode yes` 但仍需要密码(会失败) - -### 10.3 `ccxt` SOCKS 报错 / 代理不生效 - -本机 Python 依赖通常需要: - -```bash -source .venv/bin/activate -pip install PySocks -``` - -### 10.4 `.pyc` 很多导致同步慢 - -`.pyc` 是缓存,删除不影响功能: - -```bash -find . -type d -name __pycache__ -prune -exec rm -rf {} + -find . -type f -name "*.pyc" -delete +pm2 logs coinox --lines 80 +bash /opt/coinox/deploy/pull_and_restart.sh +bash /opt/coinox/deploy/manage.sh # 选 3 更新 ``` --- -## 11. 推荐的启动顺序(固定习惯) +## 安全提醒 -1. `pm2` 启动 `okx-socks-tunnel` -2. `curl --proxy socks5h://127.0.0.1:1080 ...` 验证 OKX -3. `pm2` 启动 `crypto-monitor` - ---- - -## 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='保本止盈'` - -如果你想,我还可以再给你一条“先自动备份 DB 再执行”的一键命令. \ No newline at end of file +- 不要把 `.env`、API Key 提交到 Git +- 公网务必改掉默认密码,并关闭 `APP_AUTH_DISABLED` +- 模拟盘与实盘建议使用不同服务器或至少不同子账户 API