diff --git a/user_data/strategies/grid_manager.py b/user_data/strategies/grid_manager.py index a8fc3e8..74050c1 100644 --- a/user_data/strategies/grid_manager.py +++ b/user_data/strategies/grid_manager.py @@ -26,11 +26,12 @@ class AdjustmentType(Enum): @dataclass class GridLevel: - """单个网格点记录""" + """单个网格点的状态""" price: float # 网格价格 - quantity: float # 该价格的持仓数量 + status: str # "filled" (已买入) 或 "empty" (未买/已卖出) + quantity: float # 该网格的持仓数量 + entry_price: float # 该网格的实际买入价格 entry_time: int # 建仓蜡烛线索引 - status: str # "open" 或 "closed" @dataclass @@ -58,13 +59,16 @@ class OrderFill: class GridManager: """ - 网格交易管理器 + 网格交易管理器 - 核心思想:维护网格的填充/排空状态 - 维护单个币对(如 ETH/USDT)的完整网格持仓生命周期 - 包含: - - 网格参数管理 - - 持仓状态跟踪 - - 加减仓决策 + 网格逻辑: + - 下沿到当前价格 → 所有网格必须被填满(已买入持仓) + - 当前价格到上沿 → 所有网格必须被排空(不持有 或 已卖出) + + 决策规则: + 1. 如果下方有空白网格 → 加仓(填满它们) + 2. 如果上方有持仓网格 → 平仓(排空它们) + 3. 维护一个网格状态数组,每个位置记录 "filled" 或 "empty" """ def __init__(self, @@ -78,10 +82,10 @@ class GridManager: Args: pair: 币对名称,如 "ETH/USDT" - lower_price: 网格下限价格,如 1500 - upper_price: 网格上限价格,如 4500 - step: 网格间距,如 50 - stake_per_grid: 每个网格的投资额,如 40 USDT + lower_price: 网格下限价格 + upper_price: 网格上限价格 + step: 网格间距 + stake_per_grid: 每个网格的投资额 """ self.pair = pair self.lower_price = lower_price @@ -95,11 +99,20 @@ class GridManager: # 生成所有网格点价格 self.grid_prices = [lower_price + i * step for i in range(self.total_grid_levels)] - # 持仓状态 - self.grid_levels: Dict[float, GridLevel] = {} # 价格 -> GridLevel - self.position_history: List[PositionRecord] = [] # 历史加减仓记录 + # 核心数据结构:网格状态数组 + # grid_states[i] = GridLevel 对象,记录该网格的状态(filled/empty) + self.grid_states: Dict[float, GridLevel] = {} + for price in self.grid_prices: + self.grid_states[price] = GridLevel( + price=price, + status="empty", # 初始状态:全部为 empty(未持有) + quantity=0.0, # 当前持仓数量 + entry_price=0.0, # 买入价格 + entry_time=0 + ) - # 订单执行历史 + # 持仓统计 + self.position_history: List[PositionRecord] = [] # 历史加减仓记录 self.order_fills: List[OrderFill] = [] # 所有订单成交记录 self.pending_orders: Dict[str, PositionRecord] = {} # 待成交的订单映射 @@ -110,7 +123,6 @@ class GridManager: self.avg_entry_price: float = 0.0 # 平均建仓价 self.highest_price: float = 0.0 # 持仓期间最高价 self.lowest_price: float = float('inf') # 持仓期间最低价 - self.max_positions: int = 0 # 历史最大持仓数 # 调试 self.candle_index = 0 # 当前蜡烛线索引 @@ -149,7 +161,9 @@ class GridManager: def apply_adjustment(self, adjustment: PositionRecord) -> None: """ - 应用一次加减仓操作(来自策略的决策) + 应用一次加减仓操作,并更新网格状态 + + 通过更新 grid_states 中相关网格的状态(filled/empty)来报告内部一致性 Args: adjustment: PositionRecord 对象,包含加减仓的所有信息 @@ -158,59 +172,61 @@ class GridManager: quantity = adjustment.quantity adj_type = adjustment.type + # 找到该操作所属的网格点 + grid_price = self._round_to_grid(price) + grid_state = self.grid_states.get(grid_price) + if adj_type == AdjustmentType.ENTRY or adj_type == AdjustmentType.ADD: - # 建仓或加仓 - if price not in self.grid_levels: - self.grid_levels[price] = GridLevel( - price=price, - quantity=quantity, - entry_time=self.candle_index, - status="open" - ) - else: - self.grid_levels[price].quantity += quantity + # 建仓或加仓 → 将该网格标记为 FILLED + if grid_state: + grid_state.status = "filled" + grid_state.quantity += quantity + grid_state.entry_price = price # 记录实际买入价 + grid_state.entry_time = self.candle_index - # 更新总持仓 - old_total = self.total_quantity + # 更新持仓统计 self.total_invested += quantity * price self.total_quantity += quantity - # 更新平均价 if self.total_quantity > 0: self.avg_entry_price = self.total_invested / self.total_quantity - # 更新最大持仓数 - if len(self.grid_levels) > self.max_positions: - self.max_positions = len(self.grid_levels) - - print(f"[GridManager] {self.pair} 加仓 - 价格: {price:.2f}, " + print(f"[GridManager] {self.pair} 加仓 - 网格 {grid_price:.2f} 标记为 FILLED, " f"数量: {quantity:.6f}, 总持仓: {self.total_quantity:.6f}", file=sys.stderr, flush=True) elif adj_type == AdjustmentType.REDUCE: - # 减仓 - if price in self.grid_levels: - self.grid_levels[price].quantity -= quantity - if self.grid_levels[price].quantity <= 0: - del self.grid_levels[price] + # 减仓 → 其他网格的故事,不常用(网格交易通常只有 ENTRY/ADD/EXIT) + if grid_state: + grid_state.quantity -= quantity + if grid_state.quantity <= 0: + grid_state.quantity = 0 self.total_quantity -= quantity - self.total_invested -= quantity * price + self.total_invested -= quantity * grid_state.entry_price if grid_state else 0 if self.total_quantity > 0: self.avg_entry_price = self.total_invested / self.total_quantity - print(f"[GridManager] {self.pair} 减仓 - 价格: {price:.2f}, " - f"数量: {quantity:.6f}, 剩余持仓: {self.total_quantity:.6f}", + print(f"[GridManager] {self.pair} 减仓 - 网格 {grid_price:.2f}, " + f"减少: {quantity:.6f}, 剩余持仓: {self.total_quantity:.6f}", file=sys.stderr, flush=True) elif adj_type == AdjustmentType.EXIT: - # 全部平仓 - print(f"[GridManager] {self.pair} 全部平仓 - 持仓: {self.total_quantity:.6f}, " + # 平仓 → 将所有 FILLED 的网格一次排空(或不排空对象网格) + # 此处只是记录平仓决策,实际的排空是由 decide_adjustment 程序控制 + print(f"[GridManager] {self.pair} 平仓 - 总持仓: {self.total_quantity:.6f}, " f"平均价: {self.avg_entry_price:.2f}, 当前价: {price:.2f}", file=sys.stderr, flush=True) - self.grid_levels.clear() + # 清空所有 FILLED 网格,下次套需要绍转建仓时重新填满 + for gs in self.grid_states.values(): + if gs.status == "filled": + gs.status = "empty" + gs.quantity = 0 + gs.entry_price = 0.0 + + # 清空污了持仓计数 self.total_quantity = 0.0 self.total_invested = 0.0 self.avg_entry_price = 0.0 @@ -222,13 +238,13 @@ class GridManager: def decide_adjustment(self) -> Optional[PositionRecord]: """ - 判定是否需要加减仓,并返回建议 + 判定是否需要加减仓,基于网格填充/排空逻辑 - 核心逻辑: - 1. 如果还没有建仓过,且价格在网格范围内 → 初始建仓 - 2. 如果已有头寸,价格跌入新的更低网格点 → 加仓 - 3. 如果已有头寸,价格涨超过平均价 → 全部平仓 - 4. 如果已有多个头寸,价格涨到最高点 → 可选:部分减仓 + 核心规则: + 1. 下沿到当前价格 → 所有网格必须被 FILLED(已买入) + 如果有 EMPTY 的网格 → 加仓填满它 + 2. 当前价格到上沿 → 所有网格必须被 EMPTY(不持有) + 如果有 FILLED 的网格 → 平仓排空它 Returns: PositionRecord 如果需要操作,否则 None @@ -236,61 +252,50 @@ class GridManager: if self.current_price is None: return None - # 情况 1: 还没有持仓,且价格在范围内 → 初始建仓 - if self.total_quantity == 0: - if self.lower_price <= self.current_price <= self.upper_price: - # 找到当前价格对应的网格点(向上舍入) - grid_price = (int(self.current_price / self.step) + 1) * self.step - if grid_price > self.upper_price: - grid_price = self.upper_price - - print(f"[GridManager] {self.pair} 初始建仓建议 - 价格: {grid_price:.2f}", - file=sys.stderr, flush=True) - - return PositionRecord( - level_index=self._price_to_level_index(grid_price), - price=self.current_price, - quantity=1.0, # 1个单位(实际金额由策略乘以 stake_per_grid) - type=AdjustmentType.ENTRY, - timestamp=self.candle_index - ) + # 找到当前价格所在的网格点 + current_grid_price = self._round_to_grid(self.current_price) - # 情况 2: 已有持仓,价格涨超过平均价 → 全部平仓 - if self.total_quantity > 0 and self.current_price > self.avg_entry_price: - profit_pct = (self.current_price - self.avg_entry_price) / self.avg_entry_price * 100 - print(f"[GridManager] {self.pair} 平仓建议 - 利润: {profit_pct:.2f}%", - file=sys.stderr, flush=True) - - return PositionRecord( - level_index=0, - price=self.current_price, - quantity=self.total_quantity, - type=AdjustmentType.EXIT, - timestamp=self.candle_index - ) - - # 情况 3: 已有持仓,价格跌入新的更低网格点 → 加仓 - if self.total_quantity > 0: - # 找到当前价格最接近的网格点(向下舍入) - current_grid_level = int(self.current_price / self.step) * self.step - - # 关键修复:只有当价格已经跌破平均价时,才考虑加仓 - # 并且必须是一个还没加过仓的网格点 - if current_grid_level < self.avg_entry_price and current_grid_level >= self.lower_price: - if current_grid_level not in self.grid_levels and len(self.grid_levels) < self.total_grid_levels: - print(f"[GridManager] {self.pair} 加仓建议 - 价格: {current_grid_level:.2f}, " - f"已有网格数: {len(self.grid_levels)}", + # 规则 2: 检查上方是否有需要平仓的网格 + # 当前价格到上沿之间的网格都应该是 EMPTY + for grid_price in self.grid_prices: + if grid_price > current_grid_price: + grid_state = self.grid_states.get(grid_price) + if grid_state and grid_state.status == "filled" and grid_state.quantity > 0: + # 上方有持仓,需要平仓 + print(f"[GridManager] {self.pair} 平仓信号 - 价格已涨到 {self.current_price:.2f}, " + f"上方网格 {grid_price:.2f} 有持仓需排空", file=sys.stderr, flush=True) + # 平仓该网格 return PositionRecord( - level_index=self._price_to_level_index(current_grid_level), + level_index=self._price_to_level_index(grid_price), price=self.current_price, - quantity=1.0, - type=AdjustmentType.ADD, + quantity=grid_state.quantity, + type=AdjustmentType.EXIT, timestamp=self.candle_index ) - # 没有操作建议 + # 规则 1: 检查下方是否有需要加仓的网格 + # 下沿到当前价格之间的网格都应该是 FILLED + for grid_price in self.grid_prices: + if grid_price <= current_grid_price: + grid_state = self.grid_states.get(grid_price) + if grid_state and grid_state.status == "empty" and grid_state.quantity == 0: + # 下方有空白网格,需要加仓 + print(f"[GridManager] {self.pair} 加仓信号 - 价格已跌到 {self.current_price:.2f}, " + f"下方网格 {grid_price:.2f} 为空需填满", + file=sys.stderr, flush=True) + + # 加仓该网格 + return PositionRecord( + level_index=self._price_to_level_index(grid_price), + price=self.current_price, + quantity=1.0, + type=AdjustmentType.ADD if self.total_quantity > 0 else AdjustmentType.ENTRY, + timestamp=self.candle_index + ) + + # 没有操作建议(所有网格状态都符合规则) return None def _price_to_level_index(self, price: float) -> int: @@ -298,8 +303,15 @@ class GridManager: index = int((price - self.lower_price) / self.step) return max(0, min(index, self.total_grid_levels - 1)) + def _round_to_grid(self, price: float) -> float: + """ + 将价格舍入到最接近的网格点(向下舍入) + """ + return int(price / self.step) * self.step + def get_summary(self) -> Dict[str, Any]: """获取当前持仓的完整摘要""" + filled_grids = sum(1 for gs in self.grid_states.values() if gs.status == "filled") return { "pair": self.pair, "current_price": self.current_price, @@ -307,9 +319,9 @@ class GridManager: "total_invested": self.total_invested, "avg_entry_price": self.avg_entry_price, "unrealized_profit": (self.current_price - self.avg_entry_price) * self.total_quantity if self.total_quantity > 0 else 0, - "active_grid_levels": len(self.grid_levels), - "max_positions_ever": self.max_positions, - "total_adjustments": len(self.position_history), + "filled_grids": filled_grids, # FILLED 的网格数 + "empty_grids": self.total_grid_levels - filled_grids, # EMPTY 的网格数 + "total_orders": len(self.order_fills), "highest_price": self.highest_price if self.total_quantity > 0 else None, "lowest_price": self.lowest_price if self.total_quantity > 0 else None, } @@ -383,8 +395,9 @@ class GridManager: """ 从 Freqtrade 的 Trade 对象同步状态 - 当无法直接捕获订单成交时,可以通过 Trade 对象反向同步状态 - (备选方案,不如直接记录 OrderFill 准确) + 由于 Trade 对象提供了实际持仓信息,使用其更新网格状态: + - 根据平均价找到应该填满的网格 + - 全部低于平均价的网格为 FILLED、高于平均价的为 EMPTY Args: trade: Freqtrade Trade 对象 @@ -395,29 +408,37 @@ class GridManager: self.total_invested = trade.stake_amount self.avg_entry_price = trade.open_rate - # 重新初始化 grid_levels(仅保留当前持仓) - self.grid_levels.clear() - if self.total_quantity > 0 and self.avg_entry_price > 0: - # 创建一个体现当前持仓的网格点 - # 依据平均价格找最接近的网格位置 - grid_price = round(self.avg_entry_price / self.step) * self.step - self.grid_levels[grid_price] = GridLevel( - price=grid_price, - quantity=self.total_quantity, - entry_time=candle_index, - status="open" - ) + # 根据 Trade 对象的平均价,更新所有网格的状态 + # 下沿 → 平均价:全部 FILLED + # 平均价 → 上沿:全部 EMPTY + avg_grid_price = self._round_to_grid(self.avg_entry_price) - # 如果 trade 有多个 entry(加仓的情况) - if hasattr(trade, 'trades') and trade.trades: - entries = [t for t in trade.trades if t.entry_side == 'entry'] - self.max_positions = len(entries) + for price, grid_state in self.grid_states.items(): + if price < avg_grid_price: + # 下沿:应该是 FILLED + if self.total_quantity > 0: + grid_state.status = "filled" + grid_state.entry_price = self.avg_entry_price + grid_state.entry_time = candle_index + # 简化:把所有 filled 网格的持仓信息污盖到了均价 + # 实际应用中会根据 trade.trades 进行细上漂歪 + else: + grid_state.status = "empty" + grid_state.quantity = 0 + elif price > avg_grid_price: + # 上沿:应该是 EMPTY + grid_state.status = "empty" + grid_state.quantity = 0 + else: + # 不太可能抹挡,但为了严谨标记为 FILLED + grid_state.status = "filled" + grid_state.entry_price = self.avg_entry_price + grid_state.entry_time = candle_index - print(f"[GridManager] {self.pair} 从 Trade 对象同步状态 - " + print(f"[GridManager] {self.pair} 从 Trade 对象同步 - " f"持仓: {self.total_quantity:.6f}, " - f"投入: {self.total_invested:.2f} USDT, " f"平均价: {self.avg_entry_price:.2f}, " - f"网格点数: {len(self.grid_levels)}", + f"下沿到平均价的网格标记为 FILLED", file=sys.stderr, flush=True) def record_pending_order(self, order_id: str, adjustment: PositionRecord) -> None: