first commit
This commit is contained in:
@@ -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"
|
||||
|
||||
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,93 @@
|
||||
"""
|
||||
在项目根目录执行(会加载根目录 .env):
|
||||
python scripts/verify_gate_funding.py
|
||||
|
||||
依次探测:[0] swap 余额(与 App「交易账户」同源);[1]–[3] 现货 / 统一账户资金路径。
|
||||
打印 GATE_API_KEY 前 8 位便于与 Gate 控制台核对(不含 Secret)。用于服务器自检。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if ROOT not in sys.path:
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
|
||||
def _load_app():
|
||||
path = os.path.join(ROOT, "app.py")
|
||||
spec = importlib.util.spec_from_file_location("crypto_app", path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(ROOT)
|
||||
mod = _load_app()
|
||||
print("LIVE_TRADING_ENABLED =", os.getenv("LIVE_TRADING_ENABLED"))
|
||||
ok, reason = mod.ensure_exchange_live_ready()
|
||||
print("ensure_exchange_live_ready =", ok, repr(reason))
|
||||
if not ok:
|
||||
print("跳过私有接口探测")
|
||||
return 1
|
||||
|
||||
mod.ensure_markets_loaded()
|
||||
|
||||
k = (os.getenv("GATE_API_KEY") or "").strip()
|
||||
s = (os.getenv("GATE_API_SECRET") or "").strip()
|
||||
if not k or "REPLACE" in k.upper():
|
||||
print("WARN: GATE_API_KEY 为空或仍像占位符,请核对 .env")
|
||||
if not s or "REPLACE" in s.upper():
|
||||
print("WARN: GATE_API_SECRET 为空或仍像占位符,请核对 .env")
|
||||
print("GATE_API_KEY prefix (8 chars):", (k[:8] + "…") if len(k) > 8 else "(short)")
|
||||
|
||||
# 0) swap — 与 App「交易账户」余额同源(优先看此项是否与网页一致)
|
||||
try:
|
||||
bal = mod.exchange.fetch_balance({"type": "swap"})
|
||||
v0 = mod._extract_usdt_total(bal)
|
||||
print("[0] fetch_balance(swap) USDT total =", v0)
|
||||
except Exception as e:
|
||||
print("[0] fetch_balance(swap) FAILED:", type(e).__name__, e)
|
||||
|
||||
# 1) fetch_balance spot + marginMode spot
|
||||
try:
|
||||
bal = mod.exchange.fetch_balance({"type": "spot", "marginMode": "spot"})
|
||||
v = mod._extract_usdt_total(bal)
|
||||
print("[1] fetch_balance(spot,marginMode=spot) USDT total =", v)
|
||||
except Exception as e:
|
||||
print("[1] fetch_balance(spot) FAILED:", type(e).__name__, e)
|
||||
|
||||
# 2) raw spot accounts
|
||||
try:
|
||||
resp = mod.exchange.privateSpotGetAccounts({})
|
||||
v2 = mod._parse_gate_spot_accounts_response_usdt(resp)
|
||||
print("[2] privateSpotGetAccounts USDT =", v2)
|
||||
except Exception as e:
|
||||
print("[2] privateSpotGetAccounts FAILED:", type(e).__name__, e)
|
||||
|
||||
# 3) unified accounts raw
|
||||
try:
|
||||
raw = mod.exchange.privateUnifiedGetAccounts({})
|
||||
body = raw
|
||||
if isinstance(body, dict) and isinstance(body.get("result"), dict):
|
||||
body = body["result"]
|
||||
if isinstance(body, dict):
|
||||
keys = sorted(body.keys())
|
||||
print("[3] unified top-level keys (sample):", keys[:25], "..." if len(keys) > 25 else "")
|
||||
v3 = mod._parse_usdt_from_gate_unified_accounts_body(body) if isinstance(body, dict) else None
|
||||
print("[3] parsed unified USDT =", v3)
|
||||
except Exception as e:
|
||||
print("[3] privateUnifiedGetAccounts FAILED:", type(e).__name__, e)
|
||||
|
||||
fu = mod._fetch_gate_funding_usdt()
|
||||
print(">>> _fetch_gate_funding_usdt() =", fu)
|
||||
f, t = mod.get_exchange_capitals(force=True)
|
||||
print(">>> get_exchange_capitals(force=True) funding, trading =", f, t)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user