feat: add full-margin position sizing mode across four exchanges
Env POSITION_SIZING_MODE switches risk vs full-margin (available*buffer, BTC/ETH 10x). Blocks trend/roll/key auto opens in full margin, purges breakout/fib monitors with WeChat notice, keeps RR check and initial SL snapshot for records. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""一次性:为 okx/gate/gate_bot 注入与 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 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),
|
||||
("crypto_monitor_gate_bot", 4, None, False),
|
||||
]
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user