Skip to content

Commit a511844

Browse files
committed
feat: Add cross-sectional strategy support
- Add cross-sectional strategy type (single vs cross-sectional) - Support multi-symbol portfolio management with automatic ranking - Add portfolio size, long ratio, and rebalance frequency configuration - Implement parallel order execution for cross-sectional strategies - Add frontend UI for strategy type selection and configuration - Add i18n support (Chinese and English) for cross-sectional features - Fix decimal precision issues in exchange order quantities - Add last_rebalance_at field to database schema - Add comprehensive documentation and examples Database migration required: Add last_rebalance_at column to qd_strategies_trading table
1 parent a89cc9b commit a511844

File tree

19 files changed

+1517
-36
lines changed

19 files changed

+1517
-36
lines changed

backend_api_python/app/routes/strategy.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,11 @@ def _calc_pnl_percent(entry_price: float, size: float, pnl: float) -> float:
427427
pct = _calc_pnl_percent(entry, size, pnl)
428428

429429
rr = dict(r)
430+
# 确保 entry_price 有值(如果数据库中是 NULL,使用计算出的 entry 值)
431+
if not rr.get("entry_price") or float(rr.get("entry_price") or 0.0) <= 0:
432+
rr["entry_price"] = float(entry or 0.0)
433+
else:
434+
rr["entry_price"] = float(rr.get("entry_price") or 0.0)
430435
rr["current_price"] = float(cp or 0.0)
431436
rr["unrealized_pnl"] = float(pnl)
432437
rr["pnl_percent"] = float(pct)

backend_api_python/app/services/live_trading/binance.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,51 @@ def _to_dec(x: Any) -> Decimal:
4747
return Decimal("0")
4848

4949
@staticmethod
50-
def _dec_str(d: Decimal) -> str:
50+
def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
51+
"""
52+
Convert Decimal to string with controlled precision.
53+
Binance requires quantities/prices to match LOT_SIZE/PRICE_FILTER precision.
54+
This method ensures the output string doesn't exceed the required precision.
55+
"""
5156
try:
52-
return format(d, "f")
57+
if d == 0:
58+
return "0"
59+
# Normalize to remove unnecessary trailing zeros from internal representation
60+
normalized = d.normalize()
61+
# Convert to string using fixed-point notation
62+
# Use a reasonable max_decimals to avoid excessive precision
63+
# Binance typically uses 8 decimal places for most symbols
64+
s = format(normalized, f".{max_decimals}f")
65+
# Remove trailing zeros and decimal point if not needed
66+
# This ensures we don't send "0.02874400" when "0.028744" is sufficient
67+
if '.' in s:
68+
s = s.rstrip('0').rstrip('.')
69+
return s if s else "0"
5370
except Exception:
54-
return str(d)
71+
# Fallback: try to convert safely
72+
try:
73+
# If Decimal conversion fails, try float with limited precision
74+
f = float(d)
75+
if f == 0:
76+
return "0"
77+
# Format with max_decimals and remove trailing zeros
78+
s = format(f, f".{max_decimals}f")
79+
if '.' in s:
80+
s = s.rstrip('0').rstrip('.')
81+
return s if s else "0"
82+
except Exception:
83+
# Last resort: convert to string
84+
s = str(d)
85+
# Try to remove scientific notation if present
86+
if 'e' in s.lower() or 'E' in s:
87+
try:
88+
f = float(s)
89+
s = format(f, f".{max_decimals}f")
90+
if '.' in s:
91+
s = s.rstrip('0').rstrip('.')
92+
except Exception:
93+
pass
94+
return s if s else "0"
5595

5696
@staticmethod
5797
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:
@@ -237,12 +277,34 @@ def _normalize_quantity(self, *, symbol: str, quantity: float, for_market: bool)
237277

238278
if step > 0:
239279
q = self._floor_to_step(q, step)
280+
240281
# Enforce quantity precision cap (Binance may reject quantities with too many decimals: -1111).
282+
# First try to get precision from metadata
283+
qty_precision = None
241284
try:
242285
meta = fdict.get("_meta") or {}
243-
q = self._floor_to_precision(q, (meta.get("quantityPrecision") if isinstance(meta, dict) else None))
286+
if isinstance(meta, dict):
287+
qty_precision = meta.get("quantityPrecision")
244288
except Exception:
245289
pass
290+
291+
# If precision not available, infer from stepSize
292+
if qty_precision is None and step > 0:
293+
try:
294+
# stepSize like "0.001" means 3 decimal places
295+
step_str = str(step).rstrip('0')
296+
if '.' in step_str:
297+
qty_precision = len(step_str.split('.')[1])
298+
else:
299+
# If stepSize is 1 or larger, precision is 0
300+
qty_precision = 0
301+
except Exception:
302+
pass
303+
304+
# Apply precision limit
305+
if qty_precision is not None:
306+
q = self._floor_to_precision(q, qty_precision)
307+
246308
if min_qty > 0 and q < min_qty:
247309
return Decimal("0")
248310
return q

backend_api_python/app/services/live_trading/binance_spot.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,51 @@ def _to_dec(x: Any) -> Decimal:
3838
return Decimal("0")
3939

4040
@staticmethod
41-
def _dec_str(d: Decimal) -> str:
41+
def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
42+
"""
43+
Convert Decimal to string with controlled precision.
44+
Binance requires quantities/prices to match LOT_SIZE/PRICE_FILTER precision.
45+
This method ensures the output string doesn't exceed the required precision.
46+
"""
4247
try:
43-
return format(d, "f")
48+
if d == 0:
49+
return "0"
50+
# Normalize to remove unnecessary trailing zeros from internal representation
51+
normalized = d.normalize()
52+
# Convert to string using fixed-point notation
53+
# Use a reasonable max_decimals to avoid excessive precision
54+
# Binance typically uses 8 decimal places for most symbols
55+
s = format(normalized, f".{max_decimals}f")
56+
# Remove trailing zeros and decimal point if not needed
57+
# This ensures we don't send "0.02874400" when "0.028744" is sufficient
58+
if '.' in s:
59+
s = s.rstrip('0').rstrip('.')
60+
return s if s else "0"
4461
except Exception:
45-
return str(d)
62+
# Fallback: try to convert safely
63+
try:
64+
# If Decimal conversion fails, try float with limited precision
65+
f = float(d)
66+
if f == 0:
67+
return "0"
68+
# Format with max_decimals and remove trailing zeros
69+
s = format(f, f".{max_decimals}f")
70+
if '.' in s:
71+
s = s.rstrip('0').rstrip('.')
72+
return s if s else "0"
73+
except Exception:
74+
# Last resort: convert to string
75+
s = str(d)
76+
# Try to remove scientific notation if present
77+
if 'e' in s.lower() or 'E' in s:
78+
try:
79+
f = float(s)
80+
s = format(f, f".{max_decimals}f")
81+
if '.' in s:
82+
s = s.rstrip('0').rstrip('.')
83+
except Exception:
84+
pass
85+
return s if s else "0"
4686

4787
@staticmethod
4888
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:
@@ -217,12 +257,34 @@ def _normalize_quantity(self, *, symbol: str, quantity: float, for_market: bool)
217257

218258
if step > 0:
219259
q = self._floor_to_step(q, step)
260+
220261
# Enforce quantity precision cap (Binance may reject quantities with too many decimals: -1111).
262+
# First try to get precision from metadata
263+
qty_precision = None
221264
try:
222265
meta = fdict.get("_meta") or {}
223-
q = self._floor_to_precision(q, (meta.get("quantityPrecision") if isinstance(meta, dict) else None))
266+
if isinstance(meta, dict):
267+
qty_precision = meta.get("quantityPrecision")
224268
except Exception:
225269
pass
270+
271+
# If precision not available, infer from stepSize
272+
if qty_precision is None and step > 0:
273+
try:
274+
# stepSize like "0.001" means 3 decimal places
275+
step_str = str(step).rstrip('0')
276+
if '.' in step_str:
277+
qty_precision = len(step_str.split('.')[1])
278+
else:
279+
# If stepSize is 1 or larger, precision is 0
280+
qty_precision = 0
281+
except Exception:
282+
pass
283+
284+
# Apply precision limit
285+
if qty_precision is not None:
286+
q = self._floor_to_precision(q, qty_precision)
287+
226288
if min_qty > 0 and q < min_qty:
227289
return Decimal("0")
228290
return q

backend_api_python/app/services/live_trading/bitget.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,39 @@ def _to_dec(x: Any) -> Decimal:
5454
return Decimal("0")
5555

5656
@staticmethod
57-
def _dec_str(d: Decimal) -> str:
57+
def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
58+
"""
59+
Convert Decimal to string with controlled precision.
60+
Bitget requires quantities to match sizeStep/sizePlace precision.
61+
"""
5862
try:
59-
return format(d, "f")
63+
if d == 0:
64+
return "0"
65+
normalized = d.normalize()
66+
s = format(normalized, f".{max_decimals}f")
67+
if '.' in s:
68+
s = s.rstrip('0').rstrip('.')
69+
return s if s else "0"
6070
except Exception:
61-
return str(d)
71+
try:
72+
f = float(d)
73+
if f == 0:
74+
return "0"
75+
s = format(f, f".{max_decimals}f")
76+
if '.' in s:
77+
s = s.rstrip('0').rstrip('.')
78+
return s if s else "0"
79+
except Exception:
80+
s = str(d)
81+
if 'e' in s.lower() or 'E' in s:
82+
try:
83+
f = float(s)
84+
s = format(f, f".{max_decimals}f")
85+
if '.' in s:
86+
s = s.rstrip('0').rstrip('.')
87+
except Exception:
88+
pass
89+
return s if s else "0"
6290

