feat: 账户方向与币种白名单 env 开关(三所)
Per-instance TRADE_DIRECTION / TRADE_SYMBOL_WHITELIST restricts UI and API for manual orders, key monitors, and strategies; includes sync script for deployment profiles. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
将账户方向 / 币种白名单 env 写入三所 .env(缺失则追加,已存在则 --set 时覆盖)。
|
||||
|
||||
用法(仓库根目录):
|
||||
python scripts/sync_trade_policy_env.py
|
||||
python scripts/sync_trade_policy_env.py --dry-run
|
||||
python scripts/sync_trade_policy_env.py --apply-account-profiles
|
||||
python scripts/sync_trade_policy_env.py --set-direction binance long_only
|
||||
|
||||
--apply-account-profiles:币安=仅多,Gate=BTC/ETH 白名单,OKX=默认不限制。
|
||||
修改后须 pm2 restart 对应实例。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
INSTANCES = (
|
||||
"crypto_monitor_binance",
|
||||
"crypto_monitor_okx",
|
||||
"crypto_monitor_gate",
|
||||
)
|
||||
|
||||
COMMENT_BLOCK = [
|
||||
"# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)",
|
||||
"# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)",
|
||||
"# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)",
|
||||
]
|
||||
|
||||
DEFAULTS = {
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED": "false",
|
||||
"TRADE_DIRECTION": "both",
|
||||
"TRADE_SYMBOL_RESTRICT_ENABLED": "false",
|
||||
"TRADE_SYMBOL_WHITELIST": "BTC,ETH",
|
||||
}
|
||||
|
||||
ACCOUNT_PROFILES = {
|
||||
"crypto_monitor_binance": {
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED": "true",
|
||||
"TRADE_DIRECTION": "long_only",
|
||||
"TRADE_SYMBOL_RESTRICT_ENABLED": "false",
|
||||
"TRADE_SYMBOL_WHITELIST": "BTC,ETH",
|
||||
},
|
||||
"crypto_monitor_gate": {
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED": "false",
|
||||
"TRADE_DIRECTION": "both",
|
||||
"TRADE_SYMBOL_RESTRICT_ENABLED": "true",
|
||||
"TRADE_SYMBOL_WHITELIST": "BTC,ETH",
|
||||
},
|
||||
"crypto_monitor_okx": {
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED": "false",
|
||||
"TRADE_DIRECTION": "both",
|
||||
"TRADE_SYMBOL_RESTRICT_ENABLED": "false",
|
||||
"TRADE_SYMBOL_WHITELIST": "BTC,ETH",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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 _insert_after(lines: list[str], anchor_key: str, insert: list[str]) -> list[str]:
|
||||
pat = re.compile(r"^\s*" + re.escape(anchor_key) + r"\s*=")
|
||||
for i, line in enumerate(lines):
|
||||
if pat.match(line):
|
||||
return lines[: i + 1] + insert + lines[i + 1 :]
|
||||
if lines and lines[-1].strip():
|
||||
return lines + [""] + insert
|
||||
return lines + insert
|
||||
|
||||
|
||||
def sync_one(
|
||||
dir_name: str,
|
||||
values: dict[str, 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)
|
||||
changed = False
|
||||
for key, val in values.items():
|
||||
cur = _env_get(lines, key)
|
||||
if cur is None:
|
||||
if key == "TRADE_DIRECTION_RESTRICT_ENABLED" and _env_get(
|
||||
lines, "TRADE_DIRECTION"
|
||||
) is None:
|
||||
if COMMENT_BLOCK[0] not in "\n".join(lines):
|
||||
lines = _insert_after(lines, "POSITION_SIZING_MODE", COMMENT_BLOCK)
|
||||
lines = _upsert(lines, key, val)
|
||||
changed = True
|
||||
elif force or cur != val:
|
||||
lines = _upsert(lines, key, val)
|
||||
changed = True
|
||||
if not changed:
|
||||
print(f"ok (unchanged): {dir_name}")
|
||||
return False
|
||||
text = "\n".join(lines).rstrip() + "\n"
|
||||
print(f"update: {dir_name}")
|
||||
for k, v in values.items():
|
||||
print(f" {k}={v}")
|
||||
if not dry_run:
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(text)
|
||||
return True
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="同步三所 trade policy env")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
ap.add_argument(
|
||||
"--apply-account-profiles",
|
||||
action="store_true",
|
||||
help="币安仅多、Gate BTC/ETH、OKX 默认",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--defaults-only",
|
||||
action="store_true",
|
||||
help="三所均写入默认(不限制)",
|
||||
)
|
||||
ap.add_argument("--force", action="store_true", help="覆盖已有值")
|
||||
ap.add_argument("--set-direction", nargs=2, metavar=("INSTANCE", "MODE"))
|
||||
ap.add_argument("--set-symbol-whitelist", nargs=2, metavar=("INSTANCE", "SYMS"))
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.set_direction:
|
||||
inst, mode = args.set_direction
|
||||
if inst not in INSTANCES:
|
||||
raise SystemExit(f"unknown instance: {inst}")
|
||||
sync_one(
|
||||
inst,
|
||||
{
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED": "true",
|
||||
"TRADE_DIRECTION": mode,
|
||||
},
|
||||
dry_run=args.dry_run,
|
||||
force=True,
|
||||
)
|
||||
return
|
||||
|
||||
if args.set_symbol_whitelist:
|
||||
inst, syms = args.set_symbol_whitelist
|
||||
if inst not in INSTANCES:
|
||||
raise SystemExit(f"unknown instance: {inst}")
|
||||
sync_one(
|
||||
inst,
|
||||
{
|
||||
"TRADE_SYMBOL_RESTRICT_ENABLED": "true",
|
||||
"TRADE_SYMBOL_WHITELIST": syms,
|
||||
},
|
||||
dry_run=args.dry_run,
|
||||
force=True,
|
||||
)
|
||||
return
|
||||
|
||||
profiles = (
|
||||
{k: dict(DEFAULTS) for k in INSTANCES}
|
||||
if args.defaults_only
|
||||
else dict(ACCOUNT_PROFILES)
|
||||
if args.apply_account_profiles
|
||||
else {k: dict(DEFAULTS) for k in INSTANCES}
|
||||
)
|
||||
|
||||
if not args.apply_account_profiles and not args.defaults_only:
|
||||
ap.print_help()
|
||||
print("\n提示:部署常用 --apply-account-profiles")
|
||||
return
|
||||
|
||||
any_changed = False
|
||||
for inst in INSTANCES:
|
||||
if sync_one(inst, profiles[inst], dry_run=args.dry_run, force=args.force):
|
||||
any_changed = True
|
||||
if args.dry_run and any_changed:
|
||||
print("(dry-run, 未写入)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user