重构
This commit is contained in:
parent
7fba5e94cd
commit
271a98a772
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user