# user_data/strategies/grid_strategy.py from freqtrade.strategy import IStrategy, DecimalParameter from pandas import DataFrame class GridStrategy(IStrategy): """ 以「上次成交價為中心」的網格策略 - 跌超 x% -> 每次加倉 +0.1 NAV - 漲超 y% -> 每次減倉 -0.1 NAV (或加空/回補) - 倉位上限 ±0.9 NAV - 使用 position adjustment 逐步加減碼 """ INTERFACE_VERSION = 3 can_short: bool = True position_adjustment_enable = True timeframe = "1h" process_only_new_candles = True startup_candle_count = 20 minimal_roi = {"0": 1} stoploss = -1 trailing_stop = False x = DecimalParameter(0.001, 0.02, default=0.005, decimals=4, space="buy", optimize=True, load=True) y = DecimalParameter(0.001, 0.02, default=0.005, decimals=4, space="sell", optimize=True, load=True) step: float = 0.10 # 每次調整 = 10% 的基準名義額 max_pos: float = 0.90 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 單一變數:初始為 None,等 populate_indicators() 時再設 self.last_trade_price: float | None = None def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame: # ✅ 初始化:只在第一次還沒設定時,把第一根 K 的 close 當作 last_trade_price if self.last_trade_price is None and len(df) > 0: self.last_trade_price = float(df["close"].iloc[0]) df["prev_close"] = df["close"].shift(1) df["chg"] = (df["close"] - df["prev_close"]) / df["prev_close"] return df def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: df["enter_long"] = 0 df["enter_short"] = 0 # 跌超 x% → 開多(第一筆由這裡觸發) df.loc[(df["chg"] <= -self.x.value) & (df["volume"] > 0), "enter_long"] = 1 # 漲超 y% → 開空 df.loc[(df["chg"] >= self.y.value) & (df["volume"] > 0), "enter_short"] = 1 return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: df["exit_long"] = 0 df["exit_short"] = 0 return df def adjust_trade_position(self, trade, current_time, current_rate, current_profit, **kwargs): # 1) 確保每筆交易開始時,有中心 if self.last_trade_price is None: self.last_trade_price = current_rate # 或 trade.open_rate last_trade_price = self.last_trade_price change_pct = (current_rate - last_trade_price) / last_trade_price # 名義額基準(建議用名義額,不用保證金) entry_price = trade.open_rate or current_rate base_notional = max(abs(trade.amount) * entry_price, 1e-9) current_amount = abs(getattr(trade, "amount", 0.0)) current_notional = current_amount * current_rate pos_ratio = current_notional / base_notional one_step_notional = self.step * base_notional one_step_qty = one_step_notional / current_rate eps = 1e-9 close_tolerance = 1.05 band = 0.003 # 放寬一點 0.3% delta_qty = 0.0 if not trade.is_short: if change_pct <= -self.x.value: if pos_ratio > self.max_pos - self.step + eps: delta_qty = -current_amount # 平倉 self.last_trade_price = None # 2) 平倉後清空 return float(delta_qty) delta_qty = +abs(one_step_qty) self.last_trade_price = current_rate # 3) 加倉/減倉後更新中心 elif change_pct >= self.y.value and current_amount > 0: if current_notional <= one_step_notional * close_tolerance: delta_qty = -current_amount # 平倉 self.last_trade_price = None return float(delta_qty) delta_qty = -abs(one_step_qty) self.last_trade_price = current_rate # 回中心就平(先拿掉 need profit) if abs(change_pct) < band and current_amount > 0: delta_qty = -current_amount self.last_trade_price = None return float(delta_qty) else: if change_pct >= self.y.value: if pos_ratio > self.max_pos - self.step + eps: delta_qty = +current_amount # 平空 self.last_trade_price = None return float(delta_qty) delta_qty = -abs(one_step_qty) self.last_trade_price = current_rate elif change_pct <= -self.x.value and current_amount > 0: if current_notional <= one_step_notional * close_tolerance: delta_qty = +current_amount # 平空 self.last_trade_price = None return float(delta_qty) delta_qty = +abs(one_step_qty) self.last_trade_price = current_rate if abs(change_pct) < band and current_amount > 0: delta_qty = +current_amount self.last_trade_price = None return float(delta_qty) return float(delta_qty) def on_trade_close(self, trade, order, **kwargs): # 保險:成交全平時再清一次,讓下一筆能重新設中心 self.last_trade_price = None