优化平仓
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
币安永续合约定时平仓脚本。
|
币安永续合约定时平仓脚本。
|
||||||
|
|
||||||
平仓触发条件:
|
平仓触发条件(满足其一即平仓):
|
||||||
- 多空名义价值总和 ≤ 20 USDT 且未实现盈亏 > 0.05。
|
- 多空名义价值总和 ≤ 20 USDT 且未实现盈亏 > 0.05;
|
||||||
|
- 未实现盈利 > 10 USDT;
|
||||||
|
- 未实现亏损 > 3 USDT(即未实现盈亏 < -3)。
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
1. 获取当前持仓,得到需要平仓的交易对;
|
1. 获取当前持仓,得到需要平仓的交易对;
|
||||||
@@ -30,12 +32,14 @@ SHORT_CLOSE_PRICE_RATIO = Decimal("0.997")
|
|||||||
|
|
||||||
|
|
||||||
def round_to_step(value: Decimal, step: str) -> str:
|
def round_to_step(value: Decimal, step: str) -> str:
|
||||||
"""按 step 精度舍入。"""
|
"""按 step 精度舍入。返回普通小数/整数字符串,避免科学计数法(如 5E+1),否则币安 API 会报签名错误。"""
|
||||||
step_d = Decimal(step)
|
step_d = Decimal(step)
|
||||||
if step_d <= 0:
|
if step_d <= 0:
|
||||||
return str(value)
|
return str(value)
|
||||||
q = (value / step_d).quantize(Decimal("1"), rounding="ROUND_DOWN")
|
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:
|
def round_price_to_tick(price: Decimal, tick_size: str) -> str:
|
||||||
@@ -104,11 +108,20 @@ def should_close_symbol(
|
|||||||
unrealized_profit: float,
|
unrealized_profit: float,
|
||||||
notional_threshold: float = 20,
|
notional_threshold: float = 20,
|
||||||
min_profit: float = 0.05,
|
min_profit: float = 0.05,
|
||||||
|
min_profit_large: float = 10,
|
||||||
|
max_loss: float = 3,
|
||||||
) -> bool:
|
) -> 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
|
total_notional = long_notional + short_notional
|
||||||
return total_notional <= notional_threshold and unrealized_profit > min_profit
|
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]],
|
by_symbol: dict[str, list[dict]],
|
||||||
notional_threshold: float = 20,
|
notional_threshold: float = 20,
|
||||||
min_profit: float = 0.05,
|
min_profit: float = 0.05,
|
||||||
|
min_profit_large: float = 10,
|
||||||
|
max_loss: float = 3,
|
||||||
) -> tuple[set[str], dict[str, list[dict]]]:
|
) -> 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, by_symbol_filtered)。
|
||||||
"""
|
"""
|
||||||
symbols_to_close: set[str] = set()
|
symbols_to_close: set[str] = set()
|
||||||
@@ -131,6 +146,8 @@ def get_symbols_to_close(
|
|||||||
long_n, short_n, unrealized_profit,
|
long_n, short_n, unrealized_profit,
|
||||||
notional_threshold=notional_threshold,
|
notional_threshold=notional_threshold,
|
||||||
min_profit=min_profit,
|
min_profit=min_profit,
|
||||||
|
min_profit_large=min_profit_large,
|
||||||
|
max_loss=max_loss,
|
||||||
):
|
):
|
||||||
symbols_to_close.add(symbol)
|
symbols_to_close.add(symbol)
|
||||||
by_symbol_filtered = {k: v for k, v in by_symbol.items() if k in symbols_to_close}
|
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,
|
"quantity": quantity,
|
||||||
"price": price_str,
|
"price": price_str,
|
||||||
}
|
}
|
||||||
|
# 双向持仓(对冲)模式必须传 positionSide,否则报 -4061
|
||||||
if side_upper in ("LONG", "SHORT"):
|
if side_upper in ("LONG", "SHORT"):
|
||||||
params["positionSide"] = side_upper
|
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":
|
if reduce_only or side_upper == "BOTH":
|
||||||
params["reduceOnly"] = "true"
|
params["reduceOnly"] = "true"
|
||||||
return params
|
return params
|
||||||
@@ -222,6 +243,57 @@ def build_close_order_params(
|
|||||||
|
|
||||||
# ---------- I/O 与编排 ----------
|
# ---------- 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]:
|
async def get_symbol_precision(client: BinanceAsyncClient) -> dict[str, dict]:
|
||||||
"""获取合约的 quantity/price 精度。"""
|
"""获取合约的 quantity/price 精度。"""
|
||||||
info = await client.futures_exchange_info()
|
info = await client.futures_exchange_info()
|
||||||
@@ -372,14 +444,18 @@ async def main_async() -> None:
|
|||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
api_key = (getattr(settings, "binance_api_key", None) or "").strip()
|
api_key = getattr(settings, "binance_api_key", None) or ""
|
||||||
api_secret = (getattr(settings, "binance_api_secret", None) or "").strip()
|
api_secret = getattr(settings, "binance_api_secret", None) or ""
|
||||||
if not api_key or not api_secret:
|
if not (api_key and api_secret):
|
||||||
logger.error("请在 .secrets.toml 或环境变量中设置 binance_api_key / binance_api_secret")
|
logger.error("请在 .secrets.toml 或环境变量中设置 binance_api_key / binance_api_secret")
|
||||||
return
|
return
|
||||||
|
|
||||||
import os
|
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()
|
env_dry = os.environ.get("DRY_RUN", "").strip().lower()
|
||||||
if env_dry in ("0", "false", "no"):
|
if env_dry in ("0", "false", "no"):
|
||||||
dry_run = False
|
dry_run = False
|
||||||
@@ -394,11 +470,18 @@ async def main_async() -> None:
|
|||||||
testnet = "testnet" in str(base_url).lower()
|
testnet = "testnet" in str(base_url).lower()
|
||||||
notional_threshold = float(getattr(settings, "notional_close_threshold", None) or 20)
|
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 = 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)
|
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_key=api_key,
|
||||||
api_secret=api_secret
|
api_secret=api_secret,
|
||||||
|
testnet=testnet,
|
||||||
|
recv_window=recv_window,
|
||||||
|
verbose=verbose,
|
||||||
|
https_proxy=https_proxy,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
positions = await get_positions(client)
|
positions = await get_positions(client)
|
||||||
@@ -422,6 +505,8 @@ async def main_async() -> None:
|
|||||||
by_symbol,
|
by_symbol,
|
||||||
notional_threshold=notional_threshold,
|
notional_threshold=notional_threshold,
|
||||||
min_profit=min_profit,
|
min_profit=min_profit,
|
||||||
|
min_profit_large=min_profit_large,
|
||||||
|
max_loss=max_loss,
|
||||||
)
|
)
|
||||||
for symbol, sym_positions in by_symbol_filtered.items():
|
for symbol, sym_positions in by_symbol_filtered.items():
|
||||||
long_n, short_n = get_notional_by_side(sym_positions)
|
long_n, short_n = get_notional_by_side(sym_positions)
|
||||||
|
|||||||
Reference in New Issue
Block a user