summaryrefslogtreecommitdiff
path: root/services/strategy-engine/strategies/grid_strategy.py
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 09:17:35 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 09:17:35 +0900
commit71e5942632a5a8c7cd555b2d52e5632a67186a8d (patch)
tree668a2c80c04b40b43dac39c6e22efa5cf1aad9a7 /services/strategy-engine/strategies/grid_strategy.py
parenta841b3a1f2f08caa7f82a1516c47bb5f3c4b7356 (diff)
feat(strategy): add Grid trend guard and Bollinger squeeze detection
Diffstat (limited to 'services/strategy-engine/strategies/grid_strategy.py')
-rw-r--r--services/strategy-engine/strategies/grid_strategy.py31
1 files changed, 31 insertions, 0 deletions
diff --git a/services/strategy-engine/strategies/grid_strategy.py b/services/strategy-engine/strategies/grid_strategy.py
index 70443ec..07ccaba 100644
--- a/services/strategy-engine/strategies/grid_strategy.py
+++ b/services/strategy-engine/strategies/grid_strategy.py
@@ -18,6 +18,9 @@ class GridStrategy(BaseStrategy):
self._quantity: Decimal = Decimal("0.01")
self._grid_levels: list[float] = []
self._last_zone: Optional[int] = None
+ self._exit_threshold_pct: float = 5.0
+ self._out_of_range: bool = False
+ self._in_position: bool = False # Track if we have any grid positions
@property
def warmup_period(self) -> int:
@@ -29,11 +32,17 @@ class GridStrategy(BaseStrategy):
self._grid_count = int(params.get("grid_count", 5))
self._quantity = Decimal(str(params.get("quantity", "0.01")))
+ self._exit_threshold_pct = float(params.get("exit_threshold_pct", 5.0))
+
if self._lower_price >= self._upper_price:
raise ValueError(
f"Grid lower_price must be < upper_price, "
f"got lower={self._lower_price}, upper={self._upper_price}"
)
+ if self._exit_threshold_pct <= 0:
+ raise ValueError(
+ f"exit_threshold_pct must be > 0, got {self._exit_threshold_pct}"
+ )
if self._grid_count < 2:
raise ValueError(f"Grid grid_count must be >= 2, got {self._grid_count}")
if self._quantity <= 0:
@@ -53,7 +62,9 @@ class GridStrategy(BaseStrategy):
)
def reset(self) -> None:
+ super().reset()
self._last_zone = None
+ self._out_of_range = False
def _get_zone(self, price: float) -> int:
"""Return the grid zone index for a given price.
@@ -69,6 +80,26 @@ class GridStrategy(BaseStrategy):
def on_candle(self, candle: Candle) -> Signal | None:
self._update_filter_data(candle)
price = float(candle.close)
+
+ # Check if price is out of grid range
+ if self._grid_levels:
+ lower_bound = self._grid_levels[0] * (1 - self._exit_threshold_pct / 100)
+ upper_bound = self._grid_levels[-1] * (1 + self._exit_threshold_pct / 100)
+
+ if price < lower_bound or price > upper_bound:
+ if not self._out_of_range:
+ self._out_of_range = True
+ # Exit signal — close positions
+ return self._apply_filters(Signal(
+ strategy=self.name, symbol=candle.symbol,
+ side=OrderSide.SELL, price=candle.close,
+ quantity=self._quantity, conviction=0.8,
+ reason=f"Grid: price {price:.2f} broke out of range [{self._grid_levels[0]:.2f}, {self._grid_levels[-1]:.2f}]",
+ ))
+ return None # Already out of range, no more signals
+ else:
+ self._out_of_range = False
+
current_zone = self._get_zone(price)
if self._last_zone is None: