Skip to content

Commit bac16f4

Browse files
v3.0.29
Signed-off-by: Dinger <quantdinger@gmail.com>
1 parent e5e9b96 commit bac16f4

61 files changed

Lines changed: 3948 additions & 2839 deletions

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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,9 +544,7 @@ See full examples:
544544
| Bybit | Spot, Linear Futures |
545545
| Coinbase | Spot |
546546
| Kraken | Spot, Futures |
547-
| KuCoin | Spot, Futures |
548547
| Gate.io | Spot, Futures |
549-
| Deepcoin | Derivatives integration |
550548
| HTX | Spot, USDT-margined perpetuals |
551549

552550
### Traditional Markets

backend_api_python/README.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,37 @@ Flask-based backend for QuantDinger: market data, indicators, AI analysis, backt
1616

1717
```text
1818
backend_api_python/
19-
├─ app/
20-
│ ├─ __init__.py # Flask app factory + startup hooks
21-
│ ├─ config/ # Settings (env-driven)
22-
│ ├─ data_sources/ # Data sources + factory
23-
│ ├─ routes/ # REST endpoints
24-
│ ├─ services/ # Analysis, agents, strategies, search, user_service
25-
│ └─ utils/ # PostgreSQL helpers, config loader, logging, HTTP utils
26-
├─ migrations/
27-
│ └─ init.sql # PostgreSQL schema initialization
28-
├─ env.example # Copy to .env for local config
29-
├─ requirements.txt
30-
├─ run.py # Entrypoint (loads .env, applies proxy env, starts Flask)
31-
├─ gunicorn_config.py # Optional production config
32-
└─ README.md
19+
|-- app/
20+
| |-- __init__.py # Flask app factory + startup hooks
21+
| |-- config/ # Settings (env-driven)
22+
| |-- data_sources/ # Data sources + factory
23+
| |-- routes/ # REST endpoints
24+
| |-- services/ # Analysis, agents, strategies, search, user_service
25+
| |-- utils/ # PostgreSQL helpers, config loader, logging, HTTP utils
26+
|-- migrations/
27+
| |-- init.sql # PostgreSQL schema initialization
28+
|-- env.example # Copy to .env for local config
29+
|-- requirements.txt
30+
|-- run.py # Entrypoint (loads .env, applies proxy env, starts Flask)
31+
|-- gunicorn_config.py # Optional production config
32+
|-- README.md
33+
```
34+
35+
## Architecture and quality guardrails
36+
37+
- Backend architecture guide: `docs/backend_architecture.md`
38+
- Canonical live-trading venue matrix: `app/services/live_trading/capabilities.py`
39+
- Stable live order contracts: `app/services/live_trading/contracts.py`
40+
- Structural regression guard:
41+
42+
```bash
43+
python scripts/backend_quality_check.py
44+
```
45+
46+
Exchange integrations should add offline fixtures and pass:
47+
48+
```bash
49+
python scripts/exchange_smoke_test.py --offline-contracts
3350
```
3451

3552
## Quick start (Docker - Recommended)

