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
+37 -1
View File
@@ -158,6 +158,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,
@@ -332,6 +340,7 @@ 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()
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"))
@@ -1989,6 +1998,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)
@@ -7054,6 +7069,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 "开仓(以损定仓)"
),
@@ -7790,7 +7806,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,
@@ -7800,6 +7819,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),
)
@@ -7912,6 +7932,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_pre = (d.get("direction") or "").strip().lower()
dup_msg = check_duplicate_submit(
@@ -7927,6 +7953,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)
@@ -8209,6 +8239,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()
@@ -9552,6 +9587,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),
}