优化平仓
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
币安永续合约定时平仓脚本。
|
||||
|
||||
平仓触发条件:
|
||||
- 多空名义价值总和 ≤ 20 USDT 且未实现盈亏 > 0.05。
|
||||
平仓触发条件(满足其一即平仓):
|
||||
- 多空名义价值总和 ≤ 20 USDT 且未实现盈亏 > 0.05;
|
||||
- 未实现盈利 > 10 USDT;
|
||||
- 未实现亏损 > 3 USDT(即未实现盈亏 < -3)。
|
||||
|
||||
流程:
|
||||
1. 获取当前持仓,得到需要平仓的交易对;
|
||||
@@ -30,12 +32,14 @@ SHORT_CLOSE_PRICE_RATIO = Decimal("0.997")
|
||||
|
||||
|
||||
def round_to_step(value: Decimal, step: str) -> str:
|
||||
"""按 step 精度舍入。"""
|
||||
"""按 step 精度舍入。返回普通小数/整数字符串,避免科学计数法(如 5E+1),否则币安 API 会报签名错误。"""
|
||||
step_d = Decimal(step)
|
||||
if step_d <= 0:
|
||||
return str(value)
|
||||
q = (value / step_d).quantize(Decimal("1"), rounding="ROUND_DOWN")
|
||||
return str((q * step_d).normalize())
|
||||
result = (q * step_d).normalize()
|
||||
s = f"{result:.10f}".rstrip("0").rstrip(".")
|
||||
return s if s else "0"
|
||||
|
||||
|
||||
def round_price_to_tick(price: Decimal, tick_size: str) -> str:
|
||||
@@ -104,11 +108,20 @@ def should_close_symbol(
|
||||
unrealized_profit: float,
|
||||
notional_threshold: float = 20,
|
||||
min_profit: float = 0.05,
|
||||
min_profit_large: float = 10,
|
||||
max_loss: float = 3,
|
||||
) -> bool:
|
||||
"""
|
||||
是否对该合约执行平仓。
|
||||
条件:多空名义价值总和 ≤ notional_threshold 且 未实现盈亏 > min_profit。
|
||||
条件(满足其一即平仓):
|
||||
- 多空名义价值总和 ≤ notional_threshold 且 未实现盈亏 > min_profit;
|
||||
- 未实现盈利 > min_profit_large;
|
||||
- 未实现亏损 > max_loss(即 unrealized_profit < -max_loss)。
|
||||
"""
|
||||
if unrealized_profit < -max_loss:
|
||||
return True
|
||||
if unrealized_profit > min_profit_large:
|
||||
return True
|
||||
total_notional = long_notional + short_notional
|
||||
return total_notional <= notional_threshold and unrealized_profit > min_profit
|
||||
|
||||
@@ -117,10 +130,12 @@ def get_symbols_to_close(
|
||||
by_symbol: dict[str, list[dict]],
|
||||
notional_threshold: float = 20,
|
||||
min_profit: float = 0.05,
|
||||
min_profit_large: float = 10,
|
||||
max_loss: float = 3,
|
||||
) -> tuple[set[str], dict[str, list[dict]]]:
|
||||
"""
|
||||
根据持仓得到需要平仓的交易对及其持仓子集。
|
||||
平仓条件:多空名义价值总和 ≤ notional_threshold 且 未实现盈亏 > min_profit。
|
||||
平仓条件:名义≤阈值且盈利>min_profit,或 盈利>min_profit_large,或 亏损>max_loss。
|
||||
返回 (symbols_to_close, by_symbol_filtered)。
|
||||
"""
|
||||
symbols_to_close: set[str] = set()
|
||||
@@ -131,6 +146,8 @@ def get_symbols_to_close(
|
||||
long_n, short_n, unrealized_profit,
|
||||
notional_threshold=notional_threshold,
|
||||
min_profit=min_profit,
|
||||
min_profit_large=min_profit_large,
|
||||
max_loss=max_loss,
|
||||
):
|
||||
symbols_to_close.add(symbol)
|
||||
by_symbol_filtered = {k: v for k, v in by_symbol.items() if k in symbols_to_close}
|
||||
@@ -213,8 +230,12 @@ def build_close_order_params(
|
||||
"quantity": quantity,
|
||||
"price": price_str,
|
||||
}
|
||||
# 双向持仓(对冲)模式必须传 positionSide,否则报 -4061
|
||||
if side_upper in ("LONG", "SHORT"):
|
||||
params["positionSide"] = side_upper
|
||||
elif side_upper == "BOTH":
|
||||
# 单向模式下 API 可能仍返回 BOTH;对冲模式下根据持仓方向推断
|
||||
params["positionSide"] = "LONG" if position_amt > 0 else "SHORT"
|
||||
if reduce_only or side_upper == "BOTH":
|
||||
params["reduceOnly"] = "true"
|
||||
return params
|
||||
@@ -222,6 +243,57 @@ def build_close_order_params(
|
||||
|
||||
# ---------- I/O 与编排 ----------
|
||||
|
||||
def _normalize_api_credentials(api_key: str, api_secret: str) -> tuple[str, str]:
|
||||
"""去除首尾空白、引号、换行,避免 toml/环境变量带入导致签名错误。"""
|
||||
key = (api_key or "").strip().strip('"').strip("'").replace("\r", "").replace("\n", "")
|
||||
secret = (api_secret or "").strip().strip('"').strip("'").replace("\r", "").replace("\n", "")
|
||||
return key, secret
|
||||
|
||||
|
||||
async def create_futures_client(
|
||||
api_key: str,
|
||||
api_secret: str,
|
||||
testnet: bool = False,
|
||||
recv_window: int = 60000,
|
||||
verbose: bool = False,
|
||||
https_proxy: str | None = None,
|
||||
):
|
||||
"""
|
||||
创建仅使用合约接口初始化的客户端,避免 create() 调用现货 ping/time 导致
|
||||
「仅启用合约」的 API Key 报 Signature for this request is not valid。
|
||||
recv_window: 放宽时间窗(毫秒),本机时间有偏差时可增大,默认 60 秒。
|
||||
"""
|
||||
import time
|
||||
from binance import AsyncClient as BinanceAsyncClient
|
||||
from loguru import logger
|
||||
|
||||
key, secret = _normalize_api_credentials(api_key, api_secret)
|
||||
if not key or not secret:
|
||||
raise ValueError("api_key / api_secret 不能为空")
|
||||
|
||||
client = BinanceAsyncClient(
|
||||
api_key=key,
|
||||
api_secret=secret,
|
||||
testnet=testnet,
|
||||
verbose=verbose,
|
||||
https_proxy=https_proxy,
|
||||
)
|
||||
try:
|
||||
await client.futures_ping()
|
||||
res = await client.futures_time()
|
||||
client.timestamp_offset = res["serverTime"] - int(time.time() * 1000)
|
||||
client.REQUEST_RECVWINDOW = recv_window
|
||||
logger.debug(
|
||||
"合约服务器时间差 {} ms,recvWindow={} ms",
|
||||
client.timestamp_offset,
|
||||
recv_window,
|
||||
)
|
||||
return client
|
||||
except Exception:
|
||||
await client.close_connection()
|
||||
raise
|
||||
|
||||
|
||||
async def get_symbol_precision(client: BinanceAsyncClient) -> dict[str, dict]:
|
||||
"""获取合约的 quantity/price 精度。"""
|
||||
info = await client.futures_exchange_info()
|
||||
@@ -372,14 +444,18 @@ async def main_async() -> None:
|
||||
|
||||
from config import settings
|
||||
|
||||
api_key = (getattr(settings, "binance_api_key", None) or "").strip()
|
||||
api_secret = (getattr(settings, "binance_api_secret", None) or "").strip()
|
||||
if not api_key or not api_secret:
|
||||
api_key = getattr(settings, "binance_api_key", None) or ""
|
||||
api_secret = getattr(settings, "binance_api_secret", None) or ""
|
||||
if not (api_key and api_secret):
|
||||
logger.error("请在 .secrets.toml 或环境变量中设置 binance_api_key / binance_api_secret")
|
||||
return
|
||||
|
||||
import os
|
||||
|
||||
recv_window = int(getattr(settings, "binance_recv_window", None) or 60000)
|
||||
verbose = bool(getattr(settings, "binance_verbose", None) or os.environ.get("BINANCE_VERBOSE", ""))
|
||||
https_proxy = (getattr(settings, "https_proxy", None) or os.environ.get("https_proxy") or os.environ.get("HTTPS_PROXY")) or None
|
||||
|
||||
env_dry = os.environ.get("DRY_RUN", "").strip().lower()
|
||||
if env_dry in ("0", "false", "no"):
|
||||
dry_run = False
|
||||
@@ -394,11 +470,18 @@ async def main_async() -> None:
|
||||
testnet = "testnet" in str(base_url).lower()
|
||||
notional_threshold = float(getattr(settings, "notional_close_threshold", None) or 20)
|
||||
min_profit = float(getattr(settings, "close_min_profit", None) or 0.05)
|
||||
min_profit_large = float(getattr(settings, "close_min_profit_large", None) or 10)
|
||||
max_loss = float(getattr(settings, "close_max_loss", None) or 3)
|
||||
ws_connection_timeout = float(getattr(settings, "ws_connection_timeout", None) or 30)
|
||||
|
||||
client = await BinanceAsyncClient.create(
|
||||
# 使用合约接口初始化;recv_window 放宽时间窗,本机时间有偏差时可减少签名错误
|
||||
client = await create_futures_client(
|
||||
api_key=api_key,
|
||||
api_secret=api_secret
|
||||
api_secret=api_secret,
|
||||
testnet=testnet,
|
||||
recv_window=recv_window,
|
||||
verbose=verbose,
|
||||
https_proxy=https_proxy,
|
||||
)
|
||||
try:
|
||||
positions = await get_positions(client)
|
||||
@@ -422,6 +505,8 @@ async def main_async() -> None:
|
||||
by_symbol,
|
||||
notional_threshold=notional_threshold,
|
||||
min_profit=min_profit,
|
||||
min_profit_large=min_profit_large,
|
||||
max_loss=max_loss,
|
||||
)
|
||||
for symbol, sym_positions in by_symbol_filtered.items():
|
||||
long_n, short_n = get_notional_by_side(sym_positions)
|
||||
|
||||
Reference in New Issue
Block a user