Normalize fullwidth punctuation to ASCII across codebase.

Add scripts/normalize_ambiguous_unicode.py; fix corrupted patch_instance_theme_templates.py. Preserves curly quotes in string literals; removes Git homoglyph warnings on .env.example.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-08 23:42:26 +08:00
parent aaa72c7961
commit b733e551a0
392 changed files with 71522 additions and 71369 deletions
+10 -10
View File
@@ -88,7 +88,7 @@ def precheck_risk(conn, symbol, direction):''',
return False, f"北京时间 {TRADING_DAY_RESET_HOUR}:00 前不允许持仓"
active_count = get_active_position_count(conn)
if active_count >= MAX_ACTIVE_POSITIONS:
return False, f"已达最大持仓数{active_count}/{MAX_ACTIVE_POSITIONS}"
return False, f"已达最大持仓数({active_count}/{MAX_ACTIVE_POSITIONS})"
if direction not in ("long", "short"):
return False, "方向必须为 long 或 short"
if symbol.upper().startswith("BTC") or symbol.upper().startswith("ETH"):
@@ -336,13 +336,13 @@ def cancel_okx_tpsl_slot(exchange_symbol, slot):
' f"周期 {KLINE_TIMEFRAME}|量能/突破/二确门控见箱体与收敛规则|"\n',
" can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS\n"
" key_gate_rule_text = (\n"
' f"周期 {KLINE_TIMEFRAME}|确认K突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}确认棒偏移 {KEY_CONFIRM_BAR}"\n'
' f"量能突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"\n',
' f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR},确认棒偏移 {KEY_CONFIRM_BAR}"\n'
' f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"\n',
)
text = text.replace(
' f"斐波添加后立即挂限价 @ E失效按标记价触达 H/L未成交撤单"\n',
' f"箱体/收敛可选 SL/TP 方案标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈|移动保本默认关|"\n'
' f"斐波限价 @ ESL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"\n',
' f"斐波:添加后立即挂限价 @ E,失效按标记价触达 H/L(未成交撤单)"\n',
' f"箱体/收敛可选 SL/TP 方案(标准 / 箱体1R·止盈1.5H / 趋势单+自填止盈)|移动保本默认关|"\n'
' f"斐波:限价 @ E(SL/TP 为 H/L),可选移动保本|趋势止损外侧 {KEY_TREND_STOP_OUTSIDE_PCT}%"\n',
)
text = text.replace(" total_capital=total_capital,\n", "")
text = text.replace(
@@ -465,7 +465,7 @@ def api_account_snapshot():
" if planned_rr_manual is None or planned_rr_manual < MANUAL_MIN_PLANNED_RR:\n"
" conn.close()\n"
" rr_txt = f\"{planned_rr_manual:.4f}\" if planned_rr_manual is not None else \"无法计算\"\n"
" flash(f\"风控拒绝下单计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1\")\n"
" flash(f\"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {MANUAL_MIN_PLANNED_RR}:1\")\n"
" return redirect(\"/trade\")\n"
" risk_fraction = calc_risk_fraction",
)
@@ -473,12 +473,12 @@ def api_account_snapshot():
text = text.replace(
'if get_active_position_count(conn) > 0:\n'
' conn.close()\n'
' flash("当前已有持仓无法添加「箱体突破 / 收敛突破」请先平仓或使用阻力/支撑/斐波类型")',
' flash("当前已有持仓:无法添加「箱体突破 / 收敛突破」(请先平仓或使用阻力/支撑/斐波类型)")',
'occupied = get_active_position_count(conn)\n'
' if occupied >= MAX_ACTIVE_POSITIONS:\n'
' conn.close()\n'
' flash(\n'
' f"当前持仓已达上限{occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」"\n'
' f"当前持仓已达上限({occupied}/{MAX_ACTIVE_POSITIONS}):无法添加「箱体突破 / 收敛突破」."\n'
' "请先平仓或使用阻力/支撑/斐波类型"\n'
' )',
)
@@ -530,7 +530,7 @@ def copy_env_example():
if marker not in okx:
okx += block
if "TOTAL_CAPITAL=100" in okx and "# TOTAL_CAPITAL" not in okx:
okx = okx.replace("TOTAL_CAPITAL=100", "# TOTAL_CAPITAL=100 # 已弃用资金展示读交易所")
okx = okx.replace("TOTAL_CAPITAL=100", "# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所")
okx_path.write_text(okx, encoding="utf-8")
print("updated .env.example")
+7 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""对 binance/okx 应用与 gate 相同的时间平仓代码替换"""
"""对 binance/okx 应用与 gate 相同的时间平仓代码替换."""
from __future__ import annotations
from pathlib import Path
@@ -48,20 +48,20 @@ REPLACEMENTS: list[tuple[str, str]] = [
" ok_fb, err_fb = _add_false_breakout_key_monitor(\n conn, symbol, direction_sel, upper_px, lower_px, key_px, breakeven_enabled=be_flag,\n time_close_enabled=tc_en, time_close_hours=tc_h,\n )",
),
(
" f\"|有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h|移动保本{'' if be_flag else ''}\"\n )",
" f\"|有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h|移动保本{'' if be_flag else ''}\"\n + (f\"{time_close_label(tc_h)}\" if tc_en else \"\")\n )",
" f\"|有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h|移动保本:{'' if be_flag else ''}\"\n )",
" f\"|有效期 {FALSE_BREAKOUT_VALIDITY_HOURS}h|移动保本:{'' if be_flag else ''}\"\n + (f\"{time_close_label(tc_h)}\" if tc_en else \"\")\n )",
),
(
" ok_fib, err_fib = _add_fib_key_monitor(\n conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag,\n )",
" ok_fib, err_fib = _add_fib_key_monitor(\n conn, symbol, direction_sel, mt, upper_px, lower_px, breakeven_enabled=be_flag,\n time_close_enabled=tc_en, time_close_hours=tc_h,\n )",
),
(
" f\"|移动保本{'' if be_flag else ''}\"\n )\n return redirect(\"/key_monitor\")",
" f\"|移动保本{'' if be_flag else ''}\"\n + (f\"{time_close_label(tc_h)}\" if tc_en else \"\")\n )\n return redirect(\"/key_monitor\")",
" f\"|移动保本:{'' if be_flag else ''}\"\n )\n return redirect(\"/key_monitor\")",
" f\"|移动保本:{'' if be_flag else ''}\"\n + (f\"{time_close_label(tc_h)}\" if tc_en else \"\")\n )\n return redirect(\"/key_monitor\")",
),
(
" if mt in KEY_MONITOR_AUTO_TYPES:\n extra = f\"|方案{sl_tp_mode_label(sl_tp_mode)}|移动保本{'' if be_flag else ''}\"",
" if mt in KEY_MONITOR_AUTO_TYPES:\n extra = f\"|方案{sl_tp_mode_label(sl_tp_mode)}|移动保本{'' if be_flag else ''}\"\n if tc_en:\n extra += f\"{time_close_label(tc_h)}\"",
" if mt in KEY_MONITOR_AUTO_TYPES:\n extra = f\"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'' if be_flag else ''}\"",
" if mt in KEY_MONITOR_AUTO_TYPES:\n extra = f\"|方案:{sl_tp_mode_label(sl_tp_mode)}|移动保本:{'' if be_flag else ''}\"\n if tc_en:\n extra += f\"{time_close_label(tc_h)}\"",
),
]
+248 -248
View File
@@ -1,248 +1,248 @@
#!/usr/bin/env python3
"""补录缺失的趋势回调策略结束快照strategy_trade_snapshots)。
适用gate 等在计划结束止盈/止损/手动时因 strategy_trend_cfg 未注册而漏写快照的历史数据
保本移交路径通常已有快照本脚本默认跳过「已有任意快照」的计划
用法在仓库根目录Linux 请用 python3):
python3 scripts/backfill_trend_strategy_snapshots.py \\
--db crypto_monitor_gate/crypto.db --dry-run
python3 scripts/backfill_trend_strategy_snapshots.py \\
--db crypto_monitor_gate/crypto.db --apply
"""
from __future__ import annotations
import argparse
import sqlite3
import sys
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from lib.strategy.strategy_snapshot_lib import ( # noqa: E402
STRATEGY_TREND,
init_strategy_snapshot_table,
save_trend_plan_snapshot,
)
PLAN_STATUS_LABEL = {
"stopped_sl": "止损",
"stopped_tp": "止盈",
"stopped_manual": "手动平仓",
"stopped_handoff": "保本移交",
}
TRADE_RESULT_LABEL = {
"止损": "止损",
"止盈": "止盈",
"手动平仓": "手动平仓",
"移动止盈": "止盈",
"保本止盈": "止盈",
"强制清仓": "手动平仓",
}
def _row_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def infer_exit_price(
direction: str,
entry: float | None,
margin: float | None,
leverage: float | None,
pnl: float | None,
) -> float | None:
"""由本地 calc_pnl 口径反推平仓价供补录快照 exit_price)。"""
try:
trigger = float(entry)
margin_f = float(margin)
lev = float(leverage)
pnl_f = float(pnl)
except (TypeError, ValueError):
return None
if trigger <= 0 or margin_f <= 0 or lev <= 0:
return None
notional = margin_f * lev
if notional <= 0:
return None
ratio = pnl_f / notional
if (direction or "long").strip().lower() == "short":
return round(trigger * (1.0 - ratio), 10)
return round(trigger * (1.0 + ratio), 10)
def resolve_result_label(plan: dict, trade: dict | None) -> str:
status = (plan.get("status") or "").strip()
if status in PLAN_STATUS_LABEL:
return PLAN_STATUS_LABEL[status]
if trade:
res = (trade.get("result") or "").strip()
if res in TRADE_RESULT_LABEL:
return TRADE_RESULT_LABEL[res]
if res:
return res
msg = (plan.get("message") or "").strip()
if msg:
return msg[:32]
return "结束"
def find_missing_plans(
conn: sqlite3.Connection,
*,
plan_id: int | None = None,
since: str | None = None,
) -> list[dict]:
sql = """
SELECT p.*
FROM trend_pullback_plans p
WHERE TRIM(COALESCE(p.status, '')) != 'active'
AND NOT EXISTS (
SELECT 1 FROM strategy_trade_snapshots s
WHERE s.strategy_type = ? AND s.source_id = p.id
)
"""
params: list[object] = [STRATEGY_TREND]
if plan_id is not None:
sql += " AND p.id = ?"
params.append(int(plan_id))
if since:
sql += " AND COALESCE(p.opened_at, '') >= ?"
params.append(since.strip())
sql += " ORDER BY p.id ASC"
rows = conn.execute(sql, params).fetchall()
return [_row_dict(r) for r in rows]
def fetch_trade_for_plan(conn: sqlite3.Connection, plan_id: int) -> dict | None:
row = conn.execute(
"""
SELECT * FROM trade_records
WHERE trend_plan_id = ?
ORDER BY COALESCE(closed_at_ms, 0) DESC, id DESC
LIMIT 1
""",
(int(plan_id),),
).fetchone()
return _row_dict(row) if row else None
def backfill_one(conn: sqlite3.Connection, plan: dict, *, dry_run: bool) -> dict:
plan_id = int(plan["id"])
trade = fetch_trade_for_plan(conn, plan_id)
result_label = resolve_result_label(plan, trade)
pnl_amount = None
closed_at = None
exit_price = None
entry = plan.get("avg_entry_price") or plan.get("live_price_ref")
margin = plan.get("plan_margin_capital")
leverage = plan.get("leverage")
if trade:
pnl_amount = trade.get("pnl_amount")
closed_at = trade.get("closed_at")
entry = trade.get("trigger_price") or entry
margin = trade.get("margin_capital") or margin
leverage = trade.get("leverage") or leverage
exit_price = infer_exit_price(
plan.get("direction") or trade.get("direction") or "long",
entry,
margin,
leverage,
pnl_amount,
)
info = {
"plan_id": plan_id,
"symbol": plan.get("symbol"),
"status": plan.get("status"),
"result_label": result_label,
"closed_at": closed_at,
"pnl_amount": pnl_amount,
"exit_price": exit_price,
"legs_done": plan.get("legs_done"),
"dca_legs": plan.get("dca_legs"),
"has_trade": bool(trade),
}
if dry_run:
return info
save_trend_plan_snapshot(
{},
conn,
plan,
result_label=result_label,
exit_price=exit_price,
pnl_amount=float(pnl_amount) if pnl_amount is not None else None,
closed_at=closed_at,
)
return info
def main() -> int:
parser = argparse.ArgumentParser(
description="Backfill missing trend_pullback strategy_trade_snapshots rows."
)
parser.add_argument("--db", required=True, help="Path to instance sqlite db")
parser.add_argument("--plan-id", type=int, help="Only backfill this trend plan id")
parser.add_argument(
"--since",
help="Only plans with opened_at >= YYYY-MM-DD (optional)",
)
parser.add_argument("--dry-run", action="store_true", help="Preview only (default)")
parser.add_argument("--apply", action="store_true", help="Write snapshots")
args = parser.parse_args()
if not args.dry_run and not args.apply:
args.dry_run = True
db_path = Path(args.db).expanduser().resolve()
if not db_path.is_file():
print(f"[ERR] DB not found: {db_path}")
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
init_strategy_snapshot_table(conn)
missing = find_missing_plans(
conn, plan_id=args.plan_id, since=args.since
)
if not missing:
print("[INFO] No closed trend plans missing strategy snapshots.")
conn.close()
return 0
print(f"[INFO] Found {len(missing)} plan(s) without strategy snapshot.")
applied = 0
for plan in missing:
info = backfill_one(conn, plan, dry_run=not args.apply)
trade_hint = "有交易记录" if info["has_trade"] else "无交易记录"
print(
f" - plan #{info['plan_id']} {info['symbol']} "
f"status={info['status']}{info['result_label']} "
f"closed={info['closed_at'] or ''} pnl={info['pnl_amount']} "
f"补仓 {info['legs_done']}/{info['dca_legs']} ({trade_hint})"
)
applied += 1
if args.apply:
conn.commit()
print(f"[OK] Backfilled {applied} snapshot(s).")
else:
print("[DRY-RUN] No changes written. Re-run with --apply to commit.")
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""补录缺失的趋势回调策略结束快照(strategy_trade_snapshots).
适用:gate 等在计划结束(止盈/止损/手动)时因 strategy_trend_cfg 未注册而漏写快照的历史数据.
保本移交路径通常已有快照,本脚本默认跳过「已有任意快照」的计划.
用法(在仓库根目录,Linux 请用 python3):
python3 scripts/backfill_trend_strategy_snapshots.py \\
--db crypto_monitor_gate/crypto.db --dry-run
python3 scripts/backfill_trend_strategy_snapshots.py \\
--db crypto_monitor_gate/crypto.db --apply
"""
from __future__ import annotations
import argparse
import sqlite3
import sys
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from lib.strategy.strategy_snapshot_lib import ( # noqa: E402
STRATEGY_TREND,
init_strategy_snapshot_table,
save_trend_plan_snapshot,
)
PLAN_STATUS_LABEL = {
"stopped_sl": "止损",
"stopped_tp": "止盈",
"stopped_manual": "手动平仓",
"stopped_handoff": "保本移交",
}
TRADE_RESULT_LABEL = {
"止损": "止损",
"止盈": "止盈",
"手动平仓": "手动平仓",
"移动止盈": "止盈",
"保本止盈": "止盈",
"强制清仓": "手动平仓",
}
def _row_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def infer_exit_price(
direction: str,
entry: float | None,
margin: float | None,
leverage: float | None,
pnl: float | None,
) -> float | None:
"""由本地 calc_pnl 口径反推平仓价(供补录快照 exit_price)."""
try:
trigger = float(entry)
margin_f = float(margin)
lev = float(leverage)
pnl_f = float(pnl)
except (TypeError, ValueError):
return None
if trigger <= 0 or margin_f <= 0 or lev <= 0:
return None
notional = margin_f * lev
if notional <= 0:
return None
ratio = pnl_f / notional
if (direction or "long").strip().lower() == "short":
return round(trigger * (1.0 - ratio), 10)
return round(trigger * (1.0 + ratio), 10)
def resolve_result_label(plan: dict, trade: dict | None) -> str:
status = (plan.get("status") or "").strip()
if status in PLAN_STATUS_LABEL:
return PLAN_STATUS_LABEL[status]
if trade:
res = (trade.get("result") or "").strip()
if res in TRADE_RESULT_LABEL:
return TRADE_RESULT_LABEL[res]
if res:
return res
msg = (plan.get("message") or "").strip()
if msg:
return msg[:32]
return "结束"
def find_missing_plans(
conn: sqlite3.Connection,
*,
plan_id: int | None = None,
since: str | None = None,
) -> list[dict]:
sql = """
SELECT p.*
FROM trend_pullback_plans p
WHERE TRIM(COALESCE(p.status, '')) != 'active'
AND NOT EXISTS (
SELECT 1 FROM strategy_trade_snapshots s
WHERE s.strategy_type = ? AND s.source_id = p.id
)
"""
params: list[object] = [STRATEGY_TREND]
if plan_id is not None:
sql += " AND p.id = ?"
params.append(int(plan_id))
if since:
sql += " AND COALESCE(p.opened_at, '') >= ?"
params.append(since.strip())
sql += " ORDER BY p.id ASC"
rows = conn.execute(sql, params).fetchall()
return [_row_dict(r) for r in rows]
def fetch_trade_for_plan(conn: sqlite3.Connection, plan_id: int) -> dict | None:
row = conn.execute(
"""
SELECT * FROM trade_records
WHERE trend_plan_id = ?
ORDER BY COALESCE(closed_at_ms, 0) DESC, id DESC
LIMIT 1
""",
(int(plan_id),),
).fetchone()
return _row_dict(row) if row else None
def backfill_one(conn: sqlite3.Connection, plan: dict, *, dry_run: bool) -> dict:
plan_id = int(plan["id"])
trade = fetch_trade_for_plan(conn, plan_id)
result_label = resolve_result_label(plan, trade)
pnl_amount = None
closed_at = None
exit_price = None
entry = plan.get("avg_entry_price") or plan.get("live_price_ref")
margin = plan.get("plan_margin_capital")
leverage = plan.get("leverage")
if trade:
pnl_amount = trade.get("pnl_amount")
closed_at = trade.get("closed_at")
entry = trade.get("trigger_price") or entry
margin = trade.get("margin_capital") or margin
leverage = trade.get("leverage") or leverage
exit_price = infer_exit_price(
plan.get("direction") or trade.get("direction") or "long",
entry,
margin,
leverage,
pnl_amount,
)
info = {
"plan_id": plan_id,
"symbol": plan.get("symbol"),
"status": plan.get("status"),
"result_label": result_label,
"closed_at": closed_at,
"pnl_amount": pnl_amount,
"exit_price": exit_price,
"legs_done": plan.get("legs_done"),
"dca_legs": plan.get("dca_legs"),
"has_trade": bool(trade),
}
if dry_run:
return info
save_trend_plan_snapshot(
{},
conn,
plan,
result_label=result_label,
exit_price=exit_price,
pnl_amount=float(pnl_amount) if pnl_amount is not None else None,
closed_at=closed_at,
)
return info
def main() -> int:
parser = argparse.ArgumentParser(
description="Backfill missing trend_pullback strategy_trade_snapshots rows."
)
parser.add_argument("--db", required=True, help="Path to instance sqlite db")
parser.add_argument("--plan-id", type=int, help="Only backfill this trend plan id")
parser.add_argument(
"--since",
help="Only plans with opened_at >= YYYY-MM-DD (optional)",
)
parser.add_argument("--dry-run", action="store_true", help="Preview only (default)")
parser.add_argument("--apply", action="store_true", help="Write snapshots")
args = parser.parse_args()
if not args.dry_run and not args.apply:
args.dry_run = True
db_path = Path(args.db).expanduser().resolve()
if not db_path.is_file():
print(f"[ERR] DB not found: {db_path}")
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
init_strategy_snapshot_table(conn)
missing = find_missing_plans(
conn, plan_id=args.plan_id, since=args.since
)
if not missing:
print("[INFO] No closed trend plans missing strategy snapshots.")
conn.close()
return 0
print(f"[INFO] Found {len(missing)} plan(s) without strategy snapshot.")
applied = 0
for plan in missing:
info = backfill_one(conn, plan, dry_run=not args.apply)
trade_hint = "有交易记录" if info["has_trade"] else "无交易记录"
print(
f" - plan #{info['plan_id']} {info['symbol']} "
f"status={info['status']}{info['result_label']} "
f"closed={info['closed_at'] or ''} pnl={info['pnl_amount']} "
f"补仓 {info['legs_done']}/{info['dca_legs']} ({trade_hint})"
)
applied += 1
if args.apply:
conn.commit()
print(f"[OK] Backfilled {applied} snapshot(s).")
else:
print("[DRY-RUN] No changes written. Re-run with --apply to commit.")
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+188 -188
View File
@@ -1,188 +1,188 @@
#!/usr/bin/env python3
"""补录缺失的趋势回调 trade_records策略快照已有交易记录漏写)。
典型原因gate insert_trade_record 曾不接受 entry_reason_finalize_plan 写快照后插入失败
用法
python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate/crypto.db --dry-run
python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate/crypto.db --apply
"""
from __future__ import annotations
import argparse
import json
import sqlite3
import sys
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from lib.strategy.strategy_snapshot_lib import STRATEGY_TREND # noqa: E402
from lib.strategy.strategy_trade_labels import ENTRY_REASON_TREND_PULLBACK, MONITOR_TYPE_TREND_PULLBACK # noqa: E402
STATUS_TO_RESULT = {
"stopped_sl": "止损",
"stopped_tp": "止盈",
"stopped_manual": "手动平仓",
}
def _row_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def _hold_minutes(hold_seconds: int) -> int:
try:
return max(0, int(round(float(hold_seconds) / 60.0)))
except (TypeError, ValueError):
return 0
def backfill_one(conn: sqlite3.Connection, snap: dict, *, apply: bool) -> dict:
plan_id = int(snap.get("source_id") or 0)
if plan_id <= 0:
return {"plan_id": plan_id, "skipped": True, "reason": "invalid source_id"}
exists = conn.execute(
"SELECT id FROM trade_records WHERE trend_plan_id=? LIMIT 1", (plan_id,)
).fetchone()
if exists:
return {"plan_id": plan_id, "skipped": True, "reason": "trade_exists"}
try:
payload = json.loads(snap.get("snapshot_json") or "{}")
except Exception:
payload = {}
plan = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
).fetchone()
plan_d = _row_dict(plan)
symbol = snap.get("symbol") or plan_d.get("symbol") or payload.get("symbol")
direction = snap.get("direction") or plan_d.get("direction") or payload.get("direction") or "long"
result = (snap.get("result_label") or "").strip() or STATUS_TO_RESULT.get(
plan_d.get("status") or "", "手动平仓"
)
opened_at = snap.get("opened_at") or plan_d.get("opened_at")
closed_at = snap.get("closed_at")
pnl_amount = snap.get("pnl_amount")
if pnl_amount is None:
pnl_amount = payload.get("pnl_amount")
trigger_price = payload.get("avg_entry_price") or plan_d.get("avg_entry_price")
stop_loss = payload.get("stop_loss") or plan_d.get("stop_loss")
take_profit = payload.get("take_profit") or plan_d.get("take_profit")
margin_capital = payload.get("plan_margin_capital") or plan_d.get("plan_margin_capital")
leverage = payload.get("leverage") or plan_d.get("leverage")
opened_ms = plan_d.get("opened_at_ms")
closed_ms = None
hold_seconds = 0
if opened_at and closed_at:
try:
from datetime import datetime
fmt = "%Y-%m-%d %H:%M:%S"
o = datetime.strptime(str(opened_at).strip()[:19], fmt)
c = datetime.strptime(str(closed_at).strip()[:19], fmt)
hold_seconds = max(0, int((c - o).total_seconds()))
except Exception:
hold_seconds = 0
row = {
"symbol": symbol,
"monitor_type": MONITOR_TYPE_TREND_PULLBACK,
"direction": direction,
"trigger_price": trigger_price,
"stop_loss": stop_loss,
"initial_stop_loss": plan_d.get("initial_stop_loss") or stop_loss,
"take_profit": take_profit,
"margin_capital": margin_capital,
"leverage": leverage,
"pnl_amount": pnl_amount,
"hold_seconds": hold_seconds,
"trade_style": "trend_pullback",
"result": result,
"opened_at": opened_at,
"opened_at_ms": opened_ms,
"closed_at": closed_at,
"closed_at_ms": closed_ms,
"entry_reason": ENTRY_REASON_TREND_PULLBACK,
"trend_plan_id": plan_id,
}
if not apply:
return {"plan_id": plan_id, "dry_run": True, "row": row}
conn.execute(
"""INSERT INTO trade_records (
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss,
take_profit, margin_capital, leverage, pnl_amount, hold_seconds, trade_style,
hold_minutes, opened_at, opened_at_ms, closed_at, closed_at_ms, result,
entry_reason, trend_plan_id
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
row["symbol"],
row["monitor_type"],
row["direction"],
row["trigger_price"],
row["stop_loss"],
row["initial_stop_loss"],
row["take_profit"],
row["margin_capital"],
row["leverage"],
row["pnl_amount"],
row["hold_seconds"],
row["trade_style"],
_hold_minutes(hold_seconds),
row["opened_at"],
row["opened_at_ms"],
row["closed_at"],
row["closed_at_ms"],
row["result"],
row["entry_reason"],
row["trend_plan_id"],
),
)
return {"plan_id": plan_id, "inserted": True}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--db", required=True, help="实例 sqlite 路径")
ap.add_argument("--apply", action="store_true", help="写入数据库默认 dry-run")
args = ap.parse_args()
db_path = Path(args.db)
if not db_path.is_file():
print(f"数据库不存在: {db_path}")
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
snaps = conn.execute(
"""SELECT * FROM strategy_trade_snapshots
WHERE strategy_type=? ORDER BY id DESC""",
(STRATEGY_TREND,),
).fetchall()
out = []
for s in snaps:
r = backfill_one(conn, _row_dict(s), apply=args.apply)
out.append(r)
print(r)
if args.apply:
conn.commit()
conn.close()
inserted = sum(1 for x in out if x.get("inserted"))
print(f"done: inserted={inserted} total_snapshots={len(snaps)} apply={args.apply}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""补录缺失的趋势回调 trade_records(策略快照已有,交易记录漏写).
典型原因:gate insert_trade_record 曾不接受 entry_reason,_finalize_plan 写快照后插入失败.
用法:
python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate/crypto.db --dry-run
python scripts/backfill_trend_trade_records.py --db crypto_monitor_gate/crypto.db --apply
"""
from __future__ import annotations
import argparse
import json
import sqlite3
import sys
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from lib.strategy.strategy_snapshot_lib import STRATEGY_TREND # noqa: E402
from lib.strategy.strategy_trade_labels import ENTRY_REASON_TREND_PULLBACK, MONITOR_TYPE_TREND_PULLBACK # noqa: E402
STATUS_TO_RESULT = {
"stopped_sl": "止损",
"stopped_tp": "止盈",
"stopped_manual": "手动平仓",
}
def _row_dict(row) -> dict:
if row is None:
return {}
try:
return dict(row)
except Exception:
return {}
def _hold_minutes(hold_seconds: int) -> int:
try:
return max(0, int(round(float(hold_seconds) / 60.0)))
except (TypeError, ValueError):
return 0
def backfill_one(conn: sqlite3.Connection, snap: dict, *, apply: bool) -> dict:
plan_id = int(snap.get("source_id") or 0)
if plan_id <= 0:
return {"plan_id": plan_id, "skipped": True, "reason": "invalid source_id"}
exists = conn.execute(
"SELECT id FROM trade_records WHERE trend_plan_id=? LIMIT 1", (plan_id,)
).fetchone()
if exists:
return {"plan_id": plan_id, "skipped": True, "reason": "trade_exists"}
try:
payload = json.loads(snap.get("snapshot_json") or "{}")
except Exception:
payload = {}
plan = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
).fetchone()
plan_d = _row_dict(plan)
symbol = snap.get("symbol") or plan_d.get("symbol") or payload.get("symbol")
direction = snap.get("direction") or plan_d.get("direction") or payload.get("direction") or "long"
result = (snap.get("result_label") or "").strip() or STATUS_TO_RESULT.get(
plan_d.get("status") or "", "手动平仓"
)
opened_at = snap.get("opened_at") or plan_d.get("opened_at")
closed_at = snap.get("closed_at")
pnl_amount = snap.get("pnl_amount")
if pnl_amount is None:
pnl_amount = payload.get("pnl_amount")
trigger_price = payload.get("avg_entry_price") or plan_d.get("avg_entry_price")
stop_loss = payload.get("stop_loss") or plan_d.get("stop_loss")
take_profit = payload.get("take_profit") or plan_d.get("take_profit")
margin_capital = payload.get("plan_margin_capital") or plan_d.get("plan_margin_capital")
leverage = payload.get("leverage") or plan_d.get("leverage")
opened_ms = plan_d.get("opened_at_ms")
closed_ms = None
hold_seconds = 0
if opened_at and closed_at:
try:
from datetime import datetime
fmt = "%Y-%m-%d %H:%M:%S"
o = datetime.strptime(str(opened_at).strip()[:19], fmt)
c = datetime.strptime(str(closed_at).strip()[:19], fmt)
hold_seconds = max(0, int((c - o).total_seconds()))
except Exception:
hold_seconds = 0
row = {
"symbol": symbol,
"monitor_type": MONITOR_TYPE_TREND_PULLBACK,
"direction": direction,
"trigger_price": trigger_price,
"stop_loss": stop_loss,
"initial_stop_loss": plan_d.get("initial_stop_loss") or stop_loss,
"take_profit": take_profit,
"margin_capital": margin_capital,
"leverage": leverage,
"pnl_amount": pnl_amount,
"hold_seconds": hold_seconds,
"trade_style": "trend_pullback",
"result": result,
"opened_at": opened_at,
"opened_at_ms": opened_ms,
"closed_at": closed_at,
"closed_at_ms": closed_ms,
"entry_reason": ENTRY_REASON_TREND_PULLBACK,
"trend_plan_id": plan_id,
}
if not apply:
return {"plan_id": plan_id, "dry_run": True, "row": row}
conn.execute(
"""INSERT INTO trade_records (
symbol, monitor_type, direction, trigger_price, stop_loss, initial_stop_loss,
take_profit, margin_capital, leverage, pnl_amount, hold_seconds, trade_style,
hold_minutes, opened_at, opened_at_ms, closed_at, closed_at_ms, result,
entry_reason, trend_plan_id
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
row["symbol"],
row["monitor_type"],
row["direction"],
row["trigger_price"],
row["stop_loss"],
row["initial_stop_loss"],
row["take_profit"],
row["margin_capital"],
row["leverage"],
row["pnl_amount"],
row["hold_seconds"],
row["trade_style"],
_hold_minutes(hold_seconds),
row["opened_at"],
row["opened_at_ms"],
row["closed_at"],
row["closed_at_ms"],
row["result"],
row["entry_reason"],
row["trend_plan_id"],
),
)
return {"plan_id": plan_id, "inserted": True}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--db", required=True, help="实例 sqlite 路径")
ap.add_argument("--apply", action="store_true", help="写入数据库(默认 dry-run)")
args = ap.parse_args()
db_path = Path(args.db)
if not db_path.is_file():
print(f"数据库不存在: {db_path}")
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
snaps = conn.execute(
"""SELECT * FROM strategy_trade_snapshots
WHERE strategy_type=? ORDER BY id DESC""",
(STRATEGY_TREND,),
).fetchall()
out = []
for s in snaps:
r = backfill_one(conn, _row_dict(s), apply=args.apply)
out.append(r)
print(r)
if args.apply:
conn.commit()
conn.close()
inserted = sum(1 for x in out if x.get("inserted"))
print(f"done: inserted={inserted} total_snapshots={len(snaps)} apply={args.apply}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+11 -11
View File
@@ -1,13 +1,13 @@
#!/usr/bin/env python3
"""首次部署自动生成中控通信密钥登录会话密钥并写入初始登录账号
"""首次部署:自动生成中控通信密钥,登录会话密钥,并写入初始登录账号.
- HUB_BRIDGE_TOKEN中控 + 三实例相同仅空/占位时写入不覆盖已有
- FLASK_SECRET_KEY三实例相同
- HUB_SESSION_SECRET仅中控
- APP_USERNAME=adminAPP_PASSWORD=admin123实例仅空时
- HUB_USERNAME=adminHUB_PASSWORD=admin123中控仅空时
- HUB_BRIDGE_TOKEN:中控 + 三实例(相同,仅空/占位时写入,不覆盖已有)
- FLASK_SECRET_KEY:三实例(相同)
- HUB_SESSION_SECRET:仅中控
- APP_USERNAME=admin,APP_PASSWORD=admin123:实例(仅空时)
- HUB_USERNAME=admin,HUB_PASSWORD=admin123:中控(仅空时)
已有非空且非占位符的值不会被覆盖长期密钥一次生成不轮换)。
已有非空且非占位符的值不会被覆盖(长期密钥一次生成,不轮换).
"""
from __future__ import annotations
@@ -47,7 +47,7 @@ def _should_set(current: str | None, placeholders: frozenset[str]) -> bool:
def main() -> int:
parser = argparse.ArgumentParser(description="Bootstrap deploy secrets")
parser.add_argument("--dry-run", action="store_true", help="只打印将写入的项不改文件")
parser.add_argument("--dry-run", action="store_true", help="只打印将写入的项,不改文件")
args = parser.parse_args()
hub_token = secrets.token_urlsafe(32)
@@ -88,7 +88,7 @@ def main() -> int:
planned.append((path, updates))
if not planned:
print("无需写入密钥与登录项均已配置")
print("无需写入:密钥与登录项均已配置.")
return 0
for path, updates in planned:
@@ -101,8 +101,8 @@ def main() -> int:
print(f"已写入 {rel}: {keys}")
if not args.dry_run:
print("完成初始登录admin / admin123若本次写入了密码项)。")
print("请 pm2 restart 中控与三实例使密钥生效")
print("完成.初始登录:admin / admin123(若本次写入了密码项).")
print("请 pm2 restart 中控与三实例使密钥生效.")
return 0
+13 -13
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""从 binance index.html 生成三所共用的 lib/instance/templates/index.html"""
"""从 binance index.html 生成三所共用的 lib/instance/templates/index.html."""
from __future__ import annotations
import re
@@ -12,7 +12,7 @@ OUT = ROOT / "lib" / "instance" / "templates" / "index.html"
TRANSFER_BLOCK = """ <details class="tip-collapse transfer-rule-collapse">
<summary class="tip-collapse-summary">划转规则说明</summary>
<div class="tip-collapse-body rule-tip">
划转自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试账簿按 <strong>UTC 自然日</strong>去重{{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U不足从 {{ auto_transfer_from }} 划入超出划回 {{ auto_transfer_from }}<strong>持仓中不划转</strong>并微信通知
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;{{ auto_transfer_to }} 调整至 {{ auto_transfer_amount }}U:不足从 {{ auto_transfer_from }} 划入,超出划回 {{ auto_transfer_from }};<strong>持仓中不划转</strong>并微信通知)
</div>
</details>
<form action="/manual_transfer" method="post" class="form-row">
@@ -44,9 +44,9 @@ def main() -> None:
flags=re.DOTALL,
)
# 顶栏划转 + 可选 open guard
# 顶栏:划转 + 可选 open guard
text = text.replace(
' <div class="rule-tip">实时价格更新<span id="price-last-updated">--</span>北京时间 UTC+8</div>\n',
' <div class="rule-tip">实时价格更新:<span id="price-last-updated">--</span>(北京时间 UTC+8)</div>\n',
" {% include 'instance_top_bar.html' %}\n",
)
@@ -56,7 +56,7 @@ def main() -> None:
"{% include order_rule_tips_tpl %}",
)
# 下单面板内划转块移除已上移到顶栏
# 下单面板内划转块移除(已上移到顶栏)
if TRANSFER_BLOCK in text:
text = text.replace(TRANSFER_BLOCK, "", 1)
@@ -64,7 +64,7 @@ def main() -> None:
orphan_block = """ {% if not order and orphan_live_positions %}
{% set o = orphan_live_positions[0] %}
<div id="orphan-position-recover" class="orphan-recover-banner" style="display:block;margin-bottom:10px;padding:10px 12px;background:#2a2210;border:1px solid #6b5420;border-radius:6px;font-size:.9rem;color:#e8d5a8">
<strong>检测到交易所仍有持仓本地无对应监控单</strong>
<strong>检测到交易所仍有持仓,本地无对应监控单</strong>
<span style="margin-left:8px">{{ o.exchange_symbol or o.symbol }} · {{ '' if o.direction == 'long' else '' }}</span>
<form action="/recover_orphan_order" method="post" style="display:inline;margin-left:12px">
<input type="hidden" name="symbol" value="{{ o.symbol }}">
@@ -78,7 +78,7 @@ def main() -> None:
wrapped = "{% if ui_orphan_recovery_enabled %}\n" + orphan_block + "\n {% endif %}"
text = text.replace(orphan_block, wrapped, 1)
# refreshAccountSnapshot采用 OKX 版 open_guard 逻辑
# refreshAccountSnapshot:采用 OKX 版 open_guard 逻辑
old_can_trade = """ let canTradeText = "可开仓";
if (!data.can_trade) {
const parts = [];
@@ -93,7 +93,7 @@ def main() -> None:
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
if (!parts.length) parts.push(`未到北京时间 {{ reset_hour }}:00`);
else parts.push(`或未到北京时间 {{ reset_hour }}:00`);
canTradeText = `不可开仓${parts.join("")}`;
canTradeText = `不可开仓(${parts.join(";")})`;
}"""
new_can_trade = """ let canTradeText = "可开仓";
if(!data.can_trade){
@@ -106,7 +106,7 @@ def main() -> None:
const opens = Number(data.opens_today);
if (hard > 0 && !Number.isNaN(opens) && opens >= hard) parts.push(`本交易日开仓 ${opens}/${hard} 已达上限`);
if(data.open_guard_blocks_now) parts.push(`未到北京时间 ${data.reset_hour||{{ reset_hour }}}:00`);
canTradeText = parts.length ? `不可开仓${parts.join("")}` : "不可开仓";
canTradeText = parts.length ? `不可开仓(${parts.join(";")})` : "不可开仓";
}"""
text = text.replace(old_can_trade, new_can_trade, 1)
@@ -118,11 +118,11 @@ def main() -> None:
}
if(guardStatus && typeof data.open_guard_enabled !== "undefined"){
guardStatus.innerText = data.open_guard_enabled
? `已限制${resetH}:00 前不可开仓`
: `已放开${resetH}:00 前允许开仓`;
? `已限制:${resetH}:00 前不可开仓`
: `已放开:${resetH}:00 前允许开仓`;
}"""
insert_after = """ if(tip){
tip.innerText = `规则最多 ${data.max_active_positions || {{ max_active_positions }}} 仓BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x${openCntTxt ? openCntTxt + "" : ""}${canTradeText}${avail}人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1`;
tip.innerText = `规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x / 山寨 {{ alt_leverage }}x;${openCntTxt ? openCntTxt + ";" : ""}${canTradeText}${avail};人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1`;
}
}).catch(()=>{});"""
if guard_sync not in text:
@@ -158,7 +158,7 @@ if(allowOpenBeforeResetEl){
orphan_fn_guard,
)
header = "{# 三所共用 standalone 主页 — 由 scripts/build_unified_index.py 生成勿手改三所副本 #}\n"
header = "{# 三所共用 standalone 主页 — 由 scripts/build_unified_index.py 生成,勿手改三所副本 #}\n"
if not text.startswith("{# 三所共用"):
text = header + text
+93 -93
View File
@@ -1,93 +1,93 @@
#!/usr/bin/env python3
"""清空中控 K 线 SQLite 缓存hub_kline.db),便于清库后全量重拉
用法Linux 云服务器在仓库根目录):
python3 scripts/clear_hub_kline_db.py --dry-run
python3 scripts/clear_hub_kline_db.py --apply
python3 scripts/clear_hub_kline_db.py --apply --exchange binance --symbol BTC/USDT --timeframe 15m
默认库路径环境变量 HUB_KLINE_DB_PATH或 manual_trading_hub/data/hub_kline.db
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from lib.hub.hub_kline_store import ( # noqa: E402
clear_all_bars,
clear_series_bars,
default_db_path,
init_db,
)
def main() -> int:
parser = argparse.ArgumentParser(description="Clear manual-trading-hub K-line SQLite cache.")
parser.add_argument(
"--db",
default=os.getenv("HUB_KLINE_DB_PATH", "").strip() or str(default_db_path()),
help="hub_kline.db path",
)
parser.add_argument("--exchange", default="", help="exchange_key, e.g. binance")
parser.add_argument("--symbol", default="", help="symbol, e.g. BTC/USDT")
parser.add_argument("--timeframe", default="", help="optional timeframe, e.g. 15m")
parser.add_argument("--dry-run", action="store_true", help="count only")
parser.add_argument("--apply", action="store_true", help="execute delete")
args = parser.parse_args()
db_path = Path(args.db)
if not db_path.is_file():
print(f"DB not found: {db_path}", file=sys.stderr)
return 1
init_db(db_path)
ex = (args.exchange or "").strip().lower()
sym = (args.symbol or "").strip().upper()
tf = (args.timeframe or "").strip().lower() or None
if args.dry_run and not args.apply:
import sqlite3
conn = sqlite3.connect(str(db_path))
try:
if ex and sym:
if tf:
n = conn.execute(
"SELECT COUNT(*) FROM ohlcv_bars WHERE exchange_key=? AND symbol=? AND timeframe=?",
(ex, sym, tf),
).fetchone()[0]
print(f"would delete series rows: {n} ({ex} {sym} {tf})")
else:
n = conn.execute(
"SELECT COUNT(*) FROM ohlcv_bars WHERE exchange_key=? AND symbol=?",
(ex, sym),
).fetchone()[0]
print(f"would delete symbol rows: {n} ({ex} {sym} all tf)")
else:
n = conn.execute("SELECT COUNT(*) FROM ohlcv_bars").fetchone()[0]
print(f"would delete all ohlcv_bars rows: {n}")
finally:
conn.close()
return 0
if not args.apply:
print("Specify --apply to delete (or --dry-run to preview).", file=sys.stderr)
return 1
if ex and sym:
removed = clear_series_bars(ex, sym, tf, db_path)
scope = f"{ex} {sym}" + (f" {tf}" if tf else " (all timeframes)")
print(f"cleared {removed} rows for {scope}")
else:
removed = clear_all_bars(db_path)
print(f"cleared all {removed} ohlcv_bars rows from {db_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""清空中控 K 线 SQLite 缓存(hub_kline.db),便于清库后全量重拉.
用法(Linux 云服务器,在仓库根目录):
python3 scripts/clear_hub_kline_db.py --dry-run
python3 scripts/clear_hub_kline_db.py --apply
python3 scripts/clear_hub_kline_db.py --apply --exchange binance --symbol BTC/USDT --timeframe 15m
默认库路径:环境变量 HUB_KLINE_DB_PATH,或 manual_trading_hub/data/hub_kline.db
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from lib.hub.hub_kline_store import ( # noqa: E402
clear_all_bars,
clear_series_bars,
default_db_path,
init_db,
)
def main() -> int:
parser = argparse.ArgumentParser(description="Clear manual-trading-hub K-line SQLite cache.")
parser.add_argument(
"--db",
default=os.getenv("HUB_KLINE_DB_PATH", "").strip() or str(default_db_path()),
help="hub_kline.db path",
)
parser.add_argument("--exchange", default="", help="exchange_key, e.g. binance")
parser.add_argument("--symbol", default="", help="symbol, e.g. BTC/USDT")
parser.add_argument("--timeframe", default="", help="optional timeframe, e.g. 15m")
parser.add_argument("--dry-run", action="store_true", help="count only")
parser.add_argument("--apply", action="store_true", help="execute delete")
args = parser.parse_args()
db_path = Path(args.db)
if not db_path.is_file():
print(f"DB not found: {db_path}", file=sys.stderr)
return 1
init_db(db_path)
ex = (args.exchange or "").strip().lower()
sym = (args.symbol or "").strip().upper()
tf = (args.timeframe or "").strip().lower() or None
if args.dry_run and not args.apply:
import sqlite3
conn = sqlite3.connect(str(db_path))
try:
if ex and sym:
if tf:
n = conn.execute(
"SELECT COUNT(*) FROM ohlcv_bars WHERE exchange_key=? AND symbol=? AND timeframe=?",
(ex, sym, tf),
).fetchone()[0]
print(f"would delete series rows: {n} ({ex} {sym} {tf})")
else:
n = conn.execute(
"SELECT COUNT(*) FROM ohlcv_bars WHERE exchange_key=? AND symbol=?",
(ex, sym),
).fetchone()[0]
print(f"would delete symbol rows: {n} ({ex} {sym} all tf)")
else:
n = conn.execute("SELECT COUNT(*) FROM ohlcv_bars").fetchone()[0]
print(f"would delete all ohlcv_bars rows: {n}")
finally:
conn.close()
return 0
if not args.apply:
print("Specify --apply to delete (or --dry-run to preview).", file=sys.stderr)
return 1
if ex and sym:
removed = clear_series_bars(ex, sym, tf, db_path)
scope = f"{ex} {sym}" + (f" {tf}" if tf else " (all timeframes)")
print(f"cleared {removed} rows for {scope}")
else:
removed = clear_all_bars(db_path)
print(f"cleared all {removed} ohlcv_bars rows from {db_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+67 -67
View File
@@ -1,67 +1,67 @@
#!/usr/bin/env python3
"""清理 strategy_trade_snapshots 重复行同计划 + 同结果仅保留 id 最大的一条)。
用法在实例目录如 crypto_monitor_gate):
python ../scripts/dedupe_strategy_snapshots.py
python ../scripts/dedupe_strategy_snapshots.py --db crypto.db
"""
from __future__ import annotations
import argparse
import os
import sqlite3
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from lib.strategy.strategy_snapshot_lib import dedupe_strategy_snapshots, init_strategy_snapshot_table # noqa: E402
def main() -> int:
parser = argparse.ArgumentParser(description="Dedupe strategy_trade_snapshots rows.")
parser.add_argument(
"--db",
default=os.getenv("DB_PATH", "crypto.db"),
help="SQLite database path (default: DB_PATH or crypto.db)",
)
parser.add_argument("--dry-run", action="store_true", help="Count only, do not delete")
args = parser.parse_args()
db_path = Path(args.db)
if not db_path.is_file():
print(f"DB not found: {db_path}", file=sys.stderr)
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
init_strategy_snapshot_table(conn)
before = conn.execute("SELECT COUNT(*) AS c FROM strategy_trade_snapshots").fetchone()["c"]
dup_groups = conn.execute(
"""SELECT strategy_type, source_id, result_label, COUNT(*) AS n
FROM strategy_trade_snapshots
GROUP BY strategy_type, source_id, result_label
HAVING n > 1
ORDER BY n DESC"""
).fetchall()
extra = sum(int(r["n"]) - 1 for r in dup_groups)
print(f"snapshots total={before}, duplicate rows to remove={extra}, groups={len(dup_groups)}")
for r in dup_groups[:20]:
print(
f" {r['strategy_type']} plan={r['source_id']} "
f"{r['result_label']} x{r['n']}"
)
if args.dry_run:
conn.close()
return 0
removed = dedupe_strategy_snapshots(conn)
conn.commit()
after = conn.execute("SELECT COUNT(*) AS c FROM strategy_trade_snapshots").fetchone()["c"]
conn.close()
print(f"removed={removed}, remaining={after}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""清理 strategy_trade_snapshots 重复行(同计划 + 同结果仅保留 id 最大的一条).
用法(在实例目录,如 crypto_monitor_gate):
python ../scripts/dedupe_strategy_snapshots.py
python ../scripts/dedupe_strategy_snapshots.py --db crypto.db
"""
from __future__ import annotations
import argparse
import os
import sqlite3
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from lib.strategy.strategy_snapshot_lib import dedupe_strategy_snapshots, init_strategy_snapshot_table # noqa: E402
def main() -> int:
parser = argparse.ArgumentParser(description="Dedupe strategy_trade_snapshots rows.")
parser.add_argument(
"--db",
default=os.getenv("DB_PATH", "crypto.db"),
help="SQLite database path (default: DB_PATH or crypto.db)",
)
parser.add_argument("--dry-run", action="store_true", help="Count only, do not delete")
args = parser.parse_args()
db_path = Path(args.db)
if not db_path.is_file():
print(f"DB not found: {db_path}", file=sys.stderr)
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
init_strategy_snapshot_table(conn)
before = conn.execute("SELECT COUNT(*) AS c FROM strategy_trade_snapshots").fetchone()["c"]
dup_groups = conn.execute(
"""SELECT strategy_type, source_id, result_label, COUNT(*) AS n
FROM strategy_trade_snapshots
GROUP BY strategy_type, source_id, result_label
HAVING n > 1
ORDER BY n DESC"""
).fetchall()
extra = sum(int(r["n"]) - 1 for r in dup_groups)
print(f"snapshots total={before}, duplicate rows to remove={extra}, groups={len(dup_groups)}")
for r in dup_groups[:20]:
print(
f" {r['strategy_type']} plan={r['source_id']} "
f"{r['result_label']} x{r['n']}"
)
if args.dry_run:
conn.close()
return 0
removed = dedupe_strategy_snapshots(conn)
conn.commit()
after = conn.execute("SELECT COUNT(*) AS c FROM strategy_trade_snapshots").fetchone()["c"]
conn.close()
print(f"removed={removed}, remaining={after}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+78 -78
View File
@@ -1,78 +1,78 @@
#!/usr/bin/env python3
"""修正趋势保本移交后 monitor_type 仍为「下单监控」的历史数据"""
from __future__ import annotations
import argparse
import sqlite3
from pathlib import Path
from lib.strategy.strategy_trade_labels import MONITOR_TYPE_TREND_PULLBACK
def main() -> int:
parser = argparse.ArgumentParser(description="Fix trend handoff order/trade monitor_type labels.")
parser.add_argument("--db", required=True, help="Path to instance sqlite db")
parser.add_argument("--dry-run", action="store_true", help="Preview only")
parser.add_argument("--apply", action="store_true", help="Apply updates")
args = parser.parse_args()
if not args.dry_run and not args.apply:
args.dry_run = True
db_path = Path(args.db).expanduser().resolve()
if not db_path.is_file():
print(f"[ERR] DB not found: {db_path}")
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute(
"""
SELECT COUNT(*) AS c FROM order_monitors
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
"""
)
om_n = int(cur.fetchone()["c"])
cur.execute(
"""
SELECT COUNT(*) AS c FROM trade_records
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
"""
)
tr_n = int(cur.fetchone()["c"])
print(f"[INFO] order_monitors to fix: {om_n}")
print(f"[INFO] trade_records to fix: {tr_n}")
if args.dry_run:
conn.close()
return 0
cur.execute(
"""
UPDATE order_monitors
SET monitor_type=?
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
""",
(MONITOR_TYPE_TREND_PULLBACK,),
)
cur.execute(
"""
UPDATE trade_records
SET monitor_type=?
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
""",
(MONITOR_TYPE_TREND_PULLBACK,),
)
conn.commit()
conn.close()
print("[OK] Applied.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""修正趋势保本移交后 monitor_type 仍为「下单监控」的历史数据."""
from __future__ import annotations
import argparse
import sqlite3
from pathlib import Path
from lib.strategy.strategy_trade_labels import MONITOR_TYPE_TREND_PULLBACK
def main() -> int:
parser = argparse.ArgumentParser(description="Fix trend handoff order/trade monitor_type labels.")
parser.add_argument("--db", required=True, help="Path to instance sqlite db")
parser.add_argument("--dry-run", action="store_true", help="Preview only")
parser.add_argument("--apply", action="store_true", help="Apply updates")
args = parser.parse_args()
if not args.dry_run and not args.apply:
args.dry_run = True
db_path = Path(args.db).expanduser().resolve()
if not db_path.is_file():
print(f"[ERR] DB not found: {db_path}")
return 1
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute(
"""
SELECT COUNT(*) AS c FROM order_monitors
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
"""
)
om_n = int(cur.fetchone()["c"])
cur.execute(
"""
SELECT COUNT(*) AS c FROM trade_records
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
"""
)
tr_n = int(cur.fetchone()["c"])
print(f"[INFO] order_monitors to fix: {om_n}")
print(f"[INFO] trade_records to fix: {tr_n}")
if args.dry_run:
conn.close()
return 0
cur.execute(
"""
UPDATE order_monitors
SET monitor_type=?
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
""",
(MONITOR_TYPE_TREND_PULLBACK,),
)
cur.execute(
"""
UPDATE trade_records
SET monitor_type=?
WHERE trend_plan_id IS NOT NULL AND trend_plan_id > 0
AND (monitor_type IS NULL OR TRIM(monitor_type) = '' OR monitor_type = '下单监控')
""",
(MONITOR_TYPE_TREND_PULLBACK,),
)
conn.commit()
conn.close()
print("[OK] Applied.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""生成品牌 PNG/ICOPillow),供 Chrome 快捷方式与 manifest 使用"""
"""生成品牌 PNG/ICO(Pillow),供 Chrome 快捷方式与 manifest 使用."""
from __future__ import annotations
import os
@@ -35,7 +35,7 @@ def render_icon(size: int):
inner = m + max(2, size // 28)
_rounded_rect(draw, (inner, inner, size - inner, size - inner), max(6, r - 4), PANEL)
# 渐变描边四角采样
# 渐变描边(四角采样)
border = max(2, size // 42)
for i in range(border):
t0 = i / max(1, border - 1)
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""将全角/易混淆标点规范为半角 ASCII(注释, 文档, 配置模板).
不转换弯引号 “ ” ‘ ’,避免破坏 Python/JS 字符串字面量.
"""
from __future__ import annotations
import argparse
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
SKIP_DIRS = frozenset({
".git",
".venv",
"node_modules",
"__pycache__",
".cursor",
"agent-transcripts",
})
SCAN_SUFFIXES = frozenset({
".py",
".js",
".html",
".md",
".sh",
".css",
".json",
".cjs",
".txt",
".yml",
".yaml",
".example",
})
AMBIGUOUS_CHARS = frozenset(
"\uff08\uff09\uff1a\uff0c\uff1b\uff1f\uff01\u3002\u3001\u00a0"
)
TRANSLATION = str.maketrans(
{
"\uff08": "(",
"\uff09": ")",
"\uff1a": ":",
"\uff0c": ",",
"\uff1b": ";",
"\uff1f": "?",
"\uff01": "!",
"\u3002": ".",
"\u3001": ",",
"\u00a0": " ",
}
)
def should_scan(path: Path) -> bool:
if not path.is_file():
return False
if any(part in SKIP_DIRS for part in path.parts):
return False
if path.name == ".env.example" or path.name.endswith(".env.example"):
return True
return path.suffix in SCAN_SUFFIXES
def normalize_text(text: str) -> tuple[str, int]:
count = sum(1 for ch in text if ch in AMBIGUOUS_CHARS)
if not count:
return text, 0
return text.translate(TRANSLATION), count
def read_text_strip_bom(path: Path) -> tuple[str, bool]:
raw = path.read_text(encoding="utf-8")
if raw.startswith("\ufeff"):
return raw.lstrip("\ufeff"), True
return raw, False
def iter_targets(root: Path) -> list[Path]:
return sorted(p for p in root.rglob("*") if should_scan(p))
def main() -> int:
parser = argparse.ArgumentParser(description="Normalize ambiguous Unicode punctuation")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--root", default=str(REPO))
args = parser.parse_args()
root = Path(args.root)
files_changed = 0
chars_changed = 0
for path in iter_targets(root):
try:
original, had_bom = read_text_strip_bom(path)
except (OSError, UnicodeDecodeError):
continue
normalized, n = normalize_text(original)
if not n and not had_bom:
continue
rel = path.relative_to(root)
if args.dry_run:
print(f"[dry-run] {rel}: {n} chars")
else:
# 保持原换行风格, 仅替换标点
path.write_text(normalized, encoding="utf-8", newline="")
files_changed += 1
chars_changed += n
print(f"done: {files_changed} files, {chars_changed} replacements")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,81 +1,81 @@
#!/usr/bin/env python3
"""
一次性备份三所 .env + 中控 .env / hub_settings.json不含图片不含数据库)。
用途删除 gate清库全新计划启动前在仓库根目录执行一次即可
python scripts/one_shot_backup_config_before_cleanup.py
输出目录默认backups/one-shot-YYYYMMDD-HHMMSS/config/
"""
from __future__ import annotations
import shutil
import sys
from datetime import datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CONFIG_SOURCES: list[tuple[str, Path]] = [
("crypto_monitor_binance.env", REPO_ROOT / "crypto_monitor_binance" / ".env"),
("crypto_monitor_okx.env", REPO_ROOT / "crypto_monitor_okx" / ".env"),
("crypto_monitor_gate.env", REPO_ROOT / "crypto_monitor_gate" / ".env"),
("manual_trading_hub.env", REPO_ROOT / "manual_trading_hub" / ".env"),
("hub_settings.json", REPO_ROOT / "manual_trading_hub" / "hub_settings.json"),
]
ENV_BACKUP_GLOBS = (
REPO_ROOT / "crypto_monitor_binance",
REPO_ROOT / "crypto_monitor_okx",
REPO_ROOT / "crypto_monitor_gate",
)
def main() -> int:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
out_dir = REPO_ROOT / "backups" / f"one-shot-{stamp}" / "config"
out_dir.mkdir(parents=True, exist_ok=True)
copied: list[str] = []
missing: list[str] = []
for dest_name, src in CONFIG_SOURCES:
if src.is_file():
shutil.copy2(src, out_dir / dest_name)
copied.append(dest_name)
else:
missing.append(str(src.relative_to(REPO_ROOT)))
for inst_dir in ENV_BACKUP_GLOBS:
for src in sorted(inst_dir.glob(".env.backup.*")):
dest_name = f"{inst_dir.name}.{src.name}"
shutil.copy2(src, out_dir / dest_name)
copied.append(dest_name)
manifest = out_dir.parent / "manifest.txt"
lines = [
f"created_at={stamp}",
f"repo={REPO_ROOT}",
"",
"copied:",
*[f" - {name}" for name in copied],
"",
"missing (skipped):",
*[f" - {p}" for p in missing],
"",
"not included: crypto.db, hub *.db, static/images, gate",
]
manifest.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Backup written to: {out_dir}")
if copied:
print("Copied:", ", ".join(copied))
if missing:
print("Missing (ok if fresh install):", ", ".join(missing))
print(f"Manifest: {manifest}")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
一次性备份:三所 .env + 中控 .env / hub_settings.json(不含图片,不含数据库).
用途:删除 gate,清库,全新计划启动前,在仓库根目录执行一次即可:
python scripts/one_shot_backup_config_before_cleanup.py
输出目录默认:backups/one-shot-YYYYMMDD-HHMMSS/config/
"""
from __future__ import annotations
import shutil
import sys
from datetime import datetime
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
CONFIG_SOURCES: list[tuple[str, Path]] = [
("crypto_monitor_binance.env", REPO_ROOT / "crypto_monitor_binance" / ".env"),
("crypto_monitor_okx.env", REPO_ROOT / "crypto_monitor_okx" / ".env"),
("crypto_monitor_gate.env", REPO_ROOT / "crypto_monitor_gate" / ".env"),
("manual_trading_hub.env", REPO_ROOT / "manual_trading_hub" / ".env"),
("hub_settings.json", REPO_ROOT / "manual_trading_hub" / "hub_settings.json"),
]
ENV_BACKUP_GLOBS = (
REPO_ROOT / "crypto_monitor_binance",
REPO_ROOT / "crypto_monitor_okx",
REPO_ROOT / "crypto_monitor_gate",
)
def main() -> int:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
out_dir = REPO_ROOT / "backups" / f"one-shot-{stamp}" / "config"
out_dir.mkdir(parents=True, exist_ok=True)
copied: list[str] = []
missing: list[str] = []
for dest_name, src in CONFIG_SOURCES:
if src.is_file():
shutil.copy2(src, out_dir / dest_name)
copied.append(dest_name)
else:
missing.append(str(src.relative_to(REPO_ROOT)))
for inst_dir in ENV_BACKUP_GLOBS:
for src in sorted(inst_dir.glob(".env.backup.*")):
dest_name = f"{inst_dir.name}.{src.name}"
shutil.copy2(src, out_dir / dest_name)
copied.append(dest_name)
manifest = out_dir.parent / "manifest.txt"
lines = [
f"created_at={stamp}",
f"repo={REPO_ROOT}",
"",
"copied:",
*[f" - {name}" for name in copied],
"",
"missing (skipped):",
*[f" - {p}" for p in missing],
"",
"not included: crypto.db, hub *.db, static/images, gate",
]
manifest.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Backup written to: {out_dir}")
if copied:
print("Copied:", ", ".join(copied))
if missing:
print("Missing (ok if fresh install):", ", ".join(missing))
print(f"Manifest: {manifest}")
return 0
if __name__ == "__main__":
sys.exit(main())
+202 -202
View File
@@ -1,202 +1,202 @@
#!/usr/bin/env python3
"""Patch binance/okx/gate app.py for entry_model support."""
from __future__ import annotations
import os
import re
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMPORT_BLOCK = """from lib.trade.entry_model_lib import (
build_intraday_entry_reason_options,
build_trend_div_entry_reason_options,
enrich_entry_model_display,
migrate_entry_model_columns,
order_entry_template_context,
parse_manual_order_style_fields,
resolve_trade_record_entry_reason,
trend_manual_entry_reason_count,
)
"""
KEY_IMPORT = "from lib.key_monitor.key_auto_order_lib import (\n check_monitor_type_add_allowed,\n effective_entry_reason_options,\n effective_stats_segment_defs,\n load_key_auto_order_enabled,"
KEY_IMPORT_WITH_KEY_OPTS = "from lib.key_monitor.key_auto_order_lib import (\n KEY_ENTRY_REASON_OPTIONS,\n check_monitor_type_add_allowed,\n effective_entry_reason_options,\n effective_stats_segment_defs,\n load_key_auto_order_enabled,"
def patch_file(path: str, exchange: str) -> bool:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
orig = text
if "from lib.trade.entry_model_lib import" not in text:
text = text.replace(
"from lib.trade.trade_policy_app_lib import (",
IMPORT_BLOCK + "from lib.trade.trade_policy_app_lib import (",
1,
)
if exchange == "gate":
old_er = '''# 与用户约定的固定开仓类型
ENTRY_REASON_OPTIONS = (
"趋势多头4h大结构突破前进场确认条件三次探顶5m收敛不创新低",
"趋势空头4h大结构突破前进场确认条件三次探底5m收敛不创新高",
"趋势多头小分歧低吸入场左侧),确认条件二次探底",
"趋势空头小分歧高吸入场左侧),确认条件二次探顶",
"波段单5m顺势突破确认条件2根k线+成交量放大+4h同向+日成交量前20",
"关键位箱体突破",
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS'''
new_er = """# 日内户长句开仓类型 + 关键位 + 策略大分歧 A/B/小分歧 仅趋势户
ENTRY_REASON_OPTIONS = build_intraday_entry_reason_options(
KEY_ENTRY_REASON_OPTIONS,
STRATEGY_ENTRY_REASON_OPTIONS,
)"""
text = text.replace(old_er, new_er)
if "KEY_ENTRY_REASON_OPTIONS," not in text.split("load_key_auto_order_enabled")[0]:
text = text.replace(KEY_IMPORT, KEY_IMPORT_WITH_KEY_OPTS, 1)
else:
old_er = '''# 与用户约定的固定开仓类型仅做这几类单子
ENTRY_REASON_OPTIONS = (
"趋势多头4h大结构突破前进场确认条件三次探顶5m收敛不创新低",
"趋势空头4h大结构突破前进场确认条件三次探底5m收敛不创新高",
"趋势多头小分歧低吸入场左侧),确认条件二次探底",
"趋势空头小分歧高吸入场左侧),确认条件二次探顶",
"波段单5m顺势突破确认条件2根k线+成交量放大+4h同向+日成交量前20",
"关键位箱体突破",
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS'''
new_er = """# 趋势户大分歧A/B/小分歧 + 策略关键位本实例关闭
ENTRY_REASON_OPTIONS = build_trend_div_entry_reason_options(STRATEGY_ENTRY_REASON_OPTIONS)"""
text = text.replace(old_er, new_er)
if "migrate_entry_model_columns(conn)" not in text:
text = text.replace(
" conn.commit()\n conn.close()\n\n\ndef get_db",
" migrate_entry_model_columns(conn)\n conn.commit()\n conn.close()\n\n\ndef get_db",
1,
)
text = re.sub(
r" er = \(\n \(entry_reason or \"\"\)\.strip\(\)\n or entry_reason_from_key_signal\(kst\)\n or entry_reason_for_monitor_type\(monitor_type\)\n or \"\"\n \)",
""" er = resolve_trade_record_entry_reason(
entry_reason=entry_reason,
entry_model=entry_model,
key_signal_type=kst,
monitor_type=monitor_type,
entry_reason_from_key_signal=entry_reason_from_key_signal,
entry_reason_for_monitor_type=entry_reason_for_monitor_type,
)""",
text,
count=1,
)
if "entry_model=None," not in text:
text = text.replace(
" entry_reason=None,\n trend_plan_id=None,",
" entry_reason=None,\n entry_model=None,\n trend_plan_id=None,",
1,
)
if "enrich_entry_model_display(item)" not in text:
text = text.replace(
" enrich_order_display_fields(item, calc_rr_ratio)\n try:",
" enrich_order_display_fields(item, calc_rr_ratio)\n enrich_entry_model_display(item)\n try:",
1,
)
text = text.replace(
""" trade_style = (d.get("trade_style") or DEFAULT_TRADE_STYLE or "trend").strip().lower()
if trade_style not in ("trend", "swing"):
trade_style = "trend"
available_usdt = get_available_trading_usdt()""",
""" trade_style, entry_model, style_err = parse_manual_order_style_fields(
TRADE_POLICY, d, default_trade_style=DEFAULT_TRADE_STYLE or "trend"
)
if style_err:
conn.close()
flash(style_err)
return redirect("/trade")
available_usdt = get_available_trading_usdt()""",
1,
)
old_insert = (
'"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",\n'
" (\n"
" symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,\n"
" margin_capital, leverage, trade_style, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,\n"
" breakeven_enabled,\n"
" notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,\n"
" ORDER_MONITOR_TYPE_MANUAL,\n"
" tc_en, tc_h, tc_at,\n"
" )"
)
new_insert = (
'"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, entry_model, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",\n'
" (\n"
" symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,\n"
" margin_capital, leverage, trade_style, entry_model, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,\n"
" breakeven_enabled,\n"
" notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,\n"
" ORDER_MONITOR_TYPE_MANUAL,\n"
" tc_en, tc_h, tc_at,\n"
" )"
)
text = text.replace(old_insert, new_insert)
text = text.replace(
""" effective_entry_reason_options(
ENTRY_REASON_OPTIONS,
POSITION_SIZING_MODE,
KEY_AUTO_ORDER_ENABLED,
)""",
""" effective_entry_reason_options(
ENTRY_REASON_OPTIONS,
POSITION_SIZING_MODE,
KEY_AUTO_ORDER_ENABLED,
trend_manual_count=trend_manual_entry_reason_count(TRADE_POLICY),
)""",
1,
)
if "**order_entry_template_context(TRADE_POLICY)," not in text:
text = text.replace(
" trade_policy=trade_policy_template_context(TRADE_POLICY),",
" trade_policy=trade_policy_template_context(TRADE_POLICY),\n **order_entry_template_context(TRADE_POLICY),",
1,
)
# insert_trade_record from order row: add entry_model
text = re.sub(
r"(insert_trade_record\(\n\s+conn,\n(?:[^\n]+\n)+?\s+trade_style=r\[\"trade_style\"\],\n)",
r"\1 entry_model=(r[\"entry_model\"] if \"entry_model\" in r.keys() else None),\n",
text,
)
if text != orig:
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
return True
return False
def main():
for ex in ("binance", "okx", "gate"):
path = os.path.join(REPO, f"crypto_monitor_{ex}", "app.py")
changed = patch_file(path, ex)
print(f"{ex}: {'patched' if changed else 'no change'}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Patch binance/okx/gate app.py for entry_model support."""
from __future__ import annotations
import os
import re
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMPORT_BLOCK = """from lib.trade.entry_model_lib import (
build_intraday_entry_reason_options,
build_trend_div_entry_reason_options,
enrich_entry_model_display,
migrate_entry_model_columns,
order_entry_template_context,
parse_manual_order_style_fields,
resolve_trade_record_entry_reason,
trend_manual_entry_reason_count,
)
"""
KEY_IMPORT = "from lib.key_monitor.key_auto_order_lib import (\n check_monitor_type_add_allowed,\n effective_entry_reason_options,\n effective_stats_segment_defs,\n load_key_auto_order_enabled,"
KEY_IMPORT_WITH_KEY_OPTS = "from lib.key_monitor.key_auto_order_lib import (\n KEY_ENTRY_REASON_OPTIONS,\n check_monitor_type_add_allowed,\n effective_entry_reason_options,\n effective_stats_segment_defs,\n load_key_auto_order_enabled,"
def patch_file(path: str, exchange: str) -> bool:
with open(path, "r", encoding="utf-8") as f:
text = f.read()
orig = text
if "from lib.trade.entry_model_lib import" not in text:
text = text.replace(
"from lib.trade.trade_policy_app_lib import (",
IMPORT_BLOCK + "from lib.trade.trade_policy_app_lib import (",
1,
)
if exchange == "gate":
old_er = '''# 与用户约定的固定开仓类型
ENTRY_REASON_OPTIONS = (
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
"关键位箱体突破",
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS'''
new_er = """# 日内户:长句开仓类型 + 关键位 + 策略(大分歧 A/B/小分歧 仅趋势户)
ENTRY_REASON_OPTIONS = build_intraday_entry_reason_options(
KEY_ENTRY_REASON_OPTIONS,
STRATEGY_ENTRY_REASON_OPTIONS,
)"""
text = text.replace(old_er, new_er)
if "KEY_ENTRY_REASON_OPTIONS," not in text.split("load_key_auto_order_enabled")[0]:
text = text.replace(KEY_IMPORT, KEY_IMPORT_WITH_KEY_OPTS, 1)
else:
old_er = '''# 与用户约定的固定开仓类型(仅做这几类单子)
ENTRY_REASON_OPTIONS = (
"趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低",
"趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高",
"趋势多头:小分歧低吸入场(左侧),确认条件:二次探底",
"趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶",
"波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20",
"关键位箱体突破",
"关键位收敛突破",
"关键位斐波0.618",
"关键位斐波0.786",
"关键位假突破",
"关键位回调触价开仓",
"关键位突破触价开仓",
) + STRATEGY_ENTRY_REASON_OPTIONS'''
new_er = """# 趋势户:大分歧A/B/小分歧 + 策略(关键位本实例关闭)
ENTRY_REASON_OPTIONS = build_trend_div_entry_reason_options(STRATEGY_ENTRY_REASON_OPTIONS)"""
text = text.replace(old_er, new_er)
if "migrate_entry_model_columns(conn)" not in text:
text = text.replace(
" conn.commit()\n conn.close()\n\n\ndef get_db",
" migrate_entry_model_columns(conn)\n conn.commit()\n conn.close()\n\n\ndef get_db",
1,
)
text = re.sub(
r" er = \(\n \(entry_reason or \"\"\)\.strip\(\)\n or entry_reason_from_key_signal\(kst\)\n or entry_reason_for_monitor_type\(monitor_type\)\n or \"\"\n \)",
""" er = resolve_trade_record_entry_reason(
entry_reason=entry_reason,
entry_model=entry_model,
key_signal_type=kst,
monitor_type=monitor_type,
entry_reason_from_key_signal=entry_reason_from_key_signal,
entry_reason_for_monitor_type=entry_reason_for_monitor_type,
)""",
text,
count=1,
)
if "entry_model=None," not in text:
text = text.replace(
" entry_reason=None,\n trend_plan_id=None,",
" entry_reason=None,\n entry_model=None,\n trend_plan_id=None,",
1,
)
if "enrich_entry_model_display(item)" not in text:
text = text.replace(
" enrich_order_display_fields(item, calc_rr_ratio)\n try:",
" enrich_order_display_fields(item, calc_rr_ratio)\n enrich_entry_model_display(item)\n try:",
1,
)
text = text.replace(
""" trade_style = (d.get("trade_style") or DEFAULT_TRADE_STYLE or "trend").strip().lower()
if trade_style not in ("trend", "swing"):
trade_style = "trend"
available_usdt = get_available_trading_usdt()""",
""" trade_style, entry_model, style_err = parse_manual_order_style_fields(
TRADE_POLICY, d, default_trade_style=DEFAULT_TRADE_STYLE or "trend"
)
if style_err:
conn.close()
flash(style_err)
return redirect("/trade")
available_usdt = get_available_trading_usdt()""",
1,
)
old_insert = (
'"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",\n'
" (\n"
" symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,\n"
" margin_capital, leverage, trade_style, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,\n"
" breakeven_enabled,\n"
" notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,\n"
" ORDER_MONITOR_TYPE_MANUAL,\n"
" tc_en, tc_h, tc_at,\n"
" )"
)
new_insert = (
'"INSERT INTO order_monitors (symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, margin_capital, leverage, trade_style, entry_model, risk_percent, risk_amount, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, opened_at, opened_at_ms, session_date, monitor_type, time_close_enabled, time_close_hours, time_close_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",\n'
" (\n"
" symbol, exchange_symbol, direction, trigger_price, stop_loss, stop_loss, take_profit,\n"
" margin_capital, leverage, trade_style, entry_model, risk_percent_db, risk_amount_final, breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, 0, breakeven_price,\n"
" breakeven_enabled,\n"
" notional_value, position_ratio, base_amount, amount, open_order_id, opened_at_bj, opened_at_ms, trading_day,\n"
" ORDER_MONITOR_TYPE_MANUAL,\n"
" tc_en, tc_h, tc_at,\n"
" )"
)
text = text.replace(old_insert, new_insert)
text = text.replace(
""" effective_entry_reason_options(
ENTRY_REASON_OPTIONS,
POSITION_SIZING_MODE,
KEY_AUTO_ORDER_ENABLED,
)""",
""" effective_entry_reason_options(
ENTRY_REASON_OPTIONS,
POSITION_SIZING_MODE,
KEY_AUTO_ORDER_ENABLED,
trend_manual_count=trend_manual_entry_reason_count(TRADE_POLICY),
)""",
1,
)
if "**order_entry_template_context(TRADE_POLICY)," not in text:
text = text.replace(
" trade_policy=trade_policy_template_context(TRADE_POLICY),",
" trade_policy=trade_policy_template_context(TRADE_POLICY),\n **order_entry_template_context(TRADE_POLICY),",
1,
)
# insert_trade_record from order row: add entry_model
text = re.sub(
r"(insert_trade_record\(\n\s+conn,\n(?:[^\n]+\n)+?\s+trade_style=r\[\"trade_style\"\],\n)",
r"\1 entry_model=(r[\"entry_model\"] if \"entry_model\" in r.keys() else None),\n",
text,
)
if text != orig:
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(text)
return True
return False
def main():
for ex in ("binance", "okx", "gate"):
path = os.path.join(REPO, f"crypto_monitor_{ex}", "app.py")
changed = patch_file(path, ex)
print(f"{ex}: {'patched' if changed else 'no change'}")
if __name__ == "__main__":
main()
+7 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""涓哄洓鎵€ templates 娉ㄥ叆 instance_theme 鑴氭湰/鏍峰紡涓庡垏鎹㈡寜閽€?""
#!/usr/bin/env python3
"""为四所 templates 注入 instance_theme 脚本/样式与切换按钮."""
from __future__ import annotations
from pathlib import Path
@@ -11,13 +11,13 @@ FILES = ("index.html", "login.html", "key_focus_v2.html", "order_focus_v2.html")
SCRIPT_TAG = ' <script src="/static/instance_theme.js?v=4"></script>\n'
CSS_LINK = ' <link rel="stylesheet" href="/static/instance_theme.css?v=4">\n'
THEME_TOGGLE = """ <div class="theme-toggle instance-theme-toggle" role="group" aria-label="鐣岄潰涓婚">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="鏆楄壊涓婚">
THEME_TOGGLE = """ <div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="浜壊涓婚">
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
@@ -26,12 +26,12 @@ THEME_TOGGLE = """ <div class="theme-toggle instance-theme-toggle" role="
"""
INDEX_HEADER_OLD = """ <div class="header">
<h1>鍔犲瘑璐у竵锝滀氦鏄撶洃鎺?+ AI澶嶇洏涓浣撳寲</h1>
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="exchange-tag">{{ exchange_display }}</div>
</div>"""
INDEX_HEADER_NEW = """ <div class="header">
<h1>鍔犲瘑璐у竵锝滀氦鏄撶洃鎺?+ AI澶嶇洏涓浣撳寲</h1>
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
""" + THEME_TOGGLE + """ </div>
+196 -196
View File
@@ -1,196 +1,196 @@
#!/usr/bin/env python3
"""一次性为 okx/gate 注入与 binance 一致的计仓模式补丁已 patch 过则跳过)。"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
IMPORT_BLOCK = '''from position_sizing_lib import (
OPEN_SOURCE_KEY_AUTO,
OPEN_SOURCE_MANUAL,
assert_open_source_allowed,
compute_full_margin_sizing,
full_margin_requires_flat_position,
is_full_margin_mode,
leverage_for_full_margin,
load_position_sizing_mode,
mode_label_zh,
)
from lib.key_monitor.key_monitor_full_margin_lib import (
monitor_type_disallowed_in_full_margin,
purge_disallowed_key_monitors,
)
'''
ENV_LINE = (
"# 计仓模式risk=以损定仓默认);full_margin=合约可用×比例全仓杠杆仅 env 切换须无仓\n"
"POSITION_SIZING_MODE = load_position_sizing_mode()\n"
)
PURGE_FN = '''
def _purge_key_monitors_if_full_margin():
if not is_full_margin_mode(POSITION_SIZING_MODE):
return
conn = get_db()
try:
cancel = globals().get("_cancel_fib_monitor_limit")
if not callable(cancel):
cancel = lambda _row: None
purge_disallowed_key_monitors(
conn,
sizing_mode=POSITION_SIZING_MODE,
select_rows=lambda c: c.execute("SELECT * FROM key_monitors").fetchall(),
cancel_fib_limit=cancel,
delete_monitor=lambda c, kid: c.execute("DELETE FROM key_monitors WHERE id=?", (kid,)),
send_wechat=send_wechat_msg,
)
conn.commit()
except Exception as e:
print(f"[full_margin] purge key monitors: {e}", flush=True)
finally:
conn.close()
'''
MARKET_OPEN_GUARD = ''' ok_src, src_msg = assert_open_source_allowed(POSITION_SIZING_MODE, OPEN_SOURCE_KEY_AUTO)
if not ok_src:
return False, src_msg, None
'''
ADD_KEY_GUARD = ''' if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash(
"全仓杠杆模式下不可添加箱体/收敛突破或斐波监控"
"请改用阻力/支撑仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启须无持仓)。"
)
return redirect("/key_monitor")
'''
TEMPLATE_RULE = ''' <div class="rule-tip">
计仓模式<strong>{{ position_sizing_mode_label }}</strong>仅 .env <code>POSITION_SIZING_MODE</code>须无仓后重启
{% if position_sizing_mode == 'full_margin' %}
|全仓合约可用×{{ full_margin_buffer_ratio }}BTC/ETH {{ btc_leverage }}x其它 {{ alt_leverage }}x单仓张数按交易所精度
{% else %}
|以损定仓风险 {{ risk_percent }}%
{% endif %}
|移动保本下单可勾选关闭开启时 {{ breakeven_rr_trigger }}R 触发每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>'''
APPS = [
("crypto_monitor_okx", 4, "_market_open_for_key_monitor", True),
("crypto_monitor_gate", 2, "_market_open_for_key_monitor", True),
]
def patch_app(app_dir: str, funds_dec: int, market_fn: str | None, has_fib: bool):
path = ROOT / app_dir / "app.py"
text = path.read_text(encoding="utf-8")
if "POSITION_SIZING_MODE" in text:
print(f"SKIP {app_dir}/app.py (already patched)")
return
if "from position_sizing_lib import" not in text:
anchor = "from key_monitor_lib import ("
if anchor not in text:
anchor = "from form_submit_lib import"
text = text.replace(
anchor,
IMPORT_BLOCK + "\n" + anchor,
1,
)
else:
text = text.replace(anchor, IMPORT_BLOCK + anchor, 1)
if "POSITION_SIZING_MODE = load_position_sizing_mode()" not in text:
text = text.replace(
"AUTO_TRANSFER_BJ_HOUR = int(os.getenv(\"AUTO_TRANSFER_BJ_HOUR\", \"8\"))\n",
"AUTO_TRANSFER_BJ_HOUR = int(os.getenv(\"AUTO_TRANSFER_BJ_HOUR\", \"8\"))\n" + ENV_LINE,
1,
)
if "_purge_key_monitors_if_full_margin" not in text:
text = text.replace("init_db()\n\n\ndef get_db():", "init_db()" + PURGE_FN + "\ndef get_db():", 1)
text = text.replace(
"install_strategy_trend(app,",
"_purge_key_monitors_if_full_margin()\n\ninstall_strategy_trend(app,",
1,
)
if market_fn and MARKET_OPEN_GUARD.strip() not in text:
text = text.replace(
f"def {market_fn}(\n",
f"def {market_fn}(\n",
1,
)
text = text.replace(
' """\n 与手动',
MARKET_OPEN_GUARD + ' """\n 与手动',
1,
)
# fallback: after docstring closing
if MARKET_OPEN_GUARD.strip() not in text:
pat = rf"(def {market_fn}\([^)]+\):\s*\n\s*\"\"\"[^\"\"]*\"\"\"\s*\n)"
text = re.sub(pat, r"\1" + MARKET_OPEN_GUARD, text, count=1)
if has_fib and ADD_KEY_GUARD.strip() not in text:
text = text.replace(
' if mt not in allowed_types:',
ADD_KEY_GUARD + ' if mt not in allowed_types:',
1,
) if "if mt not in allowed_types:" in text else text.replace(
' rank, total = _daily_volume_rank(symbol)',
ADD_KEY_GUARD + ' rank, total = _daily_volume_rank(symbol)',
1,
)
# render_template risk_percent= add template vars
if "position_sizing_mode=POSITION_SIZING_MODE" not in text:
text = text.replace(
"risk_percent=RISK_PERCENT,\n",
"risk_percent=RISK_PERCENT,\n"
" position_sizing_mode=POSITION_SIZING_MODE,\n"
" position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),\n"
" open_position_button_label=(\n"
' "开仓全仓杠杆" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓以损定仓"\n'
" ),\n",
1,
)
path.write_text(text, encoding="utf-8")
print(f"DONE {app_dir}/app.py (partial — verify add_order block manually if needed)")
def patch_template(app_dir: str):
tpl = ROOT / app_dir / "templates" / "index.html"
if not tpl.exists():
return
text = tpl.read_text(encoding="utf-8")
if "position_sizing_mode_label" in text:
print(f"SKIP {tpl}")
return
old = re.search(
r'<div class="rule-tip">\s*以损定仓风险 \{\{ risk_percent \}\}%.*?</div>',
text,
re.S,
)
if old:
text = text[: old.start()] + TEMPLATE_RULE + text[old.end() :]
text = text.replace(
'<button type="submit">开仓以损定仓</button>',
'<button type="submit">{{ open_position_button_label }}</button>',
)
text = text.replace(
'<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">',
'{% if position_sizing_mode != \'full_margin\' %}\n'
' <input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">\n'
' {% endif %}',
1,
)
tpl.write_text(text, encoding="utf-8")
print(f"DONE {tpl}")
def main():
for app_dir, funds, mfn, fib in APPS:
patch_app(app_dir, funds, mfn, fib)
patch_template(app_dir)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""一次性:为 okx/gate 注入与 binance 一致的计仓模式补丁(已 patch 过则跳过)."""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
IMPORT_BLOCK = '''from position_sizing_lib import (
OPEN_SOURCE_KEY_AUTO,
OPEN_SOURCE_MANUAL,
assert_open_source_allowed,
compute_full_margin_sizing,
full_margin_requires_flat_position,
is_full_margin_mode,
leverage_for_full_margin,
load_position_sizing_mode,
mode_label_zh,
)
from lib.key_monitor.key_monitor_full_margin_lib import (
monitor_type_disallowed_in_full_margin,
purge_disallowed_key_monitors,
)
'''
ENV_LINE = (
"# 计仓模式:risk=以损定仓(默认);full_margin=合约可用×比例全仓杠杆(仅 env 切换,须无仓)\n"
"POSITION_SIZING_MODE = load_position_sizing_mode()\n"
)
PURGE_FN = '''
def _purge_key_monitors_if_full_margin():
if not is_full_margin_mode(POSITION_SIZING_MODE):
return
conn = get_db()
try:
cancel = globals().get("_cancel_fib_monitor_limit")
if not callable(cancel):
cancel = lambda _row: None
purge_disallowed_key_monitors(
conn,
sizing_mode=POSITION_SIZING_MODE,
select_rows=lambda c: c.execute("SELECT * FROM key_monitors").fetchall(),
cancel_fib_limit=cancel,
delete_monitor=lambda c, kid: c.execute("DELETE FROM key_monitors WHERE id=?", (kid,)),
send_wechat=send_wechat_msg,
)
conn.commit()
except Exception as e:
print(f"[full_margin] purge key monitors: {e}", flush=True)
finally:
conn.close()
'''
MARKET_OPEN_GUARD = ''' ok_src, src_msg = assert_open_source_allowed(POSITION_SIZING_MODE, OPEN_SOURCE_KEY_AUTO)
if not ok_src:
return False, src_msg, None
'''
ADD_KEY_GUARD = ''' if is_full_margin_mode(POSITION_SIZING_MODE) and monitor_type_disallowed_in_full_margin(mt):
flash(
"全仓杠杆模式下不可添加箱体/收敛突破或斐波监控;"
"请改用阻力/支撑(仅提醒),或切换 POSITION_SIZING_MODE=risk 并重启(须无持仓)."
)
return redirect("/key_monitor")
'''
TEMPLATE_RULE = ''' <div class="rule-tip">
计仓模式:<strong>{{ position_sizing_mode_label }}</strong>(仅 .env <code>POSITION_SIZING_MODE</code>,须无仓后重启)
{% if position_sizing_mode == 'full_margin' %}
|全仓:合约可用×{{ full_margin_buffer_ratio }},BTC/ETH {{ btc_leverage }}x,其它 {{ alt_leverage }}x,单仓;张数按交易所精度
{% else %}
|以损定仓:风险 {{ risk_percent }}%
{% endif %}
|移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
</div>'''
APPS = [
("crypto_monitor_okx", 4, "_market_open_for_key_monitor", True),
("crypto_monitor_gate", 2, "_market_open_for_key_monitor", True),
]
def patch_app(app_dir: str, funds_dec: int, market_fn: str | None, has_fib: bool):
path = ROOT / app_dir / "app.py"
text = path.read_text(encoding="utf-8")
if "POSITION_SIZING_MODE" in text:
print(f"SKIP {app_dir}/app.py (already patched)")
return
if "from position_sizing_lib import" not in text:
anchor = "from key_monitor_lib import ("
if anchor not in text:
anchor = "from form_submit_lib import"
text = text.replace(
anchor,
IMPORT_BLOCK + "\n" + anchor,
1,
)
else:
text = text.replace(anchor, IMPORT_BLOCK + anchor, 1)
if "POSITION_SIZING_MODE = load_position_sizing_mode()" not in text:
text = text.replace(
"AUTO_TRANSFER_BJ_HOUR = int(os.getenv(\"AUTO_TRANSFER_BJ_HOUR\", \"8\"))\n",
"AUTO_TRANSFER_BJ_HOUR = int(os.getenv(\"AUTO_TRANSFER_BJ_HOUR\", \"8\"))\n" + ENV_LINE,
1,
)
if "_purge_key_monitors_if_full_margin" not in text:
text = text.replace("init_db()\n\n\ndef get_db():", "init_db()" + PURGE_FN + "\ndef get_db():", 1)
text = text.replace(
"install_strategy_trend(app,",
"_purge_key_monitors_if_full_margin()\n\ninstall_strategy_trend(app,",
1,
)
if market_fn and MARKET_OPEN_GUARD.strip() not in text:
text = text.replace(
f"def {market_fn}(\n",
f"def {market_fn}(\n",
1,
)
text = text.replace(
' """\n 与手动',
MARKET_OPEN_GUARD + ' """\n 与手动',
1,
)
# fallback: after docstring closing
if MARKET_OPEN_GUARD.strip() not in text:
pat = rf"(def {market_fn}\([^)]+\):\s*\n\s*\"\"\"[^\"\"]*\"\"\"\s*\n)"
text = re.sub(pat, r"\1" + MARKET_OPEN_GUARD, text, count=1)
if has_fib and ADD_KEY_GUARD.strip() not in text:
text = text.replace(
' if mt not in allowed_types:',
ADD_KEY_GUARD + ' if mt not in allowed_types:',
1,
) if "if mt not in allowed_types:" in text else text.replace(
' rank, total = _daily_volume_rank(symbol)',
ADD_KEY_GUARD + ' rank, total = _daily_volume_rank(symbol)',
1,
)
# render_template risk_percent= add template vars
if "position_sizing_mode=POSITION_SIZING_MODE" not in text:
text = text.replace(
"risk_percent=RISK_PERCENT,\n",
"risk_percent=RISK_PERCENT,\n"
" position_sizing_mode=POSITION_SIZING_MODE,\n"
" position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),\n"
" open_position_button_label=(\n"
' "开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)"\n'
" ),\n",
1,
)
path.write_text(text, encoding="utf-8")
print(f"DONE {app_dir}/app.py (partial — verify add_order block manually if needed)")
def patch_template(app_dir: str):
tpl = ROOT / app_dir / "templates" / "index.html"
if not tpl.exists():
return
text = tpl.read_text(encoding="utf-8")
if "position_sizing_mode_label" in text:
print(f"SKIP {tpl}")
return
old = re.search(
r'<div class="rule-tip">\s*以损定仓:风险 \{\{ risk_percent \}\}%.*?</div>',
text,
re.S,
)
if old:
text = text[: old.start()] + TEMPLATE_RULE + text[old.end() :]
text = text.replace(
'<button type="submit">开仓(以损定仓)</button>',
'<button type="submit">{{ open_position_button_label }}</button>',
)
text = text.replace(
'<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">',
'{% if position_sizing_mode != \'full_margin\' %}\n'
' <input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">\n'
' {% endif %}',
1,
)
tpl.write_text(text, encoding="utf-8")
print(f"DONE {tpl}")
def main():
for app_dir, funds, mfn, fib in APPS:
patch_app(app_dir, funds, mfn, fib)
patch_template(app_dir)
if __name__ == "__main__":
main()
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""
将 brand/icons 同步到中控与各所 static/iconsChrome 快捷方式 / 标签页图标)。
将 brand/icons 同步到中控与各所 static/icons(Chrome 快捷方式 / 标签页图标).
用法仓库根目录):
用法(仓库根目录):
python scripts/generate_brand_icons.py
python scripts/sync_brand_icons.py
"""
+10 -10
View File
@@ -1,15 +1,15 @@
#!/usr/bin/env python3
"""
将三所共用的交易/关键位/轮询 env 写入币安OKX 的 .env缺失则追加不覆盖已有值)。
将三所共用的交易/关键位/轮询 env 写入币安,OKX 的 .env(缺失则追加,不覆盖已有值).
以 Gate .env.example 为基准Gate 自身也可运行以补缺失项
以 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
修改后须 pm2 restart 对应实例.说明见 docs/env-sync-scripts.md
"""
from __future__ import annotations
@@ -24,7 +24,7 @@ DEFAULT_INSTANCES = (
"crypto_monitor_okx",
)
# 与 crypto_monitor_gate/.env.example 对齐不含 GATE_* / 各所 API 密钥
# 与 crypto_monitor_gate/.env.example 对齐(不含 GATE_* / 各所 API 密钥)
SHARED_DEFAULTS: dict[str, str] = {
"TRADING_DAY_RESET_OPEN_GUARD_ENABLED": "true",
"KEY_CONFIRM_BREAKOUT_BAR": "-2",
@@ -54,7 +54,7 @@ SHARED_DEFAULTS: dict[str, str] = {
"AI_TIMEOUT_SECONDS": "120",
}
# 仅 Gate 启用 0 点强制清仓币安/OKX 须保持关闭
# 仅 Gate 启用 0 点强制清仓;币安/OKX 须保持关闭
FORCE_CLOSE_POLICY: dict[str, dict[str, str]] = {
"crypto_monitor_gate": {
"FORCE_CLOSE_ENABLED": "true",
@@ -131,7 +131,7 @@ def sync_one(dir_name: str, *, dry_run: bool, force: bool) -> bool:
def apply_force_close_policy(*, dry_run: bool) -> bool:
"""Gate 开启强制清仓币安/OKX 强制关闭覆盖已有值)。"""
"""Gate 开启强制清仓;币安/OKX 强制关闭(覆盖已有值)."""
any_changed = False
for dir_name, values in FORCE_CLOSE_POLICY.items():
path = os.path.join(REPO, dir_name, ".env")
@@ -160,13 +160,13 @@ def apply_force_close_policy(*, dry_run: bool) -> bool:
def main() -> None:
ap = argparse.ArgumentParser(description="同步币安/OKX 共用 trading env缺失项追加")
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("--force", action="store_true", help="覆盖已有值(慎用)")
ap.add_argument(
"--apply-force-close-policy",
action="store_true",
help="Gate 开启 0 点强制清仓币安/OKX 强制关闭",
help="Gate 开启 0 点强制清仓,币安/OKX 强制关闭",
)
ap.add_argument(
"--instances",
+60 -60
View File
@@ -1,60 +1,60 @@
#!/usr/bin/env python3
"""
三所 .env 一次性同步计仓模式 + 自动划转调用子脚本不覆盖已有自定义值)。
用法仓库根目录):
python scripts/sync_four_exchange_env.py
python scripts/sync_four_exchange_env.py --dry-run
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
子脚本可单独运行
python scripts/sync_four_exchange_position_sizing_env.py
python scripts/sync_four_exchange_transfer_env.py
完整说明见 docs/env-sync-scripts.md
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
PY = sys.executable
def _run(script: str, extra: list[str]) -> int:
cmd = [PY, str(REPO / "scripts" / script)] + extra
print(f"\n>>> {' '.join(cmd)}")
return subprocess.call(cmd, cwd=str(REPO))
def main():
ap = argparse.ArgumentParser(description="三所 .env 统一同步计仓 + 划转")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--set-mode", choices=("risk", "full_margin"), metavar="MODE")
ap.add_argument("--set-transfer-amount", metavar="U")
ap.add_argument("--enable-auto-transfer", action="store_true")
args = ap.parse_args()
dry = ["--dry-run"] if args.dry_run else []
code = 0
ps_args = list(dry)
if args.set_mode:
ps_args.extend(["--set-mode", args.set_mode])
code |= _run("sync_four_exchange_position_sizing_env.py", ps_args)
tr_args = list(dry)
if args.set_transfer_amount:
tr_args.extend(["--set-amount", args.set_transfer_amount])
if args.enable_auto_transfer:
tr_args.append("--enable-auto-transfer")
code |= _run("sync_four_exchange_transfer_env.py", tr_args)
sys.exit(code)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
三所 .env 一次性同步:计仓模式 + 自动划转(调用子脚本,不覆盖已有自定义值).
用法(仓库根目录):
python scripts/sync_four_exchange_env.py
python scripts/sync_four_exchange_env.py --dry-run
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
子脚本可单独运行:
python scripts/sync_four_exchange_position_sizing_env.py
python scripts/sync_four_exchange_transfer_env.py
完整说明见 docs/env-sync-scripts.md
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
PY = sys.executable
def _run(script: str, extra: list[str]) -> int:
cmd = [PY, str(REPO / "scripts" / script)] + extra
print(f"\n>>> {' '.join(cmd)}")
return subprocess.call(cmd, cwd=str(REPO))
def main():
ap = argparse.ArgumentParser(description="三所 .env 统一同步(计仓 + 划转)")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--set-mode", choices=("risk", "full_margin"), metavar="MODE")
ap.add_argument("--set-transfer-amount", metavar="U")
ap.add_argument("--enable-auto-transfer", action="store_true")
args = ap.parse_args()
dry = ["--dry-run"] if args.dry_run else []
code = 0
ps_args = list(dry)
if args.set_mode:
ps_args.extend(["--set-mode", args.set_mode])
code |= _run("sync_four_exchange_position_sizing_env.py", ps_args)
tr_args = list(dry)
if args.set_transfer_amount:
tr_args.extend(["--set-amount", args.set_transfer_amount])
if args.enable_auto_transfer:
tr_args.append("--enable-auto-transfer")
code |= _run("sync_four_exchange_transfer_env.py", tr_args)
sys.exit(code)
if __name__ == "__main__":
main()
+179 -179
View File
@@ -1,179 +1,179 @@
#!/usr/bin/env python3
"""
将计仓模式相关项写入三所实例 .env已存在则保留原值缺失则追加默认值)。
用法仓库根目录):
python scripts/sync_four_exchange_position_sizing_env.py
python scripts/sync_four_exchange_position_sizing_env.py --dry-run
python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
python scripts/sync_four_exchange_position_sizing_env.py --set-mode full_margin
切换 POSITION_SIZING_MODE 须在交易所无持仓后执行并 pm2 restart 对应实例
不修改 API 密钥与其它自定义项若 .env 不存在则跳过请先从 .env.example 复制)。
完整说明见 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__)))
INSTANCES = (
"crypto_monitor_binance",
"crypto_monitor_okx",
"crypto_monitor_gate",
)
COMMENT_POSITION_SIZING = (
"# 计仓risk=以损定仓默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆须无仓后重启"
)
COMMENT_BUFFER = "# 使用可用资金时的缓冲比例如0.98代表用98%"
DEFAULT_MODE = "risk"
DEFAULT_BUFFER = "0.98"
VALID_MODES = frozenset({"risk", "full_margin"})
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 = []
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_before(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] + insert + lines[i:]
if lines and lines[-1].strip():
return lines + [""] + insert
return lines + insert
def _ensure_position_sizing(lines: list[str], *, force_mode: str | None) -> list[str]:
if force_mode is not None:
if COMMENT_POSITION_SIZING not in lines and not _env_get(lines, "POSITION_SIZING_MODE"):
lines = _insert_before(lines, "DAILY_START_CAPITAL", [COMMENT_POSITION_SIZING])
return _upsert(lines, "POSITION_SIZING_MODE", force_mode)
cur = _env_get(lines, "POSITION_SIZING_MODE")
if cur is not None:
norm = cur.strip().lower()
if norm in VALID_MODES and norm != cur:
return _upsert(lines, "POSITION_SIZING_MODE", norm)
if norm not in VALID_MODES:
return _upsert(lines, "POSITION_SIZING_MODE", DEFAULT_MODE)
return lines
block = [COMMENT_POSITION_SIZING, f"POSITION_SIZING_MODE={DEFAULT_MODE}"]
return _insert_before(lines, "DAILY_START_CAPITAL", block)
def _ensure_buffer_ratio(lines: list[str], *, force_buffer: str | None) -> list[str]:
if force_buffer is not None:
if COMMENT_BUFFER not in lines and _env_get(lines, "FULL_MARGIN_BUFFER_RATIO") is None:
lines = _insert_before(lines, "BALANCE_REFRESH_SECONDS", [COMMENT_BUFFER])
return _upsert(lines, "FULL_MARGIN_BUFFER_RATIO", force_buffer)
if _env_get(lines, "FULL_MARGIN_BUFFER_RATIO") is not None:
return lines
block = [COMMENT_BUFFER, f"FULL_MARGIN_BUFFER_RATIO={DEFAULT_BUFFER}"]
return _insert_before(lines, "BALANCE_REFRESH_SECONDS", block)
def sync_one(
dir_name: str,
dry_run: bool,
*,
set_mode: str | None,
set_buffer: str | None,
) -> str:
env_path = os.path.join(REPO, dir_name, ".env")
if not os.path.isfile(env_path):
return f"SKIP {dir_name}: 无 .env请 cp .env.example .env"
old_lines = _parse_env(env_path)
new_lines = _ensure_buffer_ratio(
_ensure_position_sizing(list(old_lines), force_mode=set_mode),
force_buffer=set_buffer,
)
mode = _env_get(new_lines, "POSITION_SIZING_MODE") or DEFAULT_MODE
buf = _env_get(new_lines, "FULL_MARGIN_BUFFER_RATIO") or DEFAULT_BUFFER
if new_lines == old_lines:
return f"OK {dir_name}: POSITION_SIZING_MODE={mode} FULL_MARGIN_BUFFER_RATIO={buf}"
if dry_run:
return (
f"DRY {dir_name}: 将写入 POSITION_SIZING_MODE={mode} "
f"FULL_MARGIN_BUFFER_RATIO={buf}"
)
with open(env_path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(new_lines))
if new_lines and new_lines[-1].strip():
f.write("\n")
return f"DONE {dir_name}: POSITION_SIZING_MODE={mode} FULL_MARGIN_BUFFER_RATIO={buf}"
def main():
ap = argparse.ArgumentParser(description="三所 .env 计仓模式项同步")
ap.add_argument("--dry-run", action="store_true", help="仅打印将做的变更")
ap.add_argument(
"--set-mode",
choices=sorted(VALID_MODES),
metavar="MODE",
help="强制三所 POSITION_SIZING_MODE须无仓后重启",
)
ap.add_argument(
"--set-buffer",
metavar="RATIO",
help=f"强制三所 FULL_MARGIN_BUFFER_RATIO缺省追加为 {DEFAULT_BUFFER}",
)
args = ap.parse_args()
if args.set_mode:
print(
f"注意将 POSITION_SIZING_MODE 设为 {args.set_mode}"
"请确认交易所无持仓后再 restart"
)
for name in INSTANCES:
print(
sync_one(
name,
args.dry_run,
set_mode=args.set_mode,
set_buffer=args.set_buffer,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
将计仓模式相关项写入三所实例 .env(已存在则保留原值,缺失则追加默认值).
用法(仓库根目录):
python scripts/sync_four_exchange_position_sizing_env.py
python scripts/sync_four_exchange_position_sizing_env.py --dry-run
python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
python scripts/sync_four_exchange_position_sizing_env.py --set-mode full_margin
切换 POSITION_SIZING_MODE 须在交易所无持仓后执行,并 pm2 restart 对应实例.
不修改 API 密钥与其它自定义项;若 .env 不存在则跳过(请先从 .env.example 复制).
完整说明见 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__)))
INSTANCES = (
"crypto_monitor_binance",
"crypto_monitor_okx",
"crypto_monitor_gate",
)
COMMENT_POSITION_SIZING = (
"# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)"
)
COMMENT_BUFFER = "# 使用可用资金时的缓冲比例(如0.98代表用98%)"
DEFAULT_MODE = "risk"
DEFAULT_BUFFER = "0.98"
VALID_MODES = frozenset({"risk", "full_margin"})
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 = []
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_before(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] + insert + lines[i:]
if lines and lines[-1].strip():
return lines + [""] + insert
return lines + insert
def _ensure_position_sizing(lines: list[str], *, force_mode: str | None) -> list[str]:
if force_mode is not None:
if COMMENT_POSITION_SIZING not in lines and not _env_get(lines, "POSITION_SIZING_MODE"):
lines = _insert_before(lines, "DAILY_START_CAPITAL", [COMMENT_POSITION_SIZING])
return _upsert(lines, "POSITION_SIZING_MODE", force_mode)
cur = _env_get(lines, "POSITION_SIZING_MODE")
if cur is not None:
norm = cur.strip().lower()
if norm in VALID_MODES and norm != cur:
return _upsert(lines, "POSITION_SIZING_MODE", norm)
if norm not in VALID_MODES:
return _upsert(lines, "POSITION_SIZING_MODE", DEFAULT_MODE)
return lines
block = [COMMENT_POSITION_SIZING, f"POSITION_SIZING_MODE={DEFAULT_MODE}"]
return _insert_before(lines, "DAILY_START_CAPITAL", block)
def _ensure_buffer_ratio(lines: list[str], *, force_buffer: str | None) -> list[str]:
if force_buffer is not None:
if COMMENT_BUFFER not in lines and _env_get(lines, "FULL_MARGIN_BUFFER_RATIO") is None:
lines = _insert_before(lines, "BALANCE_REFRESH_SECONDS", [COMMENT_BUFFER])
return _upsert(lines, "FULL_MARGIN_BUFFER_RATIO", force_buffer)
if _env_get(lines, "FULL_MARGIN_BUFFER_RATIO") is not None:
return lines
block = [COMMENT_BUFFER, f"FULL_MARGIN_BUFFER_RATIO={DEFAULT_BUFFER}"]
return _insert_before(lines, "BALANCE_REFRESH_SECONDS", block)
def sync_one(
dir_name: str,
dry_run: bool,
*,
set_mode: str | None,
set_buffer: str | None,
) -> str:
env_path = os.path.join(REPO, dir_name, ".env")
if not os.path.isfile(env_path):
return f"SKIP {dir_name}: 无 .env(请 cp .env.example .env)"
old_lines = _parse_env(env_path)
new_lines = _ensure_buffer_ratio(
_ensure_position_sizing(list(old_lines), force_mode=set_mode),
force_buffer=set_buffer,
)
mode = _env_get(new_lines, "POSITION_SIZING_MODE") or DEFAULT_MODE
buf = _env_get(new_lines, "FULL_MARGIN_BUFFER_RATIO") or DEFAULT_BUFFER
if new_lines == old_lines:
return f"OK {dir_name}: POSITION_SIZING_MODE={mode} FULL_MARGIN_BUFFER_RATIO={buf}"
if dry_run:
return (
f"DRY {dir_name}: 将写入 POSITION_SIZING_MODE={mode} "
f"FULL_MARGIN_BUFFER_RATIO={buf}"
)
with open(env_path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(new_lines))
if new_lines and new_lines[-1].strip():
f.write("\n")
return f"DONE {dir_name}: POSITION_SIZING_MODE={mode} FULL_MARGIN_BUFFER_RATIO={buf}"
def main():
ap = argparse.ArgumentParser(description="三所 .env 计仓模式项同步")
ap.add_argument("--dry-run", action="store_true", help="仅打印将做的变更")
ap.add_argument(
"--set-mode",
choices=sorted(VALID_MODES),
metavar="MODE",
help="强制三所 POSITION_SIZING_MODE(须无仓后重启)",
)
ap.add_argument(
"--set-buffer",
metavar="RATIO",
help=f"强制三所 FULL_MARGIN_BUFFER_RATIO(缺省追加为 {DEFAULT_BUFFER})",
)
args = ap.parse_args()
if args.set_mode:
print(
f"注意:将 POSITION_SIZING_MODE 设为 {args.set_mode},"
"请确认交易所无持仓后再 restart."
)
for name in INSTANCES:
print(
sync_one(
name,
args.dry_run,
set_mode=args.set_mode,
set_buffer=args.set_buffer,
)
)
if __name__ == "__main__":
main()
+212 -212
View File
@@ -1,212 +1,212 @@
#!/usr/bin/env python3
"""
将每日自动划转相关项写入三所实例 .env已有值保留缺失则追加可选强制改金额/开关)。
用法仓库根目录):
python scripts/sync_four_exchange_transfer_env.py
python scripts/sync_four_exchange_transfer_env.py --dry-run
python scripts/sync_four_exchange_transfer_env.py --set-amount 50
python scripts/sync_four_exchange_transfer_env.py --enable-auto-transfer
不修改 API 密钥与其它自定义项若 .env 不存在则跳过请先从 .env.example 复制)。
完整说明见 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__)))
INSTANCES = (
"crypto_monitor_binance",
"crypto_monitor_okx",
"crypto_monitor_gate",
)
COMMENT_BLOCK = (
"# 自动划转北京时间 AUTO_TRANSFER_BJ_HOUR 点将 swap 调整至 AUTO_TRANSFER_AMOUNT"
"不足 funding→swap超出 swap→funding持仓中不划转"
)
DEFAULTS = {
"AUTO_TRANSFER_ENABLED": "false",
"AUTO_TRANSFER_FROM": "funding",
"AUTO_TRANSFER_TO": "swap",
"TRANSFER_CCY": "USDT",
"AUTO_TRANSFER_BJ_HOUR": "8",
}
DEFAULT_AMOUNT = "50"
BINANCE_ONLY = {
"BINANCE_FUNDING_INCLUDE_SPOT": "false",
}
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 = []
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_before(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] + insert + lines[i:]
if lines and lines[-1].strip():
return lines + [""] + insert
return lines + insert
def _resolve_default_amount(lines: list[str]) -> str:
amount = _env_get(lines, "AUTO_TRANSFER_AMOUNT")
if amount is not None:
return amount
daily = _env_get(lines, "DAILY_START_CAPITAL")
if daily is not None:
return daily
return DEFAULT_AMOUNT
def _ensure_key(
lines: list[str],
key: str,
value: str,
*,
force: bool,
) -> list[str]:
if force or _env_get(lines, key) is None:
return _upsert(lines, key, value)
return lines
def _ensure_transfer_block(
lines: list[str],
extra: dict[str, str],
*,
force_amount: str | None,
force_enabled: str | None,
) -> list[str]:
amount = force_amount if force_amount is not None else _resolve_default_amount(lines)
had_amount = _env_get(lines, "AUTO_TRANSFER_AMOUNT") is not None
if not had_amount and COMMENT_BLOCK not in lines:
lines = _insert_before(
lines,
"AUTO_TRANSFER_ENABLED",
[COMMENT_BLOCK],
)
if _env_get(lines, "AUTO_TRANSFER_ENABLED") is None:
lines = _insert_before(
lines,
"BALANCE_REFRESH_SECONDS",
[COMMENT_BLOCK],
)
lines = _ensure_key(
lines,
"AUTO_TRANSFER_AMOUNT",
amount,
force=force_amount is not None,
)
for k, v in DEFAULTS.items():
if k == "AUTO_TRANSFER_ENABLED" and force_enabled is not None:
lines = _upsert(lines, k, force_enabled)
else:
lines = _ensure_key(lines, k, v, force=False)
for k, v in extra.items():
lines = _ensure_key(lines, k, v, force=False)
return lines
def sync_one(
dir_name: str,
dry_run: bool,
*,
set_amount: str | None,
enable_auto: bool | None,
) -> str:
env_path = os.path.join(REPO, dir_name, ".env")
if not os.path.isfile(env_path):
return f"SKIP {dir_name}: 无 .env请 cp .env.example .env"
old_lines = _parse_env(env_path)
extra = dict(BINANCE_ONLY) if dir_name == "crypto_monitor_binance" else {}
force_enabled = "true" if enable_auto is True else None
new_lines = _ensure_transfer_block(
old_lines,
extra,
force_amount=set_amount,
force_enabled=force_enabled,
)
enabled = _env_get(new_lines, "AUTO_TRANSFER_ENABLED") or DEFAULTS["AUTO_TRANSFER_ENABLED"]
amt = _env_get(new_lines, "AUTO_TRANSFER_AMOUNT") or DEFAULT_AMOUNT
if new_lines == old_lines:
return f"OK {dir_name}: ENABLED={enabled} AMOUNT={amt}"
if dry_run:
return f"DRY {dir_name}: 将更新 ENABLED={enabled} AMOUNT={amt}"
with open(env_path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(new_lines))
if new_lines and new_lines[-1].strip():
f.write("\n")
return f"DONE {dir_name}: ENABLED={enabled} AMOUNT={amt}"
def main():
ap = argparse.ArgumentParser(description="三所 .env 自动划转项同步")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument(
"--set-amount",
metavar="U",
help=f"强制三所 AUTO_TRANSFER_AMOUNT缺省补全默认 {DEFAULT_AMOUNT}",
)
ap.add_argument(
"--enable-auto-transfer",
action="store_true",
help="强制三所 AUTO_TRANSFER_ENABLED=true",
)
args = ap.parse_args()
for name in INSTANCES:
print(
sync_one(
name,
args.dry_run,
set_amount=args.set_amount,
enable_auto=True if args.enable_auto_transfer else None,
)
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
将每日自动划转相关项写入三所实例 .env(已有值保留,缺失则追加;可选强制改金额/开关).
用法(仓库根目录):
python scripts/sync_four_exchange_transfer_env.py
python scripts/sync_four_exchange_transfer_env.py --dry-run
python scripts/sync_four_exchange_transfer_env.py --set-amount 50
python scripts/sync_four_exchange_transfer_env.py --enable-auto-transfer
不修改 API 密钥与其它自定义项;若 .env 不存在则跳过(请先从 .env.example 复制).
完整说明见 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__)))
INSTANCES = (
"crypto_monitor_binance",
"crypto_monitor_okx",
"crypto_monitor_gate",
)
COMMENT_BLOCK = (
"# 自动划转:北京时间 AUTO_TRANSFER_BJ_HOUR 点将 swap 调整至 AUTO_TRANSFER_AMOUNT;"
"不足 funding→swap,超出 swap→funding;持仓中不划转"
)
DEFAULTS = {
"AUTO_TRANSFER_ENABLED": "false",
"AUTO_TRANSFER_FROM": "funding",
"AUTO_TRANSFER_TO": "swap",
"TRANSFER_CCY": "USDT",
"AUTO_TRANSFER_BJ_HOUR": "8",
}
DEFAULT_AMOUNT = "50"
BINANCE_ONLY = {
"BINANCE_FUNDING_INCLUDE_SPOT": "false",
}
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 = []
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_before(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] + insert + lines[i:]
if lines and lines[-1].strip():
return lines + [""] + insert
return lines + insert
def _resolve_default_amount(lines: list[str]) -> str:
amount = _env_get(lines, "AUTO_TRANSFER_AMOUNT")
if amount is not None:
return amount
daily = _env_get(lines, "DAILY_START_CAPITAL")
if daily is not None:
return daily
return DEFAULT_AMOUNT
def _ensure_key(
lines: list[str],
key: str,
value: str,
*,
force: bool,
) -> list[str]:
if force or _env_get(lines, key) is None:
return _upsert(lines, key, value)
return lines
def _ensure_transfer_block(
lines: list[str],
extra: dict[str, str],
*,
force_amount: str | None,
force_enabled: str | None,
) -> list[str]:
amount = force_amount if force_amount is not None else _resolve_default_amount(lines)
had_amount = _env_get(lines, "AUTO_TRANSFER_AMOUNT") is not None
if not had_amount and COMMENT_BLOCK not in lines:
lines = _insert_before(
lines,
"AUTO_TRANSFER_ENABLED",
[COMMENT_BLOCK],
)
if _env_get(lines, "AUTO_TRANSFER_ENABLED") is None:
lines = _insert_before(
lines,
"BALANCE_REFRESH_SECONDS",
[COMMENT_BLOCK],
)
lines = _ensure_key(
lines,
"AUTO_TRANSFER_AMOUNT",
amount,
force=force_amount is not None,
)
for k, v in DEFAULTS.items():
if k == "AUTO_TRANSFER_ENABLED" and force_enabled is not None:
lines = _upsert(lines, k, force_enabled)
else:
lines = _ensure_key(lines, k, v, force=False)
for k, v in extra.items():
lines = _ensure_key(lines, k, v, force=False)
return lines
def sync_one(
dir_name: str,
dry_run: bool,
*,
set_amount: str | None,
enable_auto: bool | None,
) -> str:
env_path = os.path.join(REPO, dir_name, ".env")
if not os.path.isfile(env_path):
return f"SKIP {dir_name}: 无 .env(请 cp .env.example .env)"
old_lines = _parse_env(env_path)
extra = dict(BINANCE_ONLY) if dir_name == "crypto_monitor_binance" else {}
force_enabled = "true" if enable_auto is True else None
new_lines = _ensure_transfer_block(
old_lines,
extra,
force_amount=set_amount,
force_enabled=force_enabled,
)
enabled = _env_get(new_lines, "AUTO_TRANSFER_ENABLED") or DEFAULTS["AUTO_TRANSFER_ENABLED"]
amt = _env_get(new_lines, "AUTO_TRANSFER_AMOUNT") or DEFAULT_AMOUNT
if new_lines == old_lines:
return f"OK {dir_name}: ENABLED={enabled} AMOUNT={amt}"
if dry_run:
return f"DRY {dir_name}: 将更新 ENABLED={enabled} AMOUNT={amt}"
with open(env_path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(new_lines))
if new_lines and new_lines[-1].strip():
f.write("\n")
return f"DONE {dir_name}: ENABLED={enabled} AMOUNT={amt}"
def main():
ap = argparse.ArgumentParser(description="三所 .env 自动划转项同步")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument(
"--set-amount",
metavar="U",
help=f"强制三所 AUTO_TRANSFER_AMOUNT(缺省补全默认 {DEFAULT_AMOUNT})",
)
ap.add_argument(
"--enable-auto-transfer",
action="store_true",
help="强制三所 AUTO_TRANSFER_ENABLED=true",
)
args = ap.parse_args()
for name in INSTANCES:
print(
sync_one(
name,
args.dry_run,
set_amount=args.set_amount,
enable_auto=True if args.enable_auto_transfer else None,
)
)
if __name__ == "__main__":
main()
+216 -216
View File
@@ -1,216 +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/ETHOKX 默认",
)
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()
#!/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()
+1 -1
View File
@@ -1,4 +1,4 @@
"""验证中控 embed-auth 与 login 返回 session_token"""
"""验证中控 embed-auth 与 login 返回 session_token."""
from __future__ import annotations
import sys
+1 -1
View File
@@ -1,4 +1,4 @@
"""验证 OKX 趋势回调止损挂单须为 stopLossPrice 条件单不得为立即市价平仓"""
"""验证 OKX 趋势回调止损挂单:须为 stopLossPrice 条件单,不得为立即市价平仓."""
from __future__ import annotations
import sys