first commit
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# =============================================================================
|
||||
# 环境配置模板(可提交 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=5004
|
||||
# 是否开启调试模式(生产建议 false)
|
||||
APP_DEBUG=false
|
||||
|
||||
# 登录账号
|
||||
APP_USERNAME=dekun
|
||||
# 登录密码(请改成你自己的强密码)
|
||||
APP_PASSWORD=ChangeMe123!
|
||||
# 是否关闭登录校验(局域网可设 true;公网务必 false)
|
||||
APP_AUTH_DISABLED=true
|
||||
# Flask 会话密钥(必须替换为长随机字符串)
|
||||
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
|
||||
|
||||
# ---------- 整机许可(也可写在仓库根目录 .env 的 LICENSE_* 变量)----------
|
||||
LICENSE_API_URL=https://license.example.com
|
||||
LICENSE_CLIENT_KEY=REPLACE_WITH_CLIENT_KEY
|
||||
LICENSE_CHECK_INTERVAL_DAYS=3
|
||||
LICENSE_OFFLINE_GRACE_DAYS=7
|
||||
LICENSE_WECHAT_ID=dekun03
|
||||
# LICENSE_DISABLED=false
|
||||
|
||||
# 企业微信机器人 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
|
||||
|
||||
# 训练总资金(U)
|
||||
TOTAL_CAPITAL=100
|
||||
# 每天起始基数(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
|
||||
|
||||
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
|
||||
LIVE_TRADING_ENABLED=true
|
||||
# OKX API Key(实盘)
|
||||
OKX_API_KEY=REPLACE_WITH_OKX_API_KEY
|
||||
# OKX API Secret(实盘)
|
||||
OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET
|
||||
# OKX API Passphrase(实盘)
|
||||
OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE
|
||||
# 保证金模式:cross=全仓,isolated=逐仓
|
||||
OKX_TD_MODE=cross
|
||||
# 持仓模式:hedge=双向持仓,net=单向净持仓
|
||||
OKX_POS_MODE=hedge
|
||||
# 仓位查询 instType(OKX)
|
||||
OKX_POSITION_INST_TYPE=SWAP
|
||||
|
||||
# 关键位监控:5m收线突破过滤参数
|
||||
KLINE_TIMEFRAME=5m
|
||||
KEY_BREAKOUT_LIMIT_PCT=1.5
|
||||
KEY_ALERT_MAX_TIMES=3
|
||||
KEY_ALERT_INTERVAL_MINUTES=5
|
||||
|
||||
# 资金与仓位刷新周期(秒)
|
||||
BALANCE_REFRESH_SECONDS=60
|
||||
# 后台监控轮询周期(秒)
|
||||
MONITOR_POLL_SECONDS=3
|
||||
# 使用可用资金时的缓冲比例(如0.98代表用98%)
|
||||
FULL_MARGIN_BUFFER_RATIO=0.98
|
||||
|
||||
# 自动划转:将目标账户补足到 AUTO_TRANSFER_AMOUNT
|
||||
AUTO_TRANSFER_ENABLED=false
|
||||
AUTO_TRANSFER_AMOUNT=30
|
||||
AUTO_TRANSFER_FROM=funding
|
||||
AUTO_TRANSFER_TO=swap
|
||||
TRANSFER_CCY=USDT
|
||||
# 强制清仓整点(北京时间,默认 0=凌晨00点)
|
||||
FORCE_CLOSE_BJ_HOUR=0
|
||||
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
|
||||
FORCE_CLOSE_ENABLED=false
|
||||
|
||||
# 推送与AI超时(秒)
|
||||
WECHAT_TIMEOUT_SECONDS=10
|
||||
AI_TIMEOUT_SECONDS=120
|
||||
|
||||
# AI 复盘服务地址(本机 Ollama 默认地址)
|
||||
OLLAMA_API=http://127.0.0.1:11434/api/generate
|
||||
# AI 模型名称
|
||||
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
|
||||
|
||||
# OKX 代理(可选):用于本机网络对 OKX TLS/SNI 不稳定时,通过 SSH 动态转发 SOCKS5 出口
|
||||
# 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=5
|
||||
# 关键位:标准方案止损外侧%、趋势单方案止损外侧%(默认 0.5 / 1)
|
||||
# KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
|
||||
# KEY_TREND_STOP_OUTSIDE_PCT=1
|
||||
# 以损定仓(按交易账户资金的百分比)
|
||||
# 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
|
||||
AUTO_TRANSFER_BJ_HOUR=8
|
||||
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日/可开仓等
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* PM2 进程定义(Ubuntu / Linux)。
|
||||
*
|
||||
* 仅托管 Flask 应用。**SSH SOCKS 隧道请在本机用 screen/tmux/systemd 等方式单独常驻**,
|
||||
* 与 `.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 PY = path.join(ROOT, ".venv", "bin", "python");
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "crypto_okx",
|
||||
cwd: ROOT,
|
||||
script: path.join(ROOT, "app.py"),
|
||||
interpreter: PY,
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: "800M",
|
||||
// app.py 从项目根目录 .env 加载(由 .env.example 复制而来,勿提交 Git)
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/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())
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
ok2
|
||||
@@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>关键位放大 | 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}
|
||||
input,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:580px;background:#0f1320;border:1px solid #2a3150;border-radius:10px;padding:8px}
|
||||
#chart{width:100%;height:100%}
|
||||
</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">关键位放大(可输入币种)</strong>
|
||||
</div>
|
||||
<div class="status">最近刷新:<span id="updated-at">--</span></div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:10px">
|
||||
<label>币种</label>
|
||||
<input id="symbol-input" value="{{ default_symbol }}" placeholder="BTC/USDT">
|
||||
|
||||
<label>关键位</label>
|
||||
<select id="key-id">
|
||||
<option value="">无(仅看K线)</option>
|
||||
{% for k in key_list %}
|
||||
<option value="{{ k.id }}" {% if selected_key and k.id == selected_key.id %}selected{% endif %}>#{{ k.id }} {{ k.symbol }} {{ k.monitor_type }} {{ '做多' if k.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>
|
||||
|
||||
<label>K线数</label>
|
||||
<select id="kline-limit">
|
||||
<option value="100" {% if default_kline_limit == 100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if default_kline_limit == 200 %}selected{% endif %}>200</option>
|
||||
</select>
|
||||
|
||||
<button id="manual-refresh" type="button">刷新</button>
|
||||
<span id="load-status" class="status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-type">-</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-upper">-</div></div>
|
||||
<div class="meta-item"><div class="k">下沿/支撑</div><div class="v" id="m-lower">-</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-updiff">-</div></div>
|
||||
<div class="meta-item"><div class="k">距下沿</div><div class="v" id="m-lowdiff">-</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card"><div id="chart-wrap"><div id="chart"></div></div></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script>
|
||||
const refreshMs = Math.max({{ price_refresh_seconds * 1000 }}, 5000);
|
||||
const keySelect = document.getElementById("key-id");
|
||||
const symbolInput = document.getElementById("symbol-input");
|
||||
const tfSelect = document.getElementById("timeframe");
|
||||
const limitSelect = document.getElementById("kline-limit");
|
||||
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);
|
||||
const fmtSigned = (v,d=4)=>{
|
||||
if(v===null||typeof v==="undefined"||Number.isNaN(Number(v))) return "-";
|
||||
const n = Number(v);
|
||||
return `${n>0?"+":""}${n.toFixed(d)}`;
|
||||
};
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let priceLines = [];
|
||||
const keyMap = {};
|
||||
{% for k in key_list %}
|
||||
keyMap["{{ k.id }}"] = "{{ k.symbol }}";
|
||||
{% endfor %}
|
||||
|
||||
function ensureChart(){
|
||||
if(chart && candleSeries) return true;
|
||||
if(!window.LightweightCharts){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "图表库加载失败";
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!chart){
|
||||
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}
|
||||
});
|
||||
window.addEventListener("resize",()=>{
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
});
|
||||
chart.applyOptions({width:chartHost.clientWidth,height:chartHost.clientHeight});
|
||||
}
|
||||
|
||||
const opts = {
|
||||
upColor: "#4cd97f",
|
||||
downColor: "#ff6666",
|
||||
borderVisible: false,
|
||||
wickUpColor: "#4cd97f",
|
||||
wickDownColor: "#ff6666"
|
||||
};
|
||||
if (typeof chart.addCandlestickSeries === "function") {
|
||||
candleSeries = chart.addCandlestickSeries(opts);
|
||||
} else if (typeof chart.addSeries === "function" && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
}
|
||||
|
||||
if(!candleSeries){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "K线序列初始化失败";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetPriceLines(){
|
||||
if(!candleSeries) return;
|
||||
priceLines.forEach(line=>{ try { candleSeries.removePriceLine(line); } catch (_) {} });
|
||||
priceLines = [];
|
||||
}
|
||||
|
||||
function addLine(price, title, color){
|
||||
if(!candleSeries || price===null || typeof price==="undefined") 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 paintMeta(data){
|
||||
const key = data.key_monitor || null;
|
||||
document.getElementById("m-symbol").innerText = data.symbol || "-";
|
||||
document.getElementById("m-price").innerText = fmt(data.current_price,8);
|
||||
|
||||
if(!key){
|
||||
document.getElementById("m-type").innerText = "未匹配到关键位";
|
||||
document.getElementById("m-direction").innerText = "-";
|
||||
document.getElementById("m-upper").innerText = "-";
|
||||
document.getElementById("m-lower").innerText = "-";
|
||||
document.getElementById("m-updiff").innerText = "-";
|
||||
document.getElementById("m-lowdiff").innerText = "-";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("m-type").innerText = key.monitor_type || "-";
|
||||
document.getElementById("m-direction").innerText = key.direction === "short" ? "做空" : "做多";
|
||||
document.getElementById("m-upper").innerText = fmt(key.upper,8);
|
||||
document.getElementById("m-lower").innerText = fmt(key.lower,8);
|
||||
document.getElementById("m-updiff").innerText = `${fmtSigned(key.upper_diff,4)} (${fmtSigned(key.upper_pct,2)}%)`;
|
||||
document.getElementById("m-lowdiff").innerText = `${fmtSigned(key.lower_diff,4)} (${fmtSigned(key.lower_pct,2)}%)`;
|
||||
}
|
||||
|
||||
function syncSymbolByKey(){
|
||||
const keyId = keySelect.value;
|
||||
if(keyId && keyMap[keyId]) symbolInput.value = keyMap[keyId];
|
||||
}
|
||||
|
||||
async function loadKeyKline(){
|
||||
if(!ensureChart()) return;
|
||||
const keyId = keySelect.value;
|
||||
const symbol = (symbolInput.value || "").trim().toUpperCase();
|
||||
const timeframe = tfSelect.value;
|
||||
const limit = limitSelect.value;
|
||||
|
||||
if(!symbol && !keyId){
|
||||
statusEl.className = "status err";
|
||||
statusEl.innerText = "请先输入币种或选择关键位";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.className = "status";
|
||||
statusEl.innerText = "加载中...";
|
||||
|
||||
try{
|
||||
const qs = new URLSearchParams();
|
||||
if(keyId) qs.set("key_id", keyId);
|
||||
if(symbol) qs.set("symbol", symbol);
|
||||
qs.set("timeframe", timeframe);
|
||||
qs.set("limit", limit);
|
||||
|
||||
const resp = await fetch(`/api/key_kline?${qs.toString()}`);
|
||||
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;
|
||||
}
|
||||
|
||||
if(!candleSeries) throw new Error("Series init failed");
|
||||
candleSeries.setData(candles);
|
||||
resetPriceLines();
|
||||
addLine(data.current_price, "现价", "#42a5f5");
|
||||
if(data.key_monitor){
|
||||
addLine(data.key_monitor.upper, "上沿/阻力", "#ffb84d");
|
||||
addLine(data.key_monitor.lower, "下沿/支撑", "#4cd97f");
|
||||
}
|
||||
chart.timeScale().fitContent();
|
||||
paintMeta(data);
|
||||
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", loadKeyKline);
|
||||
keySelect.addEventListener("change", ()=>{ syncSymbolByKey(); loadKeyKline(); });
|
||||
symbolInput.addEventListener("change", ()=>{
|
||||
if(symbolInput.value.trim()) keySelect.value = "";
|
||||
loadKeyKline();
|
||||
});
|
||||
tfSelect.addEventListener("change", loadKeyKline);
|
||||
limitSelect.addEventListener("change", loadKeyKline);
|
||||
|
||||
syncSymbolByKey();
|
||||
loadKeyKline();
|
||||
setInterval(loadKeyKline, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>系统登录</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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>交易监控系统登录</h2>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" name="username" required placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,194 @@
|
||||
<!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 = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = 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 = fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}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>
|
||||
@@ -0,0 +1,210 @@
|
||||
<!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 = fmt(order.trigger_price, 8);
|
||||
document.getElementById("m-sl").innerText = fmt(order.stop_loss, 8);
|
||||
document.getElementById("m-tp").innerText = 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 = fmt(order.current_price, 8);
|
||||
const pnlEl = document.getElementById("m-pnl");
|
||||
pnlEl.innerText = `${fmt(order.float_pnl, 4)}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 %}
|
||||
<script>
|
||||
(function(){
|
||||
if (typeof ensureChart !== 'function') return;
|
||||
const oldEnsureChart = ensureChart;
|
||||
ensureChart = function(){
|
||||
if (chart && candleSeries) return true;
|
||||
try { const ok = oldEnsureChart(); if (ok && candleSeries) return true; } catch (_) {}
|
||||
if (chart && !candleSeries && typeof chart.addSeries === 'function' && window.LightweightCharts && window.LightweightCharts.CandlestickSeries) {
|
||||
const opts = { upColor:'#4cd97f', downColor:'#ff6666', borderVisible:false, wickUpColor:'#4cd97f', wickDownColor:'#ff6666' };
|
||||
candleSeries = chart.addSeries(window.LightweightCharts.CandlestickSeries, opts);
|
||||
return !!candleSeries;
|
||||
}
|
||||
return !!candleSeries;
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
# 界面与风控更新说明(OKX 实例)
|
||||
|
||||
与 Gate / Binance 主站对齐的列表窗、统计分品类、交易记录展示、复盘与移动保本交易所同步;OKX 仍为 **三页导航**(交易执行 / 记录复盘 / 统计),关键位监控合并在 **交易执行** 页,**无** Gate 独立「关键位监控」页与斐波限价监控。
|
||||
|
||||
## 顶栏导航(3 项)
|
||||
|
||||
| 顺序 | 名称 | 路由 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 1 | 交易执行 | `/trade` | 关键位监控 + 实盘下单(**默认首页** `/` → `/trade`) |
|
||||
| 2 | 交易记录与复盘 | `/records` | 交易记录、复盘表单、AI 历史(受顶栏 UTC 时间窗筛选) |
|
||||
| 3 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
|
||||
|
||||
## 列表时间窗(UTC,全站顶栏)
|
||||
|
||||
共用模块:仓库根目录 `history_window_lib.py`(与 Gate / Binance 一致)。
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 默认 | **UTC 当日**(`win_preset=utc_today`) |
|
||||
| 可选 | 近 24 小时、近 7 天、自定义起止(UTC) |
|
||||
| 作用范围 | 关键位历史、交易记录列表、复盘 API、AI 历史 API、导出「交易记录」「关键位历史」 |
|
||||
| 与统计 | **仅影响列表/导出**;统计页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切日 |
|
||||
| 切换 | 顶栏「列表筛选(UTC)」→ 应用(保留当前路由 query) |
|
||||
|
||||
## 交易记录与复盘
|
||||
|
||||
- 列表 **止损(开仓)**:展示 `initial_stop_loss` 快照(`display_open_stop_loss`)。
|
||||
- 类型列显示 `monitor_type` 与 `key_signal_type`(若有)。
|
||||
- 平仓入库:`stop_loss` / `initial_stop_loss` 为开仓止损快照;机器单 `entry_reason` 可按 `key_signal_type` 自动映射(箱体突破 / 收敛突破 → 四条固定关键位开仓类型文案)。
|
||||
- 复盘:开仓类型下拉含四条关键位固定文案 +「其他」;离场触发含 **「止盈」**;从交易记录填入时按结果与信号预填。
|
||||
- 复盘 K 线图:以 **平仓时间** 为锚点向前约 `ORDER_CHART_LIMIT`(默认 100)根(`_fetch_ohlcv_ending_at`)。
|
||||
- `/api/journals`、`/api/reviews` 与顶栏 UTC 窗一致。
|
||||
|
||||
### 导出(交易记录 v3)
|
||||
|
||||
- 文件名:`trade_records_v3_YYYYMMDD.csv`
|
||||
- 含 `key_signal_type`、`initial_stop_loss`、计划/实际 RR、`risk_amount` 等;末列「开仓类型」为有效展示文案。
|
||||
- 受 UTC 列表窗限制;关键位历史导出同理。
|
||||
|
||||
## 实盘下单(交易执行页)
|
||||
|
||||
- **移动保本**:表单可勾选「启用移动保本」;触发阶梯上移后 **先撤后挂** 交易所 TP/SL(`replace_active_monitor_tpsl_on_exchange`),仅成功后才写库;企业微信提示含「交易所:已先撤后挂止盈止损」。未配置实盘 API 时仅更新本地止损。
|
||||
- 开仓 TP/SL 仍通过 OKX `attachAlgoOrds`(与原有逻辑一致);重挂使用 ccxt `stopLoss` / `takeProfit` 参数,触发价经 `_okx_algo_trigger_price_str` 格式化。
|
||||
|
||||
## 统计分析页(`/stats`)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 切日 | 北京时间;边界 = `TRADING_DAY_RESET_HOUR:00`(默认 8) |
|
||||
| 品类下拉 | 全部交易、下单监控、关键位箱体突破、关键位收敛结构、关键位斐波0.618、关键位斐波0.786 |
|
||||
| URL | `stats_segment=`(`all` / `manual` / `key_box` / `key_conv` / `key_fib618` / `key_fib786`) |
|
||||
| 与 UTC 窗 | 统计 **不** 随顶栏列表窗变化 |
|
||||
|
||||
## 斐波关键位监控(与 Gate / Binance 对齐)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 类型 | **斐波回调0.618**、**斐波回调0.786**(交易执行页关键位表单) |
|
||||
| 同币互斥 | 每币仅一条斐波监控 |
|
||||
| 挂单价 E | 做多 `E = H − ratio×(H−L)`;做空 `E = L + ratio×(H−L)`;SL/TP 为 L/H |
|
||||
| 添加后 | 立即在 OKX 挂限价单;卡片显示 **挂E**、限价单 ID |
|
||||
| 失效 | 标记价触达止盈侧且限价未成交 → 仅撤本条限价单(`cancel_fib_limit_order`) |
|
||||
| 成交后 | 挂交易所 TP/SL → 写入 `order_monitors`(`monitor_type=关键位监控`,`key_signal_type=斐波回调…`)→ 从关键位表移除 |
|
||||
| 轮询 | `check_fib_key_monitors()`(与箱体/收敛 `check_key_monitors()` 分离) |
|
||||
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5) |
|
||||
| 日成交量 | 排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30) |
|
||||
|
||||
计算逻辑见仓库根目录 `fib_key_monitor_lib.py`。
|
||||
|
||||
## 与 Gate 的差异(其余)
|
||||
|
||||
- 无独立「关键位监控」导航页(斐波在 **交易执行** 页添加)。
|
||||
- 无交易所已实现盈亏同步(`/api/sync_exchange_pnl`)。
|
||||
- 箱体/收敛仍为 **提醒** 模式,不自动市价开仓(Gate/Binance 主站为自动开仓)。
|
||||
|
||||
## 配置与部署
|
||||
|
||||
- 详见 `.env.example` 中 OKX(`OKX_*`)与通用风控项。
|
||||
- 代码更新后请 **重启 OKX 监控进程**;旧库行不做批量回填,展示字段有则用之、无则回退。
|
||||
@@ -0,0 +1,349 @@
|
||||
# `crypto_monitor` 本地部署 + 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
|
||||
- 用 `pm2` 托管 **SSH 隧道** 与 **Flask 应用**(你也可以只用 `screen`,但本文按你要求用 PM2)
|
||||
|
||||
> 安全提醒:不要把 `.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` 的目录)执行:
|
||||
|
||||
```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 .
|
||||
```
|
||||
|
||||
下载 `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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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` 后重填。
|
||||
|
||||
### 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
|
||||
|
||||
# Ollama(如本机跑)
|
||||
OLLAMA_API=http://127.0.0.1:11434/api/generate
|
||||
AI_MODEL=你的模型名
|
||||
```
|
||||
|
||||
> `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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 再执行”的一键命令。
|
||||
Reference in New Issue
Block a user