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:
dekun
2026-06-04 08:24:35 +08:00
parent d75527a9ca
commit f7bac11694
20 changed files with 866 additions and 107 deletions
+101 -25
View File
@@ -82,6 +82,21 @@ from key_sl_tp_lib import (
sl_tp_mode_label,
sl_tp_plan_summary_text,
)
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,
)
from key_monitor_lib import (
KEY_DIRECTION_WATCH,
KEY_MONITOR_ALERT_ONLY_TYPES,
@@ -240,6 +255,7 @@ FORCE_CLOSE_ENABLED = os.getenv("FORCE_CLOSE_ENABLED", "false").lower() == "true
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
POSITION_SIZING_MODE = load_position_sizing_mode()
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
@@ -1442,6 +1458,27 @@ def init_db():
init_db()
def _purge_key_monitors_if_full_margin():
if not is_full_margin_mode(POSITION_SIZING_MODE):
return
conn = get_db()
try:
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_fib_monitor_limit,
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()
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
@@ -4369,6 +4406,9 @@ def _market_open_for_key_monitor(
与手动实盘下单对齐的市价开仓与 order_monitors 写入
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
"""
ok_src, src_msg = assert_open_source_allowed(POSITION_SIZING_MODE, OPEN_SOURCE_KEY_AUTO)
if not ok_src:
return False, src_msg, None
now = app_now()
ok, reason = precheck_risk(conn, symbol, direction)
if not ok:
@@ -6024,6 +6064,11 @@ def render_main_page(page="trade"):
},
key_alert_max_times=KEY_ALERT_MAX_TIMES,
risk_percent=RISK_PERCENT,
position_sizing_mode=POSITION_SIZING_MODE,
position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),
open_position_button_label=(
"开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)"
),
breakeven_rr_trigger=BREAKEVEN_RR_TRIGGER,
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
occupied_miss_total=occupied_miss_total,
@@ -6732,6 +6777,12 @@ def add_key():
if mt not in allowed_types:
flash("监控类型无效")
return redirect("/key_monitor")
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")
rank, total = _daily_volume_rank(symbol)
if rank is None:
flash("日成交量排名读取失败,请稍后重试")
@@ -6930,19 +6981,6 @@ def add_order():
flash(f"风控拒绝下单:{reason_live}")
return redirect("/trade")
exchange_symbol = normalize_exchange_symbol(symbol)
default_leverage = get_synced_leverage(exchange_symbol, direction) or infer_leverage(symbol)
try:
leverage_input = parse_positive_float(d.get("leverage"))
leverage = int(leverage_input) if leverage_input is not None else default_leverage
except Exception:
conn.close()
flash("杠杆参数格式错误")
return redirect("/")
if leverage <= 0:
conn.close()
flash("杠杆必须大于0")
return redirect("/")
trading_day = get_trading_day(now)
opens_today_before = conn.execute(
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
@@ -7018,20 +7056,56 @@ def add_order():
flash("止损方向不合法:请检查入场方向与止损价格关系")
return redirect("/")
risk_percent = max(0.01, float(RISK_PERCENT))
risk_amount = round(capital_base * risk_percent / 100.0, 4)
notional_value = round(risk_amount / risk_fraction, 4)
margin_capital = round(notional_value / leverage, 4)
if capital_base and margin_capital > capital_base:
conn.close()
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
return redirect("/")
if available_usdt is not None:
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
if margin_capital > max_margin:
risk_amount = round(capital_base * risk_percent / 100.0, 2)
if is_full_margin_mode(POSITION_SIZING_MODE):
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
if not ok_flat:
conn.close()
flash(f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U")
flash(flat_msg)
return redirect("/")
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
leverage = leverage_for_full_margin(symbol, BTC_LEVERAGE, ALT_LEVERAGE)
sizing, sizing_err = compute_full_margin_sizing(
symbol=symbol,
available_usdt=available_usdt if available_usdt is not None else 0.0,
capital_base=capital_base,
buffer_ratio=FULL_MARGIN_BUFFER_RATIO,
btc_leverage=BTC_LEVERAGE,
alt_leverage=ALT_LEVERAGE,
funds_decimals=2,
)
if sizing_err:
conn.close()
flash(sizing_err)
return redirect("/")
margin_capital = sizing["margin_capital"]
notional_value = sizing["notional_value"]
position_ratio = sizing["position_ratio"]
else:
default_leverage = get_synced_leverage(exchange_symbol, direction) or infer_leverage(symbol)
try:
leverage_input = parse_positive_float(d.get("leverage"))
leverage = int(leverage_input) if leverage_input is not None else default_leverage
except Exception:
conn.close()
flash("杠杆参数格式错误")
return redirect("/")
if leverage <= 0:
conn.close()
flash("杠杆必须大于0")
return redirect("/")
notional_value = round(risk_amount / risk_fraction, 2)
margin_capital = round(notional_value / leverage, 2)
if capital_base and margin_capital > capital_base:
conn.close()
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
return redirect("/")
if available_usdt is not None:
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 2)
if margin_capital > max_margin:
conn.close()
flash(f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U")
return redirect("/")
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
try:
amount, quote_price = prepare_order_amount(exchange_symbol, margin_capital, leverage, live_price)
contract_size = get_contract_size(exchange_symbol)
@@ -8213,6 +8287,8 @@ from strategy_trend_register import install_strategy_trend
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
_purge_key_monitors_if_full_margin()
# 启动
if __name__ == "__main__":