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:
@@ -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"斐波:限价 @ E(SL/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")
|
||||
|
||||
|
||||
@@ -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)}\"",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""首次部署:自动生成中控通信密钥、登录会话密钥,并写入初始登录账号。
|
||||
"""首次部署:自动生成中控通信密钥,登录会话密钥,并写入初始登录账号.
|
||||
|
||||
- HUB_BRIDGE_TOKEN:中控 + 三实例(相同,仅空/占位时写入,不覆盖已有)
|
||||
- FLASK_SECRET_KEY:三实例(相同)
|
||||
- HUB_SESSION_SECRET:仅中控
|
||||
- APP_USERNAME=admin、APP_PASSWORD=admin123:实例(仅空时)
|
||||
- HUB_USERNAME=admin、HUB_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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成品牌 PNG/ICO(Pillow),供 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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
将 brand/icons 同步到中控与各所 static/icons(Chrome 快捷方式 / 标签页图标)。
|
||||
将 brand/icons 同步到中控与各所 static/icons(Chrome 快捷方式 / 标签页图标).
|
||||
|
||||
用法(仓库根目录):
|
||||
用法(仓库根目录):
|
||||
python scripts/generate_brand_icons.py
|
||||
python scripts/sync_brand_icons.py
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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/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()
|
||||
#!/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,4 +1,4 @@
|
||||
"""验证中控 embed-auth 与 login 返回 session_token。"""
|
||||
"""验证中控 embed-auth 与 login 返回 session_token."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""验证 OKX 趋势回调止损挂单:须为 stopLossPrice 条件单,不得为立即市价平仓。"""
|
||||
"""验证 OKX 趋势回调止损挂单:须为 stopLossPrice 条件单,不得为立即市价平仓."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
Reference in New Issue
Block a user