Enable options by default and allow full sim trading without OKX API keys.

Relax hedge start gates in sim, route perp open/close through sim broker, and mount strategy templates for records pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 09:26:38 +08:00
parent d4b970cfd5
commit b4d62ea0a6
8 changed files with 445 additions and 352 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ OKX_SHOW_PERP_FUNDS=true
# 期权模块(与上方 OKX_API_* 同源;修改启用开关后须重启 PM2) # 期权模块(与上方 OKX_API_* 同源;修改启用开关后须重启 PM2)
# 详见 docs/期权方案.md 与 docs/期权用法.md # 详见 docs/期权方案.md 与 docs/期权用法.md
# ============================================================================= # =============================================================================
OKX_OPTIONS_ENABLED=false OKX_OPTIONS_ENABLED=true
# 以下 OKX_OPTIONS_API_* 已废弃,请勿再配置(仅兼容旧部署回填) # 以下 OKX_OPTIONS_API_* 已废弃,请勿再配置(仅兼容旧部署回填)
# OKX_OPTIONS_API_KEY= # OKX_OPTIONS_API_KEY=
# OKX_OPTIONS_API_SECRET= # OKX_OPTIONS_API_SECRET=
+20 -5
View File
@@ -305,8 +305,21 @@ def resolve_path(path_value):
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "coinox_2026_secret_key") app.secret_key = os.getenv("FLASK_SECRET_KEY", "coinox_2026_secret_key")
from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled
from jinja2 import ChoiceLoader, FileSystemLoader
from lib.paths import strategy_templates_dir
attach_embed_templates(app, _REPO_ROOT) attach_embed_templates(app, _REPO_ROOT)
# 复盘页引用 strategy 模板字段;CoinOX 不装策略交易,仍挂载模板目录
_strategy_tpl = strategy_templates_dir(_REPO_ROOT)
if os.path.isdir(_strategy_tpl):
_existing = app.jinja_loader
_loaders = [FileSystemLoader(_strategy_tpl)]
if _existing is not None:
if isinstance(_existing, ChoiceLoader):
_loaders = list(_existing.loaders) + _loaders
else:
_loaders.insert(0, _existing)
app.jinja_loader = ChoiceLoader(_loaders)
# ====================== 登录配置 ====================== # ====================== 登录配置 ======================
USERNAME = os.getenv("APP_USERNAME", "dekun") USERNAME = os.getenv("APP_USERNAME", "dekun")
@@ -368,7 +381,8 @@ _promote_legacy_options_api_keys()
OKX_API_KEY = normalize_api_credential(os.getenv("OKX_API_KEY")) OKX_API_KEY = normalize_api_credential(os.getenv("OKX_API_KEY"))
OKX_API_SECRET = normalize_api_credential(os.getenv("OKX_API_SECRET")) OKX_API_SECRET = normalize_api_credential(os.getenv("OKX_API_SECRET"))
OKX_API_PASSPHRASE = normalize_api_credential(os.getenv("OKX_API_PASSPHRASE")) OKX_API_PASSPHRASE = normalize_api_credential(os.getenv("OKX_API_PASSPHRASE"))
OKX_OPTIONS_ENABLED = os.getenv("OKX_OPTIONS_ENABLED", "false").lower() in ("1", "true", "yes", "on") # CoinOX 默认开启期权;模拟盘可不配 OKX API
OKX_OPTIONS_ENABLED = os.getenv("OKX_OPTIONS_ENABLED", "true").lower() in ("1", "true", "yes", "on")
OKX_OPTIONS_TRADE_BUDGET_USDC = float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC", "10")) OKX_OPTIONS_TRADE_BUDGET_USDC = float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC", "10"))
OKX_OPTIONS_DEFAULT_UNDERLY = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() OKX_OPTIONS_DEFAULT_UNDERLY = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper()
OKX_TD_MODE = os.getenv("OKX_TD_MODE", "cross") OKX_TD_MODE = os.getenv("OKX_TD_MODE", "cross")
@@ -9911,11 +9925,12 @@ install_hedge_plan(app, _REPO_ROOT, app_module=sys.modules[__name__])
try: try:
from lib.sim.hooks import patch_options_cfg from lib.sim.hooks import patch_options_cfg
_opt_cfg = app.extensions.get("options_cfg") for _cfg_key in ("options_cfg", "hedge_plan_cfg"):
if isinstance(_opt_cfg, dict): _cfg = app.extensions.get(_cfg_key)
patch_options_cfg(_opt_cfg) if isinstance(_cfg, dict):
patch_options_cfg(_cfg)
except Exception as _sim_patch_err: except Exception as _sim_patch_err:
print(f"[sim] patch options_cfg: {_sim_patch_err}", flush=True) print(f"[sim] patch options/hedge cfg: {_sim_patch_err}", flush=True)
_purge_key_monitors_if_full_margin() _purge_key_monitors_if_full_margin()
+6 -3
View File
@@ -716,6 +716,7 @@ def gate_status(
show_options_options: bool = True, show_options_options: bool = True,
mutual_exclusive: bool = True, mutual_exclusive: bool = True,
has_standalone_option: bool = False, has_standalone_option: bool = False,
sim_mode: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
from lib.trade.position_sizing_lib import is_full_margin_mode from lib.trade.position_sizing_lib import is_full_margin_mode
@@ -739,7 +740,8 @@ def gate_status(
can_preview = False can_preview = False
can_start = False can_start = False
reasons.append("期期对冲已隐藏(HEDGE_PLAN_SHOW_OPTIONS_OPTIONS)") reasons.append("期期对冲已隐藏(HEDGE_PLAN_SHOW_OPTIONS_OPTIONS)")
if not live_order: # 模拟盘用本地钱包撮合,不要求实盘开关 / API
if not sim_mode and not live_order:
can_start = False can_start = False
reasons.append("未允许对冲真实下单(HEDGE_PLAN_LIVE_ORDER)") reasons.append("未允许对冲真实下单(HEDGE_PLAN_LIVE_ORDER)")
if active_count >= max(1, int(max_active or 1)): if active_count >= max(1, int(max_active or 1)):
@@ -749,10 +751,10 @@ def gate_status(
can_start = False can_start = False
reasons.append("存在单独期权持仓,禁止启动对冲计划(互斥门控)") reasons.append("存在单独期权持仓,禁止启动对冲计划(互斥门控)")
if pt == "perp_options": if pt == "perp_options":
if not full: if not full and not sim_mode:
can_start = False can_start = False
reasons.append("永期开仓仅全仓模式可用(当前可测算)") reasons.append("永期开仓仅全仓模式可用(当前可测算)")
if not live_trading: if not sim_mode and not live_trading:
can_start = False can_start = False
reasons.append("未开启实盘(LIVE_TRADING_ENABLED)") reasons.append("未开启实盘(LIVE_TRADING_ENABLED)")
elif pt == "options_options": elif pt == "options_options":
@@ -770,6 +772,7 @@ def gate_status(
"plan_type": pt, "plan_type": pt,
"live_order": live_order, "live_order": live_order,
"live_trading": live_trading, "live_trading": live_trading,
"sim_mode": bool(sim_mode),
"active_count": active_count, "active_count": active_count,
"max_active": max_active, "max_active": max_active,
"show_perp_options": bool(show_perp_options), "show_perp_options": bool(show_perp_options),
+11 -1
View File
@@ -93,7 +93,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
"alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5), "alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5),
"full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98), "full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98),
"funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2), "funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2),
"options_enabled": _env_bool("OKX_OPTIONS_ENABLED", False), "options_enabled": _env_bool("OKX_OPTIONS_ENABLED", True),
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(), "default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
"chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"), "chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(), "perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
@@ -195,6 +195,15 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
# fail-closed:探测失败视为不可开仓 # fail-closed:探测失败视为不可开仓
active = 10**9 active = 10**9
has_standalone = True has_standalone = True
sim_mode = False
try:
from lib.sim.mode_lib import is_sim_mode
get_db = cfg.get("get_db")
if callable(get_db):
sim_mode = bool(is_sim_mode(get_db))
except Exception:
sim_mode = False
return gate_status( return gate_status(
hedge_enabled=_hedge_enabled(), hedge_enabled=_hedge_enabled(),
sizing_mode=load_position_sizing_mode(), sizing_mode=load_position_sizing_mode(),
@@ -208,6 +217,7 @@ def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
show_options_options=_show_options_options(), show_options_options=_show_options_options(),
mutual_exclusive=mutual, mutual_exclusive=mutual,
has_standalone_option=has_standalone, has_standalone_option=has_standalone,
sim_mode=sim_mode,
) )
+2 -2
View File
@@ -160,7 +160,7 @@ def build_instance_settings_view(
} }
) )
if (exchange_key or "").strip().lower() == "okx" and _env_bool("OKX_OPTIONS_ENABLED", False): if (exchange_key or "").strip().lower() == "okx" and _env_bool("OKX_OPTIONS_ENABLED", True):
api_key = (os.getenv("OKX_API_KEY") or "").strip() api_key = (os.getenv("OKX_API_KEY") or "").strip()
sections.append( sections.append(
{ {
@@ -194,7 +194,7 @@ def build_instance_settings_view(
"data_export_version": int(data_export_version), "data_export_version": int(data_export_version),
"show_transfer": (exchange_key or "").strip().lower() in ("gate", "binance", "okx"), "show_transfer": (exchange_key or "").strip().lower() in ("gate", "binance", "okx"),
"options_settings_enabled": (exchange_key or "").strip().lower() == "okx" "options_settings_enabled": (exchange_key or "").strip().lower() == "okx"
and _env_bool("OKX_OPTIONS_ENABLED", False), and _env_bool("OKX_OPTIONS_ENABLED", True),
"auto_transfer_enabled": auto_transfer_on, "auto_transfer_enabled": auto_transfer_on,
"auto_transfer_bj_hour": _env_int("AUTO_TRANSFER_BJ_HOUR", 8), "auto_transfer_bj_hour": _env_int("AUTO_TRANSFER_BJ_HOUR", 8),
"auto_transfer_amount": _env_float("AUTO_TRANSFER_AMOUNT", 30), "auto_transfer_amount": _env_float("AUTO_TRANSFER_AMOUNT", 30),
+2 -2
View File
@@ -54,7 +54,7 @@ def attach_options_templates(app: Flask, repo_root: str) -> None:
def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None: def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None:
enabled = _env_bool("OKX_OPTIONS_ENABLED", False) enabled = _env_bool("OKX_OPTIONS_ENABLED", True)
attach_options_templates(app, repo_root) attach_options_templates(app, repo_root)
cfg = _build_cfg(app_module) cfg = _build_cfg(app_module)
app.extensions["options_cfg"] = cfg app.extensions["options_cfg"] = cfg
@@ -95,7 +95,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
) )
return { return {
"enabled": _env_bool("OKX_OPTIONS_ENABLED", False), "enabled": _env_bool("OKX_OPTIONS_ENABLED", True),
"get_db": app_module.get_db, "get_db": app_module.get_db,
"login_required": app_module.login_required, "login_required": app_module.login_required,
"exchange_options": getattr(app_module, "exchange_options", None), "exchange_options": getattr(app_module, "exchange_options", None),
+63
View File
@@ -74,6 +74,69 @@ def apply_sim_hooks(app_module: Any) -> None:
app_module.get_available_trading_usdt = get_available_trading_usdt app_module.get_available_trading_usdt = get_available_trading_usdt
app_module.get_live_position_contracts = get_live_position_contracts app_module.get_live_position_contracts = get_live_position_contracts
_orig_place = getattr(app_module, "place_exchange_order", None)
_orig_close = getattr(app_module, "close_exchange_order", None)
if callable(_orig_place):
def place_exchange_order(
exchange_symbol,
direction,
amount,
leverage,
stop_loss=None,
take_profit=None,
):
if is_sim_mode(app_module.get_db):
ex = getattr(app_module, "exchange", None)
if ex is None:
raise RuntimeError("sim: 无公开行情 exchange")
ensure = getattr(app_module, "ensure_markets_loaded", None)
if callable(ensure):
try:
ensure()
except Exception:
pass
return broker().place_perp_market(
ex,
symbol=exchange_symbol,
direction=direction,
contracts=float(amount),
leverage=int(leverage or 1),
stop_loss=stop_loss,
take_profit=take_profit,
)
return _orig_place(
exchange_symbol,
direction,
amount,
leverage,
stop_loss=stop_loss,
take_profit=take_profit,
)
app_module.place_exchange_order = place_exchange_order
if callable(_orig_close):
def close_exchange_order(order_row):
if is_sim_mode(app_module.get_db):
ex = getattr(app_module, "exchange", None)
if ex is None:
raise RuntimeError("sim: 无公开行情 exchange")
row = order_row or {}
symbol = row.get("exchange_symbol") or row.get("symbol")
direction = row.get("direction")
contracts = float(row.get("order_amount") or 0)
return broker().close_perp_market(
ex,
symbol=str(symbol or ""),
direction=str(direction or ""),
contracts=contracts if contracts > 0 else None,
)
return _orig_close(order_row)
app_module.close_exchange_order = close_exchange_order
_orig_xfer = getattr(app_module, "execute_transfer_usdt", None) _orig_xfer = getattr(app_module, "execute_transfer_usdt", None)
if callable(_orig_xfer): if callable(_orig_xfer):
+2
View File
@@ -64,6 +64,8 @@ def main() -> int:
updates["OKX_OPTIONS_MARGIN_MODE"] = "coin" updates["OKX_OPTIONS_MARGIN_MODE"] = "coin"
if not (env_get(lines, "EXCHANGE_DISPLAY_NAME") or "").strip(): if not (env_get(lines, "EXCHANGE_DISPLAY_NAME") or "").strip():
updates["EXCHANGE_DISPLAY_NAME"] = "CoinOX" updates["EXCHANGE_DISPLAY_NAME"] = "CoinOX"
if not (env_get(lines, "OKX_OPTIONS_ENABLED") or "").strip():
updates["OKX_OPTIONS_ENABLED"] = "true"
live = (env_get(lines, "LIVE_TRADING_ENABLED") or "").strip().lower() live = (env_get(lines, "LIVE_TRADING_ENABLED") or "").strip().lower()
if live in ("",): if live in ("",):
updates["LIVE_TRADING_ENABLED"] = "false" updates["LIVE_TRADING_ENABLED"] = "false"