first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
{% if page == 'key_monitor' %}
|
||||
<motion class="dual-panel-grid" style="grid-column:1/-1">
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Daily backup: SQLite DB + static/images → /root/backups/<instance>/<YYYY-MM-DD>/
|
||||
# Prune backup folders older than RETENTION_DAYS (default 30).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
BACKUP_ROOT="${BACKUP_ROOT:-/root/backups}"
|
||||
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
||||
INSTANCE_NAME="${BACKUP_INSTANCE:-$(basename "$PROJECT_DIR")}"
|
||||
TZ_NAME="${BACKUP_TZ:-Asia/Shanghai}"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(TZ="$TZ_NAME" date '+%Y-%m-%d %H:%M:%S %Z')" "$*"
|
||||
}
|
||||
|
||||
read_env_var() {
|
||||
local key="$1"
|
||||
local default="$2"
|
||||
local line
|
||||
if [[ ! -f .env ]]; then
|
||||
printf '%s' "$default"
|
||||
return
|
||||
fi
|
||||
line="$(grep -E "^${key}=" .env 2>/dev/null | tail -1 || true)"
|
||||
if [[ -z "$line" ]]; then
|
||||
printf '%s' "$default"
|
||||
return
|
||||
fi
|
||||
printf '%s' "${line#*=}" | tr -d '\r'
|
||||
}
|
||||
|
||||
resolve_project_path() {
|
||||
local p="$1"
|
||||
if [[ "$p" == /* ]]; then
|
||||
printf '%s' "$p"
|
||||
else
|
||||
printf '%s' "$PROJECT_DIR/$p"
|
||||
fi
|
||||
}
|
||||
|
||||
prune_old_backups() {
|
||||
local base="$BACKUP_ROOT/$INSTANCE_NAME"
|
||||
[[ -d "$base" ]] || return 0
|
||||
local cutoff
|
||||
cutoff="$(TZ="$TZ_NAME" date -d "-${RETENTION_DAYS} days" +%Y-%m-%d 2>/dev/null || true)"
|
||||
if [[ -z "$cutoff" ]]; then
|
||||
find "$base" -mindepth 1 -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" -print0 |
|
||||
xargs -r -0 rm -rf
|
||||
return 0
|
||||
fi
|
||||
local dir name
|
||||
for dir in "$base"/*/; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
name="$(basename "$dir")"
|
||||
[[ "$name" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || continue
|
||||
if [[ "$name" < "$cutoff" ]]; then
|
||||
log "prune: remove $dir (older than ${RETENTION_DAYS} days)"
|
||||
rm -rf "$dir"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
DB_REL="$(read_env_var DB_PATH crypto.db)"
|
||||
UPLOAD_REL="$(read_env_var UPLOAD_DIR static/images)"
|
||||
BACKUP_ROOT="$(read_env_var BACKUP_ROOT "$BACKUP_ROOT")"
|
||||
RETENTION_DAYS="$(read_env_var BACKUP_RETENTION_DAYS "$RETENTION_DAYS")"
|
||||
INSTANCE_NAME="$(read_env_var BACKUP_INSTANCE "$INSTANCE_NAME")"
|
||||
|
||||
DB_PATH="$(resolve_project_path "$DB_REL")"
|
||||
UPLOAD_DIR="$(resolve_project_path "$UPLOAD_REL")"
|
||||
DATE_TAG="$(TZ="$TZ_NAME" date +%Y-%m-%d)"
|
||||
DEST="$BACKUP_ROOT/$INSTANCE_NAME/$DATE_TAG"
|
||||
|
||||
if [[ ! -f "$DB_PATH" ]]; then
|
||||
log "error: database not found: $DB_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST"
|
||||
log "start backup instance=$INSTANCE_NAME dest=$DEST"
|
||||
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 "$DB_PATH" ".backup '$DEST/crypto.db'"
|
||||
log "db: sqlite3 backup -> $DEST/crypto.db"
|
||||
else
|
||||
cp -a "$DB_PATH" "$DEST/crypto.db"
|
||||
log "db: cp -> $DEST/crypto.db (sqlite3 not installed)"
|
||||
fi
|
||||
|
||||
if [[ -d "$UPLOAD_DIR" ]]; then
|
||||
tar -czf "$DEST/static_images.tar.gz" -C "$(dirname "$UPLOAD_DIR")" "$(basename "$UPLOAD_DIR")"
|
||||
log "images: $UPLOAD_DIR -> $DEST/static_images.tar.gz"
|
||||
else
|
||||
log "warn: upload dir missing, skip images: $UPLOAD_DIR"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "instance=$INSTANCE_NAME"
|
||||
echo "project_dir=$PROJECT_DIR"
|
||||
echo "backup_date=$DATE_TAG"
|
||||
echo "db_path=$DB_PATH"
|
||||
echo "upload_dir=$UPLOAD_DIR"
|
||||
} >"$DEST/manifest.txt"
|
||||
|
||||
prune_old_backups
|
||||
log "done"
|
||||
@@ -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())
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install daily backup cron: Beijing 00:00 (CRON_TZ=Asia/Shanghai).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BACKUP_SCRIPT="$SCRIPT_DIR/backup_data.sh"
|
||||
INSTANCE_NAME="${BACKUP_INSTANCE:-$(basename "$PROJECT_DIR")}"
|
||||
LOG_FILE="${BACKUP_CRON_LOG:-/var/log/crypto-monitor-backup-${INSTANCE_NAME}.log}"
|
||||
if [[ ! -x "$BACKUP_SCRIPT" ]]; then
|
||||
chmod +x "$BACKUP_SCRIPT"
|
||||
fi
|
||||
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "$TMP"' EXIT
|
||||
|
||||
{
|
||||
crontab -l 2>/dev/null | grep -vF "$BACKUP_SCRIPT" || true
|
||||
echo "CRON_TZ=Asia/Shanghai"
|
||||
echo "0 0 * * * $BACKUP_SCRIPT >> $LOG_FILE 2>&1"
|
||||
} >"$TMP"
|
||||
|
||||
# Keep a single CRON_TZ line at top.
|
||||
awk '
|
||||
BEGIN { tz = 0 }
|
||||
/^CRON_TZ=Asia\/Shanghai$/ {
|
||||
if (tz++) next
|
||||
}
|
||||
{ print }
|
||||
' "$TMP" >"${TMP}.2"
|
||||
mv "${TMP}.2" "$TMP"
|
||||
|
||||
crontab "$TMP"
|
||||
echo "Installed cron for $INSTANCE_NAME"
|
||||
echo " Schedule : daily 00:00 Asia/Shanghai"
|
||||
echo " Script : $BACKUP_SCRIPT"
|
||||
echo " Log : $LOG_FILE"
|
||||
crontab -l | grep -F "$BACKUP_SCRIPT" || true
|
||||
@@ -0,0 +1,358 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Patch index.html layout for key_monitor / trade split."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
TAG = "div"
|
||||
|
||||
PATHS = [
|
||||
Path(__file__).resolve().parent.parent / "templates" / "index.html",
|
||||
Path(r"c:\Users\dekun\Desktop\crypto_monitor\crypto_monitor_gate\templates\index.html"),
|
||||
]
|
||||
|
||||
KEY_START = " {% if page == 'key_monitor' %}"
|
||||
KEY_START_ALT = " {% if page == 'trade' %}"
|
||||
RECORDS_START = " {% if page == 'records' %}"
|
||||
|
||||
|
||||
def build_section(order_loop: str) -> str:
|
||||
t = TAG
|
||||
return f""" {{% if page == 'key_monitor' %}}
|
||||
<{t} class="dual-panel-grid" style="grid-column:1/-1">
|
||||
<{t} class="card">
|
||||
<{t} style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
||||
<h2 style="margin-bottom:0">关键位监控</h2>
|
||||
{{% if focus_key_id %}}
|
||||
<a href="/key_focus?key_id={{{{ focus_key_id }}}}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(默认200根)</a>
|
||||
{{% else %}}
|
||||
<a href="/key_focus" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">输入币种查看K线</a>
|
||||
{{% endif %}}
|
||||
</{t}>
|
||||
<form id="key-form" action="/add_key" method="post" class="form-row">
|
||||
<input name="symbol" placeholder="BTC 或 BTC/USDT" required>
|
||||
<select name="type" required>
|
||||
<option value="箱体突破">箱体突破</option>
|
||||
<option value="收敛突破">收敛突破</option>
|
||||
<option value="关键阻力位">关键阻力位</option>
|
||||
<option value="关键支撑位">关键支撑位</option>
|
||||
</select>
|
||||
<select name="direction" required>
|
||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||
</select>
|
||||
<input name="upper" step="0.0001" placeholder="上沿/阻力" required>
|
||||
<input name="lower" step="0.0001" placeholder="下沿/支撑" required>
|
||||
<button type="submit">添加</button>
|
||||
</form>
|
||||
<{t} class="rule-tip">{{{{ key_gate_rule_text }}}}</{t}>
|
||||
<{t} class="panel-scroll pos-list">
|
||||
{{% for k in key %}}
|
||||
<{t} class="pos-card" id="key-row-{{{{ k.id }}}}">
|
||||
<{t} class="pos-card-head">
|
||||
<{t} class="pos-card-symbol">
|
||||
<strong>{{{{ k.symbol }}}}</strong>
|
||||
<span class="pos-side-badge {{{{ 'pos-side-long' if k.direction == 'long' else 'pos-side-short' }}}}">{{{{ '做多' if k.direction == 'long' else '做空' }}}}</span>
|
||||
<span class="badge direction" style="margin-left:4px">{{{{ k.monitor_type }}}}</span>
|
||||
</{t}>
|
||||
<button type="button" class="pos-close-btn" style="border:none;cursor:pointer" onclick="deleteKeyMonitor({{{{ k.id }}}})">删</button>
|
||||
</{t}>
|
||||
<{t} class="pos-meta">
|
||||
<span class="pos-meta-item">上沿: {{{{ k.upper }}}}</span>
|
||||
<span class="pos-meta-item">下沿: {{{{ k.lower }}}}</span>
|
||||
<span class="pos-meta-item">已提醒: {{{{ k.notification_count or 0 }}}}/{{{{ k.max_notify or 3 }}}}</span>
|
||||
</{t}>
|
||||
<{t} class="pos-grid">
|
||||
<{t} class="pos-cell"><span class="pos-label">现价</span><span class="pos-value" id="key-price-{{{{ k.id }}}}">-</span></{t}>
|
||||
<{t} class="pos-cell"><span class="pos-label">距上沿</span><span class="pos-value" id="key-up-diff-{{{{ k.id }}}}">-</span></{t}>
|
||||
<{t} class="pos-cell"><span class="pos-label">距下沿</span><span class="pos-value" id="key-low-diff-{{{{ k.id }}}}">-</span></{t}>
|
||||
<{t} class="pos-cell"><span class="pos-label">门控</span><span class="pos-value" id="key-gate-{{{{ k.id }}}}" style="color:#9aa">-</span></{t}>
|
||||
</{t}>
|
||||
<{t} class="pos-meta" style="margin-top:8px"><span class="pos-meta-item" id="key-gate-metrics-{{{{ k.id }}}}" style="color:#8fc8ff"></span></{t}>
|
||||
</{t}>
|
||||
{{% else %}}
|
||||
<{t} class="pos-empty">暂无监控中的关键位</{t}>
|
||||
{{% endfor %}}
|
||||
</{t}>
|
||||
</{t}>
|
||||
<{t} class="card">
|
||||
<h2 style="margin-bottom:8px">关键位历史</h2>
|
||||
<{t} class="sub" style="font-size:.72rem;color:#8892b0;margin-bottom:8px">失效或已结案的关键位</{t}>
|
||||
<{t} class="panel-scroll pos-list">
|
||||
{{% for h in key_history %}}
|
||||
<{t} class="pos-card">
|
||||
<{t} class="pos-card-head">
|
||||
<{t} class="pos-card-symbol">
|
||||
<strong>{{{{ h.symbol }}}}</strong>
|
||||
<span class="pos-side-badge {{{{ 'pos-side-long' if h.direction == 'long' else 'pos-side-short' }}}}">{{{{ '做多' if h.direction == 'long' else '做空' }}}}</span>
|
||||
</{t}>
|
||||
<button type="button" class="table-del" onclick="deleteKeyHistory({{{{ h.id }}}})">删除</button>
|
||||
</{t}>
|
||||
<{t} class="pos-meta">
|
||||
<span class="pos-meta-item">{{{{ h.monitor_type }}}}</span>
|
||||
<span class="pos-meta-item">{{{{ h.close_reason }}}}</span>
|
||||
<span class="pos-meta-item">{{{{ (h.closed_at or '-')[:16] }}}}</span>
|
||||
</{t}>
|
||||
<{t} class="pos-meta">
|
||||
<span class="pos-meta-item">上: {{{{ h.upper }}}} 下: {{{{ h.lower }}}}</span>
|
||||
<span class="pos-meta-item">提醒: {{{{ h.notification_count }}}}</span>
|
||||
</{t}>
|
||||
{{% if h.last_alert_message %}}<{t} style="font-size:.75rem;color:#aab;margin-top:6px;white-space:pre-wrap">{{{{ h.last_alert_message[:180] }}}}{{% if h.last_alert_message|length > 180 %}}…{{% endif %}}</{t}>{{% endif %}}
|
||||
</{t}>
|
||||
{{% else %}}
|
||||
<{t} class="pos-empty">暂无历史</{t}>
|
||||
{{% endfor %}}
|
||||
</{t}>
|
||||
</{t}>
|
||||
</{t}>
|
||||
{{% elif page == 'trade' %}}
|
||||
<{t} class="dual-panel-grid" style="grid-column:1/-1">
|
||||
<{t} class="card">
|
||||
<{t} style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
||||
<h2 style="margin-bottom:0">实盘下单监控</h2>
|
||||
{{% if focus_order_id %}}
|
||||
<a href="/order_focus?order_id={{{{ focus_order_id }}}}" class="btn-del" style="text-decoration:none;background:#1f3a5a;color:#8fc8ff">放大查看K线(100根)</a>
|
||||
{{% else %}}
|
||||
<span class="btn-del" style="background:#2f2f44;color:#9aa;cursor:not-allowed">暂无持仓可放大</span>
|
||||
{{% endif %}}
|
||||
</{t}>
|
||||
<{t} class="rule-tip" id="order-rule-tip">
|
||||
规则:最多 {{{{ max_active_positions }}}} 仓;BTC {{{{ btc_leverage }}}}x / 山寨 {{{{ alt_leverage }}}}x;
|
||||
{{% if can_trade %}}可开仓{{% else %}}不可开仓(持仓已满或未到北京时间 {{{{ reset_hour }}}}:00){{% endif %}};
|
||||
人工开仓盈亏比不得低于 {{{{ manual_min_planned_rr }}}}:1
|
||||
</{t}>
|
||||
<{t} class="rule-tip">
|
||||
以损定仓:风险 {{{{ risk_percent }}}}% |移动保本:下单可勾选关闭;开启时 {{{{ breakeven_rr_trigger }}}}R 触发(每 1R 阶梯上移),偏移 {{{{ breakeven_offset_pct }}}}%
|
||||
</{t}>
|
||||
<{t} class="rule-tip">
|
||||
划转:自动划转 {{{{ '开启' if auto_transfer_enabled else '关闭' }}}}(每天<strong>北京时间 {{{{ auto_transfer_bj_hour }}}}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{{{ auto_transfer_to }}}} 补足到 {{{{ auto_transfer_amount }}}}U,来自 {{{{ auto_transfer_from }}}})
|
||||
</{t}>
|
||||
<form action="/manual_transfer" method="post" class="form-row">
|
||||
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
|
||||
<select name="from_account">
|
||||
<option value="funding" {{% if auto_transfer_from == 'funding' %}}selected{{% endif %}}>from: funding</option>
|
||||
<option value="swap" {{% if auto_transfer_from == 'swap' %}}selected{{% endif %}}>from: swap</option>
|
||||
<option value="spot" {{% if auto_transfer_from == 'spot' %}}selected{{% endif %}}>from: spot</option>
|
||||
</select>
|
||||
<select name="to_account">
|
||||
<option value="swap" {{% if auto_transfer_to == 'swap' %}}selected{{% endif %}}>to: swap</option>
|
||||
<option value="funding" {{% if auto_transfer_to == 'funding' %}}selected{{% endif %}}>to: funding</option>
|
||||
<option value="spot" {{% if auto_transfer_to == 'spot' %}}selected{{% endif %}}>to: spot</option>
|
||||
</select>
|
||||
<button type="submit">手动划转</button>
|
||||
</form>
|
||||
<form id="add-order-form" action="/add_order" method="post" class="form-row">
|
||||
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required>
|
||||
<select id="order-direction" name="direction" required>
|
||||
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
|
||||
</select>
|
||||
<select id="sltp-mode" name="sltp_mode">
|
||||
<option value="price">止盈止损:价格模式</option>
|
||||
<option value="pct">止盈止损:百分比模式</option>
|
||||
</select>
|
||||
<select name="trade_style" required>
|
||||
<option value="trend">趋势单</option>
|
||||
<option value="swing">波段单</option>
|
||||
</select>
|
||||
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
|
||||
</label>
|
||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
||||
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
|
||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||
<button type="submit">开仓(以损定仓)</button>
|
||||
</form>
|
||||
</{t}>
|
||||
<{t} class="card">
|
||||
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||
<{t} class="panel-scroll pos-list">
|
||||
{order_loop}
|
||||
</{t}>
|
||||
</{t}>
|
||||
</{t}>
|
||||
{{% endif %}}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def patch_nav(text: str) -> str:
|
||||
old = '<a href="/trade" class="{% if page == \'trade\' %}active{% endif %}">交易执行</a>'
|
||||
new = (
|
||||
'<a href="/key_monitor" class="{% if page == \'key_monitor\' %}active{% endif %}">关键位监控</a>\n'
|
||||
' <a href="/trade" class="{% if page == \'trade\' %}active{% endif %}">实盘下单</a>'
|
||||
)
|
||||
if "关键位监控" not in text:
|
||||
text = text.replace(old, new)
|
||||
return text
|
||||
|
||||
|
||||
def patch_js(text: str) -> str:
|
||||
# page id on body
|
||||
if 'id="page-trade"' not in text:
|
||||
text = text.replace("<body>", '<body data-page="{{ page }}">', 1)
|
||||
if "MANUAL_MIN_PLANNED_RR" not in text:
|
||||
insert = """
|
||||
const MANUAL_MIN_PLANNED_RR = {{ manual_min_planned_rr }};
|
||||
function calcClientRr(direction, entry, sl, tp){
|
||||
const e = Number(entry), s = Number(sl), t = Number(tp);
|
||||
if(!Number.isFinite(e) || !Number.isFinite(s) || !Number.isFinite(t)) return null;
|
||||
if(direction === 'short'){
|
||||
if(s <= e || t >= e) return null;
|
||||
return (e - t) / (s - e);
|
||||
}
|
||||
if(s >= e || t <= e) return null;
|
||||
return (t - e) / (e - s);
|
||||
}
|
||||
"""
|
||||
text = text.replace("let latestAvailableUsdt = null;", insert + "\nlet latestAvailableUsdt = null;")
|
||||
if "add-order-form" not in text or "calcClientRr" in text and "addOrderForm" not in text:
|
||||
hook = """
|
||||
const addOrderForm = document.getElementById("add-order-form");
|
||||
if(addOrderForm){
|
||||
addOrderForm.addEventListener("submit", function(ev){
|
||||
const direction = (document.getElementById("order-direction")||{}).value || "long";
|
||||
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
|
||||
let sl, tp, entry;
|
||||
if(mode === "pct"){
|
||||
alert("百分比模式请确认盈亏比后再提交;建议使用价格模式以便校验。");
|
||||
return;
|
||||
}
|
||||
sl = Number((document.getElementById("order-sl")||{}).value);
|
||||
tp = Number((document.getElementById("order-tp")||{}).value);
|
||||
entry = sl;
|
||||
fetch(`/api/order_defaults?symbol=${encodeURIComponent((document.getElementById("order-symbol")||{}).value||"")}&direction=${encodeURIComponent(direction)}`)
|
||||
.then(r=>r.json())
|
||||
.then(data=>{
|
||||
const px = data.last_price || data.price;
|
||||
if(px) entry = Number(px);
|
||||
const rr = calcClientRr(direction, entry, sl, tp);
|
||||
if(rr === null || rr < MANUAL_MIN_PLANNED_RR){
|
||||
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单。`);
|
||||
return;
|
||||
}
|
||||
addOrderForm.submit();
|
||||
})
|
||||
.catch(()=>{ ev.preventDefault(); alert("无法校验盈亏比,请稍后重试"); });
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
"""
|
||||
text = text.replace("refreshOrderDefaults();", hook + "\nrefreshOrderDefaults();")
|
||||
if "max_active_positions" not in text and "order-rule-tip" in text:
|
||||
text = text.replace(
|
||||
"规则:单仓;",
|
||||
"规则:最多 {{ max_active_positions }} 仓;",
|
||||
)
|
||||
# account snapshot tip
|
||||
old_tip = '`规则:单仓;BTC {{ btc_leverage }}x'
|
||||
if old_tip in text:
|
||||
text = text.replace(
|
||||
old_tip,
|
||||
"`规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x",
|
||||
)
|
||||
text = text.replace(
|
||||
'const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00)";',
|
||||
'const canTradeText = data.can_trade ? "可开仓" : `不可开仓(持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}} 或未到北京时间 {{ reset_hour }}:00)`;',
|
||||
)
|
||||
text = text.replace(
|
||||
"if(!data.in_top30){",
|
||||
"const rankMax = data.rank_max || 30;\n if(!data.in_top30){",
|
||||
)
|
||||
text = text.replace(
|
||||
"不在前30,已拦截",
|
||||
"不在前${rankMax},已拦截",
|
||||
)
|
||||
# conditional price refresh
|
||||
if "data-page" in text and "refreshPriceSnapshotConditional" not in text:
|
||||
text = text.replace(
|
||||
"setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});",
|
||||
"""function refreshPriceSnapshotConditional(){
|
||||
const page = document.body.getAttribute("data-page") || "";
|
||||
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
|
||||
const updatedEl = document.getElementById("price-last-updated");
|
||||
if(data.updated_at && updatedEl) updatedEl.innerText = data.updated_at;
|
||||
if(page === "key_monitor"){
|
||||
(data.key_prices || []).forEach(k=>{
|
||||
const pEl = document.getElementById(`key-price-${k.id}`);
|
||||
if(pEl){ pEl.innerText = k.price_display || (Number.isFinite(Number(k.price)) ? Number(k.price).toFixed(6) : "-"); paintPriceTrend(pEl, `k-${k.id}`, Number(k.price)); }
|
||||
const upEl = document.getElementById(`key-up-diff-${k.id}`);
|
||||
if(upEl) upEl.innerText = `${formatSigned(k.upper_diff, 4)} (${formatSigned(k.upper_pct, 2)}%)`;
|
||||
const lowEl = document.getElementById(`key-low-diff-${k.id}`);
|
||||
if(lowEl) lowEl.innerText = `${formatSigned(k.lower_diff, 4)} (${formatSigned(k.lower_pct, 2)}%)`;
|
||||
const gateEl = document.getElementById(`key-gate-${k.id}`);
|
||||
if(gateEl){ gateEl.innerText = k.gate_summary || "-"; gateEl.style.color = k.gate_ok ? "#4cd97f" : "#ff8f8f"; }
|
||||
const gateMetricEl = document.getElementById(`key-gate-metrics-${k.id}`);
|
||||
if(gateMetricEl) gateMetricEl.innerText = k.gate_metrics || "";
|
||||
});
|
||||
}
|
||||
if(page === "trade"){
|
||||
(data.order_prices || []).forEach(o=>{
|
||||
const pEl = document.getElementById(`order-price-${o.id}`);
|
||||
if(pEl){
|
||||
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
|
||||
let disp = "";
|
||||
if(hasMark && o.exchange_mark_price_display) disp = o.exchange_mark_price_display;
|
||||
else if(o.price_display) disp = o.price_display;
|
||||
else { const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price); disp = Number.isFinite(px) ? px.toFixed(6) : "-"; }
|
||||
pEl.innerText = disp;
|
||||
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
|
||||
}
|
||||
const exM = document.getElementById(`order-ex-margin-${o.id}`);
|
||||
if(exM){
|
||||
const mv = o.exchange_initial_margin;
|
||||
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||
if(!Number.isNaN(mn)) exM.innerText = `${mn.toFixed(2)}U`;
|
||||
else { const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; exM.innerText = (prc === 0) ? "无仓数据" : "-"; }
|
||||
}
|
||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||
if(pnlEl){
|
||||
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
|
||||
pnlEl.classList.remove("price-up","price-down","price-flat");
|
||||
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
|
||||
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
|
||||
else pnlEl.classList.add("price-flat");
|
||||
}
|
||||
const rrEl = document.getElementById(`order-rr-${o.id}`);
|
||||
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
|
||||
});
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});""",
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def main():
|
||||
for path in PATHS:
|
||||
if not path.exists():
|
||||
print("skip", path)
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
start = text.find(KEY_START)
|
||||
if start < 0:
|
||||
start = text.find(KEY_START_ALT)
|
||||
end = text.find(RECORDS_START)
|
||||
if start < 0 or end < 0:
|
||||
raise SystemExit(f"markers not found: {path}")
|
||||
old = text[start:end]
|
||||
m = re.search(r"(\{% for o in order %\}.*?\{% endfor %\})", old, re.S)
|
||||
if not m:
|
||||
raise SystemExit(f"order loop not found: {path}")
|
||||
order_loop = m.group(1)
|
||||
section = build_section(order_loop)
|
||||
section = section.replace("{{%", "{%").replace("%}}", "%}").replace("{{{{", "{{").replace("}}}}", "}}")
|
||||
out = text[:start] + section + "\n\n" + text[end:]
|
||||
out = patch_nav(out)
|
||||
out = patch_js(out)
|
||||
path.write_text(out, encoding="utf-8")
|
||||
print("patched", path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Apply binance app.py risk/layout changes to gate app.py (pattern replace)."""
|
||||
from pathlib import Path
|
||||
|
||||
binance = Path(__file__).resolve().parent.parent / "app.py"
|
||||
gate = Path(r"c:\Users\dekun\Desktop\crypto_monitor\crypto_monitor_gate\app.py")
|
||||
|
||||
b = binance.read_text(encoding="utf-8")
|
||||
g = gate.read_text(encoding="utf-8")
|
||||
|
||||
# 1) env block
|
||||
old_env = """KEY_BREAKOUT_LIMIT_PCT = float(os.getenv("KEY_BREAKOUT_LIMIT_PCT", "1.5"))
|
||||
KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR", "1.5"))
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT = float(os.getenv("KEY_STOP_OUTSIDE_BREAKOUT_PCT", "0.5"))"""
|
||||
|
||||
new_env = """KEY_AUTO_MIN_PLANNED_RR = float(os.getenv("KEY_AUTO_MIN_PLANNED_RR", "1.5"))
|
||||
KEY_STOP_OUTSIDE_BREAKOUT_PCT = float(os.getenv("KEY_STOP_OUTSIDE_BREAKOUT_PCT", "0.5"))
|
||||
MANUAL_MIN_PLANNED_RR = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
MAX_ACTIVE_POSITIONS = max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", "1")))
|
||||
KEY_VOLUME_MA_BARS = max(1, int(os.getenv("KEY_VOLUME_MA_BARS", "20")))
|
||||
KEY_VOLUME_RATIO_MIN = float(os.getenv("KEY_VOLUME_RATIO_MIN", "1.3"))
|
||||
KEY_BREAKOUT_AMP_MIN_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MIN_PCT", "0.03"))
|
||||
KEY_BREAKOUT_AMP_MAX_PCT = float(os.getenv("KEY_BREAKOUT_AMP_MAX_PCT", "0.5"))
|
||||
KEY_DAILY_VOLUME_RANK_MAX = max(1, int(os.getenv("KEY_DAILY_VOLUME_RANK_MAX", "30")))
|
||||
KEY_CONFIRM_BREAKOUT_BAR = int(os.getenv("KEY_CONFIRM_BREAKOUT_BAR", "-2"))
|
||||
KEY_CONFIRM_BAR = int(os.getenv("KEY_CONFIRM_BAR", "-1"))
|
||||
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT = os.getenv("KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT", "true").lower() == "true")"""
|
||||
|
||||
if old_env in g:
|
||||
g = g.replace(old_env, new_env)
|
||||
|
||||
# 2) DB migration snippet
|
||||
snip = """ try:
|
||||
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
c.execute("""
|
||||
if snip not in g and 'key_sizing_capital_snapshot' not in g:
|
||||
g = g.replace(
|
||||
' c.execute(\n """CREATE TABLE IF NOT EXISTS key_monitor_history',
|
||||
""" try:
|
||||
c.execute("ALTER TABLE trading_sessions ADD COLUMN key_sizing_capital_snapshot REAL")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
c.execute(
|
||||
\"\"\"CREATE TABLE IF NOT EXISTS key_monitor_history""",
|
||||
1,
|
||||
)
|
||||
|
||||
# 3) precheck block - extract from binance
|
||||
import re
|
||||
m = re.search(
|
||||
r"def get_active_position_count\(conn\):.*?return True, \"\"\n\n\ndef prepare_order_amount",
|
||||
b,
|
||||
re.S,
|
||||
)
|
||||
if m and "get_active_position_count" not in g:
|
||||
g = g.replace(
|
||||
"def precheck_risk(conn, symbol, direction):\n now = app_now()\n if not trading_day_reset_allows_new_open(now):\n return False, f\"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓\"\n active_count = conn.execute(\"SELECT COUNT(*) FROM order_monitors WHERE status='active'\").fetchone()[0]\n if active_count > 0:\n return False, \"一次只能持有一个仓位\"\n if direction not in (\"long\", \"short\"):\n return False, \"方向必须为 long 或 short\"\n if symbol.upper().startswith(\"BTC\") or symbol.upper().startswith(\"ETH\"):\n expected = BTC_LEVERAGE\n else:\n expected = ALT_LEVERAGE\n if expected <= 0:\n return False, \"杠杆配置异常\"\n return True, \"\"\n\n\ndef prepare_order_amount",
|
||||
m.group(0),
|
||||
)
|
||||
|
||||
# 4) render_main_page can_trade + template vars + route
|
||||
if "key_monitor_page" not in g:
|
||||
g = g.replace(
|
||||
" can_trade = trading_day_reset_allows_new_open(now) and active_count == 0\n conn.close()\n return render_template(",
|
||||
""" can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
|
||||
key_gate_rule_text = (
|
||||
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}、确认棒偏移 {KEY_CONFIRM_BAR}|"
|
||||
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}|"
|
||||
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}"
|
||||
)
|
||||
conn.close()
|
||||
return render_template(""",
|
||||
)
|
||||
g = g.replace(
|
||||
" exchange_display=EXCHANGE_DISPLAY_NAME,\n )\n\n\n@app.route(\"/\")\n@login_required\ndef index():\n return redirect(\"/trade\")\n\n\n@app.route(\"/trade\")",
|
||||
""" exchange_display=EXCHANGE_DISPLAY_NAME,
|
||||
max_active_positions=MAX_ACTIVE_POSITIONS,
|
||||
manual_min_planned_rr=MANUAL_MIN_PLANNED_RR,
|
||||
key_auto_min_planned_rr=KEY_AUTO_MIN_PLANNED_RR,
|
||||
key_gate_rule_text=key_gate_rule_text,
|
||||
kline_timeframe=KLINE_TIMEFRAME,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
return redirect("/trade")
|
||||
|
||||
|
||||
@app.route("/key_monitor")
|
||||
@login_required
|
||||
def key_monitor_page():
|
||||
return render_main_page("key_monitor")
|
||||
|
||||
|
||||
@app.route("/trade")""",
|
||||
)
|
||||
|
||||
# api account
|
||||
g = g.replace(
|
||||
" active_count = conn.execute(\"SELECT COUNT(*) FROM order_monitors WHERE status='active'\").fetchone()[0]\n conn.close()\n can_trade = trading_day_reset_allows_new_open(now) and active_count == 0",
|
||||
" active_count = get_active_position_count(conn)\n conn.close()\n can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS",
|
||||
)
|
||||
if '"max_active_positions"' not in g:
|
||||
g = g.replace(
|
||||
'"can_trade": can_trade,\n "trading_day": trading_day\n })',
|
||||
'"can_trade": can_trade,\n "max_active_positions": MAX_ACTIVE_POSITIONS,\n "manual_min_planned_rr": MANUAL_MIN_PLANNED_RR,\n "trading_day": trading_day\n })',
|
||||
)
|
||||
|
||||
gate.write_text(g, encoding="utf-8")
|
||||
print("gate app partially synced; manual review _key_hard_checks add_order still needed")
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
python scripts/verify_binance_funding.py
|
||||
|
||||
打印 BINANCE_API_KEY 前 8 位便于与 Binance 控制台核对(不含 Secret)。用于服务器自检。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, BASE)
|
||||
|
||||
|
||||
def load_env(path):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
for line in open(path, "r", encoding="utf-8", errors="ignore"):
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
k = k.strip().lstrip("\ufeff")
|
||||
if k.replace("_", "").isalnum():
|
||||
os.environ[k] = v.strip().strip('"').strip("'")
|
||||
|
||||
|
||||
def main():
|
||||
load_env(os.path.join(BASE, ".env"))
|
||||
k = (os.getenv("BINANCE_API_KEY") or "").strip()
|
||||
s = (os.getenv("BINANCE_API_SECRET") or "").strip()
|
||||
if not k or "REPLACE" in k.upper():
|
||||
print("WARN: BINANCE_API_KEY 为空或仍像占位符,请核对 .env")
|
||||
if not s or "REPLACE" in s.upper():
|
||||
print("WARN: BINANCE_API_SECRET 为空或仍像占位符,请核对 .env")
|
||||
print("BINANCE_API_KEY prefix (8 chars):", (k[:8] + "…") if len(k) > 8 else "(short)")
|
||||
|
||||
import app as mod # noqa: E402
|
||||
|
||||
mod.ensure_markets_loaded()
|
||||
fu = mod._fetch_binance_funding_usdt()
|
||||
print(">>> _fetch_binance_funding_usdt() =", fu)
|
||||
try:
|
||||
sw = mod._fetch_binance_swap_usdt_total()
|
||||
print(">>> _fetch_binance_swap_usdt_total() (合约账户) =", sw)
|
||||
sf = mod._fetch_binance_swap_usdt_free()
|
||||
print(">>> _fetch_binance_swap_usdt_free() (合约可用) =", sf)
|
||||
except Exception as e:
|
||||
print(">>> swap balance fetch error:", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user