backend_api_python/app/data_sources/crypto.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,8 @@ def resolve_ccxt_for_live_trading(exchange_id: str, market_type: str) -> Tuple[s
4141
opts["defaultType"] = "swap" if mt == "swap" else "spot"
4242
elif e == "gate":
4343
opts["defaultType"] = "swap" if mt == "swap" else "spot"
44-
elif e == "kucoin":
45-
ccxt_id = "kucoinfutures" if mt == "swap" else "kucoin"
4644
elif e == "kraken":
4745
ccxt_id = "krakenfutures" if mt == "swap" else "kraken"
48-
elif e == "deepcoin":
49-
opts["defaultType"] = "swap" if mt == "swap" else "spot"
5046
elif e == "htx" or e == "huobi":
5147
ccxt_id = "htx"
5248
opts["defaultType"] = "swap" if mt == "swap" else "spot"

backend_api_python/app/data_sources/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def get_data_source(cls, name: str) -> BaseDataSource:
9999
In the localized Python backend we primarily use `get_source("Crypto")`.
100100
"""
101101
key = (name or "").strip().lower()
102-
if key in ("crypto", "binance", "okx", "bybit", "bitget", "kucoin", "gate", "mexc", "kraken", "coinbase", "alpaca_crypto"):
102+
if key in ("crypto", "binance", "okx", "bybit", "bitget", "gate", "mexc", "kraken", "coinbase", "alpaca_crypto"):
103103
return cls.get_source("Crypto")
104104
if key in ("futures",):
105105
return cls.get_source("Futures")

backend_api_python/app/data_sources/forex.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ def normalize_forex_pair_symbol(symbol: str) -> str:
2222
if not symbol:
2323
return symbol
2424
s = str(symbol).strip().upper().replace(" ", "").replace("-", "")
25-
return s.replace("/", "")
25+
s = s.replace("/", "")
26+
aliases = {
27+
"XAU": "XAUUSD",
28+
"GOLD": "XAUUSD",
29+
"XAG": "XAGUSD",
30+
"SILVER": "XAGUSD",
31+
}
32+
return aliases.get(s, s)
2633

2734

2835
# 全局缓存

backend_api_python/app/routes/credentials.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from app.utils.auth import login_required
1717
from app.utils.credential_crypto import encrypt_credential_blob, decrypt_credential_blob
1818
from app.services.live_trading.factory import exchange_demo_mode_enabled
19+
from app.services.live_trading.capabilities import supported_crypto_exchange_ids
1920

2021
logger = get_logger(__name__)
2122

@@ -94,10 +95,7 @@ def list_credentials():
9495
return jsonify({'code': 0, 'msg': str(e), 'data': {'items': []}}), 500
9596

9697

97-
CRYPTO_EXCHANGES = [
98-
'binance', 'okx', 'bitget', 'bybit', 'coinbaseexchange',
99-
'kraken', 'kucoin', 'gate', 'deepcoin', 'htx'
100-
]
98+
CRYPTO_EXCHANGES = sorted(supported_crypto_exchange_ids(include_aliases=True))
10199

102100

103101
def _egress_ipify(url: str) -> str:

backend_api_python/app/routes/quick_trade.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,6 @@ def _limit_order_kwargs(client, symbol, amount, price, side, market_type, client
964964
from app.services.live_trading.binance_spot import BinanceSpotClient
965965
from app.services.live_trading.okx import OkxClient
966966
from app.services.live_trading.bybit import BybitClient
967-
from app.services.live_trading.deepcoin import DeepcoinClient
968967

969968
if isinstance(client, (BinanceFuturesClient, BinanceSpotClient)):
970969
return {"quantity": amount, "price": price, "client_order_id": client_order_id}
@@ -977,7 +976,7 @@ def _limit_order_kwargs(client, symbol, amount, price, side, market_type, client
977976
pos_side = "long" if side.lower() == "buy" else "short"
978977
kwargs["pos_side"] = pos_side
979978
return kwargs
980-
if isinstance(client, (BybitClient, DeepcoinClient)):
979+
if isinstance(client, BybitClient):
981980
return {"qty": amount, "price": price, "client_order_id": client_order_id}
982981
# Generic fallback
983982
return {"size": amount, "price": price, "client_order_id": client_order_id}
@@ -1501,31 +1500,28 @@ def _fetch_exchange_positions_raw(
15011500
"""
15021501
Fetch raw position payload for quick-trade / close-position.
15031502
1504-
Many clients do not accept ``symbol=`` on ``get_positions()`` (Gate, KuCoin),
1503+
Many clients do not accept ``symbol=`` on ``get_positions()`` (Gate),
15051504
or need extra args (Bitget ``product_type``, OKX ``inst_type``). Centralize here.
15061505
"""
15071506
from app.services.live_trading.binance import BinanceFuturesClient
15081507
from app.services.live_trading.binance_spot import BinanceSpotClient
15091508
from app.services.live_trading.bitget import BitgetMixClient
15101509
from app.services.live_trading.bitget_spot import BitgetSpotClient
15111510
from app.services.live_trading.bybit import BybitClient
1512-
from app.services.live_trading.deepcoin import DeepcoinClient
15131511
from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient
15141512
from app.services.live_trading.htx import HtxClient
1515-
from app.services.live_trading.kucoin import KucoinFuturesClient, KucoinSpotClient
15161513
from app.services.live_trading.okx import OkxClient
15171514
from app.services.live_trading.symbols import (
15181515
to_bybit_symbol,
15191516
to_gate_currency_pair,
1520-
to_kucoin_futures_symbol,
15211517
to_okx_spot_inst_id,
15221518
to_okx_swap_inst_id,
15231519
)
15241520

15251521
mt = (market_type or "swap").strip().lower()
15261522

15271523
if mt == "spot" and isinstance(
1528-
client, (BinanceSpotClient, BitgetSpotClient, KucoinSpotClient, OkxClient)
1524+
client, (BinanceSpotClient, BitgetSpotClient, OkxClient)
15291525
):
15301526
return _fetch_spot_holdings_raw(client, symbol=symbol)
15311527

@@ -1619,19 +1615,6 @@ def _fetch_exchange_positions_raw(
16191615
[(p.get("size"), p.get("positionAmt")) for p in out])
16201616
return out
16211617

1622-
if isinstance(client, KucoinFuturesClient):
1623-
raw = client.get_positions()
1624-
data = raw.get("data") if isinstance(raw, dict) else []
1625-
sym = to_kucoin_futures_symbol(symbol)
1626-
if not isinstance(data, list):
1627-
data = []
1628-
filtered = [p for p in data if isinstance(p, dict) and str(p.get("symbol") or "").strip() == sym]
1629-
if isinstance(raw, dict):
1630-
out = dict(raw)
1631-
out["data"] = filtered
1632-
return out
1633-
return {"data": filtered}
1634-
16351618
if isinstance(client, HtxClient):
16361619
if mt == "spot":
16371620
return client.get_positions(symbol=symbol)
@@ -1667,9 +1650,6 @@ def _fetch_exchange_positions_raw(
16671650
[(p.get("contract_code"), p.get("volume"), p.get("positionAmt")) for p in out_items])
16681651
return {"data": out_items}
16691652

1670-
if isinstance(client, DeepcoinClient):
1671-
return client.get_positions(symbol=symbol)
1672-
16731653
if hasattr(client, "get_positions"):
16741654
try:
16751655
return client.get_positions(symbol=symbol)

backend_api_python/app/routes/strategy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,8 @@ def get_trades():
802802
# the stored wall clock is UTC. Naive datetime must not use .timestamp() alone — that would
803803
# interpret it in the Python process local TZ and shift the instant (e.g. +8h on CN laptops).
804804
from datetime import datetime as _dt, timezone as _tz
805+
from app.utils.trade_close_reason import enrich_trade_row
806+
from app.utils.trade_net_pnl import enrich_trades_net_pnl
805807
processed_rows = []
806808
for row in rows:
807809
trade = dict(row)
@@ -820,8 +822,6 @@ def get_trades():
820822
trade['created_at'] = int(dt.timestamp())
821823
except Exception:
822824
pass
823-
from app.utils.trade_close_reason import enrich_trade_row
824-
from app.utils.trade_net_pnl import enrich_trades_net_pnl
825825

826826
trade = enrich_trade_row(trade, bot_type=bot_type, lang=lang)
827827
processed_rows.append(trade)

backend_api_python/app/services/broker_market_policy.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,13 @@
2020

2121
from typing import Dict, Optional, Set
2222

23+
from app.services.live_trading.capabilities import CRYPTO_VENUE_CAPABILITIES
24+
2325

2426
# ---------------------------------------------------------------------------
2527
# Canonical sets
2628
# ---------------------------------------------------------------------------
2729

28-
# All crypto exchanges QuantDinger has wired up in live_trading/factory.py.
29-
# Each is assumed to support both spot and swap unless noted otherwise.
30-
_CRYPTO_EXCHANGES_SPOT_AND_SWAP: Set[str] = {
31-
"binance", "okx", "bitget", "bybit",
32-
"kraken", "kucoin", "gate", "deepcoin", "htx",
33-
}
34-
# Coinbase Exchange API is institutional spot-only on our side.
35-
_CRYPTO_EXCHANGES_SPOT_ONLY: Set[str] = {"coinbaseexchange"}
36-
37-
3830
def _build_broker_markets() -> Dict[str, Dict[str, Set[str]]]:
3931
"""Construct the master matrix.
4032
@@ -54,10 +46,8 @@ def _build_broker_markets() -> Dict[str, Dict[str, Set[str]]]:
5446
"Crypto": {"spot"},
5547
},
5648
}
57-
for ex in _CRYPTO_EXCHANGES_SPOT_AND_SWAP:
58-
matrix[ex] = {"Crypto": {"spot", "swap"}}
59-
for ex in _CRYPTO_EXCHANGES_SPOT_ONLY:
60-
matrix[ex] = {"Crypto": {"spot"}}
49+
for ex, capability in CRYPTO_VENUE_CAPABILITIES.items():
50+
matrix[ex] = {"Crypto": set(capability.market_types)}
6151
return matrix
6252

