From 3ad9ff6231b9741bf0c0f19202d24a9b0018175e Mon Sep 17 00:00:00 2001 From: yhydev Date: Wed, 4 Feb 2026 00:52:19 +0800 Subject: [PATCH] =?UTF-8?q?position=5Fcloser:=20=E5=B9=B3=E4=BB=93?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E4=B8=8E=E6=97=A5=E5=BF=97=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 平仓条件改为:多空名义≤20 USDT 且未实现盈亏>0.05 - 去掉总盈亏前置条件及 Redis 盈亏历史相关逻辑 - 每个合约打印未实现盈亏、多头/空头名义(按未实现盈亏排序) Co-authored-by: Cursor --- position_closer.py | 191 +++++++++------------------------------------ 1 file changed, 35 insertions(+), 156 deletions(-) diff --git a/position_closer.py b/position_closer.py index e0719a8..69ffb7d 100644 --- a/position_closer.py +++ b/position_closer.py @@ -1,18 +1,12 @@ """ 币安永续合约定时平仓脚本。 -平仓前置条件(不满足则直接退出,不执行平仓): -- 当前总未实现盈亏 - 近 N 分钟最小总未实现盈亏 > 配置阈值(默认 2 USDT);未实现盈亏历史存 Redis Sorted Set。 - -平仓触发条件(满足其一即平仓): -- 在 Redis 指定 Set 中的合约强制平仓; -- 小仓位:多空名义价值总和 < 30 USDT 且未实现盈亏 > 0.03; -- 大仓位:多空名义价值总和 > 50 USDT 且未实现盈亏 > 0.3。 +平仓触发条件: +- 多空名义价值总和 ≤ 20 USDT 且未实现盈亏 > 0.05。 流程: -1. 获取当前总未实现盈亏,写入 Redis 并取近 N 分钟最小;若不满足「当前 - 最小 > 阈值」则退出; -2. 获取当前持仓,得到需要平仓的交易对; -3. 创建 BinanceSocketManager,监听需平仓合约,收到 WS 事件后按规则创建平仓订单。 +1. 获取当前持仓,得到需要平仓的交易对; +2. 创建 BinanceSocketManager,监听需平仓合约,收到 WS 事件后按规则创建平仓订单。 平仓规则:多头限价卖出 = 当前价 * 1.003,空头限价买入 = 当前价 * 0.997。 纯逻辑为顶层函数,便于单元测试(from position_closer import round_to_step, ...); @@ -108,50 +102,35 @@ def should_close_symbol( long_notional: float, short_notional: float, unrealized_profit: float, - threshold: float, - in_redis_set: bool = False, - small_threshold: float = 30, - small_min_profit: float = 0.03, - large_min_profit: float = 0.3, + notional_threshold: float = 20, + min_profit: float = 0.05, ) -> bool: """ 是否对该合约执行平仓。 - 条件(满足其一即平仓): - - 在 Redis 集合中; - - 多空名义价值总和 < small_threshold 且 未实现盈亏 > small_min_profit(小仓位); - - 多空名义价值总和 > threshold 且 未实现盈亏 > large_min_profit(大仓位)。 + 条件:多空名义价值总和 ≤ notional_threshold 且 未实现盈亏 > min_profit。 """ - if in_redis_set: - return True total_notional = long_notional + short_notional - if total_notional < small_threshold and unrealized_profit > small_min_profit: - return True - if total_notional > threshold and unrealized_profit > large_min_profit: - return True - return False + return total_notional <= notional_threshold and unrealized_profit > min_profit def get_symbols_to_close( by_symbol: dict[str, list[dict]], - threshold: float, - redis_contracts: set[str], - small_threshold: float = 30, - small_min_profit: float = 0.03, - large_min_profit: float = 0.3, + notional_threshold: float = 20, + min_profit: float = 0.05, ) -> tuple[set[str], dict[str, list[dict]]]: """ - 根据持仓和 Redis 集合,得到需要平仓的交易对及其持仓子集。 - 平仓条件:在 Redis 中 或 (小仓位且盈利>small_min_profit) 或 (大仓位且盈利>large_min_profit)。 + 根据持仓得到需要平仓的交易对及其持仓子集。 + 平仓条件:多空名义价值总和 ≤ notional_threshold 且 未实现盈亏 > min_profit。 返回 (symbols_to_close, by_symbol_filtered)。 """ symbols_to_close: set[str] = set() for symbol, sym_positions in by_symbol.items(): long_n, short_n = get_notional_by_side(sym_positions) unrealized_profit = get_unrealized_profit(sym_positions) - in_redis = symbol in redis_contracts if should_close_symbol( - long_n, short_n, unrealized_profit, threshold, in_redis, - small_threshold, small_min_profit, large_min_profit, + long_n, short_n, unrealized_profit, + notional_threshold=notional_threshold, + min_profit=min_profit, ): symbols_to_close.add(symbol) by_symbol_filtered = {k: v for k, v in by_symbol.items() if k in symbols_to_close} @@ -249,16 +228,6 @@ async def get_symbol_precision(client: BinanceAsyncClient) -> dict[str, dict]: return parse_exchange_info_to_precisions(info) -async def get_total_unrealized_profit(client: BinanceAsyncClient) -> float: - """获取账户永续合约总未实现盈亏(所有持仓的 unrealizedProfit 之和)。""" - account = await client.futures_account() - positions = account.get("positions", []) - total = 0.0 - for p in positions: - total += float(p.get("unrealizedProfit", 0) or 0) - return total - - async def get_positions(client: BinanceAsyncClient) -> list[dict]: """获取永续合约持仓(只保留有仓位的)。""" account = await client.futures_account() @@ -266,65 +235,6 @@ async def get_positions(client: BinanceAsyncClient) -> list[dict]: return filter_nonzero_positions(positions) -async def save_unrealized_profit_history( - redis_url: str, - history_key: str, - profit: float, - window_seconds: float, -) -> None: - """ - 将当前总未实现盈亏写入 Redis Sorted Set(score=时间戳, member=时间戳:盈亏值), - 并删除早于 (now - window_seconds) 的旧数据。 - """ - import time - from redis.asyncio import Redis as Aioredis - - now = time.time() - member = f"{now}:{profit}" - redis_client = Aioredis.from_url(redis_url, decode_responses=True) - try: - await redis_client.zadd(history_key, {member: now}) - await redis_client.zremrangebyscore(history_key, "-inf", f"({now - window_seconds}") - finally: - await redis_client.aclose() - - -async def get_min_unrealized_profit_in_window( - redis_url: str, - history_key: str, - window_seconds: float, -) -> float | None: - """ - 从 Redis Sorted Set 中取近 window_seconds 秒内记录的最小未实现盈亏。 - 无数据或解析失败时返回 None。 - """ - import time - from redis.asyncio import Redis as Aioredis - - now = time.time() - min_score = now - window_seconds - redis_client = Aioredis.from_url(redis_url, decode_responses=True) - try: - members = await redis_client.zrangebyscore( - history_key, min_score, now, start=0, num=-1 - ) - finally: - await redis_client.aclose() - - if not members: - return None - values: list[float] = [] - for m in members: - # member 格式为 "timestamp:profit" - parts = str(m).split(":", 1) - if len(parts) == 2: - try: - values.append(float(parts[1])) - except ValueError: - continue - return min(values) if values else None - - async def place_close_order( client: BinanceAsyncClient, symbol: str, @@ -497,64 +407,33 @@ async def main_async() -> None: base_url = getattr(settings, "binance_base_url", None) or "https://fapi.binance.com" testnet = "testnet" in str(base_url).lower() redis_url = getattr(settings, "redis_url", None) or "redis://localhost:6379/0" - threshold = float(getattr(settings, "notional_threshold", None) or 50) - small_threshold = float(getattr(settings, "notional_small_close_threshold", None) or 30) - small_min_profit = float(getattr(settings, "small_close_min_profit", None) or 0.03) - large_min_profit = float(getattr(settings, "notional_large_close_min_profit", None) or 0.3) + notional_threshold = float(getattr(settings, "notional_close_threshold", None) or 20) + min_profit = float(getattr(settings, "close_min_profit", None) or 0.05) redis_key = getattr(settings, "redis_close_key", None) or "close_position:contracts" ws_connection_timeout = float(getattr(settings, "ws_connection_timeout", None) or 30) - redis_profit_history_key = ( - getattr(settings, "redis_unrealized_profit_history_key", None) - or "close_position:unrealized_profit_history" - ) - profit_window_seconds = float( - getattr(settings, "unrealized_profit_window_seconds", None) or 300 - ) - profit_min_rise = float( - getattr(settings, "unrealized_profit_min_rise", None) or 2 - ) client = await BinanceAsyncClient.create( api_key=api_key, api_secret=api_secret ) try: - total_profit = await get_total_unrealized_profit(client) - await save_unrealized_profit_history( - redis_url, redis_profit_history_key, total_profit, profit_window_seconds - ) - min_profit = await get_min_unrealized_profit_in_window( - redis_url, redis_profit_history_key, profit_window_seconds - ) - if min_profit is None: - logger.info( - "近 {} 秒内无未实现盈亏历史,本次不执行平仓(已记录当前总未实现盈亏 {:.2f})", - profit_window_seconds, - total_profit, - ) - return - if total_profit - min_profit <= profit_min_rise: - logger.info( - "未满足平仓前置条件:当前总未实现盈亏 {:.2f} - 近 {} 秒最小 {:.2f} = {:.2f} <= {},退出", - total_profit, - profit_window_seconds, - min_profit, - total_profit - min_profit, - profit_min_rise, - ) - return - logger.info( - "满足平仓前置条件:当前总未实现盈亏 {:.2f},近 {} 秒最小 {:.2f},差值 {:.2f} > {}", - total_profit, - profit_window_seconds, - min_profit, - total_profit - min_profit, - profit_min_rise, - ) - positions = await get_positions(client) by_symbol = group_positions_by_symbol(positions) + logger.info("当前持仓合约数: {}", len(by_symbol)) + by_symbol_sorted = sorted( + by_symbol.items(), + key=lambda x: get_unrealized_profit(x[1]), + reverse=True, + ) + for symbol, sym_positions in by_symbol_sorted: + unrealized = get_unrealized_profit(sym_positions) + long_n, short_n = get_notional_by_side(sym_positions) + logger.info( + "合约 {} 未实现盈亏={:.2f} 多头名义={:.2f} 空头名义={:.2f}", + symbol, unrealized, long_n, short_n, + ) + try: redis_client = Aioredis.from_url(redis_url, decode_responses=True) redis_contracts = set(await redis_client.smembers(redis_key) or []) @@ -564,19 +443,19 @@ async def main_async() -> None: redis_contracts = set() symbols_to_close, by_symbol_filtered = get_symbols_to_close( - by_symbol, threshold, redis_contracts, - small_threshold, small_min_profit, large_min_profit, + by_symbol, + notional_threshold=notional_threshold, + min_profit=min_profit, ) for symbol, sym_positions in by_symbol_filtered.items(): long_n, short_n = get_notional_by_side(sym_positions) profit = get_unrealized_profit(sym_positions) logger.info( - "需平仓: symbol={} long_notional={:.2f} short_notional={:.2f} unrealized_profit={:.2f} in_redis={}", + "需平仓: symbol={} long_notional={:.2f} short_notional={:.2f} unrealized_profit={:.2f}", symbol, long_n, short_n, profit, - symbol in redis_contracts, ) if not symbols_to_close: