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:
@@ -18,6 +18,8 @@ cd crypto_monitor
|
|||||||
|
|
||||||
会为各子项目创建 `.venv`、安装依赖、从 `.env.example` 生成 `.env`(不覆盖已有)。详见 **[deploy/README.md](./deploy/README.md)**。
|
会为各子项目创建 `.venv`、安装依赖、从 `.env.example` 生成 `.env`(不覆盖已有)。详见 **[deploy/README.md](./deploy/README.md)**。
|
||||||
|
|
||||||
|
计仓模式(以损定仓 / 全仓杠杆,四所统一):见 **[docs/position-sizing-mode.md](./docs/position-sizing-mode.md)**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一、仓库目录一览
|
## 一、仓库目录一览
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ UPLOAD_DIR=static/images
|
|||||||
# TOTAL_CAPITAL=100
|
# TOTAL_CAPITAL=100
|
||||||
# 页顶「资金账户」默认仅 Binance Funding 钱包;若 USDT 主要在现货,可改为 true 合并 Spot
|
# 页顶「资金账户」默认仅 Binance Funding 钱包;若 USDT 主要在现货,可改为 true 合并 Spot
|
||||||
# BINANCE_FUNDING_INCLUDE_SPOT=false
|
# BINANCE_FUNDING_INCLUDE_SPOT=false
|
||||||
|
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||||
|
POSITION_SIZING_MODE=risk
|
||||||
# 每天起始基数(U)
|
# 每天起始基数(U)
|
||||||
DAILY_START_CAPITAL=30
|
DAILY_START_CAPITAL=30
|
||||||
# 日内回撤后基数(U)
|
# 日内回撤后基数(U)
|
||||||
|
|||||||
@@ -81,6 +81,23 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
|
from position_sizing_lib import (
|
||||||
|
OPEN_SOURCE_KEY_AUTO,
|
||||||
|
OPEN_SOURCE_MANUAL,
|
||||||
|
OPEN_SOURCE_ROLL,
|
||||||
|
OPEN_SOURCE_TREND,
|
||||||
|
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 (
|
from key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
@@ -243,6 +260,8 @@ FORCE_CLOSE_ENABLED = os.getenv("FORCE_CLOSE_ENABLED", "false").lower() == "true
|
|||||||
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
||||||
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
|
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
|
||||||
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
||||||
|
# 计仓模式:risk=以损定仓(默认);full_margin=合约可用保证金×比例全仓杠杆(仅 env 切换,须无仓)
|
||||||
|
POSITION_SIZING_MODE = load_position_sizing_mode()
|
||||||
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
|
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
|
||||||
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
||||||
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
||||||
@@ -1445,6 +1464,27 @@ def init_db():
|
|||||||
|
|
||||||
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():
|
def get_db():
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -4544,6 +4584,9 @@ def _market_open_for_key_monitor(
|
|||||||
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入(Binance U 本位)。
|
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入(Binance U 本位)。
|
||||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
返回 (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()
|
now = app_now()
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
@@ -6065,6 +6108,11 @@ def render_main_page(page="trade"):
|
|||||||
},
|
},
|
||||||
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
||||||
risk_percent=RISK_PERCENT,
|
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_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||||
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
||||||
occupied_miss_total=occupied_miss_total,
|
occupied_miss_total=occupied_miss_total,
|
||||||
@@ -6726,6 +6774,12 @@ def add_key():
|
|||||||
if mt not in allowed_types:
|
if mt not in allowed_types:
|
||||||
flash("监控类型无效")
|
flash("监控类型无效")
|
||||||
return redirect("/key_monitor")
|
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)
|
rank, total = _daily_volume_rank(symbol)
|
||||||
if rank is None:
|
if rank is None:
|
||||||
flash("日成交量排名读取失败,请稍后重试")
|
flash("日成交量排名读取失败,请稍后重试")
|
||||||
@@ -6896,19 +6950,6 @@ def add_order():
|
|||||||
flash(f"风控拒绝下单:{reason_live}")
|
flash(f"风控拒绝下单:{reason_live}")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol)
|
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)
|
trading_day = get_trading_day(now)
|
||||||
opens_today_before = conn.execute(
|
opens_today_before = conn.execute(
|
||||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||||
@@ -6972,6 +7013,42 @@ def add_order():
|
|||||||
return redirect("/")
|
return redirect("/")
|
||||||
risk_percent = max(0.01, float(RISK_PERCENT))
|
risk_percent = max(0.01, float(RISK_PERCENT))
|
||||||
risk_amount = round(capital_base * risk_percent / 100.0, FUNDS_DECIMALS)
|
risk_amount = round(capital_base * risk_percent / 100.0, FUNDS_DECIMALS)
|
||||||
|
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(flat_msg)
|
||||||
|
return redirect("/")
|
||||||
|
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=FUNDS_DECIMALS,
|
||||||
|
)
|
||||||
|
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, FUNDS_DECIMALS)
|
notional_value = round(risk_amount / risk_fraction, FUNDS_DECIMALS)
|
||||||
margin_capital = round(notional_value / leverage, FUNDS_DECIMALS)
|
margin_capital = round(notional_value / leverage, FUNDS_DECIMALS)
|
||||||
if capital_base and margin_capital > capital_base:
|
if capital_base and margin_capital > capital_base:
|
||||||
@@ -8169,6 +8246,8 @@ from strategy_trend_register import install_strategy_trend
|
|||||||
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -417,7 +417,13 @@
|
|||||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||||
</div>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="rule-tip">
|
||||||
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
计仓模式:<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>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="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 }})
|
划转:自动划转 {{ '开启' 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 }})
|
||||||
@@ -449,7 +455,9 @@
|
|||||||
<option value="trend">趋势单</option>
|
<option value="trend">趋势单</option>
|
||||||
<option value="swing">波段单</option>
|
<option value="swing">波段单</option>
|
||||||
</select>
|
</select>
|
||||||
|
{% if position_sizing_mode != 'full_margin' %}
|
||||||
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
||||||
|
{% endif %}
|
||||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||||
</label>
|
</label>
|
||||||
@@ -461,7 +469,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
||||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ UPLOAD_DIR=static/images
|
|||||||
|
|
||||||
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
|
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
|
||||||
# TOTAL_CAPITAL=100
|
# TOTAL_CAPITAL=100
|
||||||
|
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||||
|
POSITION_SIZING_MODE=risk
|
||||||
# 每天起始基数(U)
|
# 每天起始基数(U)
|
||||||
DAILY_START_CAPITAL=30
|
DAILY_START_CAPITAL=30
|
||||||
# 日内回撤后基数(U)
|
# 日内回撤后基数(U)
|
||||||
|
|||||||
+93
-17
@@ -82,6 +82,21 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
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 (
|
from key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
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"))
|
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
||||||
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
|
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
|
||||||
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
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"))
|
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
|
||||||
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
||||||
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
||||||
@@ -1442,6 +1458,27 @@ def init_db():
|
|||||||
|
|
||||||
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():
|
def get_db():
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -4369,6 +4406,9 @@ def _market_open_for_key_monitor(
|
|||||||
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入。
|
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入。
|
||||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
返回 (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()
|
now = app_now()
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
@@ -6024,6 +6064,11 @@ def render_main_page(page="trade"):
|
|||||||
},
|
},
|
||||||
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
||||||
risk_percent=RISK_PERCENT,
|
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_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||||
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
||||||
occupied_miss_total=occupied_miss_total,
|
occupied_miss_total=occupied_miss_total,
|
||||||
@@ -6732,6 +6777,12 @@ def add_key():
|
|||||||
if mt not in allowed_types:
|
if mt not in allowed_types:
|
||||||
flash("监控类型无效")
|
flash("监控类型无效")
|
||||||
return redirect("/key_monitor")
|
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)
|
rank, total = _daily_volume_rank(symbol)
|
||||||
if rank is None:
|
if rank is None:
|
||||||
flash("日成交量排名读取失败,请稍后重试")
|
flash("日成交量排名读取失败,请稍后重试")
|
||||||
@@ -6930,19 +6981,6 @@ def add_order():
|
|||||||
flash(f"风控拒绝下单:{reason_live}")
|
flash(f"风控拒绝下单:{reason_live}")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol)
|
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)
|
trading_day = get_trading_day(now)
|
||||||
opens_today_before = conn.execute(
|
opens_today_before = conn.execute(
|
||||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||||
@@ -7018,15 +7056,51 @@ def add_order():
|
|||||||
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
risk_percent = max(0.01, float(RISK_PERCENT))
|
risk_percent = max(0.01, float(RISK_PERCENT))
|
||||||
risk_amount = round(capital_base * risk_percent / 100.0, 4)
|
risk_amount = round(capital_base * risk_percent / 100.0, 2)
|
||||||
notional_value = round(risk_amount / risk_fraction, 4)
|
if is_full_margin_mode(POSITION_SIZING_MODE):
|
||||||
margin_capital = round(notional_value / leverage, 4)
|
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
|
||||||
|
if not ok_flat:
|
||||||
|
conn.close()
|
||||||
|
flash(flat_msg)
|
||||||
|
return redirect("/")
|
||||||
|
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:
|
if capital_base and margin_capital > capital_base:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
if available_usdt is not None:
|
if available_usdt is not None:
|
||||||
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
|
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 2)
|
||||||
if margin_capital > max_margin:
|
if margin_capital > max_margin:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash(f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U")
|
flash(f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {round(max_margin, 2)}U")
|
||||||
@@ -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_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -417,7 +417,13 @@
|
|||||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||||
</div>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="rule-tip">
|
||||||
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
计仓模式:<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>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="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 }})
|
划转:自动划转 {{ '开启' 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 }})
|
||||||
@@ -449,7 +455,9 @@
|
|||||||
<option value="trend">趋势单</option>
|
<option value="trend">趋势单</option>
|
||||||
<option value="swing">波段单</option>
|
<option value="swing">波段单</option>
|
||||||
</select>
|
</select>
|
||||||
|
{% if position_sizing_mode != 'full_margin' %}
|
||||||
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
||||||
|
{% endif %}
|
||||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||||
</label>
|
</label>
|
||||||
@@ -461,7 +469,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
||||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ UPLOAD_DIR=static/images
|
|||||||
|
|
||||||
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
|
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
|
||||||
# TOTAL_CAPITAL=100
|
# TOTAL_CAPITAL=100
|
||||||
|
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||||
|
POSITION_SIZING_MODE=risk
|
||||||
# 每天起始基数(U)
|
# 每天起始基数(U)
|
||||||
DAILY_START_CAPITAL=30
|
DAILY_START_CAPITAL=30
|
||||||
# 日内回撤后基数(U)
|
# 日内回撤后基数(U)
|
||||||
|
|||||||
@@ -36,6 +36,19 @@ if _REPO_ROOT not in sys.path:
|
|||||||
sys.path.insert(0, _REPO_ROOT)
|
sys.path.insert(0, _REPO_ROOT)
|
||||||
from ai_client import ai_generate, ai_review, ai_short_advice
|
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 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 form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit_scope_add_order
|
||||||
from order_monitor_display_lib import (
|
from order_monitor_display_lib import (
|
||||||
apply_order_price_display_fields,
|
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"))
|
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
||||||
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
|
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日便于对账
|
||||||
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
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"))
|
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
|
||||||
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
||||||
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
||||||
@@ -1480,6 +1494,27 @@ def init_db():
|
|||||||
|
|
||||||
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():
|
def get_db():
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -5533,6 +5568,11 @@ def render_main_page(page="trade"):
|
|||||||
data_export_version=3,
|
data_export_version=3,
|
||||||
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
||||||
risk_percent=RISK_PERCENT,
|
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_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||||
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
||||||
occupied_miss_total=occupied_miss_total,
|
occupied_miss_total=occupied_miss_total,
|
||||||
@@ -6128,19 +6168,6 @@ def add_order():
|
|||||||
flash(f"风控拒绝下单:{reason_live}")
|
flash(f"风控拒绝下单:{reason_live}")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
exchange_symbol = normalize_exchange_symbol(symbol)
|
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)
|
trading_day = get_trading_day(now)
|
||||||
opens_today_before = conn.execute(
|
opens_today_before = conn.execute(
|
||||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||||
@@ -6191,24 +6218,67 @@ def add_order():
|
|||||||
conn.close()
|
conn.close()
|
||||||
flash("价格参数必须大于0")
|
flash("价格参数必须大于0")
|
||||||
return redirect("/")
|
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)
|
risk_fraction = calc_risk_fraction(direction, live_price, stop_loss)
|
||||||
if risk_fraction is None:
|
if risk_fraction is None:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
risk_percent = max(0.01, float(RISK_PERCENT))
|
risk_percent = max(0.01, float(RISK_PERCENT))
|
||||||
risk_amount = round(capital_base * risk_percent / 100.0, 4)
|
risk_amount = round(capital_base * risk_percent / 100.0, 2)
|
||||||
notional_value = round(risk_amount / risk_fraction, 4)
|
if is_full_margin_mode(POSITION_SIZING_MODE):
|
||||||
margin_capital = round(notional_value / leverage, 4)
|
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
|
||||||
|
if not ok_flat:
|
||||||
|
conn.close()
|
||||||
|
flash(flat_msg)
|
||||||
|
return redirect("/")
|
||||||
|
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:
|
if capital_base and margin_capital > capital_base:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
if available_usdt is not None:
|
if available_usdt is not None:
|
||||||
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
|
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 2)
|
||||||
if margin_capital > max_margin:
|
if margin_capital > max_margin:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash(f"保证金不足:交易账户可用约 {round(available_usdt,4)}U,当前最多建议 {max_margin}U")
|
flash(f"保证金不足:交易账户可用约 {round(available_usdt, 2)}U,当前最多建议 {max_margin}U")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
||||||
try:
|
try:
|
||||||
@@ -7676,6 +7746,8 @@ install_strategy_trading(
|
|||||||
trend_enabled=True,
|
trend_enabled=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_purge_key_monitors_if_full_margin()
|
||||||
|
|
||||||
|
|
||||||
# 启动
|
# 启动
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -302,7 +302,13 @@
|
|||||||
按风险比例自动计算仓位
|
按风险比例自动计算仓位
|
||||||
</div>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="rule-tip">
|
||||||
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
计仓模式:<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>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="rule-tip">
|
||||||
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ money_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }})
|
划转:自动划转 {{ '开启' if auto_transfer_enabled else '关闭' }}(每天<strong>北京时间 {{ auto_transfer_bj_hour }}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;将 {{ auto_transfer_to }} 补足到 {{ money_fmt(auto_transfer_amount) }}U,来自 {{ auto_transfer_from }})
|
||||||
@@ -346,7 +352,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
||||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="order-live-positions">
|
<div class="order-live-positions">
|
||||||
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff">实时持仓</h3>
|
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff">实时持仓</h3>
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ UPLOAD_DIR=static/images
|
|||||||
|
|
||||||
# 训练总资金(U)
|
# 训练总资金(U)
|
||||||
# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所
|
# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所
|
||||||
|
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||||
|
POSITION_SIZING_MODE=risk
|
||||||
# 每天起始基数(U)
|
# 每天起始基数(U)
|
||||||
DAILY_START_CAPITAL=30
|
DAILY_START_CAPITAL=30
|
||||||
# 日内回撤后基数(U)
|
# 日内回撤后基数(U)
|
||||||
|
|||||||
+94
-18
@@ -82,6 +82,21 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
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 (
|
from key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
@@ -213,6 +228,7 @@ FORCE_CLOSE_ENABLED = os.getenv("FORCE_CLOSE_ENABLED", "false").lower() == "true
|
|||||||
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
|
||||||
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日(与 OKX 日界一致便于对账)
|
# 自动划转:仅在北京时间该整点「小时」内尝试;transfer_logs.transfer_day 存 UTC 自然日(与 OKX 日界一致便于对账)
|
||||||
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
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"))
|
WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10"))
|
||||||
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120"))
|
||||||
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3"))
|
||||||
@@ -1389,6 +1405,27 @@ def init_db():
|
|||||||
|
|
||||||
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():
|
def get_db():
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
@@ -4639,6 +4676,9 @@ def _market_open_for_key_monitor(
|
|||||||
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入(OKX 永续)。
|
与手动「实盘下单」对齐的市价开仓与 order_monitors 写入(OKX 永续)。
|
||||||
返回 (ok: bool, err_msg: Optional[str], detail: Optional[dict])
|
返回 (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()
|
now = app_now()
|
||||||
ok, reason = precheck_risk(conn, symbol, direction)
|
ok, reason = precheck_risk(conn, symbol, direction)
|
||||||
if not ok:
|
if not ok:
|
||||||
@@ -5669,6 +5709,11 @@ def render_main_page(page="trade"):
|
|||||||
},
|
},
|
||||||
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
key_alert_max_times=KEY_ALERT_MAX_TIMES,
|
||||||
risk_percent=RISK_PERCENT,
|
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_rr_trigger=BREAKEVEN_RR_TRIGGER,
|
||||||
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
breakeven_offset_pct=BREAKEVEN_OFFSET_PCT,
|
||||||
occupied_miss_total=occupied_miss_total,
|
occupied_miss_total=occupied_miss_total,
|
||||||
@@ -6403,6 +6448,12 @@ def add_key():
|
|||||||
if mt not in allowed_types:
|
if mt not in allowed_types:
|
||||||
flash("监控类型无效")
|
flash("监控类型无效")
|
||||||
return redirect("/key_monitor")
|
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)
|
rank, total = _daily_volume_rank(symbol)
|
||||||
if rank is None:
|
if rank is None:
|
||||||
flash("日成交量排名读取失败,请稍后重试")
|
flash("日成交量排名读取失败,请稍后重试")
|
||||||
@@ -6573,19 +6624,6 @@ def add_order():
|
|||||||
flash(f"风控拒绝下单:{reason_live}")
|
flash(f"风控拒绝下单:{reason_live}")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
exchange_symbol = normalize_okx_symbol(symbol)
|
exchange_symbol = normalize_okx_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("/trade")
|
|
||||||
if leverage <= 0:
|
|
||||||
conn.close()
|
|
||||||
flash("杠杆必须大于0")
|
|
||||||
return redirect("/trade")
|
|
||||||
|
|
||||||
trading_day = get_trading_day(now)
|
trading_day = get_trading_day(now)
|
||||||
opens_today_before = conn.execute(
|
opens_today_before = conn.execute(
|
||||||
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
"SELECT COUNT(*) FROM order_monitors WHERE session_date=?",
|
||||||
@@ -6648,18 +6686,54 @@ def add_order():
|
|||||||
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
flash("止损方向不合法:请检查入场方向与止损价格关系")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
risk_percent = max(0.01, float(RISK_PERCENT))
|
risk_percent = max(0.01, float(RISK_PERCENT))
|
||||||
risk_amount = round(capital_base * risk_percent / 100.0, 4)
|
risk_amount = round(capital_base * risk_percent / 100.0, FUNDS_DECIMALS)
|
||||||
notional_value = round(risk_amount / risk_fraction, 4)
|
if is_full_margin_mode(POSITION_SIZING_MODE):
|
||||||
margin_capital = round(notional_value / leverage, 4)
|
ok_flat, flat_msg = full_margin_requires_flat_position(get_active_position_count(conn))
|
||||||
|
if not ok_flat:
|
||||||
|
conn.close()
|
||||||
|
flash(flat_msg)
|
||||||
|
return redirect("/trade")
|
||||||
|
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=FUNDS_DECIMALS,
|
||||||
|
)
|
||||||
|
if sizing_err:
|
||||||
|
conn.close()
|
||||||
|
flash(sizing_err)
|
||||||
|
return redirect("/trade")
|
||||||
|
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("/trade")
|
||||||
|
if leverage <= 0:
|
||||||
|
conn.close()
|
||||||
|
flash("杠杆必须大于0")
|
||||||
|
return redirect("/trade")
|
||||||
|
notional_value = round(risk_amount / risk_fraction, FUNDS_DECIMALS)
|
||||||
|
margin_capital = round(notional_value / leverage, FUNDS_DECIMALS)
|
||||||
if capital_base and margin_capital > capital_base:
|
if capital_base and margin_capital > capital_base:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
flash("以损定仓后保证金超过当前交易资金,请放宽止损或降低风险比例")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
if available_usdt is not None:
|
if available_usdt is not None:
|
||||||
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), 4)
|
max_margin = round(max(available_usdt * FULL_MARGIN_BUFFER_RATIO, 0), FUNDS_DECIMALS)
|
||||||
if margin_capital > max_margin:
|
if margin_capital > max_margin:
|
||||||
conn.close()
|
conn.close()
|
||||||
flash(f"保证金不足:交易账户可用约 {round(available_usdt,4)}U,当前最多建议 {max_margin}U")
|
flash(f"保证金不足:交易账户可用约 {round(available_usdt, FUNDS_DECIMALS)}U,当前最多建议 {max_margin}U")
|
||||||
return redirect("/trade")
|
return redirect("/trade")
|
||||||
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
position_ratio = round(margin_capital / capital_base * 100, 2) if capital_base else 0
|
||||||
try:
|
try:
|
||||||
@@ -7825,6 +7899,8 @@ from strategy_trend_register import install_strategy_trend
|
|||||||
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
|
||||||
install_strategy_trend(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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -426,7 +426,13 @@
|
|||||||
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
人工开仓盈亏比不得低于 {{ manual_min_planned_rr }}:1
|
||||||
</div>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="rule-tip">
|
||||||
以损定仓:风险 {{ risk_percent }}% |移动保本:下单可勾选关闭;开启时 {{ breakeven_rr_trigger }}R 触发(每 1R 阶梯上移),偏移 {{ breakeven_offset_pct }}%
|
计仓模式:<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>
|
</div>
|
||||||
<div class="rule-tip">
|
<div class="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 }})
|
划转:自动划转 {{ '开启' 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 }})
|
||||||
@@ -458,7 +464,9 @@
|
|||||||
<option value="trend">趋势单</option>
|
<option value="trend">趋势单</option>
|
||||||
<option value="swing">波段单</option>
|
<option value="swing">波段单</option>
|
||||||
</select>
|
</select>
|
||||||
|
{% if position_sizing_mode != 'full_margin' %}
|
||||||
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
|
||||||
|
{% endif %}
|
||||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||||
</label>
|
</label>
|
||||||
@@ -470,7 +478,7 @@
|
|||||||
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
|
||||||
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
|
||||||
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
|
||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">{{ open_position_button_label }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 计仓模式(四所统一)
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
在各实例 `.env` 中设置(**仅能通过 env 切换,修改后须重启进程**):
|
||||||
|
|
||||||
|
```env
|
||||||
|
# risk(默认)= 以损定仓
|
||||||
|
# full_margin = 全仓杠杆(合约可用保证金 × 比例)
|
||||||
|
POSITION_SIZING_MODE=risk
|
||||||
|
FULL_MARGIN_BUFFER_RATIO=0.98
|
||||||
|
```
|
||||||
|
|
||||||
|
切换为全仓杠杆前:**交易所须无持仓**(`MAX_ACTIVE_POSITIONS` 默认 1,全仓模式会强制单仓)。
|
||||||
|
|
||||||
|
## 模式说明
|
||||||
|
|
||||||
|
| 模式 | 保证金计算 | 杠杆 | 允许入口 |
|
||||||
|
|------|------------|------|----------|
|
||||||
|
| `risk` | `RISK_PERCENT` × 交易资金,按止损距离反推 | 表单可选 / 同步交易所 | 实盘人工、关键位自动、趋势回调、顺势加仓 |
|
||||||
|
| `full_margin` | **合约账户可用 USDT × `FULL_MARGIN_BUFFER_RATIO`**(保留 2 位小数) | BTC/ETH **10x**,其它 **5x**(与 `BTC_LEVERAGE`/`ALT_LEVERAGE` 一致) | **仅** 实盘人工下单;阻力/支撑仅提醒 |
|
||||||
|
|
||||||
|
全仓模式下:
|
||||||
|
|
||||||
|
- 仍校验 **计划盈亏比**(`MANUAL_MIN_PLANNED_RR`)。
|
||||||
|
- 下单张数由 `prepare_order_amount` + 交易所 `amount_to_precision` 决定。
|
||||||
|
- `order_monitors.initial_stop_loss` 仍记录**开仓时**止损快照;交易记录复盘以该快照为准。
|
||||||
|
- 已存在的 **箱体突破 / 收敛突破 / 斐波** 监控:进程启动时**自动撤销**并企业微信通知。
|
||||||
|
|
||||||
|
## 不允许(全仓模式)
|
||||||
|
|
||||||
|
- 关键位:箱体突破、收敛突破、斐波自动单(添加时拒绝;已存在则启动时撤销)。
|
||||||
|
- 趋势回调、顺势加仓(策略入口返回明确错误)。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
# 四所 .env 增加 POSITION_SIZING_MODE=risk 或 full_margin
|
||||||
|
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate crypto-monitor-gate-bot
|
||||||
|
```
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
全仓杠杆模式下:撤销已添加的箱体/收敛/斐波关键位监控并微信说明。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable, Iterable, Optional
|
||||||
|
|
||||||
|
from fib_key_monitor_lib import FIB_KEY_MONITOR_TYPES, is_fib_key_monitor_type
|
||||||
|
from key_monitor_lib import KEY_MONITOR_AUTO_TYPES
|
||||||
|
from position_sizing_lib import is_full_margin_mode, mode_label_zh
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_type_disallowed_in_full_margin(monitor_type: str) -> bool:
|
||||||
|
mt = (monitor_type or "").strip()
|
||||||
|
if mt in KEY_MONITOR_AUTO_TYPES:
|
||||||
|
return True
|
||||||
|
return is_fib_key_monitor_type(mt)
|
||||||
|
|
||||||
|
|
||||||
|
def purge_disallowed_key_monitors(
|
||||||
|
conn: Any,
|
||||||
|
*,
|
||||||
|
sizing_mode: str,
|
||||||
|
select_rows: Callable[[Any], Iterable[Any]],
|
||||||
|
cancel_fib_limit: Callable[[Any], None],
|
||||||
|
delete_monitor: Callable[[Any, int], None],
|
||||||
|
send_wechat: Callable[[str], None],
|
||||||
|
row_symbol: Callable[[Any], str] = lambda r: str(r["symbol"] or ""),
|
||||||
|
row_monitor_type: Callable[[Any], str] = lambda r: str(r["monitor_type"] or ""),
|
||||||
|
row_id: Callable[[Any], int] = lambda r: int(r["id"]),
|
||||||
|
) -> int:
|
||||||
|
if not is_full_margin_mode(sizing_mode):
|
||||||
|
return 0
|
||||||
|
removed = []
|
||||||
|
for row in select_rows(conn):
|
||||||
|
mt = row_monitor_type(row)
|
||||||
|
if not monitor_type_disallowed_in_full_margin(mt):
|
||||||
|
continue
|
||||||
|
sym = row_symbol(row)
|
||||||
|
kid = row_id(row)
|
||||||
|
if is_fib_key_monitor_type(mt):
|
||||||
|
try:
|
||||||
|
cancel_fib_limit(row)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
delete_monitor(conn, kid)
|
||||||
|
removed.append((sym, mt, kid))
|
||||||
|
if removed:
|
||||||
|
lines = [f"· {s} {t} (#{i})" for s, t, i in removed[:12]]
|
||||||
|
if len(removed) > 12:
|
||||||
|
lines.append(f"… 共 {len(removed)} 条")
|
||||||
|
send_wechat(
|
||||||
|
"# ⚠️ 全仓杠杆模式:已自动撤销关键位监控\n"
|
||||||
|
f"计仓模式:{mode_label_zh(sizing_mode)}(仅 env 可切换,须无仓)\n"
|
||||||
|
"已撤销:箱体突破 / 收敛突破 / 斐波回调监控(不可与全仓杠杆并存)\n"
|
||||||
|
+ "\n".join(lines)
|
||||||
|
)
|
||||||
|
return len(removed)
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
四所共用:计仓模式 risk(以损定仓)| full_margin(全仓杠杆)。
|
||||||
|
仅 env POSITION_SIZING_MODE 切换;须无持仓(由部署流程保证)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any, Optional, Tuple
|
||||||
|
|
||||||
|
MODE_RISK = "risk"
|
||||||
|
MODE_FULL_MARGIN = "full_margin"
|
||||||
|
VALID_MODES = frozenset({MODE_RISK, MODE_FULL_MARGIN})
|
||||||
|
|
||||||
|
OPEN_SOURCE_MANUAL = "manual"
|
||||||
|
OPEN_SOURCE_KEY_AUTO = "key_auto"
|
||||||
|
OPEN_SOURCE_KEY_FIB = "key_fib"
|
||||||
|
OPEN_SOURCE_TREND = "trend"
|
||||||
|
OPEN_SOURCE_ROLL = "roll"
|
||||||
|
|
||||||
|
FULL_MARGIN_BLOCKED_SOURCES = frozenset(
|
||||||
|
{OPEN_SOURCE_KEY_AUTO, OPEN_SOURCE_KEY_FIB, OPEN_SOURCE_TREND, OPEN_SOURCE_ROLL}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_position_sizing_mode(raw: Optional[str]) -> str:
|
||||||
|
v = (raw or MODE_RISK).strip().lower()
|
||||||
|
if v in ("full", "full_margin", "fullmargin", "全仓", "全仓杠杆"):
|
||||||
|
return MODE_FULL_MARGIN
|
||||||
|
return MODE_RISK if v in ("risk", "r", "以损定仓", "") else MODE_RISK
|
||||||
|
|
||||||
|
|
||||||
|
def load_position_sizing_mode(env: Optional[dict] = None) -> str:
|
||||||
|
e = env if env is not None else os.environ
|
||||||
|
return normalize_position_sizing_mode(e.get("POSITION_SIZING_MODE"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_full_margin_mode(mode: str) -> bool:
|
||||||
|
return normalize_position_sizing_mode(mode) == MODE_FULL_MARGIN
|
||||||
|
|
||||||
|
|
||||||
|
def mode_label_zh(mode: str) -> str:
|
||||||
|
return "全仓杠杆" if is_full_margin_mode(mode) else "以损定仓"
|
||||||
|
|
||||||
|
|
||||||
|
def leverage_for_full_margin(symbol: str, btc_leverage: int, alt_leverage: int) -> int:
|
||||||
|
sym = (symbol or "").strip().upper()
|
||||||
|
if sym.startswith("BTC") or sym.startswith("ETH"):
|
||||||
|
return max(1, int(btc_leverage or 10))
|
||||||
|
return max(1, int(alt_leverage or 5))
|
||||||
|
|
||||||
|
|
||||||
|
def round_funds(value: float, decimals: int = 2) -> float:
|
||||||
|
return round(float(value), int(decimals))
|
||||||
|
|
||||||
|
|
||||||
|
def assert_open_source_allowed(mode: str, source: str) -> Tuple[bool, str]:
|
||||||
|
if not is_full_margin_mode(mode):
|
||||||
|
return True, ""
|
||||||
|
src = (source or "").strip().lower()
|
||||||
|
if src in FULL_MARGIN_BLOCKED_SOURCES:
|
||||||
|
return False, (
|
||||||
|
"当前为全仓杠杆模式(POSITION_SIZING_MODE=full_margin),"
|
||||||
|
"不允许关键位突破/斐波自动开仓、趋势回调与顺势加仓;"
|
||||||
|
"仅支持实盘人工下单与阻力/支撑提醒。"
|
||||||
|
)
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def full_margin_requires_flat_position(active_count: int) -> Tuple[bool, str]:
|
||||||
|
if active_count > 0:
|
||||||
|
return False, "全仓杠杆模式仅允许单仓且无其它持仓,请先平仓后再开仓"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def compute_full_margin_sizing(
|
||||||
|
*,
|
||||||
|
symbol: str,
|
||||||
|
available_usdt: float,
|
||||||
|
capital_base: float,
|
||||||
|
buffer_ratio: float,
|
||||||
|
btc_leverage: int,
|
||||||
|
alt_leverage: int,
|
||||||
|
funds_decimals: int = 2,
|
||||||
|
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||||
|
if available_usdt is None or float(available_usdt) <= 0:
|
||||||
|
return None, "全仓杠杆:无法读取合约账户可用保证金"
|
||||||
|
lev = leverage_for_full_margin(symbol, btc_leverage, alt_leverage)
|
||||||
|
margin = round_funds(float(available_usdt) * float(buffer_ratio), funds_decimals)
|
||||||
|
if margin <= 0:
|
||||||
|
return None, "全仓杠杆:可用保证金不足"
|
||||||
|
notional = round_funds(margin * lev, funds_decimals)
|
||||||
|
ratio = round(margin / float(capital_base) * 100, 2) if capital_base else 0.0
|
||||||
|
return {
|
||||||
|
"margin_capital": margin,
|
||||||
|
"leverage": lev,
|
||||||
|
"notional_value": notional,
|
||||||
|
"position_ratio": ratio,
|
||||||
|
"mode": MODE_FULL_MARGIN,
|
||||||
|
}, None
|
||||||
@@ -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()
|
||||||
@@ -194,6 +194,7 @@ def build_strategy_config(
|
|||||||
"趋势回调(自动补仓)请在 Gate 趋势机器人实例使用:/strategy/trend"
|
"趋势回调(自动补仓)请在 Gate 趋势机器人实例使用:/strategy/trend"
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
"app_module": m,
|
||||||
"exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""),
|
"exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""),
|
||||||
"trend_enabled": trend_enabled,
|
"trend_enabled": trend_enabled,
|
||||||
"trend_disabled_note": note,
|
"trend_disabled_note": note,
|
||||||
|
|||||||
@@ -103,6 +103,17 @@ def _count_active_trends(conn, cfg: dict) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict:
|
def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict:
|
||||||
|
m = cfg.get("app_module")
|
||||||
|
if m is not None:
|
||||||
|
try:
|
||||||
|
from position_sizing_lib import OPEN_SOURCE_ROLL, assert_open_source_allowed
|
||||||
|
|
||||||
|
mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk"
|
||||||
|
ok_src, src_msg = assert_open_source_allowed(mode, OPEN_SOURCE_ROLL)
|
||||||
|
if not ok_src:
|
||||||
|
return {"ok": False, "msg": src_msg}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
get_db = cfg["get_db"]
|
get_db = cfg["get_db"]
|
||||||
symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
|
symbol = cfg["normalize_symbol_input"](data.get("symbol") or "")
|
||||||
if not symbol:
|
if not symbol:
|
||||||
|
|||||||
@@ -106,6 +106,15 @@ def _row(cfg, row) -> dict:
|
|||||||
|
|
||||||
def precheck_trend_start(cfg: dict, conn) -> tuple[bool, str]:
|
def precheck_trend_start(cfg: dict, conn) -> tuple[bool, str]:
|
||||||
m = _m(cfg)
|
m = _m(cfg)
|
||||||
|
mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk"
|
||||||
|
try:
|
||||||
|
from position_sizing_lib import OPEN_SOURCE_TREND, assert_open_source_allowed
|
||||||
|
|
||||||
|
ok_src, src_msg = assert_open_source_allowed(mode, OPEN_SOURCE_TREND)
|
||||||
|
if not ok_src:
|
||||||
|
return False, src_msg
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
now = m.app_now()
|
now = m.app_now()
|
||||||
if not m.trading_day_reset_allows_new_open(now):
|
if not m.trading_day_reset_allows_new_open(now):
|
||||||
return False, f"北京时间 {cfg['reset_hour']}:00 前不允许持仓"
|
return False, f"北京时间 {cfg['reset_hour']}:00 前不允许持仓"
|
||||||
|
|||||||
Reference in New Issue
Block a user