feat: Refactor settings UI and fix commission fee recording

Settings improvements:
- Reorganize config groups with logical ordering (server, auth, ai, trading, etc.)
- Add description/tooltip for each config item with question mark icon
- Add icon to each group header
- Support i18n for descriptions (zh-CN, zh-TW, en-US)
- Move order execution config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS) to env

Commission fee fixes:
- Fix fee extraction in exchange clients: Bybit, Coinbase, Kraken, Gate, Kucoin, Bitfinex
- Properly accumulate and record commission fees in pending_order_worker
- Add fee/fee_ccy fields to wait_for_fill returns

Frontend updates:
- Remove order_mode config from trading-assistant frontend (now uses env config)
- Add sorted schema display by order field
- Add tooltip with description on hover
This commit is contained in:
TIANHE
2026-01-12 00:15:52 +08:00
parent e7cb9c6493
commit 5a4c770279
16 changed files with 1121 additions and 233 deletions
@@ -234,8 +234,11 @@ class BitfinexDerivativesClient(BitfinexClient):
last = last or []
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
status = ""
# best-effort parsing from array fields
# Bitfinex order response format: [ID, GID, CID, SYMBOL, MTS_CREATE, MTS_UPDATE, AMOUNT, AMOUNT_ORIG, TYPE, TYPE_PREV, MTS_TIF, _, FLAGS, STATUS, _, PRICE_AVG, ...]
try:
if isinstance(last, list) and len(last) >= 15:
status = str(last[13] or "")
@@ -245,12 +248,14 @@ class BitfinexDerivativesClient(BitfinexClient):
avg_price = float(last[14] or 0.0)
except Exception:
pass
# Note: Bitfinex order response doesn't include fee; fee is typically in trades.
# We return 0.0 here; actual fee can be fetched via trades endpoint if needed.
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if isinstance(status, str) and ("EXECUTED" in status.upper() or "CANCELED" in status.upper()):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -315,12 +315,22 @@ class BybitClient(BaseRestClient):
avg_price = float(last.get("avgPrice") or 0.0)
except Exception:
avg_price = 0.0
# Extract fee from cumExecFee (Bybit API field for cumulative execution fee)
fee = 0.0
fee_ccy = ""
try:
fee = abs(float(last.get("cumExecFee") or 0.0))
except Exception:
fee = 0.0
# Bybit linear contracts are settled in USDT
if fee > 0:
fee_ccy = "USDT"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("filled", "cancelled", "canceled", "rejected"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
def get_positions(self) -> Dict[str, Any]:
@@ -170,6 +170,8 @@ class CoinbaseExchangeClient(BaseRestClient):
status = str(last.get("status") or "")
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
try:
filled = float(last.get("filled_size") or 0.0)
except Exception:
@@ -180,12 +182,20 @@ class CoinbaseExchangeClient(BaseRestClient):
avg_price = executed_value / filled
except Exception:
avg_price = 0.0
# Extract fee from fill_fees (Coinbase API field)
try:
fee = abs(float(last.get("fill_fees") or 0.0))
except Exception:
fee = 0.0
# Coinbase fees are typically in the quote currency (e.g., USD)
if fee > 0:
fee_ccy = "USD"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("done", "rejected", "canceled", "cancelled"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -134,6 +134,8 @@ class GateSpotClient(_GateBase):
status = str(last.get("status") or "")
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
try:
filled = float(last.get("filled_amount") or 0.0)
except Exception:
@@ -144,12 +146,18 @@ class GateSpotClient(_GateBase):
avg_price = filled_total / filled
except Exception:
avg_price = 0.0
# Extract fee from Gate API
try:
fee = abs(float(last.get("fee") or 0.0))
except Exception:
fee = 0.0
fee_ccy = str(last.get("fee_currency") or "").strip()
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("closed", "cancelled", "canceled"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -321,6 +329,8 @@ class GateUsdtFuturesClient(_GateBase):
status = str(last.get("status") or "")
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
try:
# Gate futures often returns "filled_size" in contracts.
filled_ct = abs(float(last.get("filled_size") or last.get("filledSize") or 0.0))
@@ -331,12 +341,20 @@ class GateUsdtFuturesClient(_GateBase):
avg_price = float(last.get("fill_price") or last.get("fillPrice") or last.get("price") or 0.0)
except Exception:
avg_price = 0.0
# Extract fee from Gate Futures API
try:
fee = abs(float(last.get("fee") or 0.0))
except Exception:
fee = 0.0
# Gate USDT futures fees are in USDT
if fee > 0:
fee_ccy = "USDT"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if str(status).lower() in ("finished", "cancelled", "canceled"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -153,6 +153,8 @@ class KrakenClient(BaseRestClient):
status = str(last.get("status") or "")
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
try:
filled = float(last.get("vol_exec") or 0.0)
except Exception:
@@ -164,12 +166,20 @@ class KrakenClient(BaseRestClient):
avg_price = cost / filled
except Exception:
avg_price = 0.0
# Extract fee from Kraken API
try:
fee = abs(float(last.get("fee") or 0.0))
except Exception:
fee = 0.0
# Kraken fees are typically in the quote currency (e.g., USD)
if fee > 0:
fee_ccy = "USD"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("closed", "canceled", "cancelled", "expired"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -186,6 +186,8 @@ class KrakenFuturesClient(BaseRestClient):
status = str(last.get("status") or last.get("orderStatus") or "")
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
try:
filled = float(last.get("filledSize") or last.get("filled_size") or 0.0)
except Exception:
@@ -194,12 +196,20 @@ class KrakenFuturesClient(BaseRestClient):
avg_price = float(last.get("avgFillPrice") or last.get("avg_fill_price") or 0.0)
except Exception:
avg_price = 0.0
# Extract fee from Kraken Futures API (if available)
try:
fee = abs(float(last.get("fee") or 0.0))
except Exception:
fee = 0.0
# Kraken Futures fees are typically in USD
if fee > 0:
fee_ccy = "USD"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("filled", "cancelled", "canceled", "rejected"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -475,6 +475,8 @@ class KucoinFuturesClient(BaseRestClient):
status = str(od.get("status") or "")
filled = 0.0
avg_price = 0.0
fee = 0.0
fee_ccy = ""
try:
# dealSize is in contracts; convert back to base using multiplier best-effort.
deal_ct = float(od.get("dealSize") or 0.0)
@@ -497,12 +499,20 @@ class KucoinFuturesClient(BaseRestClient):
filled = abs(float(deal_ct or 0.0)) * float(mult)
if filled > 0 and deal_value > 0:
avg_price = float(deal_value) / float(filled)
# Extract fee from KuCoin Futures API (orderMargin contains fee info in some cases)
try:
fee = abs(float(od.get("fee") or od.get("orderFee") or 0.0))
except Exception:
fee = 0.0
# KuCoin Futures fees are typically in USDT
if fee > 0:
fee_ccy = "USDT"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("done", "canceled", "cancelled", "filled"):
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if time.time() >= end_ts:
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
time.sleep(float(poll_interval_sec or 0.5))
@@ -12,7 +12,7 @@ import json
import os
import threading
import time
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
from app.services.signal_notifier import SignalNotifier
from app.services.exchange_execution import load_strategy_configs, resolve_exchange_config, safe_exchange_config_for_log
@@ -722,12 +722,17 @@ class PendingOrderWorker:
_notify_live_best_effort(status="failed", error="spot_market_does_not_support_short_signals")
return
# Unified maker->market fallback settings (defaults: 10 seconds)
order_mode = str(payload.get("order_mode") or payload.get("orderMode") or "maker").strip().lower()
maker_wait_sec = float(payload.get("maker_wait_sec") or payload.get("makerWaitSec") or 10.0)
maker_offset_bps = float(payload.get("maker_offset_bps") or payload.get("makerOffsetBps") or 2.0)
# Unified maker->market fallback settings
# Priority: payload config > environment variable > default value
_default_order_mode = os.getenv("ORDER_MODE", "maker").strip().lower()
_default_maker_wait_sec = float(os.getenv("MAKER_WAIT_SEC", "10"))
_default_maker_offset_bps = float(os.getenv("MAKER_OFFSET_BPS", "2"))
order_mode = str(payload.get("order_mode") or payload.get("orderMode") or _default_order_mode).strip().lower()
maker_wait_sec = float(payload.get("maker_wait_sec") or payload.get("makerWaitSec") or _default_maker_wait_sec)
maker_offset_bps = float(payload.get("maker_offset_bps") or payload.get("makerOffsetBps") or _default_maker_offset_bps)
if maker_wait_sec <= 0:
maker_wait_sec = 10.0
maker_wait_sec = _default_maker_wait_sec if _default_maker_wait_sec > 0 else 10.0
if maker_offset_bps < 0:
maker_offset_bps = 0.0
maker_offset = maker_offset_bps / 10000.0
@@ -1070,18 +1075,22 @@ class PendingOrderWorker:
q = client.wait_for_fill(symbol=str(symbol), order_id=limit_order_id, client_order_id=limit_client_oid, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, CoinbaseExchangeClient):
q = client.wait_for_fill(order_id=limit_order_id, client_order_id=limit_client_oid, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, KrakenClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, KrakenFuturesClient):
q = client.wait_for_fill(order_id=limit_order_id, client_order_id=limit_client_oid, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, KucoinSpotClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
@@ -1091,22 +1100,27 @@ class PendingOrderWorker:
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, GateSpotClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, GateUsdtFuturesClient):
q = client.wait_for_fill(order_id=limit_order_id, contract=to_gate_currency_pair(str(symbol)), max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, BitfinexClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
elif isinstance(client, BitfinexDerivativesClient):
q = client.wait_for_fill(order_id=limit_order_id, max_wait_sec=maker_wait_sec)
phases["limit_query"] = q
_apply_fill(float(q.get("filled") or 0.0), float(q.get("avg_price") or 0.0))
_apply_fee(float(q.get("fee") or 0.0), str(q.get("fee_ccy") or ""))
remaining = max(0.0, float(amount or 0.0) - total_base)
@@ -1386,18 +1400,22 @@ class PendingOrderWorker:
q2 = client.wait_for_fill(symbol=str(symbol), order_id=market_order_id, client_order_id=market_client_oid, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, CoinbaseExchangeClient):
q2 = client.wait_for_fill(order_id=market_order_id, client_order_id=market_client_oid, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, KrakenClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, KrakenFuturesClient):
q2 = client.wait_for_fill(order_id=market_order_id, client_order_id=market_client_oid, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, KucoinSpotClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2
@@ -1407,22 +1425,27 @@ class PendingOrderWorker:
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, GateSpotClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, GateUsdtFuturesClient):
q2 = client.wait_for_fill(order_id=market_order_id, contract=to_gate_currency_pair(str(symbol)), max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, BitfinexClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
elif isinstance(client, BitfinexDerivativesClient):
q2 = client.wait_for_fill(order_id=market_order_id, max_wait_sec=3.0)
phases["market_query"] = q2
_apply_fill(float(q2.get("filled") or 0.0), float(q2.get("avg_price") or 0.0))
_apply_fee(float(q2.get("fee") or 0.0), str(q2.get("fee_ccy") or ""))
except LiveTradingError as e:
logger.warning(f"live market phase failed: pending_id={order_id}, strategy_id={strategy_id}, cfg={safe_cfg}, err={e}")
phases["market_error"] = str(e)
@@ -2221,8 +2221,11 @@ class TradingExecutor:
margin_mode: str = 'cross',
stop_loss_price: float = None,
take_profit_price: float = None,
order_mode: str = 'maker',
maker_wait_sec: float = 8.0,
# Order execution params (order_mode, maker_wait_sec, maker_offset_bps) are now
# configured via environment variables: ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS
# These parameters are kept for backward compatibility but will be ignored.
order_mode: str = None,
maker_wait_sec: float = None,
maker_retries: int = 3,
close_fallback_to_market: bool = True,
open_fallback_to_market: bool = True,
@@ -2236,6 +2239,9 @@ class TradingExecutor:
A separate worker will poll `pending_orders` and dispatch:
- execution_mode='signal': dispatch notifications (no real trading).
- execution_mode='live': reserved for future live trading execution (not implemented).
Note: Order execution settings (order_mode, maker_wait_sec, maker_offset_bps) are now
configured via environment variables and not passed from strategy config.
"""
try:
# Reference price at enqueue time: use current tick price if provided to avoid extra fetch.
@@ -2249,8 +2255,7 @@ class TradingExecutor:
"stop_loss_price": float(stop_loss_price or 0.0) if stop_loss_price is not None else 0.0,
"take_profit_price": float(take_profit_price or 0.0) if take_profit_price is not None else 0.0,
"margin_mode": str(margin_mode or "cross"),
"order_mode": str(order_mode or "maker"),
"maker_wait_sec": float(maker_wait_sec or 0.0),
# Order execution params moved to env config (ORDER_MODE, MAKER_WAIT_SEC, MAKER_OFFSET_BPS)
"maker_retries": int(maker_retries or 0),
"close_fallback_to_market": bool(close_fallback_to_market),
"open_fallback_to_market": bool(open_fallback_to_market),