diff --git a/deploy/pull_and_restart.sh b/deploy/pull_and_restart.sh new file mode 100644 index 0000000..7f10ff3 --- /dev/null +++ b/deploy/pull_and_restart.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# 服务器上拉代码、同步 env、应用强制清仓策略并重启 PM2。 +# 用法(/opt/crypto_monitor 下 root): +# bash deploy/pull_and_restart.sh +# bash deploy/pull_and_restart.sh --dry-run +set -euo pipefail + +REPO="${REPO:-/opt/crypto_monitor}" +DRY=() +if [[ "${1:-}" == "--dry-run" ]]; then + DRY=(--dry-run) + echo "(dry-run mode)" +fi + +cd "$REPO" +echo ">>> git pull" +git pull + +echo ">>> sync common trading env (binance + okx missing keys)" +python3 scripts/sync_common_trading_env.py "${DRY[@]}" + +echo ">>> force-close policy: gate=ON, binance/okx=OFF" +python3 scripts/sync_common_trading_env.py --apply-force-close-policy "${DRY[@]}" + +if [[ ${#DRY[@]} -gt 0 ]]; then + echo "(dry-run, skip pm2 restart)" + exit 0 +fi + +echo ">>> pm2 restart" +pm2 restart crypto_gate crypto_binance crypto_okx manual-trading-hub manual-agent-gate 2>/dev/null \ + || pm2 restart crypto-monitor-gate crypto-monitor-binance crypto-monitor-okx manual-trading-hub 2>/dev/null \ + || pm2 restart all + +echo ">>> FORCE_CLOSE settings" +grep -E '^FORCE_CLOSE_' crypto_monitor_gate/.env crypto_monitor_binance/.env crypto_monitor_okx/.env || true + +echo "done" diff --git a/docs/env-sync-scripts.md b/docs/env-sync-scripts.md index 4b31d7a..f23c496 100644 --- a/docs/env-sync-scripts.md +++ b/docs/env-sync-scripts.md @@ -111,9 +111,18 @@ python scripts/sync_four_exchange_position_sizing_env.py --set-buffer 0.98 python scripts/sync_common_trading_env.py python scripts/sync_common_trading_env.py --dry-run python scripts/sync_common_trading_env.py --instances crypto_monitor_okx +python scripts/sync_common_trading_env.py --apply-force-close-policy ``` -补全项含:`RECONCILE_*`、`PRICE_REFRESH_SECONDS`、`KEY_*` 门控、`FORCE_CLOSE_*`、`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT` 等。 +补全项含:`RECONCILE_*`、`PRICE_REFRESH_SECONDS`、`KEY_*` 门控、`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT` 等(**不含** `FORCE_CLOSE_*`,由 `--apply-force-close-policy` 单独处理)。 + +**强制清仓策略**(仅 Gate 开启): + +```bash +python scripts/sync_common_trading_env.py --apply-force-close-policy +``` + +或服务器一键:`bash deploy/pull_and_restart.sh` --- diff --git a/scripts/sync_common_trading_env.py b/scripts/sync_common_trading_env.py index 947e5d7..84c7c67 100644 --- a/scripts/sync_common_trading_env.py +++ b/scripts/sync_common_trading_env.py @@ -50,12 +50,26 @@ SHARED_DEFAULTS: dict[str, str] = { "RECONCILE_STARTUP_GRACE_SEC": "90", "RECONCILE_FLAT_CONFIRM_POLLS": "3", "FULL_MARGIN_BUFFER_RATIO": "0.98", - "FORCE_CLOSE_BJ_HOUR": "0", - "FORCE_CLOSE_ENABLED": "false", "WECHAT_TIMEOUT_SECONDS": "10", "AI_TIMEOUT_SECONDS": "120", } +# 仅 Gate 启用 0 点强制清仓;币安/OKX 须保持关闭 +FORCE_CLOSE_POLICY: dict[str, dict[str, str]] = { + "crypto_monitor_gate": { + "FORCE_CLOSE_ENABLED": "true", + "FORCE_CLOSE_BJ_HOUR": "0", + }, + "crypto_monitor_binance": { + "FORCE_CLOSE_ENABLED": "false", + "FORCE_CLOSE_BJ_HOUR": "0", + }, + "crypto_monitor_okx": { + "FORCE_CLOSE_ENABLED": "false", + "FORCE_CLOSE_BJ_HOUR": "0", + }, +} + def _parse_env(path: str) -> list[str]: if not os.path.isfile(path): @@ -116,10 +130,44 @@ def sync_one(dir_name: str, *, dry_run: bool, force: bool) -> bool: return True +def apply_force_close_policy(*, dry_run: bool) -> bool: + """Gate 开启强制清仓;币安/OKX 强制关闭(覆盖已有值)。""" + any_changed = False + for dir_name, values in FORCE_CLOSE_POLICY.items(): + path = os.path.join(REPO, dir_name, ".env") + if not os.path.isfile(path): + print(f"skip (no .env): {dir_name}") + continue + lines = _parse_env(path) + changed_keys: list[str] = [] + for key, val in values.items(): + cur = _env_get(lines, key) + if cur != val: + lines = _upsert(lines, key, val) + changed_keys.append(key) + if not changed_keys: + print(f"ok (force-close policy): {dir_name}") + continue + any_changed = True + print(f"force-close policy: {dir_name}") + for key in changed_keys: + print(f" = {key}={values[key]}") + if not dry_run: + text = "\n".join(lines).rstrip() + "\n" + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write(text) + return any_changed + + def main() -> None: ap = argparse.ArgumentParser(description="同步币安/OKX 共用 trading env(缺失项追加)") ap.add_argument("--dry-run", action="store_true") ap.add_argument("--force", action="store_true", help="覆盖已有值(慎用)") + ap.add_argument( + "--apply-force-close-policy", + action="store_true", + help="Gate 开启 0 点强制清仓,币安/OKX 强制关闭", + ) ap.add_argument( "--instances", nargs="+", @@ -133,6 +181,9 @@ def main() -> None: for inst in instances: if sync_one(inst, dry_run=args.dry_run, force=args.force): any_changed = True + if args.apply_force_close_policy: + if apply_force_close_policy(dry_run=args.dry_run): + any_changed = True if args.dry_run and any_changed: print("(dry-run, 未写入)")