Skip to content

Commit 04e1e44

Browse files
committed
v4.0.7
Signed-off-by: TIANHE <[email protected]>
1 parent e34de3f commit 04e1e44

10 files changed

Lines changed: 208 additions & 13 deletions

File tree

backend_api_python/app/services/backtest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2593,7 +2593,7 @@ def _execute_indicator(self, code: str, df: pd.DataFrame, backtest_params: dict
25932593
# Supported indicator signal formats:
25942594
# - Preferred (simple): df['buy'], df['sell'] as boolean
25952595
# - Backtest/internal (4-way): df['open_long'], df['close_long'], df['open_short'], df['close_short'] as boolean
2596-
signals = pd.Series(0, index=df.index)
2596+
signals = {}
25972597

25982598
try:
25992599
# Reset DatetimeIndex to integer so user code can use df.at[0, ...] or df.iloc[0, ...]
@@ -2717,6 +2717,7 @@ def _execute_indicator(self, code: str, df: pd.DataFrame, backtest_params: dict
27172717
except Exception as e:
27182718
logger.error(f"Indicator code execution error: {e}")
27192719
logger.error(traceback.format_exc())
2720+
raise
27202721

27212722
return signals
27222723

backend_api_python/app/services/live_trading/binance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def get_fee_for_order(self, *, symbol: str, order_id: str, max_retries: int = 3)
475475
return 0.0, ""
476476

477477
def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[str, float]]:
478-
sym = symbol.upper().replace("-", "").replace("/", "")
478+
sym = to_binance_futures_symbol(symbol)
479479
try:
480480
data = self._signed_request("GET", "/fapi/v1/commissionRate", params={"symbol": sym})
481481
if isinstance(data, dict):
@@ -484,7 +484,7 @@ def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[
484484
if maker > 0 or taker > 0:
485485
return {"maker": maker, "taker": taker}
486486
except Exception as e:
487-
logger.warning(f"Binance get_fee_rate({symbol}) failed: {e}")
487+
logger.warning(f"Binance get_fee_rate({symbol}, symbol_param={sym}) failed: {e}")
488488
return None
489489

490490
def set_leverage(self, *, symbol: str, leverage: float) -> Dict[str, Any]:

backend_api_python/app/services/live_trading/binance_spot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ def get_fee_for_order(self, *, symbol: str, order_id: str, max_retries: int = 3)
685685
return 0.0, ""
686686

687687
def get_fee_rate(self, symbol: str, market_type: str = "spot") -> Optional[Dict[str, float]]:
688-
sym = symbol.upper().replace("-", "").replace("/", "")
688+
sym = to_binance_futures_symbol(symbol)
689689
try:
690690
data = self._signed_request("GET", "/sapi/v1/asset/tradeFee", params={"symbol": sym})
691691
if isinstance(data, list) and data and isinstance(data[0], dict):
@@ -695,7 +695,7 @@ def get_fee_rate(self, symbol: str, market_type: str = "spot") -> Optional[Dict[
695695
if maker > 0 or taker > 0:
696696
return {"maker": maker, "taker": taker}
697697
except Exception as e:
698-
logger.warning(f"BinanceSpot get_fee_rate({symbol}) failed: {e}")
698+
logger.warning(f"BinanceSpot get_fee_rate({symbol}, symbol_param={sym}) failed: {e}")
699699
return None
700700

701701
def cancel_order(self, *, symbol: str, order_id: str = "", client_order_id: str = "") -> Dict[str, Any]:

backend_api_python/app/services/live_trading/bitget.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -649,18 +649,21 @@ def get_positions(self, *, product_type: str = "USDT-FUTURES", symbol: str = "")
649649
return out
650650

651651
def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[str, float]]:
652-
sym = to_bitget_um_symbol(symbol) if market_type != "spot" else symbol.upper().replace("/", "")
653-
product_type = "USDT-FUTURES" if market_type != "spot" else "SPOT"
652+
mt = str(market_type or "swap").strip().lower()
653+
if mt in ("futures", "future", "perp", "perpetual"):
654+
mt = "swap"
655+
sym = to_bitget_um_symbol(symbol)
656+
business_type = "spot" if mt == "spot" else "mix"
654657
try:
655-
raw = self._signed_request("GET", "/api/v2/common/trade-rate", params={"symbol": sym, "businessType": product_type})
658+
raw = self._signed_request("GET", "/api/v2/common/trade-rate", params={"symbol": sym, "businessType": business_type})
656659
data = raw.get("data") if isinstance(raw, dict) else None
657660
if isinstance(data, dict):
658661
maker = abs(float(data.get("makerFeeRate") or 0))
659662
taker = abs(float(data.get("takerFeeRate") or 0))
660663
if maker > 0 or taker > 0:
661664
return {"maker": maker, "taker": taker}
662665
except Exception as e:
663-
logger.warning(f"Bitget get_fee_rate({symbol}) failed: {e}")
666+
logger.warning(f"Bitget get_fee_rate({symbol}, businessType={business_type}, symbol_param={sym}) failed: {e}")
664667
return None
665668

666669
def set_leverage(

backend_api_python/app/services/live_trading/bitget_spot.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,24 @@ def _public_request(self, method: str, path: str, *, params: Optional[Dict[str,
263263
raise LiveTradingError(f"BitgetSpot error: {data}")
264264
return data if isinstance(data, dict) else {"raw": data}
265265

266+
def get_fee_rate(self, symbol: str, market_type: str = "spot") -> Optional[Dict[str, float]]:
267+
sym = to_bitget_um_symbol(symbol)
268+
try:
269+
raw = self._signed_request(
270+
"GET",
271+
"/api/v2/common/trade-rate",
272+
params={"symbol": sym, "businessType": "spot"},
273+
)
274+
data = raw.get("data") if isinstance(raw, dict) else None
275+
if isinstance(data, dict):
276+
maker = abs(float(data.get("makerFeeRate") or 0))
277+
taker = abs(float(data.get("takerFeeRate") or 0))
278+
if maker > 0 or taker > 0:
279+
return {"maker": maker, "taker": taker}
280+
except Exception as e:
281+
logger.warning(f"BitgetSpot get_fee_rate({symbol}, businessType=spot, symbol_param={sym}) failed: {e}")
282+
return None
283+
266284
def get_symbol_meta(self, *, symbol: str) -> Dict[str, Any]:
267285
"""
268286
Fetch spot symbol metadata (best-effort).

backend_api_python/app/services/live_trading/okx.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,11 @@ def get_balance(self) -> Dict[str, Any]:
426426
return self._signed_request("GET", "/api/v5/account/balance")
427427

428428
def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[str, float]]:
429-
inst_type = "SPOT" if market_type == "spot" else "SWAP"
430-
inst_id = symbol.upper()
429+
mt = str(market_type or "swap").strip().lower()
430+
if mt in ("futures", "future", "perp", "perpetual"):
431+
mt = "swap"
432+
inst_type = "SPOT" if mt == "spot" else "SWAP"
433+
inst_id = to_okx_spot_inst_id(symbol) if mt == "spot" else to_okx_swap_inst_id(symbol)
431434
try:
432435
raw = self._signed_request("GET", "/api/v5/account/trade-fee", params={"instType": inst_type, "instId": inst_id})
433436
data = (raw.get("data") or []) if isinstance(raw, dict) else []
@@ -438,7 +441,7 @@ def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[
438441
if maker > 0 or taker > 0:
439442
return {"maker": maker, "taker": taker}
440443
except Exception as e:
441-
logger.warning(f"OKX get_fee_rate({symbol}) failed: {e}")
444+
logger.warning(f"OKX get_fee_rate({symbol}, market_type={mt}, inst_id={inst_id}) failed: {e}")
442445
return None
443446

444447
def get_positions(self, *, inst_id: str = "", inst_type: str = "SWAP") -> Dict[str, Any]:

backend_api_python/app/services/trading_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1957,7 +1957,7 @@ def _is_fatal_error(err: Exception, msg: str) -> bool:
19571957
self._log_crypto_kline_source(
19581958
strategy_id, market_category, execution_mode, kline_exchange_id, kline_market_type
19591959
)
1960-
if exchange_config and exchange_config.get('api_key') or exchange_config.get('apiKey'):
1960+
if exchange_config and (exchange_config.get('api_key') or exchange_config.get('apiKey')):
19611961
try:
19621962
self._query_exchange_fee_rate(strategy_id, exchange_config, symbol, market_type)
19631963
except Exception as e:

backend_api_python/tests/test_backtest_four_way_signals.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import pandas as pd
6+
import pytest
67

78
from app.services.backtest import BacktestService
89
from app.services.builtin_indicators import _builtin_specs
@@ -65,3 +66,14 @@ def test_builtin_indicator_sample_executes_with_four_way_contract():
6566
for col in ("open_long", "close_long", "open_short", "close_short"):
6667
assert col in out
6768
assert len(out[col]) == len(df)
69+
70+
71+
def test_execute_indicator_missing_signal_columns_raises_clear_error():
72+
svc = BacktestService()
73+
df = _sample_df()
74+
code = """
75+
df = df.copy()
76+
df['some_plot_only_value'] = close.rolling(3).mean()
77+
"""
78+
with pytest.raises(ValueError, match="Indicator must define either 4-way columns"):
79+
svc._execute_indicator(code, df, backtest_params={})
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from __future__ import annotations
2+
3+
from app.services.live_trading.binance import BinanceFuturesClient
4+
from app.services.live_trading.binance_spot import BinanceSpotClient
5+
from app.services.live_trading.bitget import BitgetMixClient
6+
from app.services.live_trading.bitget_spot import BitgetSpotClient
7+
from app.services.live_trading.bybit import BybitClient
8+
from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient
9+
from app.services.live_trading.okx import OkxClient
10+
11+
12+
def _fake_client(cls, response):
13+
client = object.__new__(cls)
14+
client.seen = []
15+
16+
def fake_signed_request(method, path, params=None, **kwargs):
17+
client.seen.append({"method": method, "path": path, "params": dict(params or {})})
18+
return response
19+
20+
client._signed_request = fake_signed_request
21+
return client
22+
23+
24+
def test_binance_futures_fee_rate_uses_concatenated_symbol():
25+
client = _fake_client(
26+
BinanceFuturesClient,
27+
{"makerCommissionRate": "0.0002", "takerCommissionRate": "0.0005"},
28+
)
29+
30+
result = client.get_fee_rate("BTC/USDT:USDT")
31+
32+
assert result == {"maker": 0.0002, "taker": 0.0005}
33+
assert client.seen[-1]["path"] == "/fapi/v1/commissionRate"
34+
assert client.seen[-1]["params"] == {"symbol": "BTCUSDT"}
35+
36+
37+
def test_binance_spot_fee_rate_uses_concatenated_symbol():
38+
client = _fake_client(
39+
BinanceSpotClient,
40+
[{"makerCommission": "0.001", "takerCommission": "0.001"}],
41+
)
42+
43+
result = client.get_fee_rate("ETH/USDT:USDT")
44+
45+
assert result == {"maker": 0.001, "taker": 0.001}
46+
assert client.seen[-1]["path"] == "/sapi/v1/asset/tradeFee"
47+
assert client.seen[-1]["params"] == {"symbol": "ETHUSDT"}
48+
49+
50+
def test_bitget_mix_fee_rate_uses_official_business_type_mix():
51+
client = _fake_client(
52+
BitgetMixClient,
53+
{"data": {"makerFeeRate": "0.0002", "takerFeeRate": "0.0006"}},
54+
)
55+
56+
result = client.get_fee_rate("SOL/USDT:USDT", market_type="swap")
57+
58+
assert result == {"maker": 0.0002, "taker": 0.0006}
59+
assert client.seen[-1]["path"] == "/api/v2/common/trade-rate"
60+
assert client.seen[-1]["params"] == {"symbol": "SOLUSDT", "businessType": "mix"}
61+
62+
63+
def test_bitget_spot_fee_rate_uses_official_business_type_spot():
64+
client = _fake_client(
65+
BitgetSpotClient,
66+
{"data": {"makerFeeRate": "0.001", "takerFeeRate": "0.001"}},
67+
)
68+
69+
result = client.get_fee_rate("SOL/USDT:USDT", market_type="spot")
70+
71+
assert result == {"maker": 0.001, "taker": 0.001}
72+
assert client.seen[-1]["path"] == "/api/v2/common/trade-rate"
73+
assert client.seen[-1]["params"] == {"symbol": "SOLUSDT", "businessType": "spot"}
74+
75+
76+
def test_bybit_fee_rate_uses_category_and_concatenated_symbol():
77+
client = _fake_client(
78+
BybitClient,
79+
{"result": {"list": [{"makerFeeRate": "0.0001", "takerFeeRate": "0.00055"}]}},
80+
)
81+
82+
result = client.get_fee_rate("BTC/USDT:USDT", market_type="swap")
83+
84+
assert result == {"maker": 0.0001, "taker": 0.00055}
85+
assert client.seen[-1]["path"] == "/v5/account/fee-rate"
86+
assert client.seen[-1]["params"] == {"category": "linear", "symbol": "BTCUSDT"}
87+
88+
89+
def test_gate_spot_fee_rate_uses_currency_pair():
90+
client = _fake_client(
91+
GateSpotClient,
92+
{"maker_fee_rate": "0.001", "taker_fee_rate": "0.001"},
93+
)
94+
95+
result = client.get_fee_rate("BTC/USDT:USDT", market_type="spot")
96+
97+
assert result == {"maker": 0.001, "taker": 0.001}
98+
assert client.seen[-1]["path"] == "/api/v4/wallet/fee"
99+
assert client.seen[-1]["params"] == {"currency_pair": "BTC_USDT"}
100+
101+
102+
def test_gate_futures_fee_rate_uses_contract_path():
103+
client = _fake_client(
104+
GateUsdtFuturesClient,
105+
{"maker_fee_rate": "0.0002", "taker_fee_rate": "0.0005"},
106+
)
107+
108+
result = client.get_fee_rate("BTC/USDT:USDT", market_type="swap")
109+
110+
assert result == {"maker": 0.0002, "taker": 0.0005}
111+
assert client.seen[-1]["path"] == "/api/v4/futures/usdt/contracts/BTC_USDT"
112+
assert client.seen[-1]["params"] == {}
113+
114+
115+
def test_okx_fee_rate_uses_inst_type_and_inst_id():
116+
client = _fake_client(
117+
OkxClient,
118+
{"data": [{"maker": "-0.0002", "taker": "-0.0005"}]},
119+
)
120+
121+
result = client.get_fee_rate("SOL/USDT:USDT", market_type="swap")
122+
123+
assert result == {"maker": 0.0002, "taker": 0.0005}
124+
assert client.seen[-1]["path"] == "/api/v5/account/trade-fee"
125+
assert client.seen[-1]["params"] == {"instType": "SWAP", "instId": "SOL-USDT-SWAP"}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
from app.services.live_trading.okx import OkxClient
4+
5+
6+
def _fake_okx_client() -> OkxClient:
7+
client = object.__new__(OkxClient)
8+
client.seen_params = []
9+
10+
def fake_signed_request(method, path, params=None, body=None):
11+
client.seen_params.append({"method": method, "path": path, "params": dict(params or {})})
12+
return {"data": [{"maker": "-0.0002", "taker": "-0.0005"}]}
13+
14+
client._signed_request = fake_signed_request
15+
return client
16+
17+
18+
def test_okx_fee_rate_uses_swap_inst_id():
19+
client = _fake_okx_client()
20+
21+
result = client.get_fee_rate("SOL/USDT", market_type="swap")
22+
23+
assert result == {"maker": 0.0002, "taker": 0.0005}
24+
assert client.seen_params[-1]["params"] == {"instType": "SWAP", "instId": "SOL-USDT-SWAP"}
25+
26+
27+
def test_okx_fee_rate_uses_spot_inst_id():
28+
client = _fake_okx_client()
29+
30+
result = client.get_fee_rate("SOL/USDT", market_type="spot")
31+
32+
assert result == {"maker": 0.0002, "taker": 0.0005}
33+
assert client.seen_params[-1]["params"] == {"instType": "SPOT", "instId": "SOL-USDT"}

0 commit comments

Comments
 (0)