Initial standalone crypto_okx with one-click deploy.

Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-13 20:00:59 +08:00
commit a1abe159fa
238 changed files with 69214 additions and 0 deletions
+109
View File
@@ -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"
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""首次部署:自动生成登录会话密钥与初始账号(仅空/占位时写入,不覆盖已有).
- FLASK_SECRET_KEY
- APP_USERNAME=admin / APP_PASSWORD=admin123(仅空时)
"""
from __future__ import annotations
import argparse
import os
import secrets
import sys
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO not in sys.path:
sys.path.insert(0, _REPO)
from lib.env.env_file_lib import apply_env_updates, env_get, read_env_lines
FLASK_PLACEHOLDERS = frozenset(
{"", "CHANGE_TO_LONG_RANDOM_SECRET", "crypto_monitor_2026_secret_key"}
)
PASSWORD_PLACEHOLDERS = frozenset({"", "CHANGE_ME_STRONG_PASSWORD"})
def main() -> int:
parser = argparse.ArgumentParser(description="Bootstrap deploy secrets for crypto_okx")
parser.add_argument("--dry-run", action="store_true", help="只打印将写入的项,不改文件")
args = parser.parse_args()
env_path = os.path.join(_REPO, ".env")
if not os.path.isfile(env_path):
print(f" 跳过: 未找到 {env_path}")
return 0
lines = read_env_lines(env_path)
updates: dict[str, str] = {}
cur_secret = env_get(lines, "FLASK_SECRET_KEY")
if (cur_secret or "").strip() in FLASK_PLACEHOLDERS:
updates["FLASK_SECRET_KEY"] = secrets.token_urlsafe(48)
if not (env_get(lines, "APP_USERNAME") or "").strip():
updates["APP_USERNAME"] = "admin"
cur_pass = env_get(lines, "APP_PASSWORD")
if (cur_pass or "").strip() in PASSWORD_PLACEHOLDERS:
updates["APP_PASSWORD"] = "admin123"
if not updates:
print(" 密钥已存在,无需写入")
return 0
print(f" 将更新 {env_path}: {', '.join(updates.keys())}")
if args.dry_run:
for k, v in updates.items():
shown = v if k != "APP_PASSWORD" and "SECRET" not in k else "***"
print(f" {k}={shown}")
return 0
apply_env_updates(env_path, updates)
print(" 已写入初始密钥/账号(请尽快修改 APP_PASSWORD)")
return 0
if __name__ == "__main__":
sys.exit(main())
+108
View File
@@ -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())
+38
View File
@@ -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
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
python scripts/verify_okx_funding.py
打印 OKX_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("OKX_API_KEY") or "").strip()
s = (os.getenv("OKX_API_SECRET") or "").strip()
if not k or "REPLACE" in k.upper():
print("WARN: OKX_API_KEY 为空或仍像占位符,请核对 .env")
if not s or "REPLACE" in s.upper():
print("WARN: OKX_API_SECRET 为空或仍像占位符,请核对 .env")
print("OKX_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_okx_funding_usdt()
print(">>> _fetch_okx_funding_usdt() =", fu)
try:
sw = mod._fetch_okx_swap_usdt_total()
print(">>> _fetch_okx_swap_usdt_total() (合约账户) =", sw)
sf = mod._fetch_okx_swap_usdt_free()
print(">>> _fetch_okx_swap_usdt_free() (合约可用) =", sf)
except Exception as e:
print(">>> swap balance fetch error:", e)
if __name__ == "__main__":
main()