Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3994224
fix script ENV_FILE path
May 27, 2026
09bd05f
feat(ktx): add to_ktx_symbol helper
May 27, 2026
c4a5b3c
feat(ktx): add KTX REST client for spot and futures trading
May 27, 2026
aea3ba9
feat(ktx): register KTX client in factory
May 27, 2026
2e038aa
feat(ktx): wire KTX into order execution pipeline
May 27, 2026
806d65d
feat(ktx): add KTX to crypto exchange registry
May 27, 2026
95e92af
feat(ktx): add wait_for_fill and set_leverage to KtxClient
May 27, 2026
22dd619
feat(ktx): wire KTX dispatch into pending order worker (positions + o…
May 27, 2026
38eadc9
feat(ktx): add KTX to strategy test-connection and symbol listing
May 27, 2026
ab7f34d
test(ktx): add KTX client smoke tests
May 27, 2026
fb2410b
feat(ktx): wire native KtxClient.get_ticker into CryptoDataSource for…
May 28, 2026
b993508
test(ktx): add comprehensive test suite for KTX client (32 tests)
May 28, 2026
7f3beff
fix(ktx): split account endpoints - main vs trade accounts, add get_t…
May 29, 2026
6d4df7d
fix(ktx): get_positions uses /v1/positions API with full param support
May 29, 2026
f7c3068
fix(ktx): get_positions always filters market=lpc, filters zero-size …
May 29, 2026
c500693
feat(ktx): complete spot/futures separation - market param, new endpo…
May 29, 2026
9e6e713
fix(ktx): clarify account hierarchy - wallet vs trade accounts
May 29, 2026
28061b1
docs: add KTX exchange integration documentation
May 29, 2026
06817d6
KTX交易所接入完成
May 29, 2026
363bdfb
fix KTX bug
May 29, 2026
32556a8
update redis info
May 31, 2026
1405ee7
添加ktx候选
May 31, 2026
1f38d68
Merge commit '7aad07280f499c8460e98a14d1a845bc1618b476' into ktx-main
May 31, 2026
7420ad2
Merge branch 'main' into ktx-main
Jun 2, 2026
780cd59
ktxclient.get_kline
Jun 2, 2026
48912fb
ktx not use cctx
Jun 2, 2026
59894c6
Merge branch 'main' into ktx-main
Jun 3, 2026
fdb53c1
ktx
Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ See full examples:
| Kraken | Spot, Futures |
| Gate.io | Spot, Futures |
| HTX | Spot, USDT-margined perpetuals |
| KTX | Spot, Futures |

### Traditional Markets

Expand Down
211 changes: 184 additions & 27 deletions backend_api_python/app/data_sources/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ def _init_ccxt_exchange(self, ccxt_exchange_id: str, options: Optional[Dict[str,
config.setdefault("options", {}).update(dict(options))

exchange_id = (ccxt_exchange_id or "").strip().lower()

# KTX 不在 CCXT 中,已有原生分支(ticker + kline),跳过 CCXT 初始化
if exchange_id == "ktx":
logger.info("KTX has native client support, skipping CCXT initialization")
self.exchange = None
self._markets_loaded = False
self._markets_cache = None
return

if not hasattr(ccxt, exchange_id):
logger.warning("CCXT exchange '%s' not found, falling back to 'binance'", exchange_id)
exchange_id = "binance"
Expand Down Expand Up @@ -298,59 +307,203 @@ def _normalize_symbol_for_exchange(self, symbol: str) -> str:

def get_ticker(self, symbol: str) -> Dict[str, Any]:
"""
Get latest ticker for a crypto symbol via CCXT.
Get latest ticker for a crypto symbol.

Accepts common formats:
- BTC/USDT, BTCUSDT, BTC/USDT:USDT
- PI, TRX (will be normalized and searched across exchanges)
- 自动适配不同交易所的符号格式要求
KTX: uses native KtxClient.get_ticker() directly (CCXT has no KTX support).
All other exchanges: falls back to CCXT fetch_ticker().
"""
if not symbol or not symbol.strip():
return {'last': 0, 'symbol': symbol}

normalized = self._symbol_for_scoped_market(symbol)
return {"last": 0, "symbol": symbol}

# KTX native ticker path — CCXT has no KTX, use the native client directly.
if (getattr(self, "_scoped_exchange_id", "") or "").strip().lower() == "ktx":
return self._get_ticker_ktx(symbol)

normalized = self._symbol_for_scoped_market(symbol)
if not normalized:
logger.warning(f"Failed to normalize symbol: {symbol}")
return {'last': 0, 'symbol': symbol}
# 尝试获取 ticker
return {"last": 0, "symbol": symbol}

# Try CCXT
try:
ticker = self.exchange.fetch_ticker(normalized)
if ticker and isinstance(ticker, dict):
return ticker
except Exception as e:
error_msg = str(e).lower()
is_symbol_error = any(keyword in error_msg for keyword in [
'does not have market symbol',
'symbol not found',
'invalid symbol',
'market does not exist',
'trading pair not found'
])

is_symbol_error = any(
kw in error_msg
for kw in [
"does not have market symbol",
"symbol not found",
"invalid symbol",
"market does not exist",
"trading pair not found",
]
)

if is_symbol_error:
# 尝试查找替代符号
base = normalized.split('/')[0] if '/' in normalized else normalized
base = normalized.split("/")[0] if "/" in normalized else normalized
if self._ensure_markets_loaded():
valid_symbol = self._find_valid_symbol(base)
if valid_symbol and valid_symbol != normalized:
try:
logger.debug(f"Trying alternative symbol: {valid_symbol} (original: {symbol}, first attempt: {normalized})")
logger.debug(
f"Trying alternative symbol: {valid_symbol} "
f"(original: {symbol}, first attempt: {normalized})"
)
ticker = self.exchange.fetch_ticker(valid_symbol)
if ticker and isinstance(ticker, dict):
return ticker
except Exception as e2:
logger.debug(f"Alternative symbol {valid_symbol} also failed: {e2}")

# 如果所有尝试都失败,记录警告并返回默认值

logger.warning(
f"Symbol '{symbol}' (normalized: {normalized}) not found on {self.exchange.id}. "
f"Error: {str(e)[:100]}"
)

return {'last': 0, 'symbol': symbol}


return {"last": 0, "symbol": symbol}

def _get_ticker_ktx(self, symbol: str) -> Dict[str, Any]:
"""
Fetch KTX ticker via native KtxClient (no CCXT support for KTX).

Uses API-key-free public endpoint: GET /api/v1/ticker?market=lpc&symbol=BTC_USDT_SWAP
"""
try:
from app.services.live_trading.ktx import KtxClient

# Resolve market_type from the scoped instance
mt = getattr(self, "_scoped_market_type", "swap") or "swap"
if mt in ("futures", "future", "perp", "perpetual"):
mt = "swap"

# Try to fetch ticker via native client (uses public endpoint, no auth needed).
# Use a lightweight ephemeral client — no keys required for public market data.
client = KtxClient(
api_key="__placeholder__",
secret_key="__placeholder__",
market_type=mt,
)
raw = client.get_ticker(symbol=symbol)
if not isinstance(raw, dict) or not raw:
return {"last": 0, "symbol": symbol}

# Normalize KTX ticker response to CCXT-like format for consumers.
last = 0.0
try:
last = float(raw.get("last") or raw.get("lastPrice") or raw.get("price") or 0.0)
except Exception:
last = 0.0

change = 0.0
change_pct = 0.0
try:
change = float(raw.get("change") or raw.get("priceChange") or 0.0)
except Exception:
change = 0.0
try:
change_pct = float(
raw.get("changePercent") or raw.get("priceChangePercent") or 0.0
)
except Exception:
change_pct = 0.0

return {
"last": last,
"change": change,
"changePercent": change_pct,
"high": float(raw.get("high") or raw.get("highPrice") or raw.get("priceHigh") or 0.0),
"low": float(raw.get("low") or raw.get("lowPrice") or raw.get("priceLow") or 0.0),
"open": float(raw.get("open") or raw.get("openPrice") or 0.0),
"volume": float(raw.get("volume") or raw.get("vol") or 0.0),
"symbol": symbol,
}
except Exception as e:
logger.warning(f"KTX ticker fetch failed for {symbol}: {e}")
return {"last": 0, "symbol": symbol}

def _get_kline_ktx(
self,
symbol: str,
timeframe: str,
limit: int,
before_time: Optional[int] = None,
after_time: Optional[int] = None,
) -> List[Dict[str, Any]]:
"""Fetch KTX candles via native KtxClient (no CCXT support for KTX).

Uses API-key-free public endpoint:
GET /api/v1/candles?symbol=BTC_USDT_SWAP&market=lpc&time_frame=1h&limit=500
"""
try:
from app.services.live_trading.ktx import KtxClient

# Resolve market_type from the scoped instance
mt = getattr(self, "_scoped_market_type", "swap") or "swap"
if mt in ("futures", "future", "perp", "perpetual"):
mt = "swap"

# Ephemeral client for public market data — no real keys needed.
client = KtxClient(
api_key="__placeholder__",
secret_key="__placeholder__",
market_type=mt,
)
raw_candles = client.get_kline(symbol=symbol, timeframe=timeframe, limit=limit)
if not raw_candles:
logger.warning(f"KTX get_kline returned no candles for {symbol} {timeframe}")
return []

# Normalize KTX candle response to the standard kline format.
# KTX candle fields: open_time (ms), open, high, low, close, volume (strings).
klines = []
for c in raw_candles:
try:
ts = int(c.get("open_time", c.get("timestamp", 0)))
if ts > 1e12: # milliseconds → seconds
ts = int(ts / 1000)
o = float(c.get("open", 0) or 0)
h = float(c.get("high", 0) or 0)
l = float(c.get("low", 0) or 0)
cl = float(c.get("close", 0) or 0)
v = float(c.get("volume", c.get("vol", 0)) or 0)
klines.append(self.format_kline(
timestamp=ts,
open_price=o,
high=h,
low=l,
close=cl,
volume=v,
))
except (ValueError, TypeError):
continue

# Apply time filters and limit
klines = self.filter_and_limit(
klines, limit, before_time, after_time,
truncate=(after_time is None),
)

# Concise trace
if klines:
try:
from datetime import datetime as _dt
first_ts = _dt.utcfromtimestamp(klines[0]['time']).isoformat()
last_ts = _dt.utcfromtimestamp(klines[-1]['time']).isoformat()
logger.info(
f"[CryptoKline] {symbol} {timeframe} returned {len(klines)} candles (KTX native), "
f"utc_range={first_ts}~{last_ts}, limit={limit}, before_time={before_time}"
)
except Exception:
pass

return klines
except Exception as e:
logger.error(f"KTX kline fetch failed for {symbol} {timeframe}: {e}")
return []

def get_kline(
self,
symbol: str,
Expand All @@ -360,6 +513,10 @@ def get_kline(
after_time: Optional[int] = None,
) -> List[Dict[str, Any]]:
"""获取加密货币K线数据"""
# KTX native kline path — CCXT has no KTX, use the native client directly.
if (getattr(self, "_scoped_exchange_id", "") or "").strip().lower() == "ktx":
return self._get_kline_ktx(symbol, timeframe, limit, before_time, after_time)

klines = []

try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def supports_swap(self) -> bool:
"kraken": VenueCapability("kraken", frozenset({"spot", "swap"})),
"gate": VenueCapability("gate", frozenset({"spot", "swap"})),
"htx": VenueCapability("htx", frozenset({"spot", "swap"})),
"ktx": VenueCapability("ktx", frozenset({"spot", "swap"})),
}


Expand Down
23 changes: 22 additions & 1 deletion backend_api_python/app/services/live_trading/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Translate a strategy signal into a direct-exchange order call.

Supports:
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, Gate, HTX
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, Gate, HTX, KTX
- Traditional brokers: Interactive Brokers (IBKR) for US stocks
- Forex brokers: MetaTrader 5 (MT5)
"""
Expand All @@ -27,6 +27,9 @@
# Lazy import HTX
HtxClient = None

# Lazy import KTX
KtxClient = None

# Lazy import IBKR
IBKRClient = None

Expand Down Expand Up @@ -280,6 +283,24 @@ def place_order_from_signal(
client_order_id=client_order_id,
)

global KtxClient
if KtxClient is None:
try:
from app.services.live_trading.ktx import KtxClient as _KtxClient
KtxClient = _KtxClient
except ImportError:
pass

if KtxClient is not None and isinstance(client, KtxClient):
return client.place_market_order(
symbol=symbol,
side=side,
qty=qty,
reduce_only=reduce_only,
pos_side=pos_side,
client_order_id=client_order_id,
)

# Check for IBKR client (lazy import to avoid circular dependency)
global IBKRClient
if IBKRClient is None:
Expand Down
22 changes: 21 additions & 1 deletion backend_api_python/app/services/live_trading/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Factory for direct exchange clients.

Supports:
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, Gate, HTX
- Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, Gate, HTX, KTX
- Traditional brokers: Interactive Brokers (IBKR) for US stocks
- Forex brokers: MetaTrader 5 (MT5)
"""
Expand All @@ -27,6 +27,7 @@
from app.services.live_trading.kraken_futures import KrakenFuturesClient
from app.services.live_trading.gate import GateSpotClient, GateUsdtFuturesClient
from app.services.live_trading.htx import HtxClient
from app.services.live_trading.ktx import KtxClient

# Lazy import IBKR to avoid ImportError if ib_insync not installed
IBKRClient = None
Expand Down Expand Up @@ -257,6 +258,25 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap")
broker_id=broker_id,
)

if exchange_id == "ktx":
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.ktx.app"
_ktx_lev = _get(exchange_config, "leverage") or 0
try:
_ktx_lev = int(float(_ktx_lev))
except (TypeError, ValueError):
_ktx_lev = 0
_ktx_margin = str(
_get(exchange_config, "margin_method", "marginMethod", "margin_mode", "marginMode") or ""
).strip().lower()
return KtxClient(
api_key=api_key,
secret_key=secret_key,
base_url=base_url,
market_type=mt,
leverage=_ktx_lev if _ktx_lev > 0 else 0,
margin_method=_ktx_margin,
)

# Traditional brokers (IBKR for US stocks only)
if exchange_id == "ibkr":
# Note: Market category validation should be done at the caller level
Expand Down
Loading