Files
crypto_monitor_user/scripts/sync_common_trading_env.py
dekun 53863559f4 Initialize crypto_monitor_user (user edition) from monitor codebase.
Retarget git remote, install path, and deploy docs from crypto_monitor to crypto_monitor_user.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:18:13 +08:00

194 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""
将三所共用的交易/关键位/轮询 env 写入币安,OKX 的 .env(缺失则追加,不覆盖已有值).
以 Gate .env.example 为基准;Gate 自身也可运行以补缺失项.
用法(仓库根目录):
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
修改后须 pm2 restart 对应实例.说明见 docs/env-sync-scripts.md
"""
from __future__ import annotations
import argparse
import os
import re
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_INSTANCES = (
"crypto_monitor_binance",
"crypto_monitor_okx",
)
# 与 crypto_monitor_gate/.env.example 对齐(不含 GATE_* / 各所 API 密钥)
SHARED_DEFAULTS: dict[str, str] = {
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED": "true",
"KEY_CONFIRM_BREAKOUT_BAR": "-2",
"KEY_CONFIRM_BAR": "-1",
"KEY_VOLUME_MA_BARS": "20",
"KEY_VOLUME_RATIO_MIN": "1.3",
"KEY_BREAKOUT_AMP_MIN_PCT": "0.03",
"KEY_BREAKOUT_AMP_MAX_PCT": "0.5",
"KEY_ALERT_MAX_TIMES": "3",
"KEY_ALERT_INTERVAL_MINUTES": "5",
"KEY_DAILY_VOLUME_RANK_MAX": "30",
"KEY_AUTO_MIN_PLANNED_RR": "1.5",
"KEY_STOP_OUTSIDE_BREAKOUT_PCT": "0.5",
"KEY_TREND_STOP_OUTSIDE_PCT": "1",
"MAX_ACTIVE_POSITIONS": "1",
"MANUAL_MIN_PLANNED_RR": "1.4",
"KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT": "true",
"DAILY_OPEN_ALERT_THRESHOLD": "5",
"DAILY_OPEN_HARD_LIMIT": "0",
"BALANCE_REFRESH_SECONDS": "60",
"PRICE_REFRESH_SECONDS": "5",
"MONITOR_POLL_SECONDS": "3",
"RECONCILE_STARTUP_GRACE_SEC": "90",
"RECONCILE_FLAT_CONFIRM_POLLS": "3",
"FULL_MARGIN_BUFFER_RATIO": "0.98",
"WECHAT_TIMEOUT_SECONDS": "10",
"AI_TIMEOUT_SECONDS": "120",
}
# 仅当某实例 .env 缺少 FORCE_CLOSE_* 时补默认:
# 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):
return []
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return f.read().replace("\r\n", "\n").replace("\r", "\n").splitlines()
def _env_get(lines: list[str], key: str) -> str | None:
pat = re.compile(r"^\s*" + re.escape(key) + r"\s*=\s*(.*)\s*$")
for line in lines:
m = pat.match(line)
if m:
return m.group(1).strip().strip('"').strip("'")
return None
def _upsert(lines: list[str], key: str, value: str) -> list[str]:
pat = re.compile(r"^\s*" + re.escape(key) + r"\s*=")
out: list[str] = []
replaced = False
for line in lines:
if pat.match(line):
if not replaced:
out.append(f"{key}={value}")
replaced = True
continue
out.append(line)
if not replaced:
if out and out[-1].strip():
out.append("")
out.append(f"{key}={value}")
return out
def sync_one(dir_name: str, *, dry_run: bool, force: bool) -> bool:
path = os.path.join(REPO, dir_name, ".env")
if not os.path.isfile(path):
print(f"skip (no .env): {dir_name}")
return False
lines = _parse_env(path)
added: list[str] = []
for key, val in SHARED_DEFAULTS.items():
cur = _env_get(lines, key)
if cur is None or (force and cur != val):
lines = _upsert(lines, key, val)
added.append(key)
if not added:
print(f"ok (unchanged): {dir_name}")
return False
print(f"update: {dir_name}")
for key in added:
print(f" + {key}={SHARED_DEFAULTS[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 True
def apply_force_close_policy(*, dry_run: bool) -> bool:
"""仅在 FORCE_CLOSE_* 缺失时补默认值;已有手调值绝不覆盖."""
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)
added_keys: list[str] = []
for key, val in values.items():
cur = _env_get(lines, key)
if cur is None:
lines = _upsert(lines, key, val)
added_keys.append(key)
if not added_keys:
print(f"ok (force-close unchanged): {dir_name}")
continue
any_changed = True
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:
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="仅补全缺失的 FORCE_CLOSE_* 默认值(不覆盖手调)",
)
ap.add_argument(
"--instances",
nargs="+",
metavar="DIR",
help="默认 crypto_monitor_binance crypto_monitor_okx",
)
args = ap.parse_args()
instances = tuple(args.instances) if args.instances else DEFAULT_INSTANCES
any_changed = False
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, 未写入)")
if __name__ == "__main__":
main()