feat: 账户方向与币种白名单 env 开关(三所)

Per-instance TRADE_DIRECTION / TRADE_SYMBOL_WHITELIST restricts UI and API for manual orders, key monitors, and strategies; includes sync script for deployment profiles.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-05 00:30:49 +08:00
parent 65b911994c
commit c0ad50a7b5
22 changed files with 814 additions and 35 deletions
+7
View File
@@ -52,6 +52,13 @@ UPLOAD_DIR=static/images
# BINANCE_FUNDING_INCLUDE_SPOT=false
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数(U
DAILY_START_CAPITAL=30
# 日内回撤后基数(U
+37 -1
View File
@@ -159,6 +159,14 @@ from lib.trade.position_sizing_lib import (
mode_label_zh,
risk_percent_for_storage,
)
from lib.trade.trade_policy_lib import load_trade_policy
from lib.trade.trade_policy_app_lib import (
check_direction_policy,
check_open_policy,
check_symbol_policy,
default_symbol_for_policy,
trade_policy_template_context,
)
from lib.key_monitor.key_monitor_full_margin_lib import (
monitor_type_disallowed_in_full_margin,
purge_disallowed_key_monitors,
@@ -342,6 +350,7 @@ FORCE_CLOSE_BJ_HOUR = int(os.getenv("FORCE_CLOSE_BJ_HOUR", "0"))
AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
# 计仓模式:risk=以损定仓(默认);full_margin=合约可用保证金×比例全仓杠杆(仅 env 切换,须无仓)
POSITION_SIZING_MODE = load_position_sizing_mode()
TRADE_POLICY = load_trade_policy()
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"))
@@ -2035,6 +2044,12 @@ def normalize_symbol_input(symbol):
return f"{sym}/USDT"
def validate_trade_policy_open(symbol, direction):
return check_open_policy(
TRADE_POLICY, symbol, direction, normalize_symbol_input
)
def normalize_kline_limit(limit_raw, default=200):
try:
n = int(limit_raw)
@@ -7277,6 +7292,7 @@ def render_main_page(page="trade", embed_mode=None):
risk_percent=RISK_PERCENT,
position_sizing_mode=POSITION_SIZING_MODE,
position_sizing_mode_label=mode_label_zh(POSITION_SIZING_MODE),
trade_policy=trade_policy_template_context(TRADE_POLICY),
open_position_button_label=(
"开仓(全仓杠杆)" if is_full_margin_mode(POSITION_SIZING_MODE) else "开仓(以损定仓)"
),
@@ -7996,7 +8012,10 @@ def key_focus():
selected_key = next((k for k in key_list if (k.get("symbol") or "").upper() == symbol_query), None)
if selected_key is None and key_list:
selected_key = key_list[0]
default_symbol = symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT"
default_symbol = default_symbol_for_policy(
TRADE_POLICY,
symbol_query or ((selected_key or {}).get("symbol")) or "BTC/USDT",
)
return render_template(
"key_focus_v2.html",
key_list=key_list,
@@ -8006,6 +8025,7 @@ def key_focus():
default_kline_limit=200,
price_refresh_seconds=PRICE_REFRESH_SECONDS,
exchange_display=EXCHANGE_DISPLAY_NAME,
trade_policy=trade_policy_template_context(TRADE_POLICY),
)
@@ -8116,6 +8136,12 @@ def add_key():
if not symbol:
flash("symbol 不能为空")
return redirect("/key_monitor")
ok_sym, sym_msg = check_symbol_policy(
TRADE_POLICY, symbol, normalize_symbol_input
)
if not ok_sym:
flash(sym_msg)
return redirect("/key_monitor")
mt = (d.get("type") or "").strip()
direction_sel = (d.get("direction") or "").strip().lower()
dup_msg = check_duplicate_submit(
@@ -8130,6 +8156,10 @@ def add_key():
elif direction_sel not in ("long", "short"):
flash("箱体/收敛突破请选择做多或做空")
return redirect("/key_monitor")
ok_dir, dir_msg = check_direction_policy(TRADE_POLICY, direction_sel)
if not ok_dir:
flash(dir_msg)
return redirect("/key_monitor")
allowed_types = (
tuple(KEY_MONITOR_AUTO_TYPES)
+ tuple(KEY_MONITOR_ALERT_ONLY_TYPES)
@@ -8375,6 +8405,11 @@ def add_order():
conn.close()
flash("symbol 不能为空")
return redirect("/")
ok_pol, pol_msg = validate_trade_policy_open(symbol, direction)
if not ok_pol:
conn.close()
flash(f"账户限制:{pol_msg}")
return redirect("/trade")
dup_msg = check_duplicate_submit(session, submit_scope_add_order(symbol, direction))
if dup_msg:
conn.close()
@@ -9703,6 +9738,7 @@ def _hub_meta_bundle():
"max_active_positions": MAX_ACTIVE_POSITIONS,
"btc_leverage": BTC_LEVERAGE,
"alt_leverage": ALT_LEVERAGE,
"trade_policy": trade_policy_template_context(TRADE_POLICY),
}
+7 -5
View File
@@ -243,7 +243,7 @@
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
</style>
<link rel="stylesheet" href="/static/instance_theme.css?v=48">
<link rel="stylesheet" href="/static/instance_theme.css?v=49">
</head>
<body
@@ -278,6 +278,9 @@
<h1>加密货币|交易监控 + AI复盘一体化</h1>
<div class="header-row">
<div class="exchange-tag">{{ exchange_display }}</div>
{% if trade_policy.badge_text %}
<span class="trade-policy-badge" title="账户交易限制(.env">{{ trade_policy.badge_text }}</span>
{% endif %}
<span class="risk-status-badge risk-status-{{ risk_status.status|default('normal') }}" id="account-risk-badge" role="status" title="{{ risk_status.reason|default('', true) }}" data-status-label="{{ risk_status.status_label|default('正常') }}"{% if risk_status.freeze_until_ms %} data-freeze-until-ms="{{ risk_status.freeze_until_ms }}"{% endif %}>{{ risk_status.status_label|default('正常') }}</span>
<div class="theme-toggle instance-theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
@@ -374,10 +377,9 @@
<button type="submit">手动划转</button>
</form>
<form id="add-order-form" action="/add_order" method="post" class="form-row" data-risk-percent="{{ risk_percent }}">
<input id="order-symbol" name="symbol" placeholder="BTC 或 BTC/USDT" required>
<select id="order-direction" name="direction" required>
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
{% from 'trade_policy_fields.html' import trade_policy_symbol, trade_policy_direction %}
{{ trade_policy_symbol('symbol', 'order-symbol') }}
{{ trade_policy_direction('direction', 'order-direction') }}
<select id="sltp-mode" name="sltp_mode">
<option value="fixed_rr" selected>止盈止损:固定盈亏比</option>
<option value="price">止盈止损:价格模式</option>