6353

backend_api_python/app/services/exchange_execution.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from app.utils.db import get_db_connection
1717
from app.utils.logger import get_logger
1818
from app.utils.credential_crypto import decrypt_credential_blob
19+
from app.services.live_trading.capabilities import canonical_exchange_id, supported_crypto_exchange_ids
1920

2021
logger = get_logger(__name__)
2122

@@ -49,10 +50,7 @@ def _safe_json_loads(value: Any, default: Any) -> Any:
4950
"alpaca": "USStock", # Alpaca primarily for US stocks; crypto is opt-in via market_category override
5051
"mt5": "Forex",
5152
}
52-
_CRYPTO_EXCHANGES = {
53-
"binance", "okx", "bitget", "bybit", "coinbaseexchange",
54-
"kraken", "kucoin", "gate", "deepcoin", "htx",
55-
}
53+
_CRYPTO_EXCHANGES = supported_crypto_exchange_ids()
5654

5755

5856
def _infer_market_category_from_exchange(exchange_id: str) -> str:
@@ -61,7 +59,7 @@ def _infer_market_category_from_exchange(exchange_id: str) -> str:
6159
6260
Returns 'Crypto' as the legacy default only if exchange_id is empty or unknown.
6361
"""
64-
eid = (exchange_id or "").strip().lower()
62+
eid = canonical_exchange_id(exchange_id)
6563
if not eid:
6664
return "Crypto"
6765
if eid in _EXCHANGE_TO_MARKET:

0 commit comments

Comments
 (0)