Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
89 changes: 76 additions & 13 deletions src/api/binance_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from datetime import datetime
from src.config import config
from src.utils.logger import log
from decimal import Decimal, ROUND_DOWN
import time


class BinanceClient:
Expand All @@ -30,6 +32,13 @@ def __init__(self, api_key: str = None, api_secret: str = None, testnet: bool =
)
else:
self.client = Client(self.api_key, self.api_secret)

self._futures_rules = {}
self._futures_rules_ts = 0.0
self._futures_rules_ttl = 3600 # 1h
if self.client is not None:
self._refresh_futures_rules()

except Exception as e:
# Allow dashboard to start even if Binance is unreachable
self.client = None
Expand All @@ -44,6 +53,53 @@ def __init__(self, api_key: str = None, api_secret: str = None, testnet: bool =

log.info(f"Binance client initialized (testnet: {self.testnet})")

def _refresh_futures_rules(self) -> None:
info = self.client.futures_exchange_info()
rules = {}
for s in info.get("symbols", []):
fs = {f["filterType"]: f for f in s.get("filters", [])}
lot = fs.get("LOT_SIZE", {})
mlot = fs.get("MARKET_LOT_SIZE", {})
pf = fs.get("PRICE_FILTER", {})
rules[s["symbol"]] = {
"lot_step": Decimal(str(lot.get("stepSize", "0"))),
"lot_min": Decimal(str(lot.get("minQty", "0"))),
"mkt_step": Decimal(str(mlot.get("stepSize", "0"))),
"mkt_min": Decimal(str(mlot.get("minQty", "0"))),
"tick": Decimal(str(pf.get("tickSize", "0"))),
}
self._futures_rules = rules
self._futures_rules_ts = time.time()
# print(rules)

def _get_rules(self, symbol: str):
if time.time() - self._futures_rules_ts > self._futures_rules_ttl:
self._refresh_futures_rules()
if symbol not in self._futures_rules:
self._refresh_futures_rules()
return self._futures_rules[symbol]
Comment on lines +75 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be made more efficient and clearer. The two if statements can be combined into one to avoid a potential redundant call to _refresh_futures_rules(). Also, adding a return type hint (-> Dict) would improve code readability and allow static analysis tools to catch potential issues.

    def _get_rules(self, symbol: str) -> Dict:
        if time.time() - self._futures_rules_ts > self._futures_rules_ttl or symbol not in self._futures_rules:
            self._refresh_futures_rules()
        return self._futures_rules[symbol]


@staticmethod
def _floor_to_step(v: Decimal, step: Decimal) -> Decimal:
if step <= 0:
return v
return (v / step).to_integral_value(rounding=ROUND_DOWN) * step

def normalize_futures_qty(self, symbol: str, qty: float, is_market: bool = True) -> str:
r = self._get_rules(symbol)
q = Decimal(str(qty))
step = r["mkt_step"] if (is_market and r["mkt_step"] > 0) else r["lot_step"]
min_q = r["mkt_min"] if (is_market and r["mkt_step"] > 0) else r["lot_min"]
q = self._floor_to_step(q, step)
if q <= 0 or q < min_q:
raise ValueError(f"{symbol} qty invalid: {q} < minQty {min_q}")
return format(q.normalize(), "f")

def normalize_futures_price(self, symbol: str, price: float) -> str:
r = self._get_rules(symbol)
p = self._floor_to_step(Decimal(str(price)), r["tick"])
return format(p.normalize(), "f")

def get_klines(self, symbol: str, interval: str, limit: int = 500, start_time: int = None) -> List[Dict]:
"""
获取K线数据
Expand Down Expand Up @@ -348,13 +404,14 @@ def place_market_order(
position_side: 持仓方向 (BOTH/LONG/SHORT), 双向持仓用LONG/SHORT
"""
try:
# 构建订单参数
norm_qty = self.normalize_futures_qty(symbol, quantity, is_market=True)
# Build order parameters
order_params = {
'symbol': symbol,
'side': side,
'type': 'MARKET',
'quantity': quantity,
'positionSide': position_side
'quantity': norm_qty
# 'positionSide': position_side
Comment on lines +413 to +414
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Commenting out the positionSide parameter is a significant change that breaks functionality for users operating in hedge mode. In hedge mode, the positionSide (LONG or SHORT) is mandatory for placing orders. By removing it, this method will only work for accounts in one-way mode. This parameter should be restored to support both modes.

                'quantity': norm_qty,
                'positionSide': position_side

}

# 只在需要时添加 reduceOnly 参数
Expand All @@ -363,7 +420,7 @@ def place_market_order(

order = self.client.futures_create_order(**order_params)

log.info(f"Market order placed: {side} {quantity} {symbol} (positionSide={position_side})")
log.info(f"Market order placed: {side} {norm_qty} {symbol} (positionSide={position_side})")
return order

except BinanceAPIException as e:
Expand All @@ -380,16 +437,18 @@ def place_limit_order(
) -> Dict:
"""下限价单"""
try:
norm_qty = self.normalize_futures_qty(symbol, quantity, is_market=False)
norm_price = self.normalize_futures_price(symbol, price)
order = self.client.futures_create_order(
symbol=symbol,
side=side,
type='LIMIT',
quantity=quantity,
price=price,
quantity=norm_qty,
price=norm_price,
timeInForce=time_in_force
)

log.info(f"Limit order placed: {side} {quantity} {symbol} @ {price}")
log.info(f"Limit order placed: {side} {norm_qty} {symbol} @ {norm_price}")
return order

except BinanceAPIException as e:
Expand Down Expand Up @@ -426,29 +485,33 @@ def set_stop_loss_take_profit(

# 止损单
if stop_loss_price:
norm_sl = self.normalize_futures_price(symbol, stop_loss_price)
sl_order = self.client.futures_create_order(
symbol=symbol,
side=side,
type='STOP_MARKET',
stopPrice=stop_loss_price,
stopPrice=norm_sl,
closePosition=True,
positionSide=position_side # 添加持仓方向
positionSide='BOTH'
# positionSide=position_side # specify position side
Comment on lines +495 to +496
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Hardcoding positionSide to 'BOTH' forces the order to be placed in one-way mode, which breaks functionality for users in hedge mode. The original implementation correctly used the position_side parameter passed to the method. This should be reverted to support both one-way and hedge modes.

                    positionSide=position_side

)
orders.append(sl_order)
log.info(f"Stop loss set: {stop_loss_price} (positionSide={position_side})")
log.info(f"Stop loss set: {norm_sl} (positionSide={position_side})")

# 止盈单
if take_profit_price:
norm_tp = self.normalize_futures_price(symbol, take_profit_price)
tp_order = self.client.futures_create_order(
symbol=symbol,
side=side,
type='TAKE_PROFIT_MARKET',
stopPrice=take_profit_price,
stopPrice=norm_tp,
closePosition=True,
positionSide=position_side # 添加持仓方向
positionSide='BOTH'
# positionSide=position_side # specify position side
)
orders.append(tp_order)
log.info(f"Take profit set: {take_profit_price} (positionSide={position_side})")
log.info(f"Take profit set: {norm_tp} (positionSide={position_side})")

return orders

Expand Down
6 changes: 3 additions & 3 deletions src/api/quant_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class QuantClient:
"""外部量化 API 客户端"""

BASE_URL = "http://nofxaios.com:30006/api/coin"
BASE_URL = "http://nofxos.ai/api/coin"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

This line uses the unencrypted HTTP protocol to communicate with nofxos.ai, which is a high-severity security vulnerability. Sensitive QUANT_AUTH_TOKEN is transmitted over this insecure channel, risking credential theft. Additionally, hardcoding the domain http://nofxos.ai impacts maintainability. It is crucial to switch to HTTPS and define the base URL as a class-level constant for both security and improved maintainability.

Suggested change
BASE_URL = "http://nofxos.ai/api/coin"
BASE_URL = "https://nofxos.ai/api/coin"

@property
def auth_token(self) -> str:
"""从环境变量动态获取最新的认证令牌"""
Expand Down Expand Up @@ -78,7 +78,7 @@ async def fetch_ai500_list(self) -> Dict:
"""
获取 AI500 优质币池列表
"""
url = f"http://nofxaios.com:30006/api/ai500/list?auth={self.auth_token}"
url = f"http://nofxos.ai/api/ai500/list?auth={self.auth_token}"

try:
session = await self._get_session()
Expand All @@ -103,7 +103,7 @@ async def fetch_oi_ranking(self, ranking_type: str = 'top', limit: int = 20, dur
duration: 时间周期 (1h, 4h, 24h)
"""
endpoint = "top-ranking" if ranking_type == 'top' else "low-ranking"
url = f"http://nofxaios.com:30006/api/oi/{endpoint}?limit={limit}&duration={duration}&auth={self.auth_token}"
url = f"http://nofxos.ai/api/oi/{endpoint}?limit={limit}&duration={duration}&auth={self.auth_token}"

try:
session = await self._get_session()
Expand Down