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:
@@ -36,6 +36,19 @@ if _REPO_ROOT not in sys.path:
|
||||
sys.path.insert(0, _REPO_ROOT)
|
||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
||||
from ai_review_lib import build_journal_ai_chart_path, collect_images_for_ai_review
|
||||
from position_sizing_lib import (
|
||||
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 form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||
from order_monitor_display_lib import (
|
||||
apply_order_price_display_fields,
|
||||
@@ -198,6 +211,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"))
|
||||
@@ -1480,6 +1494,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=lambda _row: None,
|
||||
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
|
||||
@@ -5533,6 +5568,11 @@ def render_main_page(page="trade"):
|
||||
data_export_version=3,
|
||||
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,
|
||||
@@ -6128,19 +6168,6 @@ def add_order():
|
||||
flash(f"风控拒绝下单:{reason_live}")
|
||||
return redirect("/")
|
||||
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=?",
|
||||
@@ -6191,26 +6218,69 @@ def add_order():
|
||||
conn.close()
|
||||
flash("价格参数必须大于0")
|
||||
return redirect("/")
|
||||
_min_rr = float(os.getenv("MANUAL_MIN_PLANNED_RR", "1.4"))
|
||||
planned_rr_manual = calc_rr_ratio(direction, live_price, stop_loss, take_profit)
|
||||
if planned_rr_manual is None or planned_rr_manual < _min_rr:
|
||||
conn.close()
|
||||
rr_txt = f"{planned_rr_manual:.4f}" if planned_rr_manual is not None else "无法计算"
|
||||
flash(f"风控拒绝下单:计划盈亏比 {rr_txt}:1 低于最低要求 {_min_rr}:1")
|
||||
return redirect("/")
|
||||
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
|
||||
if risk_fraction is None:
|
||||
conn.close()
|
||||
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,4)}U,当前最多建议 {max_margin}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,当前最多建议 {max_margin}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)
|
||||
@@ -7676,6 +7746,8 @@ install_strategy_trading(
|
||||
trend_enabled=True,
|
||||
)
|
||||
|
||||
_purge_key_monitors_if_full_margin()
|
||||
|
||||
|
||||
# 启动
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user