fix: hot-reload trade direction/whitelist on env save without forced restart.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,11 +52,11 @@ UPLOAD_DIR=static/images
|
||||
# BINANCE_FUNDING_INCLUDE_SPOT=false
|
||||
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||
POSITION_SIZING_MODE=risk
|
||||
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
|
||||
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制;保存后热生效)
|
||||
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
|
||||
TRADE_DIRECTION_RESTRICT_ENABLED=false
|
||||
TRADE_DIRECTION=both
|
||||
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
|
||||
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉;保存后热生效)
|
||||
TRADE_SYMBOL_RESTRICT_ENABLED=false
|
||||
TRADE_SYMBOL_WHITELIST=BTC,ETH
|
||||
# 每天起始基数(U)
|
||||
|
||||
@@ -408,6 +408,17 @@ AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
||||
POSITION_SIZING_MODE = load_position_sizing_mode()
|
||||
KEY_AUTO_ORDER_ENABLED = load_key_auto_order_enabled()
|
||||
TRADE_POLICY = load_trade_policy()
|
||||
|
||||
|
||||
def _reload_trade_policy_from_env(changed_keys=None):
|
||||
"""env 保存后热更新方向/白名单策略(无需重启)."""
|
||||
global TRADE_POLICY
|
||||
keys = set(changed_keys or [])
|
||||
from lib.env.env_schema import TRADE_POLICY_ENV_KEYS
|
||||
|
||||
if keys and not keys.intersection(TRADE_POLICY_ENV_KEYS):
|
||||
return
|
||||
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"))
|
||||
@@ -10052,6 +10063,7 @@ try:
|
||||
exchange_key="binance",
|
||||
username=USERNAME,
|
||||
password=PASSWORD,
|
||||
on_env_changed=lambda keys: _reload_trade_policy_from_env(keys),
|
||||
)
|
||||
except Exception as _settings_err:
|
||||
print(f"[instance_settings] binance: {_settings_err}")
|
||||
|
||||
@@ -50,11 +50,11 @@ UPLOAD_DIR=static/images
|
||||
# TOTAL_CAPITAL=100
|
||||
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||
POSITION_SIZING_MODE=risk
|
||||
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
|
||||
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制;保存后热生效)
|
||||
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
|
||||
TRADE_DIRECTION_RESTRICT_ENABLED=false
|
||||
TRADE_DIRECTION=both
|
||||
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
|
||||
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉;保存后热生效)
|
||||
TRADE_SYMBOL_RESTRICT_ENABLED=false
|
||||
TRADE_SYMBOL_WHITELIST=BTC,ETH
|
||||
# 每天起始基数(U)
|
||||
|
||||
@@ -401,6 +401,17 @@ AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
||||
POSITION_SIZING_MODE = load_position_sizing_mode()
|
||||
KEY_AUTO_ORDER_ENABLED = load_key_auto_order_enabled()
|
||||
TRADE_POLICY = load_trade_policy()
|
||||
|
||||
|
||||
def _reload_trade_policy_from_env(changed_keys=None):
|
||||
"""env 保存后热更新方向/白名单策略(无需重启)."""
|
||||
global TRADE_POLICY
|
||||
keys = set(changed_keys or [])
|
||||
from lib.env.env_schema import TRADE_POLICY_ENV_KEYS
|
||||
|
||||
if keys and not keys.intersection(TRADE_POLICY_ENV_KEYS):
|
||||
return
|
||||
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"))
|
||||
@@ -9895,6 +9906,7 @@ try:
|
||||
exchange_key="gate",
|
||||
username=USERNAME,
|
||||
password=PASSWORD,
|
||||
on_env_changed=lambda keys: _reload_trade_policy_from_env(keys),
|
||||
)
|
||||
except Exception as _settings_err:
|
||||
print(f"[instance_settings] gate: {_settings_err}")
|
||||
|
||||
@@ -55,11 +55,11 @@ UPLOAD_DIR=static/images
|
||||
# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所
|
||||
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
|
||||
POSITION_SIZING_MODE=risk
|
||||
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
|
||||
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制;保存后热生效)
|
||||
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
|
||||
TRADE_DIRECTION_RESTRICT_ENABLED=false
|
||||
TRADE_DIRECTION=both
|
||||
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
|
||||
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉;保存后热生效)
|
||||
TRADE_SYMBOL_RESTRICT_ENABLED=false
|
||||
TRADE_SYMBOL_WHITELIST=BTC,ETH
|
||||
# 每天起始基数(U)
|
||||
|
||||
@@ -377,6 +377,17 @@ AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8"))
|
||||
POSITION_SIZING_MODE = load_position_sizing_mode()
|
||||
KEY_AUTO_ORDER_ENABLED = load_key_auto_order_enabled()
|
||||
TRADE_POLICY = load_trade_policy()
|
||||
|
||||
|
||||
def _reload_trade_policy_from_env(changed_keys=None):
|
||||
"""env 保存后热更新方向/白名单策略(无需重启)."""
|
||||
global TRADE_POLICY
|
||||
keys = set(changed_keys or [])
|
||||
from lib.env.env_schema import TRADE_POLICY_ENV_KEYS
|
||||
|
||||
if keys and not keys.intersection(TRADE_POLICY_ENV_KEYS):
|
||||
return
|
||||
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"))
|
||||
@@ -9614,6 +9625,7 @@ try:
|
||||
exchange_key="okx",
|
||||
username=USERNAME,
|
||||
password=PASSWORD,
|
||||
on_env_changed=lambda keys: _reload_trade_policy_from_env(keys),
|
||||
)
|
||||
except Exception as _settings_err:
|
||||
print(f"[instance_settings] okx: {_settings_err}")
|
||||
|
||||
@@ -376,9 +376,15 @@
|
||||
const needRestart = restartAfter || data.restart_required;
|
||||
if (needRestart) {
|
||||
setStatus(status, "已保存,正在重启实例…");
|
||||
await restartInstance();
|
||||
setStatus(status, "保存并重启完成");
|
||||
await loadEnvConfig();
|
||||
try {
|
||||
await restartInstance();
|
||||
setStatus(status, "保存并重启完成,若页面异常请手动刷新");
|
||||
} catch (re) {
|
||||
setStatus(status, (re && re.message) || "重启超时,请稍后刷新页面查看", true);
|
||||
}
|
||||
try {
|
||||
await loadEnvConfig();
|
||||
} catch (_) {}
|
||||
} else {
|
||||
setStatus(status, "已保存(即时生效项已应用)");
|
||||
await loadEnvConfig();
|
||||
|
||||
Vendored
+33
@@ -93,6 +93,24 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
|
||||
})
|
||||
|
||||
TRADE_POLICY_ENV_KEYS = frozenset({
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED",
|
||||
"TRADE_DIRECTION",
|
||||
"TRADE_SYMBOL_RESTRICT_ENABLED",
|
||||
"TRADE_SYMBOL_WHITELIST",
|
||||
})
|
||||
|
||||
|
||||
def normalize_trade_direction(raw: str) -> str:
|
||||
s = (raw or "").strip().lower().replace("-", "_").replace(" ", "")
|
||||
if s in ("long_only", "long", "longonly", "多", "只多", "仅多", "做多"):
|
||||
return "long_only"
|
||||
if s in ("short_only", "short", "shortonly", "空", "只空", "仅空", "做空"):
|
||||
return "short_only"
|
||||
if s in ("both", "双向", "多空", "all"):
|
||||
return "both"
|
||||
return (raw or "").strip()
|
||||
|
||||
SENSITIVE_EXACT = frozenset({
|
||||
"APP_PASSWORD",
|
||||
"FLASK_SECRET_KEY",
|
||||
@@ -315,6 +333,14 @@ def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[d
|
||||
if val not in ("risk", "full_margin"):
|
||||
errors.append("计仓模式须为 risk(以损定仓)或 full_margin(全仓杠杆)")
|
||||
continue
|
||||
if key == "TRADE_DIRECTION":
|
||||
val = normalize_trade_direction(val)
|
||||
if val not in ("long_only", "short_only", "both"):
|
||||
errors.append("允许方向须为 long_only / short_only / both")
|
||||
continue
|
||||
if key == "TRADE_SYMBOL_RESTRICT_ENABLED":
|
||||
# 开启白名单但名单为空时,后面统一补默认
|
||||
pass
|
||||
if ftype == "bool":
|
||||
low = val.lower()
|
||||
if low not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
||||
@@ -334,6 +360,13 @@ def validate_env_updates(groups: list[dict], updates: dict[str, str]) -> tuple[d
|
||||
errors.append(f"{key} 须为整数")
|
||||
continue
|
||||
clean[key] = val
|
||||
# 开启币种白名单且名单为空时,补默认 BTC,ETH,避免「开了等于没开」
|
||||
if clean.get("TRADE_SYMBOL_RESTRICT_ENABLED") == "true":
|
||||
wl = (clean.get("TRADE_SYMBOL_WHITELIST") or "").strip()
|
||||
if not wl:
|
||||
# 若本次没提交白名单,仍允许用已有 env;这里只在明确提交空串时补默认
|
||||
if "TRADE_SYMBOL_WHITELIST" in (updates or {}):
|
||||
clean["TRADE_SYMBOL_WHITELIST"] = "BTC,ETH"
|
||||
return clean, errors
|
||||
|
||||
|
||||
|
||||
Vendored
+19
-4
@@ -12,6 +12,7 @@ from lib.env.env_schema import (
|
||||
_mask_value,
|
||||
_restart_required,
|
||||
normalize_position_sizing_mode,
|
||||
normalize_trade_direction,
|
||||
parse_env_example_schema,
|
||||
)
|
||||
|
||||
@@ -61,10 +62,10 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
|
||||
("FULL_MARGIN_BUFFER_RATIO", "全仓资金缓冲比例", "如 0.98"),
|
||||
("BTC_LEVERAGE", "BTC 默认杠杆", ""),
|
||||
("ALT_LEVERAGE", "山寨默认杠杆", ""),
|
||||
("TRADE_DIRECTION_RESTRICT_ENABLED", "方向限制开关", ""),
|
||||
("TRADE_DIRECTION", "允许方向", "long_only / short_only / both"),
|
||||
("TRADE_SYMBOL_RESTRICT_ENABLED", "币种白名单开关", ""),
|
||||
("TRADE_SYMBOL_WHITELIST", "白名单币种", "逗号分隔,如 BTC,ETH"),
|
||||
("TRADE_DIRECTION_RESTRICT_ENABLED", "方向限制开关", "关闭=双向均可;开启后按下方「允许方向」限制。保存后立即生效,一般无需重启。"),
|
||||
("TRADE_DIRECTION", "允许方向", "仅在方向限制开启时生效"),
|
||||
("TRADE_SYMBOL_RESTRICT_ENABLED", "币种白名单开关", "关闭=可手输任意币种;开启后仅白名单可选。保存后立即生效,一般无需重启。"),
|
||||
("TRADE_SYMBOL_WHITELIST", "白名单币种", "逗号分隔,如 BTC,ETH;开启白名单时勿留空"),
|
||||
("TRADING_DAY_RESET_HOUR", "交易日切点(北京时间)", "整点,默认 8"),
|
||||
("TRADING_DAY_RESET_OPEN_GUARD_ENABLED", "切点前禁止新开仓", ""),
|
||||
("MAX_ACTIVE_POSITIONS", "最大同时持仓", ""),
|
||||
@@ -163,6 +164,12 @@ _POSITION_SIZING_OPTIONS: list[dict[str, str]] = [
|
||||
{"value": "full_margin", "label": "全仓杠杆 (full_margin)"},
|
||||
]
|
||||
|
||||
_TRADE_DIRECTION_OPTIONS: list[dict[str, str]] = [
|
||||
{"value": "both", "label": "双向 (both)"},
|
||||
{"value": "long_only", "label": "仅做多 (long_only)"},
|
||||
{"value": "short_only", "label": "仅做空 (short_only)"},
|
||||
]
|
||||
|
||||
|
||||
def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str:
|
||||
if key in file_values:
|
||||
@@ -219,6 +226,14 @@ def _build_field(
|
||||
cur = "risk"
|
||||
out["current"] = cur
|
||||
out["default"] = cur
|
||||
if key == "TRADE_DIRECTION":
|
||||
out["type"] = "enum"
|
||||
out["options"] = list(_TRADE_DIRECTION_OPTIONS)
|
||||
cur = normalize_trade_direction(out["current"] or out["default"] or "both")
|
||||
if cur not in ("long_only", "short_only", "both"):
|
||||
cur = "both"
|
||||
out["current"] = cur
|
||||
out["default"] = cur
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ def register_instance_settings_routes(
|
||||
exchange_key: str,
|
||||
username: str,
|
||||
password: str,
|
||||
on_env_changed: Callable[[list[str]], None] | None = None,
|
||||
) -> None:
|
||||
env_path = os.path.join(base_dir, ".env")
|
||||
example_path = os.path.join(base_dir, ".env.example")
|
||||
@@ -107,6 +108,11 @@ def register_instance_settings_routes(
|
||||
changed = apply_env_updates(env_path, clean)
|
||||
groups = parse_env_example_schema(example_path)
|
||||
reload_info = apply_env_reload(env_path, get_db, changed, groups)
|
||||
if changed and callable(on_env_changed):
|
||||
try:
|
||||
on_env_changed(changed)
|
||||
except Exception as e:
|
||||
print(f"[instance_settings] on_env_changed: {e}", flush=True)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
|
||||
@@ -115,7 +115,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=16"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=17"></script>
|
||||
<script src="/static/instance_live.js?v=6"></script>
|
||||
<script src="/static/instance_embed.js?v=25"></script>
|
||||
</body>
|
||||
|
||||
@@ -2020,6 +2020,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=16"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=17"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user