diff --git a/backend/database/add_market_cache.sql b/backend/database/add_market_cache.sql new file mode 100644 index 0000000..5067330 --- /dev/null +++ b/backend/database/add_market_cache.sql @@ -0,0 +1,13 @@ +-- 市场缓存表:存放较固定的交易所数据(交易对信息、资金费率规则等),减少 API 调用 +-- 执行: mysql -u root -p auto_trade_sys < add_market_cache.sql + +USE `auto_trade_sys`; + +CREATE TABLE IF NOT EXISTS `market_cache` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `cache_key` VARCHAR(128) NOT NULL COMMENT '如 exchange_info, funding_info', + `cache_value` LONGTEXT NOT NULL COMMENT 'JSON 内容', + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY `uk_cache_key` (`cache_key`), + INDEX `idx_updated_at` (`updated_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场数据缓存(交易对/费率规则等)'; diff --git a/backend/database/models.py b/backend/database/models.py index 370a623..410ae8d 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -360,6 +360,128 @@ class TradingConfig: return str(value) +# 缓存键常量(与 market_cache 表配合使用) +MARKET_CACHE_KEY_EXCHANGE_INFO = "exchange_info" +MARKET_CACHE_KEY_FUNDING_INFO = "funding_info" + + +class MarketCache: + """ + 市场数据缓存:交易对信息(exchange_info)、资金费率规则(funding_info) 等较固定内容入库, + 减少对币安 API 的调用。trading_system 可优先从 DB 读取,过期或缺失时再拉 API 并回写。 + """ + @staticmethod + def _table_exists(): + try: + db.execute_one("SELECT 1 FROM market_cache LIMIT 1") + return True + except Exception: + return False + + @staticmethod + def get(cache_key: str): + """获取缓存:返回 dict { cache_value: 解析后的对象, updated_at } 或 None。""" + if not MarketCache._table_exists(): + return None + try: + row = db.execute_one( + "SELECT cache_value, updated_at FROM market_cache WHERE cache_key = %s", + (cache_key,), + ) + if not row: + return None + raw = row.get("cache_value") + updated_at = row.get("updated_at") + if raw is None: + return None + try: + value = json.loads(raw) if isinstance(raw, str) else raw + except Exception: + value = raw + return {"cache_value": value, "updated_at": updated_at} + except Exception as e: + logger.debug("MarketCache.get %s: %s", cache_key, e) + return None + + @staticmethod + def set(cache_key: str, value) -> bool: + """写入缓存,value 可为 dict/list(将序列化为 JSON)。""" + if not MarketCache._table_exists(): + return False + try: + payload = json.dumps(value, ensure_ascii=False) if not isinstance(value, str) else value + db.execute_update( + """INSERT INTO market_cache (cache_key, cache_value) + VALUES (%s, %s) + ON DUPLICATE KEY UPDATE cache_value = VALUES(cache_value), updated_at = CURRENT_TIMESTAMP""", + (cache_key, payload), + ) + return True + except Exception as e: + logger.warning("MarketCache.set %s: %s", cache_key, e) + return False + + @staticmethod + def get_exchange_info(max_age_seconds: int = 86400): + """ + 获取缓存的 exchange_info(币安 GET /fapi/v1/exchangeInfo)。 + 若缓存存在且 updated_at 在 max_age_seconds 内,返回解析后的 exchange_info 字典; + 否则返回 None(调用方应拉 API 并调用 set_exchange_info 回写)。 + """ + out = MarketCache.get(MARKET_CACHE_KEY_EXCHANGE_INFO) + if not out: + return None + updated_at = out.get("updated_at") + if max_age_seconds is not None and updated_at: + try: + from datetime import datetime, timezone, timedelta + if isinstance(updated_at, (int, float)): + utc_ts = float(updated_at) + else: + # datetime 转 UTC 时间戳 + utc_ts = updated_at.timestamp() if hasattr(updated_at, "timestamp") else 0 + age = datetime.now(timezone.utc).timestamp() - utc_ts + if age > max_age_seconds: + return None + except Exception: + pass + return out.get("cache_value") + + @staticmethod + def set_exchange_info(data: dict) -> bool: + """写入 exchange_info 到 market_cache。""" + return MarketCache.set(MARKET_CACHE_KEY_EXCHANGE_INFO, data) + + @staticmethod + def get_funding_info(max_age_seconds: int = 86400): + """ + 获取缓存的 funding_info(币安 GET /fapi/v1/fundingInfo)。 + 若缓存存在且在 max_age_seconds 内,返回解析后的列表/字典;否则返回 None。 + """ + out = MarketCache.get(MARKET_CACHE_KEY_FUNDING_INFO) + if not out: + return None + updated_at = out.get("updated_at") + if max_age_seconds is not None and updated_at: + try: + from datetime import datetime, timezone + if isinstance(updated_at, (int, float)): + utc_ts = float(updated_at) + else: + utc_ts = updated_at.timestamp() if hasattr(updated_at, "timestamp") else 0 + age = datetime.now(timezone.utc).timestamp() - utc_ts + if age > max_age_seconds: + return None + except Exception: + pass + return out.get("cache_value") + + @staticmethod + def set_funding_info(data) -> bool: + """写入 funding_info 到 market_cache。""" + return MarketCache.set(MARKET_CACHE_KEY_FUNDING_INFO, data) + + class GlobalStrategyConfig: """全局策略配置模型(独立于账户,管理员专用)""" diff --git a/docs/后续方向实施说明.md b/docs/后续方向实施说明.md new file mode 100644 index 0000000..8fa8ceb --- /dev/null +++ b/docs/后续方向实施说明.md @@ -0,0 +1,58 @@ +# 后续方向实施说明 + +本文档说明「策略与信号评估及改进建议」中未实现或部分实现的方向,以及固定内容入库的用法。 + +--- + +## 一、已完成的改动 + +### 1. 固定内容入库(交易对 / 费率规则) + +- **表**:`market_cache`(见 `backend/database/add_market_cache.sql`) + - 字段:`cache_key`(如 `exchange_info`、`funding_info`)、`cache_value`(JSON)、`updated_at` +- **模型**:`backend/database/models.py` 中的 `MarketCache` + - `get_exchange_info(max_age_seconds=86400)` / `set_exchange_info(data)` + - `get_funding_info(max_age_seconds=86400)` / `set_funding_info(data)` +- **使用**:`trading_system/binance_client.py` + - `get_all_usdt_pairs()`:优先从 DB 读 `exchange_info`(24 小时内有效),命中则直接解析返回;否则请求 API 并回写 DB。 + - `get_symbol_info(symbol)`:在内存/Redis 未命中时,尝试从 DB 的 `exchange_info` 解析该 symbol;API 拉取后也会回写 DB。 + +**部署**:执行 `mysql -u root -p auto_trade_sys < backend/database/add_market_cache.sql` 创建表。无需改配置即可生效。 + +### 2. 扫描阶段资金费率过滤 + +- **配置**(`trading_system/config.py` 与 DB `trading_config`): + - `SCAN_FUNDING_RATE_FILTER_ENABLED`:是否启用(默认 True) + - `SCAN_FUNDING_RATE_MAX_ABS`:阈值,默认 0.001(即 \|lastFundingRate\| > 0.1% 的标的从扫描候选剔除) +- **逻辑**:在 `market_scanner.scan_market()` 中,在得到「初步筛选后」的 `pre_filtered_symbols` 后,若启用则调用 `get_premium_index(None)` 取全量费率,剔除 \|lastFundingRate\| > 阈值的 symbol,再进入详细分析(K 线/技术指标)。 + +--- + +## 二、建议实现顺序(未实现部分) + +| 顺序 | 方向 | 说明 | +|------|----------------|------| +| 1 | 跟踪止损 | 浮盈达到 X% 后,用 `TRAILING_STOP_MARKET` 替代或补充固定止盈;配置:`TRAILING_STOP_ENABLED`、`TRAILING_ACTIVATION_PCT`、`TRAILING_CALLBACK_RATE`;在 position_manager 挂完固定 SL/TP 后,达标则下跟踪止损单。 | +| 2 | 时间/无盈利止损 | 持仓时长 > N 小时且浮盈 < 某阈值(或浮亏)→ 市价平仓或提高止损优先级;配置:`MAX_HOLD_HOURS`、`MIN_PNL_PCT_TO_HOLD`;在 position_manager 的检查循环里按 `entry_time` 与当前 pnl% 判断。 | +| 3 | WS 订单/成交优化 | 下单后根据 ORDER_TRADE_UPDATE/TRADE 做快速反馈、部分成交后的加仓/减仓逻辑;实现成本较高,可排在跟踪止损和时间止损之后。 | + +--- + +## 三、跟踪止损(待实现)要点 + +- **币安**:条件单类型 `TRAILING_STOP_MARKET`,需 `callbackRate`、`activationPrice`(或由系统按比例计算)。 +- **配置建议**:与现有 `USE_TRAILING_STOP`、`TRAILING_STOP_ACTIVATION`、`TRAILING_STOP_PROTECT` 对齐,或新增 `TRAILING_ACTIVATION_PCT`、`TRAILING_CALLBACK_RATE` 从 config/DB 读取。 +- **位置**:`trading_system/position_manager.py` 中,在挂完固定 SL/TP 后(或定时检查持仓时),若启用且当前浮盈% ≥ 激活比例,则下 `TRAILING_STOP_MARKET`;若已有跟踪止损单则跳过。 + +--- + +## 四、时间/无盈利止损(待实现)要点 + +- **逻辑**:在 position_manager 的持仓检查循环中,读取每仓的 `entry_time` 和当前 pnl%;若 `持仓时长 > MAX_HOLD_HOURS` 且 `pnl_percent < MIN_PNL_PCT_TO_HOLD`(或为负),则触发市价平仓或标记为「建议平仓」。 +- **配置**:`MAX_HOLD_HOURS`、`MIN_PNL_PCT_TO_HOLD`,从 config/DB 读。 + +--- + +## 五、手续费等固定费率(可选) + +若后续要做「预估盈亏」或「最小盈利目标」,可将 maker/taker 费率存入 config 或单独表(如 `symbol_fee`),在算目标价/预期收益时读取。当前未实现。 diff --git a/docs/策略与信号评估及改进建议.md b/docs/策略与信号评估及改进建议.md new file mode 100644 index 0000000..abeee75 --- /dev/null +++ b/docs/策略与信号评估及改进建议.md @@ -0,0 +1,82 @@ +# 策略与信号评估及改进建议 + +基于持仓记录(2026-02-16)、当前策略逻辑与币安接口文档,对「长时间持仓、盈利不明显」做简要诊断,并给出可落地的改进方向。 + +--- + +## 一、持仓与信号现状简要诊断 + +### 1. 持仓样本(来自 持仓记录_2026-02-16) + +- **数量**:约 12 个持仓,多空混合(BUY 少、SELL 多)。 +- **盈亏分布**:部分有 +15%、+13%、+9% 等浮盈,多数在 -5%~+2% 区间横盘,整体「拿得久、盈利不突出」。 +- **共性**: + - 入场依赖 1H/4H 技术信号(MACD + EMA + 4H 趋势),**无资金费率、多空结构、订单流等过滤**,容易在「结构不利」时开仓。 + - 止损/止盈为固定比例或 ATR,**无跟踪止损、无时间止损**,盈利单容易回吐,亏损单容易长期挂单。 + - 选币主要按 24h 涨跌幅 + 成交量 + signal_strength 排序,**未用资金费率、大户多空比、主动买卖量等** 做过滤或排序。 + +### 2. 当前信号与选单逻辑(简要) + +- **扫描**:24h 涨跌幅 ≥ 阈值 + 成交量 ≥ MIN_VOLUME_24H_STRICT → 对每个 symbol 拉 1H/4H K 线 → 算 RSI、MACD、EMA、4H 趋势 → 算 **signal_strength**(0–10),按强度排序取 TOP_N。 +- **开仓条件**:signal_strength ≥ MIN_SIGNAL_STRENGTH(8)、4H 趋势不逆势、RSI/24h 涨跌幅过滤、短周期方向过滤、大盘 Beta 过滤等。 +- **问题点**: + - 信号完全来自 K 线技术指标,**没有合约特有的「成本与结构」信息**(资金费率、多空比、主动买卖)→ 容易在费率极高或多空极度拥挤时进场。 + - 入场时机只看扫描周期(如 15 分钟一次),**没有用 WS 订单/成交推送做更细的时机优化**。 + - 出场只有固定 SL/TP,**没有跟踪止损、没有「持仓时间 + 无盈利」类时间止损**。 + +--- + +## 二、基于现有接口的改进方向(可落地) + +### 1. 资金费率过滤(已接入 REST,建议**优先上**) + +- **接口**:`GET /fapi/v1/premiumIndex`(已封装为 `get_premium_index(symbol)`),含 `lastFundingRate`。 +- **思路**: + - **做多**:若 `lastFundingRate > 某上限`(例如 0.001,即 0.1%),说明多头付费高,开多成本高、易被费率侵蚀 → **跳过开多**。 + - **做空**:若 `lastFundingRate < 某下限`(例如 -0.001),说明空头付费高 → **跳过开空**。 +- **效果**:减少在「费率极端不利于己方」时开仓,提高单笔期望收益;配合现有技术信号,不改变主逻辑,只做过滤。 +- **实现**:在策略层开仓前增加「资金费率过滤」步骤(见下节代码)。 + +### 2. 大户多空比 / 主动买卖量(已接入 REST,可选增强) + +- **接口**:`get_top_long_short_position_ratio`、`get_taker_long_short_ratio`(或账户数多空比)。 +- **思路**: + - **做多**:优先选择「近期主动买入占比高」或「大户多空比」偏多的标的(顺势);或极端多头拥挤时做「逆势空」需谨慎、可提高信号门槛。 + - **做空**:优先选择「主动卖出占比高」或大户偏空的标的。 +- **实现**:可在扫描后、或策略决定开仓前,对候选 symbol 取最近 1 条 taker 或大户多空比,与方向一致则通过、相反则降权或跳过(可选,先上资金费率再考虑)。 + +### 3. 订单/成交推送(WS)对「下单时机」的改进 + +- **文档**:订单交易更新推送(ORDER_TRADE_UPDATE)、条件单更新(ALGO_UPDATE)、账户信息流等。 +- **思路**: + - 已有 User Data Stream 时,**订单状态、成交回报**已经实时;可在此基础上做「下单后快速反馈、部分成交/全部成交的后续动作」。 + - 若引入 **aggTrade / depth** 等行情 WS:可在「信号已满足」时,结合最近几笔大单或盘口变化再点一次火,减少「信号刚满足就追在尖顶/尖底」的情况(实现成本较高,可作为二期)。 + +### 4. 止盈/止损与持仓管理 + +- **跟踪止损**:文档支持 `TRAILING_STOP_MARKET`(激活价、回调比例)。可在「浮盈达到 X% 后」改为跟踪止损,锁定利润、让利润奔跑。 +- **时间止损**:若持仓超过 N 小时且浮盈 < 某阈值(或浮亏),可考虑平仓释放保证金,避免长期无效挂单(需在 position_manager 或策略层加「持仓时长」判断)。 +- **分批止盈**:当前有 take_profit_1/2 等设计;可结合 ATR 或固定比例,先平一部分、留一部分用跟踪止损(逻辑已有基础,可逐步细化)。 + +### 5. 扫描与选币 + +- **资金费率**:扫描阶段可排除「费率极端」的标的(例如 |lastFundingRate| > 0.001),避免进入 TOP_N 的 symbol 本身就不适合当前方向。 +- **成交量/流动性**:继续用 24h 成交量 + 深度(若有)保证可执行性;深度接口已接入,可用于大单前的冲击成本估算。 +- **多空比/主动买卖**:作为排序加分项(与方向一致则排前),而不是硬过滤,避免过度减少开仓次数。 + +--- + +## 三、建议实施顺序 + +1. **立即**:在开仓前增加**资金费率过滤**(做多/做空各一个阈值),并加配置开关与日志。 +2. **短期**:在配置中提供**跟踪止损**开关与参数(如激活盈利率、回调比例),对已有持仓在达到条件时切到跟踪止损(或先对部分新开仓做实验)。 +3. **中期**:在扫描或策略中引入**大户多空比 / taker 买卖比**,作为过滤或排序加分,并观察胜率与频率变化。 +4. **可选**:结合 WS 订单/成交与行情(aggTrade/depth)做更细的入场时机优化;以及「持仓时间 + 无盈利」的时间止损。 + +--- + +## 四、小结 + +- **现状**:信号与选单主要依赖 K 线技术指标和 4H 趋势,缺少「资金费率、多空结构、订单流」等维度,且出场方式单一,容易导致持仓时间长、盈利不明显。 +- **改进**:在**不推翻现有技术信号**的前提下,优先用**资金费率**做开仓过滤,再用**多空比/主动买卖**做过滤或排序,并逐步加上**跟踪止损**和**时间/无盈利止损**,有望在控制回撤的同时提高单笔质量与资金效率。 +- 上述改进均可基于你已接入的 REST 接口(premiumIndex、fundingRate、topLongShort、takerlongshortRatio 等)和现有 WS 文档实现;具体参数(费率阈值、跟踪止损比例等)可在实盘或回测中微调。 diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 17b697e..8657958 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -5,7 +5,9 @@ import asyncio import json import logging import random +import sys import time +from pathlib import Path from typing import Dict, List, Optional, Any from binance import AsyncClient, BinanceSocketManager from binance.exceptions import BinanceAPIException @@ -19,6 +21,92 @@ except ImportError: logger = logging.getLogger(__name__) +# 交易对/费率等固定内容优先从 DB 读取(market_cache 表),减少 API 调用 +def _get_market_cache(): + """延迟导入 MarketCache(需 backend 在 sys.path)。失败返回 None。""" + try: + backend_path = Path(__file__).resolve().parent.parent / "backend" + if backend_path.exists() and str(backend_path) not in sys.path: + sys.path.insert(0, str(backend_path)) + from database.models import MarketCache + return MarketCache + except Exception as e: + logger.debug("MarketCache 不可用: %s", e) + return None + + +def _load_exchange_info_from_db(max_age_seconds: int = 86400): + """同步从 DB 读取 exchange_info(供 run_in_executor 调用)。""" + mc = _get_market_cache() + if not mc: + return None + return mc.get_exchange_info(max_age_seconds=max_age_seconds) + + +def _save_exchange_info_to_db(data: dict) -> bool: + """同步将 exchange_info 写入 DB。""" + mc = _get_market_cache() + if not mc: + return False + return mc.set_exchange_info(data) + + +def _parse_usdt_pairs_from_exchange_info(exchange_info: dict) -> tuple: + """从 exchange_info 解析 USDT 永续列表和 display_to_api 映射。返回 (usdt_pairs, display_to_api_symbol).""" + if not exchange_info or not isinstance(exchange_info.get("symbols"), list): + return [], {} + usdt_pairs = [] + display_to_api = {} + for s in exchange_info["symbols"]: + if not (s.get("symbol", "").endswith("USDT") and s.get("status") == "TRADING" and s.get("contractType") == "PERPETUAL"): + continue + sym = s["symbol"] + if sym.isascii(): + usdt_pairs.append(sym) + continue + api_sym = (s.get("baseAsset") or "") + (s.get("quoteAsset") or "") + if api_sym and api_sym.isascii(): + usdt_pairs.append(api_sym) + display_to_api[sym] = api_sym + else: + usdt_pairs.append(sym) + return usdt_pairs, display_to_api + + +def _parse_symbol_info_from_exchange_info(exchange_info: dict, symbol: str) -> Optional[Dict]: + """从 exchange_info 中解析单个 symbol 的精度/限制信息,与 get_symbol_info 返回格式一致。""" + if not exchange_info or not isinstance(exchange_info.get("symbols"), list): + return None + for s in exchange_info["symbols"]: + if s.get("symbol") != symbol: + continue + quantity_precision = s.get("quantityPrecision", 8) + price_precision = s.get("pricePrecision", 8) + min_qty, step_size, min_notional, tick_size = None, None, None, None + for f in s.get("filters", []): + if f.get("filterType") == "LOT_SIZE": + min_qty = float(f.get("minQty", 0)) + step_size = float(f.get("stepSize", 0)) + elif f.get("filterType") == "PRICE_FILTER": + tick_size = float(f.get("tickSize", 0) or 0) + elif f.get("filterType") == "MIN_NOTIONAL": + min_notional = float(f.get("notional", 0)) + if min_notional is None or min_notional == 0: + min_notional = 5.0 + max_leverage_supported = 125 + if s.get("leverageBracket") and len(s.get("leverageBracket", [])) > 0: + max_leverage_supported = s["leverageBracket"][0].get("maxLeverage", 125) + return { + "quantityPrecision": quantity_precision, + "pricePrecision": price_precision, + "minQty": min_qty or 0, + "stepSize": step_size or 0, + "tickSize": tick_size or 0, + "minNotional": min_notional, + "maxLeverage": int(max_leverage_supported), + } + return None + def _format_exception(e: Exception) -> str: """格式化异常用于日志,避免空 message 导致日志无内容""" @@ -458,7 +546,8 @@ class BinanceClient: async def get_all_usdt_pairs(self, max_retries: int = 3, timeout: int = 30) -> List[str]: """ 获取所有USDT交易对 - 添加超时处理和重试机制,避免推荐系统因网络超时中断 + 优先从 DB market_cache 读取 exchange_info(24 小时内有效),减少 API 调用。 + 添加超时处理和重试机制,避免推荐系统因网络超时中断。 Args: max_retries: 最大重试次数(默认3次) @@ -467,10 +556,24 @@ class BinanceClient: Returns: USDT交易对列表(失败时返回空列表) """ + loop = asyncio.get_event_loop() + # 优先从 DB 读取(同步调用放至线程池) + try: + exchange_info = await loop.run_in_executor(None, lambda: _load_exchange_info_from_db(86400)) + if exchange_info and isinstance(exchange_info.get("symbols"), list): + usdt_pairs, display_to_api = _parse_usdt_pairs_from_exchange_info(exchange_info) + if usdt_pairs: + self._display_to_api_symbol.clear() + self._display_to_api_symbol.update(display_to_api) + if display_to_api: + logger.info(f"已映射 {len(display_to_api)} 个中文/非ASCII交易对到英文 symbol") + logger.info(f"从 DB 缓存获取到 {len(usdt_pairs)} 个USDT永续合约交易对") + return usdt_pairs + except Exception as e: + logger.debug("从 DB 读取 exchange_info 失败,将请求 API: %s", e) + for attempt in range(1, max_retries + 1): try: - # 使用 _rate_limited_request 包装请求,添加速率限制 - # 同时使用 asyncio.wait_for 添加超时处理 exchange_info = await asyncio.wait_for( self._rate_limited_request( 'futures_exchange_info', @@ -478,35 +581,17 @@ class BinanceClient: ), timeout=timeout ) - - # 支持中文名交易对:若 symbol 含非 ASCII,尝试用 baseAsset+quoteAsset 作为 API 下单用 symbol - # 如果 baseAsset+quoteAsset 也是非 ASCII,则直接使用原始 symbol + usdt_pairs, display_to_api = _parse_usdt_pairs_from_exchange_info(exchange_info) self._display_to_api_symbol.clear() - usdt_pairs = [] - for s in exchange_info['symbols']: - if not (s['symbol'].endswith('USDT') and s['status'] == 'TRADING' and s.get('contractType') == 'PERPETUAL'): - continue - sym = s['symbol'] - - # 1. 如果是 ASCII,直接使用 - if sym.isascii(): - usdt_pairs.append(sym) - continue - - # 2. 如果非 ASCII,尝试构建英文 symbol (base+quote) - api_sym = (s.get('baseAsset') or '') + (s.get('quoteAsset') or '') - if api_sym and api_sym.isascii(): - usdt_pairs.append(api_sym) - self._display_to_api_symbol[sym] = api_sym - logger.debug(f"交易对显示名 -> API symbol: '{sym}' -> '{api_sym}'") - else: - # 3. 如果 base+quote 也是非 ASCII(如 '币安人生USDT'),则直接使用原始 symbol - # 用户反馈希望交易这些特殊币种 - usdt_pairs.append(sym) - logger.info(f"添加非ASCII交易对: {sym!r} (baseAsset={s.get('baseAsset')!r})") - if self._display_to_api_symbol: - logger.info(f"已映射 {len(self._display_to_api_symbol)} 个中文/非ASCII交易对到英文 symbol,均可正常下单") + self._display_to_api_symbol.update(display_to_api) + if display_to_api: + logger.info(f"已映射 {len(display_to_api)} 个中文/非ASCII交易对到英文 symbol,均可正常下单") logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对") + # 回写 DB 供下次使用 + try: + await loop.run_in_executor(None, lambda: _save_exchange_info_to_db(exchange_info)) + except Exception as e: + logger.debug("exchange_info 写入 DB 失败: %s", e) return usdt_pairs except asyncio.TimeoutError: @@ -1112,6 +1197,7 @@ class BinanceClient: 交易对信息字典,包含 quantityPrecision, minQty, stepSize 等 """ symbol = self._resolve_api_symbol(symbol) + loop = asyncio.get_event_loop() # 1. 先检查内存缓存 if symbol in self._symbol_info_cache: cached_mem = self._symbol_info_cache[symbol] @@ -1133,11 +1219,23 @@ class BinanceClient: if isinstance(cached, dict) and ("tickSize" not in cached or "pricePrecision" not in cached): logger.info(f"{symbol} symbol_info 缓存缺少 tickSize/pricePrecision,自动刷新一次") else: - # 同时更新内存缓存 self._symbol_info_cache[symbol] = cached return cached - - # 3. 缓存未命中,调用 API(加超时与重试,避免网络抖动导致开仓前获取交易对信息失败) + + # 3. 尝试从 DB market_cache 读取 exchange_info 并解析本 symbol + try: + exchange_info_db = await loop.run_in_executor(None, lambda: _load_exchange_info_from_db(86400)) + info = _parse_symbol_info_from_exchange_info(exchange_info_db, symbol) if exchange_info_db else None + if info: + self._symbol_info_cache[symbol] = info + if self.redis_cache: + await self.redis_cache.set(cache_key, info, ttl=3600) + logger.debug(f"从 DB 缓存解析 {symbol} 交易对信息") + return info + except Exception as e: + logger.debug("从 DB 读取 exchange_info 解析 %s 失败: %s", symbol, e) + + # 4. 缓存未命中,调用 API(加超时与重试) read_timeout = getattr(config, 'READ_ONLY_REQUEST_TIMEOUT', 60) last_err = None for attempt in range(3): @@ -1146,6 +1244,11 @@ class BinanceClient: self.client.futures_exchange_info(), timeout=read_timeout ) + # 回写 DB 供后续请求使用 + try: + await loop.run_in_executor(None, lambda: _save_exchange_info_to_db(exchange_info)) + except Exception as e: + logger.debug("exchange_info 写入 DB 失败: %s", e) break except (asyncio.TimeoutError, BinanceAPIException, OSError) as e: last_err = e diff --git a/trading_system/config.py b/trading_system/config.py index 711a0e9..f65675b 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -231,6 +231,17 @@ DEFAULT_TRADING_CONFIG = { 'MIN_VOLUME_24H_STRICT': 50000000, # 严格过滤≥5000万美元 'MIN_VOLATILITY': 0.03, # 最小波动率3%,过滤死币 'MIN_SIGNAL_STRENGTH': 8, # 信号强度≥8(2026-01-29优化:从7提高到8,减少低质量信号,提升胜率) + # ===== 资金费率过滤(开仓前)===== + 'FUNDING_RATE_FILTER_ENABLED': True, # 开仓前检查资金费率,避免在费率极端不利于己方时进场 + 'MAX_FUNDING_RATE_FOR_LONG': 0.001, # 做多时 lastFundingRate 超过此值则跳过(0.1%=多头付费高) + 'MIN_FUNDING_RATE_FOR_SHORT': -0.001, # 做空时 lastFundingRate 低于此值则跳过(-0.1%=空头付费高) + # 扫描阶段资金费率过滤:排除 |lastFundingRate| 过大的标的,避免进入 TOP_N + 'SCAN_FUNDING_RATE_FILTER_ENABLED': True, + 'SCAN_FUNDING_RATE_MAX_ABS': 0.001, # 绝对值超过 0.1% 则从扫描候选剔除 + # 主动买卖量过滤(可选):与方向一致时通过,极端反向时跳过 + 'TAKER_RATIO_FILTER_ENABLED': False, # 默认关闭,开启后做多要求 buySellRatio≥下限、做空要求≤上限 + 'MIN_TAKER_BUY_RATIO_FOR_LONG': 0.85, # 做多时最近1h buySellRatio 低于此值则跳过(主动买不足) + 'MAX_TAKER_BUY_RATIO_FOR_SHORT': 1.15, # 做空时最近1h buySellRatio 高于此值则跳过(主动卖不足) # ===== 动态过滤优化 ===== 'BETA_FILTER_ENABLED': True, # 大盘共振过滤:BTC/ETH下跌时屏蔽多单 diff --git a/trading_system/market_scanner.py b/trading_system/market_scanner.py index c8a1be6..29b1f6a 100644 --- a/trading_system/market_scanner.py +++ b/trading_system/market_scanner.py @@ -119,7 +119,35 @@ class MarketScanner: pre_filtered_symbols.append(symbol) logger.info(f"初步筛选后,需要详细分析的交易对: {len(pre_filtered_symbols)} 个") - + + # 扫描阶段资金费率过滤:排除 |lastFundingRate| 过大的标的(文档建议的后续方向) + scan_funding_enabled = cfg.get('SCAN_FUNDING_RATE_FILTER_ENABLED', False) + scan_funding_max_abs = float(cfg.get('SCAN_FUNDING_RATE_MAX_ABS', 0.001)) + if scan_funding_enabled and pre_filtered_symbols: + try: + premium_all = await self.client.get_premium_index(None) # 全量,一次请求 + rate_map = {} + if isinstance(premium_all, list): + for item in premium_all: + if isinstance(item, dict) and 'symbol' in item: + try: + rate_map[item['symbol']] = float(item.get('lastFundingRate', 0)) + except (TypeError, ValueError): + pass + elif isinstance(premium_all, dict) and premium_all.get('symbol'): + rate_map[premium_all['symbol']] = float(premium_all.get('lastFundingRate', 0)) + if rate_map: + before = len(pre_filtered_symbols) + pre_filtered_symbols = [ + s for s in pre_filtered_symbols + if abs(rate_map.get(s, 0)) <= scan_funding_max_abs + ] + excluded = before - len(pre_filtered_symbols) + if excluded > 0: + logger.info(f"扫描阶段资金费率过滤: 排除 {excluded} 个标的 (|lastFundingRate| > {scan_funding_max_abs}),剩余 {len(pre_filtered_symbols)} 个") + except Exception as e: + logger.debug("扫描阶段资金费率过滤失败,继续使用原列表: %s", e) + # 只对符合条件的交易对进行详细分析(获取K线和技术指标) # ⚠️ 并发数说明: # - 这是单个账户扫描时,同时分析多少个交易对(不是用户进程数) diff --git a/trading_system/strategy.py b/trading_system/strategy.py index d2fcbbc..ca759a5 100644 --- a/trading_system/strategy.py +++ b/trading_system/strategy.py @@ -178,6 +178,14 @@ class TradingStrategy: # 确定交易方向(基于技术指标) trade_direction = trade_signal['direction'] + # 开仓前资金费率过滤:避免在费率极端不利于己方时进场 + if not await self._check_funding_rate_filter(symbol, trade_direction): + logger.info(f"{symbol} 资金费率过滤未通过,跳过开仓") + continue + # 开仓前主动买卖量过滤(可选):与方向一致时通过 + if not await self._check_taker_ratio_filter(symbol, trade_direction): + logger.info(f"{symbol} 主动买卖量过滤未通过,跳过开仓") + continue entry_reason = trade_signal['reason'] signal_strength = trade_signal.get('strength', 0) @@ -353,6 +361,65 @@ class TradingStrategy: await asyncio.sleep(check_interval) waited += check_interval + async def _check_funding_rate_filter(self, symbol: str, direction: str) -> bool: + """ + 开仓前资金费率过滤:避免在费率极端不利于己方时进场(做多时费率过高、做空时费率过负则跳过)。 + 依赖 REST get_premium_index(symbol) 的 lastFundingRate。 + """ + enabled = bool(config.TRADING_CONFIG.get('FUNDING_RATE_FILTER_ENABLED', True)) + if not enabled: + return True + try: + data = await self.client.get_premium_index(symbol) + if not data or not isinstance(data, dict): + return True + rate_raw = data.get('lastFundingRate') + if rate_raw is None: + return True + rate = float(rate_raw) + max_long = float(config.TRADING_CONFIG.get('MAX_FUNDING_RATE_FOR_LONG', 0.001)) + min_short = float(config.TRADING_CONFIG.get('MIN_FUNDING_RATE_FOR_SHORT', -0.001)) + if direction == 'BUY' and rate > max_long: + logger.info(f"{symbol} 资金费率过滤:做多跳过(lastFundingRate={rate:.6f} > {max_long})") + return False + if direction == 'SELL' and rate < min_short: + logger.info(f"{symbol} 资金费率过滤:做空跳过(lastFundingRate={rate:.6f} < {min_short})") + return False + return True + except Exception as e: + logger.debug(f"{symbol} 资金费率检查失败(放行): {e}") + return True + + async def _check_taker_ratio_filter(self, symbol: str, direction: str) -> bool: + """ + 开仓前主动买卖量过滤(可选):做多要求近期主动买占比不低于下限,做空要求主动卖占比不低于下限。 + 依赖 get_taker_long_short_ratio(symbol, period='1h', limit=1),取最近一条的 buySellRatio。 + """ + enabled = bool(config.TRADING_CONFIG.get('TAKER_RATIO_FILTER_ENABLED', False)) + if not enabled: + return True + try: + arr = await self.client.get_taker_long_short_ratio(symbol, period='1h', limit=3) + if not arr or not isinstance(arr, list): + return True + latest = arr[0] if arr else {} + ratio_raw = latest.get('buySellRatio') + if ratio_raw is None: + return True + ratio = float(ratio_raw) + min_long = float(config.TRADING_CONFIG.get('MIN_TAKER_BUY_RATIO_FOR_LONG', 0.85)) + max_short = float(config.TRADING_CONFIG.get('MAX_TAKER_BUY_RATIO_FOR_SHORT', 1.15)) + if direction == 'BUY' and ratio < min_long: + logger.info(f"{symbol} 主动买卖量过滤:做多跳过(buySellRatio={ratio:.3f} < {min_long})") + return False + if direction == 'SELL' and ratio > max_short: + logger.info(f"{symbol} 主动买卖量过滤:做空跳过(buySellRatio={ratio:.3f} > {max_short})") + return False + return True + except Exception as e: + logger.debug(f"{symbol} 主动买卖量检查失败(放行): {e}") + return True + async def _check_volume_confirmation(self, symbol_info: Dict) -> bool: """ 成交量确认 - 确保有足够的成交量支撑 diff --git a/持仓记录_2026-02-16T09-45-37.json b/持仓记录_2026-02-16T09-45-37.json new file mode 100644 index 0000000..ce15456 --- /dev/null +++ b/持仓记录_2026-02-16T09-45-37.json @@ -0,0 +1,870 @@ +[ + { + "id": 5110, + "symbol": "1000BONKUSDT", + "side": "BUY", + "quantity": 3531, + "entry_price": 0.006565, + "entry_value_usdt": 23.181015, + "notional_usdt": 23.459964, + "margin_usdt": 5.864991, + "original_notional_usdt": 23.181015, + "original_margin_usdt": 5.79525375, + "mark_price": 0.006644, + "pnl": 0.278949, + "pnl_percent": 4.7561709813365445, + "leverage": 4, + "entry_time": 1771214454, + "stop_loss_price": 0.00636805, + "take_profit_price": 0.00697531, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460584754, + "type": "TAKE_PROFIT_MARKET", + "side": "SELL", + "stopPrice": 0.006975, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584735, + "type": "STOP_MARKET", + "side": "SELL", + "stopPrice": 0.006369, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.006369 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.006975 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584754, + "type": "TAKE_PROFIT_MARKET", + "side": "SELL", + "stopPrice": 0.006975, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584735, + "type": "STOP_MARKET", + "side": "SELL", + "stopPrice": 0.006369, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5157, + "symbol": "ZECUSDT", + "side": "SELL", + "quantity": 0.077, + "entry_price": 284.54, + "entry_value_usdt": 21.909580000000002, + "notional_usdt": 22.23683, + "margin_usdt": 5.5592075, + "original_notional_usdt": 21.92881912, + "original_margin_usdt": 5.48220478, + "mark_price": 288.79, + "pnl": -0.32725, + "pnl_percent": -5.886630423491117, + "leverage": 4, + "entry_time": 1771226616, + "stop_loss_price": null, + "take_profit_price": null, + "take_profit_1": null, + "take_profit_2": null, + "atr": 14.62428571, + "entry_order_id": 797717260491, + "entry_order_type": "LIMIT", + "open_orders": [ + { + "orderId": 2000000460584806, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 266.76, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584795, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 293.07, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 293.07 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 266.76 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584806, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 266.76, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584795, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 293.07, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5109, + "symbol": "HYPEUSDT", + "side": "SELL", + "quantity": 0.7, + "entry_price": 30.158, + "entry_value_usdt": 21.110599999999998, + "notional_usdt": 20.984715002999998, + "margin_usdt": 5.2461787507499995, + "original_notional_usdt": 21.1106, + "original_margin_usdt": 5.27765, + "mark_price": 29.97816429, + "pnl": 0.12588499, + "pnl_percent": 2.3995558668679244, + "leverage": 4, + "entry_time": 1771214453, + "stop_loss_price": null, + "take_profit_price": null, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": 5837357832, + "entry_order_type": "LIMIT", + "open_orders": [ + { + "orderId": 2000000460584837, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 28.274, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584833, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 31.062, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 31.062 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 28.274 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584837, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 28.274, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584833, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 31.062, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5112, + "symbol": "FILUSDT", + "side": "SELL", + "quantity": 22, + "entry_price": 0.963, + "entry_value_usdt": 21.186, + "notional_usdt": 21.37234946, + "margin_usdt": 5.343087365, + "original_notional_usdt": 21.186, + "original_margin_usdt": 5.2965, + "mark_price": 0.97147043, + "pnl": -0.18634946, + "pnl_percent": -3.4876738348073033, + "leverage": 4, + "entry_time": 1771214455, + "stop_loss_price": 0.99189, + "take_profit_price": 0.9028125, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460584866, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.903, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584857, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.991, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.991 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.903 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584866, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.903, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584857, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.991, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5113, + "symbol": "TAKEUSDT", + "side": "SELL", + "quantity": 172, + "entry_price": 0.03629, + "entry_value_usdt": 6.24188, + "notional_usdt": 5.387599, + "margin_usdt": 5.387599, + "original_notional_usdt": 6.24188, + "original_margin_usdt": 6.24188, + "mark_price": 0.03132325, + "pnl": 0.854281, + "pnl_percent": 15.856432522168037, + "leverage": 1, + "entry_time": 1771214456, + "stop_loss_price": 0.0373787, + "take_profit_price": 0.0272175, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460584904, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.02722, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584895, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.03737, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.03737 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.02722 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584904, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.02722, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584895, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.03737, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5135, + "symbol": "ZROUSDT", + "side": "SELL", + "quantity": 13.5, + "entry_price": 1.6807, + "entry_value_usdt": 22.68945, + "notional_usdt": 22.76581086, + "margin_usdt": 5.691452715, + "original_notional_usdt": 22.68945, + "original_margin_usdt": 5.6723625, + "mark_price": 1.68635636, + "pnl": -0.07636086, + "pnl_percent": -1.3416760855932017, + "leverage": 4, + "entry_time": 1771223082, + "stop_loss_price": 1.731121, + "take_profit_price": 1.57565625, + "take_profit_1": 1.57565625, + "take_profit_2": 1.49288286, + "atr": 0.07825714, + "entry_order_id": 6021914660, + "entry_order_type": "LIMIT", + "open_orders": [ + { + "orderId": 2000000460584948, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 1.4929, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584947, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 1.7311, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 1.7311 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 1.4929 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584948, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 1.4929, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584947, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 1.7311, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5115, + "symbol": "AXSUSDT", + "side": "SELL", + "quantity": 18, + "entry_price": 1.388, + "entry_value_usdt": 24.983999999999998, + "notional_usdt": 24.42318858, + "margin_usdt": 6.105797145, + "original_notional_usdt": 24.984, + "original_margin_usdt": 6.246, + "mark_price": 1.35684381, + "pnl": 0.56081142, + "pnl_percent": 9.184900950390155, + "leverage": 4, + "entry_time": 1771214457, + "stop_loss_price": 1.42964, + "take_profit_price": 1.30125, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460584986, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 1.302, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584975, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 1.429, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 1.429 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 1.302 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460584986, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 1.302, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460584975, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 1.429, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5137, + "symbol": "XLMUSDT", + "side": "SELL", + "quantity": 127, + "entry_price": 0.16858, + "entry_value_usdt": 21.409660000000002, + "notional_usdt": 21.68884283, + "margin_usdt": 5.4222107075, + "original_notional_usdt": 21.40966, + "original_margin_usdt": 5.352415, + "mark_price": 0.17077829, + "pnl": -0.27918283, + "pnl_percent": -5.1488746022694105, + "leverage": 4, + "entry_time": 1771223321, + "stop_loss_price": 0.1736374, + "take_profit_price": 0.15804375, + "take_profit_1": 0.15804375, + "take_profit_2": 0.15550343, + "atr": 0.00544857, + "entry_order_id": 20994894391, + "entry_order_type": "LIMIT", + "open_orders": [ + { + "orderId": 2000000460585021, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.15551, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585020, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.17363, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.17363 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.15551 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460585021, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.15551, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585020, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.17363, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5116, + "symbol": "DASHUSDT", + "side": "SELL", + "quantity": 0.571, + "entry_price": 37.25, + "entry_value_usdt": 21.26975, + "notional_usdt": 21.304009999999998, + "margin_usdt": 5.3260024999999995, + "original_notional_usdt": 21.26975, + "original_margin_usdt": 5.3174375, + "mark_price": 37.31, + "pnl": -0.03426, + "pnl_percent": -0.6432591798445457, + "leverage": 4, + "entry_time": 1771214458, + "stop_loss_price": 38.3675, + "take_profit_price": 34.921875, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460585100, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 34.93, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585093, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 38.36, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 38.36 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 34.93 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460585100, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 34.93, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585093, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 38.36, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5117, + "symbol": "USELESSUSDT", + "side": "BUY", + "quantity": 150, + "entry_price": 0.04083, + "entry_value_usdt": 6.124499999999999, + "notional_usdt": 7.085999999999999, + "margin_usdt": 7.085999999999999, + "original_notional_usdt": 6.1245, + "original_margin_usdt": 6.1245, + "mark_price": 0.04724, + "pnl": 0.9615, + "pnl_percent": 13.56900931414056, + "leverage": 1, + "entry_time": 1771214459, + "stop_loss_price": 0.0396051, + "take_profit_price": 0.0510375, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460585145, + "type": "TAKE_PROFIT_MARKET", + "side": "SELL", + "stopPrice": 0.05103, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585138, + "type": "STOP_MARKET", + "side": "SELL", + "stopPrice": 0.03961, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.03961 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.05103 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460585145, + "type": "TAKE_PROFIT_MARKET", + "side": "SELL", + "stopPrice": 0.05103, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585138, + "type": "STOP_MARKET", + "side": "SELL", + "stopPrice": 0.03961, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5118, + "symbol": "ENAUSDT", + "side": "SELL", + "quantity": 194, + "entry_price": 0.1196, + "entry_value_usdt": 23.2024, + "notional_usdt": 23.53230864, + "margin_usdt": 5.88307716, + "original_notional_usdt": 23.2024, + "original_margin_usdt": 5.8006, + "mark_price": 0.12130056, + "pnl": -0.32990864, + "pnl_percent": -5.607756468725288, + "leverage": 4, + "entry_time": 1771214460, + "stop_loss_price": 0.123188, + "take_profit_price": 0.112125, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460585199, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.1122, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585168, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.1231, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.1231 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.1122 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460585199, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.1122, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460585168, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.1231, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + }, + { + "id": 5151, + "symbol": "WLFIUSDT", + "side": "SELL", + "quantity": 219, + "entry_price": 0.1011, + "entry_value_usdt": 22.1409, + "notional_usdt": 22.23684171, + "margin_usdt": 5.5592104275, + "original_notional_usdt": 22.1409, + "original_margin_usdt": 22.1409, + "mark_price": 0.10153809, + "pnl": -0.09594171, + "pnl_percent": -1.7258154058245532, + "leverage": 4, + "entry_time": 1771226264, + "stop_loss_price": null, + "take_profit_price": null, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": 2846730648, + "entry_order_type": "LIMIT", + "open_orders": [], + "active_sl_orders": "", + "active_tp_orders": "", + "binance_open_orders_raw": [] + }, + { + "id": 5123, + "symbol": "WLDUSDT", + "side": "SELL", + "quantity": 56, + "entry_price": 0.4028, + "entry_value_usdt": 22.5568, + "notional_usdt": 22.65973696, + "margin_usdt": 5.66493424, + "original_notional_usdt": 22.5568, + "original_margin_usdt": 5.6392, + "mark_price": 0.40463816, + "pnl": -0.10293696, + "pnl_percent": -1.8170901133002384, + "leverage": 4, + "entry_time": 1771214479, + "stop_loss_price": 0.414884, + "take_profit_price": 0.377625, + "take_profit_1": null, + "take_profit_2": null, + "atr": null, + "entry_order_id": null, + "entry_order_type": null, + "open_orders": [ + { + "orderId": 2000000460523250, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.3777, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460523242, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.4148, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ], + "active_sl_orders": "STOP_MARKET @ 0.4148 (NEW)", + "active_tp_orders": "TAKE_PROFIT_MARKET @ 0.3777 (NEW)", + "binance_open_orders_raw": [ + { + "orderId": 2000000460523250, + "type": "TAKE_PROFIT_MARKET", + "side": "BUY", + "stopPrice": 0.3777, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + }, + { + "orderId": 2000000460523242, + "type": "STOP_MARKET", + "side": "BUY", + "stopPrice": 0.4148, + "price": 0, + "origType": "CONDITIONAL", + "reduceOnly": true, + "status": "NEW" + } + ] + } +] \ No newline at end of file