6391
@staticmethod
6492
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:

backend_api_python/app/services/live_trading/bitget_spot.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,39 @@ def _to_dec(x: Any) -> Decimal:
5454
return Decimal("0")
5555

5656
@staticmethod
57-
def _dec_str(d: Decimal) -> str:
57+
def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
58+
"""
59+
Convert Decimal to string with controlled precision.
60+
Bitget requires quantities to match quantityStep/quantityScale precision.
61+
"""
5862
try:
59-
return format(d, "f")
63+
if d == 0:
64+
return "0"
65+
normalized = d.normalize()
66+
s = format(normalized, f".{max_decimals}f")
67+
if '.' in s:
68+
s = s.rstrip('0').rstrip('.')
69+
return s if s else "0"
6070
except Exception:
61-
return str(d)
71+
try:
72+
f = float(d)
73+
if f == 0:
74+
return "0"
75+
s = format(f, f".{max_decimals}f")
76+
if '.' in s:
77+
s = s.rstrip('0').rstrip('.')
78+
return s if s else "0"
79+
except Exception:
80+
s = str(d)
81+
if 'e' in s.lower() or 'E' in s:
82+
try:
83+
f = float(s)
84+
s = format(f, f".{max_decimals}f")
85+
if '.' in s:
86+
s = s.rstrip('0').rstrip('.')
87+
except Exception:
88+
pass
89+
return s if s else "0"
6290

6391
@staticmethod
6492
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:

backend_api_python/app/services/live_trading/bybit.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,39 @@ def _to_dec(x: Any) -> Decimal:
6161
return Decimal("0")
6262

6363
@staticmethod
64-
def _dec_str(d: Decimal) -> str:
64+
def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
65+
"""
66+
Convert Decimal to string with controlled precision.
67+
Bybit requires quantities to match qtyStep precision.
68+
"""
6569
try:
66-
return format(d, "f")
70+
if d == 0:
71+
return "0"
72+
normalized = d.normalize()
73+
s = format(normalized, f".{max_decimals}f")
74+
if '.' in s:
75+
s = s.rstrip('0').rstrip('.')
76+
return s if s else "0"
6777
except Exception:
68-
return str(d)
78+
try:
79+
f = float(d)
80+
if f == 0:
81+
return "0"
82+
s = format(f, f".{max_decimals}f")
83+
if '.' in s:
84+
s = s.rstrip('0').rstrip('.')
85+
return s if s else "0"
86+
except Exception:
87+
s = str(d)
88+
if 'e' in s.lower() or 'E' in s:
89+
try:
90+
f = float(s)
91+
s = format(f, f".{max_decimals}f")
92+
if '.' in s:
93+
s = s.rstrip('0').rstrip('.')
94+
except Exception:
95+
pass
96+
return s if s else "0"
6997

7098
@staticmethod
7199
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:

backend_api_python/app/services/live_trading/deepcoin.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,39 @@ def _to_dec(x: Any) -> Decimal:
7474
return Decimal("0")
7575

7676
@staticmethod
77-
def _dec_str(d: Decimal) -> str:
77+
def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
78+
"""
79+
Convert Decimal to string with controlled precision.
80+
Deepcoin requires quantities to match lotSz/qtyStep precision.
81+
"""
7882
try:
79-
return format(d, "f")
83+
if d == 0:
84+
return "0"
85+
normalized = d.normalize()
86+
s = format(normalized, f".{max_decimals}f")
87+
if '.' in s:
88+
s = s.rstrip('0').rstrip('.')
89+
return s if s else "0"
8090
except Exception:
81-
return str(d)
91+
try:
92+
f = float(d)
93+
if f == 0:
94+
return "0"
95+
s = format(f, f".{max_decimals}f")
96+
if '.' in s:
97+
s = s.rstrip('0').rstrip('.')
98+
return s if s else "0"
99+
except Exception:
100+
s = str(d)
101+
if 'e' in s.lower() or 'E' in s:
102+
try:
103+
f = float(s)
104+
s = format(f, f".{max_decimals}f")
105+
if '.' in s:
106+
s = s.rstrip('0').rstrip('.')
107+
except Exception:
108+
pass
109+
return s if s else "0"
82110

83111
@staticmethod
84112
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:

0 commit comments

Comments
 (0)