Skip to content

Commit df52bb0

Browse files
v3.0.1
Signed-off-by: Dinger <quantdinger@gmail.com>
1 parent f5c9a9b commit df52bb0

110 files changed

Lines changed: 770 additions & 686 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,6 @@ The same links are available in the web app under **Profile → Open account** (
426426
| Kraken | Spot, Futures |
427427
| KuCoin | Spot, Futures |
428428
| Gate.io | Spot, Futures |
429-
| Bitfinex | Spot, Derivatives |
430429

431430
### Traditional Brokers & Markets
432431

‎backend_api_python/app/data_sources/crypto.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ def _normalize_symbol_for_exchange(self, symbol: str) -> str:
147147
- OKX: BTC/USDT (标准格式,但某些币种可能不支持)
148148
- Coinbase: BTC/USD (通常使用 USD 而不是 USDT)
149149
- Kraken: XBT/USD (BTC 映射为 XBT)
150-
- Bitfinex: tBTCUST (特殊格式)
151150
"""
152151
normalized, base = self._normalize_symbol(symbol)
153152

‎backend_api_python/app/routes/credentials.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def list_credentials():
7272

7373
CRYPTO_EXCHANGES = [
7474
'binance', 'okx', 'bitget', 'bybit', 'coinbaseexchange',
75-
'kraken', 'kucoin', 'gate', 'bitfinex', 'deepcoin', 'htx'
75+
'kraken', 'kucoin', 'gate', 'deepcoin', 'htx'
7676
]
7777

7878

‎backend_api_python/app/routes/dashboard.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ def _format_datetime(dt: Any) -> Any:
4545
if dt is None:
4646
return None
4747
if hasattr(dt, 'isoformat'):
48+
from datetime import timezone as _tz
49+
if hasattr(dt, 'tzinfo') and getattr(dt, 'tzinfo', None) is None:
50+
dt = dt.replace(tzinfo=_tz.utc)
4851
return dt.isoformat()
4952
return dt
5053

@@ -403,11 +406,15 @@ def _truthy(v: Any) -> bool:
403406
cur.close()
404407

405408
# Convert datetime to timestamp for frontend compatibility
409+
from datetime import timezone as _tz
406410
recent_trades = []
407411
for t in recent_trades_raw:
408412
trade = dict(t)
409-
if trade.get('created_at') and hasattr(trade['created_at'], 'timestamp'):
410-
trade['created_at'] = int(trade['created_at'].timestamp())
413+
ca = trade.get('created_at')
414+
if ca and hasattr(ca, 'timestamp'):
415+
if getattr(ca, 'tzinfo', None) is None:
416+
ca = ca.replace(tzinfo=_tz.utc)
417+
trade['created_at'] = int(ca.timestamp())
411418
recent_trades.append(trade)
412419

413420
# Total equity/pnl (best-effort) - calculate before performance stats for drawdown calculation

‎backend_api_python/app/routes/quick_trade.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,7 @@ def _fetch_exchange_positions_raw(
867867
"""
868868
Fetch raw position payload for quick-trade / close-position.
869869
870-
Many clients do not accept ``symbol=`` on ``get_positions()`` (Gate, KuCoin, Bitfinex),
870+
Many clients do not accept ``symbol=`` on ``get_positions()`` (Gate, KuCoin),
871871
or need extra args (Bitget ``product_type``, OKX ``inst_type``). Centralize here.
872872
"""
873873
from app.services.live_trading.binance import BinanceFuturesClient

‎backend_api_python/app/routes/strategy.py‎

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@
2727

2828
strategy_bp = Blueprint('strategy', __name__)
2929

30+
31+
def _normalize_trade_row_for_api(trade: dict) -> dict:
32+
"""Ensure numeric fields are JSON-friendly floats (PostgreSQL DECIMAL → float)."""
33+
try:
34+
from decimal import Decimal
35+
except Exception: # pragma: no cover
36+
Decimal = () # type: ignore
37+
out = dict(trade)
38+
for k in ("price", "amount", "value", "commission", "profit"):
39+
v = out.get(k)
40+
if isinstance(v, Decimal):
41+
out[k] = float(v)
42+
return out
43+
44+
3045
# ---------------------------------------------------------------------------
3146
# Strategy templates (loaded once from JSON file)
3247
# ---------------------------------------------------------------------------
@@ -542,25 +557,30 @@ def get_trades():
542557
rows = cur.fetchall() or []
543558
cur.close()
544559

545-
# Convert created_at to UTC timestamp (seconds) for frontend
546-
# This ensures consistent timezone handling
560+
# Convert created_at to Unix seconds (UTC instant).
561+
# qd_strategy_trades.created_at is TIMESTAMP WITHOUT TIME ZONE; with PostgreSQL session UTC
562+
# the stored wall clock is UTC. Naive datetime must not use .timestamp() alone — that would
563+
# interpret it in the Python process local TZ and shift the instant (e.g. +8h on CN laptops).
564+
from datetime import datetime as _dt, timezone as _tz
547565
processed_rows = []
548566
for row in rows:
549567
trade = dict(row)
550568
created_at = trade.get('created_at')
551569
if created_at:
552570
if hasattr(created_at, 'timestamp'):
553-
# datetime object - convert to UTC timestamp
554-
trade['created_at'] = int(created_at.timestamp())
571+
dt = created_at
572+
if getattr(dt, 'tzinfo', None) is None:
573+
dt = dt.replace(tzinfo=_tz.utc)
574+
trade['created_at'] = int(dt.timestamp())
555575
elif isinstance(created_at, str):
556-
# ISO string - parse and convert
557576
try:
558-
from datetime import datetime
559-
dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
577+
dt = _dt.fromisoformat(created_at.replace('Z', '+00:00'))
578+
if getattr(dt, 'tzinfo', None) is None:
579+
dt = dt.replace(tzinfo=_tz.utc)
560580
trade['created_at'] = int(dt.timestamp())
561581
except Exception:
562582
pass
563-
processed_rows.append(trade)
583+
processed_rows.append(_normalize_trade_row_for_api(trade))
564584

565585
# Frontend expects data.trades; keep data.items for compatibility with list-style components.
566586
return jsonify({'code': 1, 'msg': 'success', 'data': {'trades': processed_rows, 'items': processed_rows}})
@@ -1491,6 +1511,9 @@ def get_strategy_logs():
14911511
if not isinstance(r, dict):
14921512
continue
14931513
rr = dict(r)
1514+
msg = str(rr.get('message') or '')
1515+
if msg.startswith('tick price=') or msg.startswith('tick price '):
1516+
continue
14941517
ts = rr.get('timestamp')
14951518
if ts is not None and hasattr(ts, 'isoformat'):
14961519
rr['timestamp'] = ts.isoformat()

‎backend_api_python/app/services/live_trading/base.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,8 @@ def _now_ms() -> int:
150150
def _json_dumps(obj: Any) -> str:
151151
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
152152

153+
def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[str, float]]:
154+
"""Query account fee rate from exchange. Returns {"maker": 0.0002, "taker": 0.0005} or None."""
155+
return None
156+
153157

‎backend_api_python/app/services/live_trading/binance.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77

88
from __future__ import annotations
99

10-
import hmac
1110
import hashlib
11+
import hmac
12+
import logging
1213
import time
1314
from decimal import Decimal, ROUND_DOWN
1415
from typing import Any, Dict, Optional, Tuple
1516
from urllib.parse import urlencode
1617

1718
from app.services.live_trading.base import BaseRestClient, LiveOrderResult, LiveTradingError
19+
20+
logger = logging.getLogger(__name__)
1821
from app.services.live_trading.symbols import to_binance_futures_symbol
1922

2023

@@ -484,6 +487,19 @@ def get_fee_for_order(self, *, symbol: str, order_id: str) -> Tuple[float, str]:
484487
fee_ccy = ccy
485488
return float(total_fee), str(fee_ccy or "")
486489

490+
def get_fee_rate(self, symbol: str, market_type: str = "swap") -> Optional[Dict[str, float]]:
491+
sym = symbol.upper().replace("-", "").replace("/", "")
492+
try:
493+
data = self._signed_request("GET", "/fapi/v1/commissionRate", params={"symbol": sym})
494+
if isinstance(data, dict):
495+
maker = abs(float(data.get("makerCommissionRate") or 0))
496+
taker = abs(float(data.get("takerCommissionRate") or 0))
497+
if maker > 0 or taker > 0:
498+
return {"maker": maker, "taker": taker}
499+
except Exception as e:
500+
logger.warning(f"Binance get_fee_rate({symbol}) failed: {e}")
501+
return None
502+
487503
def set_leverage(self, *, symbol: str, leverage: float) -> Dict[str, Any]:
488504
"""
489505
Set futures leverage for a symbol (USDT-M).

‎backend_api_python/app/services/live_trading/binance_spot.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44

55
from __future__ import annotations
66

7-
import hmac
87
import hashlib
8+
import hmac
9+
import logging
910
import time
1011
from decimal import Decimal, ROUND_DOWN
1112
from typing import Any, Dict, Optional, Tuple
1213
from urllib.parse import urlencode
1314

1415
from app.services.live_trading.base import BaseRestClient, LiveOrderResult, LiveTradingError
16+
17+
logger = logging.getLogger(__name__)
1518
from app.services.live_trading.symbols import to_binance_futures_symbol
1619

1720

@@ -554,6 +557,20 @@ def get_fee_for_order(self, *, symbol: str, order_id: str) -> Tuple[float, str]:
554557
fee_ccy = ccy
555558
return float(total_fee), str(fee_ccy or "")
556559

560+
def get_fee_rate(self, symbol: str, market_type: str = "spot") -> Optional[Dict[str, float]]:
561+
sym = symbol.upper().replace("-", "").replace("/", "")
562+
try:
563+
data = self._signed_request("GET", "/sapi/v1/asset/tradeFee", params={"symbol": sym})
564+
if isinstance(data, list) and data and isinstance(data[0], dict):
565+
rec = data[0]
566+
maker = abs(float(rec.get("makerCommission") or 0))
567+
taker = abs(float(rec.get("takerCommission") or 0))
568+
if maker > 0 or taker > 0:
569+
return {"maker": maker, "taker": taker}
570+
except Exception as e:
571+
logger.warning(f"BinanceSpot get_fee_rate({symbol}) failed: {e}")
572+
return None
573+
557574
def cancel_order(self, *, symbol: str, order_id: str = "", client_order_id: str = "") -> Dict[str, Any]:
558575
sym = to_binance_futures_symbol(symbol)
559576
params: Dict[str, Any] = {"symbol": sym}

0 commit comments

Comments
 (0)