diff --git a/deploy/pull_and_restart.sh b/deploy/pull_and_restart.sh index 903ceac..257002f 100644 --- a/deploy/pull_and_restart.sh +++ b/deploy/pull_and_restart.sh @@ -19,7 +19,7 @@ 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" +echo ">>> force-close defaults only if missing (never overwrite manual)" python3 scripts/sync_common_trading_env.py --apply-force-close-policy "${DRY[@]}" if [[ ${#DRY[@]} -gt 0 ]]; then diff --git a/scripts/sync_common_trading_env.py b/scripts/sync_common_trading_env.py index ff04a54..9186e4e 100644 --- a/scripts/sync_common_trading_env.py +++ b/scripts/sync_common_trading_env.py @@ -54,7 +54,8 @@ SHARED_DEFAULTS: dict[str, str] = { "AI_TIMEOUT_SECONDS": "120", } -# 仅 Gate 启用 0 点强制清仓;币安/OKX 须保持关闭 +# 仅当某实例 .env 缺少 FORCE_CLOSE_* 时补默认: +# Gate 默认开 0 点强制清仓;币安/OKX 默认关.已有手调值绝不覆盖. FORCE_CLOSE_POLICY: dict[str, dict[str, str]] = { "crypto_monitor_gate": { "FORCE_CLOSE_ENABLED": "true", @@ -131,7 +132,7 @@ def sync_one(dir_name: str, *, dry_run: bool, force: bool) -> bool: def apply_force_close_policy(*, dry_run: bool) -> bool: - """Gate 开启强制清仓;币安/OKX 强制关闭(覆盖已有值).""" + """仅在 FORCE_CLOSE_* 缺失时补默认值;已有手调值绝不覆盖.""" any_changed = False for dir_name, values in FORCE_CLOSE_POLICY.items(): path = os.path.join(REPO, dir_name, ".env") @@ -139,19 +140,19 @@ def apply_force_close_policy(*, dry_run: bool) -> bool: print(f"skip (no .env): {dir_name}") continue lines = _parse_env(path) - changed_keys: list[str] = [] + added_keys: list[str] = [] for key, val in values.items(): cur = _env_get(lines, key) - if cur != val: + if cur is None: lines = _upsert(lines, key, val) - changed_keys.append(key) - if not changed_keys: - print(f"ok (force-close policy): {dir_name}") + added_keys.append(key) + if not added_keys: + print(f"ok (force-close unchanged): {dir_name}") continue any_changed = True - print(f"force-close policy: {dir_name}") - for key in changed_keys: - print(f" = {key}={values[key]}") + print(f"force-close fill-missing: {dir_name}") + for key in added_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: @@ -166,7 +167,7 @@ def main() -> None: ap.add_argument( "--apply-force-close-policy", action="store_true", - help="Gate 开启 0 点强制清仓,币安/OKX 强制关闭", + help="仅补全缺失的 FORCE_CLOSE_* 默认值(不覆盖手调)", ) ap.add_argument( "--instances", diff --git a/tests/test_sync_force_close_policy.py b/tests/test_sync_force_close_policy.py new file mode 100644 index 0000000..5d8b548 --- /dev/null +++ b/tests/test_sync_force_close_policy.py @@ -0,0 +1,41 @@ +"""FORCE_CLOSE 部署策略:只补缺失,不覆盖手调.""" +import os +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from scripts import sync_common_trading_env as sync + + +class TestForceCloseFillMissing(unittest.TestCase): + def test_does_not_overwrite_existing(self): + with tempfile.TemporaryDirectory() as td: + gate = Path(td) / "crypto_monitor_gate" + gate.mkdir() + (gate / ".env").write_text( + "FORCE_CLOSE_ENABLED=false\nFORCE_CLOSE_BJ_HOUR=1\n", + encoding="utf-8", + ) + with mock.patch.object(sync, "REPO", td): + changed = sync.apply_force_close_policy(dry_run=False) + self.assertFalse(changed) + text = (gate / ".env").read_text(encoding="utf-8") + self.assertIn("FORCE_CLOSE_ENABLED=false", text) + self.assertIn("FORCE_CLOSE_BJ_HOUR=1", text) + + def test_fills_missing_keys(self): + with tempfile.TemporaryDirectory() as td: + gate = Path(td) / "crypto_monitor_gate" + gate.mkdir() + (gate / ".env").write_text("APP_USERNAME=x\n", encoding="utf-8") + with mock.patch.object(sync, "REPO", td): + changed = sync.apply_force_close_policy(dry_run=False) + self.assertTrue(changed) + text = (gate / ".env").read_text(encoding="utf-8") + self.assertIn("FORCE_CLOSE_ENABLED=true", text) + self.assertIn("FORCE_CLOSE_BJ_HOUR=0", text) + + +if __name__ == "__main__": + unittest.main()