Files
QuantDinger/backend_api_python/tests/test_grid_engine.py
T

1196 lines
37 KiB
Python

"""Tests for professional grid engine."""
from __future__ import annotations
import pytest
from app.services.grid.config import GridBotConfig
from app.services.grid.levels import generate_cells, generate_levels
from app.services.grid.validator import validate_grid_config
def test_generate_levels_arithmetic():
levels = generate_levels(90000, 100000, 10, "arithmetic")
assert len(levels) == 10
assert levels[0] == 90000
assert abs(levels[-1] - 100000) < 1e-6
def test_generate_cells_count():
levels = generate_levels(100, 200, 5, "arithmetic")
cells = generate_cells(levels)
assert len(cells) == 4
def test_validate_long_grid_ok():
cfg = GridBotConfig(
upper_price=100000,
lower_price=90000,
grid_count=10,
amount_per_grid=100,
grid_mode="arithmetic",
grid_direction="long",
initial_position_pct=0.3,
order_mode="maker",
boundary_action="pause",
leverage=5,
market_type="swap",
margin_mode="cross",
)
ok, msg, _ = validate_grid_config(cfg, initial_capital=10000)
assert ok is True
assert msg == ""
def test_validate_rejects_bad_bounds():
cfg = GridBotConfig(
upper_price=100,
lower_price=200,
grid_count=10,
amount_per_grid=50,
grid_mode="arithmetic",
grid_direction="long",
initial_position_pct=0,
order_mode="maker",
boundary_action="pause",
leverage=1,
market_type="swap",
margin_mode="cross",
)
ok, msg, _ = validate_grid_config(cfg)
assert ok is False
assert "upperPrice" in msg
def test_config_from_trading_config_initial_pct():
tc = {
"leverage": 5,
"market_type": "swap",
"bot_params": {
"upperPrice": 100000,
"lowerPrice": 90000,
"gridCount": 10,
"amountPerGrid": 100,
"gridDirection": "long",
"initialPositionPct": 30,
},
}
cfg = GridBotConfig.from_trading_config(tc)
assert cfg.initial_position_pct == 0.3
assert cfg.grid_direction == "long"
def test_initial_market_target_qty_100u_20pct_20x():
"""100 USDT * 20% margin * 20x leverage ≈ 400 USDT notional at 72710."""
from app.services.grid.engine import GridEngine
from app.services.grid.exchange_orders import make_grid_initial_client_order_id
tc = {
"initial_capital": 100,
"leverage": 20,
"market_type": "swap",
"bot_params": {
"upperPrice": 80200,
"lowerPrice": 69800,
"gridCount": 24,
"amountPerGrid": 4,
"gridDirection": "long",
"initialPositionPct": 20,
},
}
engine = GridEngine(
42,
"BTC/USDT",
tc,
{},
create_client_fn=lambda: None,
enqueue_market=lambda *a, **k: False,
)
qty = engine._target_initial_base_qty(72710.0)
assert qty == pytest.approx(400.0 / 72710.0, rel=1e-4)
assert make_grid_initial_client_order_id(42, leg="long") == make_grid_initial_client_order_id(42, leg="long")
assert make_grid_initial_client_order_id(42, leg="long") != make_grid_initial_client_order_id(42, leg="short")
def test_grid_line_qty_uses_quote_amount_times_leverage():
from app.services.grid.engine import GridEngine
tc = {
"initial_capital": 100,
"leverage": 20,
"market_type": "swap",
"bot_params": {
"upperPrice": 80200,
"lowerPrice": 69800,
"gridCount": 24,
"amountPerGrid": 4,
"gridDirection": "long",
"initialPositionPct": 0,
},
}
engine = GridEngine(
42,
"BTC/USDT",
tc,
{},
create_client_fn=lambda: None,
enqueue_market=lambda *a, **k: False,
)
assert engine._grid_base_qty(72710.0) == pytest.approx(4.0 * 20.0 / 72710.0, rel=1e-4)
def test_boundary_stop_loss_auto_stops_neutral_grid(monkeypatch):
from app.services.grid.engine import GridEngine
tc = {
"initial_capital": 100,
"leverage": 5,
"market_type": "swap",
"bot_params": {
"upperPrice": 81200,
"lowerPrice": 70200,
"gridCount": 28,
"amountPerGrid": 3,
"gridDirection": "neutral",
"boundaryAction": "stop_loss",
},
}
enqueued = []
stopped = []
logs = []
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *args: logs.append(args))
monkeypatch.setattr("app.services.grid.engine.GridEngine.cancel_entry_orders_on_exchange", lambda self: None)
monkeypatch.setattr(
"app.services.strategy_lifecycle.auto_stop_live_strategy",
lambda sid, reason, source="": stopped.append((sid, reason, source)) or True,
)
engine = GridEngine(
77,
"BTC/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *args: enqueued.append(args) or True,
)
assert engine.handle_boundary(69000.0) is True
assert engine.stop_requested is True
assert "out of bounds" in engine.stop_reason
assert enqueued == [
("close_long", 0, 69000.0, "grid_boundary_stop"),
("close_short", 0, 69000.0, "grid_boundary_stop"),
]
assert stopped and stopped[0][0] == 77
assert stopped[0][2] == "grid_boundary"
assert any("69000.0000" in str(row[-1]) and "70200.0000" in str(row[-1]) for row in logs)
def test_boundary_pause_does_not_auto_stop(monkeypatch):
from app.services.grid.engine import GridEngine
tc = {
"initial_capital": 100,
"leverage": 5,
"market_type": "swap",
"bot_params": {
"upperPrice": 81200,
"lowerPrice": 70200,
"gridCount": 28,
"amountPerGrid": 3,
"gridDirection": "neutral",
"boundaryAction": "pause",
},
}
enqueued = []
stopped = []
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine.cancel_entry_orders_on_exchange", lambda self: None)
monkeypatch.setattr(
"app.services.strategy_lifecycle.auto_stop_live_strategy",
lambda *args, **kwargs: stopped.append((args, kwargs)) or True,
)
engine = GridEngine(
78,
"BTC/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *args: enqueued.append(args) or True,
)
assert engine.handle_boundary(69000.0) is True
assert engine.stop_requested is False
assert engine._paused_entries is True
assert enqueued == []
assert stopped == []
def test_neutral_grid_rehangs_held_cell_exits(monkeypatch):
from types import SimpleNamespace
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
from app.services.live_trading.grid_cells import GridCellState
tc = {
"initial_capital": 100,
"leverage": 5,
"market_type": "swap",
"bot_params": {
"upperPrice": 96,
"lowerPrice": 72,
"gridCount": 5,
"amountPerGrid": 5,
"gridDirection": "neutral",
},
}
levels = generate_levels(72, 96, 5, "arithmetic")
cells = generate_cells(levels)
placed = []
engine = GridEngine(
79,
"SOL/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *args: True,
)
engine._bootstrapped = True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._levels_and_cells", lambda self: (levels, cells))
monkeypatch.setattr("app.services.grid.engine.GridEngine._normalize_grid_base_qty", lambda self, qty, px: qty)
monkeypatch.setattr(
engine._cells,
"list_cells",
lambda sid, symbol: [
SimpleNamespace(cell_index=1, state=GridCellState.LONG_HELD, leg_size=1.2),
SimpleNamespace(cell_index=2, state=GridCellState.SHORT_HELD, leg_size=0.8),
],
)
monkeypatch.setattr(engine._orders, "has_open_for_cell", lambda *args: False)
def fake_place(self, cell, purpose, side, price, *, reduce_only, pos_side, quantity=None):
placed.append((cell.index, purpose, side, price, reduce_only, pos_side, quantity))
return True
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
assert engine.sync_held_cell_exits(80.0) == 2
assert placed == [
(1, "long_exit", "sell", cells[1].upper_price, True, "long", 1.2),
(2, "short_exit", "buy", cells[2].lower_price, True, "short", 0.8),
]
def test_grid_shutdown_releases_cancelled_cell_states(monkeypatch):
from app.services.grid.engine import GridEngine
tc = {
"initial_capital": 100,
"leverage": 5,
"market_type": "swap",
"bot_params": {
"upperPrice": 96,
"lowerPrice": 72,
"gridCount": 5,
"amountPerGrid": 5,
"gridDirection": "neutral",
},
}
calls = []
engine = GridEngine(
80,
"SOL/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *args: True,
)
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a: None)
monkeypatch.setattr(engine, "cancel_all_orders_on_exchange", lambda: calls.append("exchange_cancel"))
monkeypatch.setattr(engine._orders, "cancel_all", lambda sid, symbol: calls.append(("orders_cancel", sid, symbol)) or 3)
monkeypatch.setattr(engine._cells, "release_cancelled_working_orders", lambda sid, symbol: calls.append(("cells_release", sid, symbol)) or 4)
engine.shutdown()
assert calls == [
"exchange_cancel",
("orders_cancel", 80, "SOL/USDT"),
("cells_release", 80, "SOL/USDT"),
]
def test_initial_market_recovers_from_exchange_without_new_order(monkeypatch):
from app.services.grid.engine import GridEngine
tc = {
"initial_capital": 100,
"leverage": 20,
"market_type": "swap",
"bot_params": {
"upperPrice": 80200,
"lowerPrice": 69800,
"gridCount": 24,
"amountPerGrid": 4,
"gridDirection": "long",
"initialPositionPct": 20,
},
}
recorded = {"calls": 0}
def fake_record(*args, **kwargs):
recorded["calls"] += 1
monkeypatch.setattr("app.services.grid.engine.record_grid_market_fill", fake_record)
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.persist_grid_resting_state", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._has_initial_market_trade", lambda self: False)
engine = GridEngine(
7,
"BTC/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
target = engine._target_initial_base_qty(72710.0)
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: target)
ok = engine.run_initial_market_position(72710.0)
assert ok is True
assert engine._initial_done is True
assert recorded["calls"] == 1
def test_sync_exit_coverage_places_long_exit_for_uncovered_position(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
tc = {
"initial_capital": 1000,
"leverage": 2,
"market_type": "swap",
"bot_params": {
"upperPrice": 758,
"lowerPrice": 588,
"gridCount": 23,
"amountPerGrid": 20,
"gridDirection": "long",
"initialPositionPct": 20,
},
}
placed = []
def fake_place(self, cell, purpose, side, price, *, reduce_only, pos_side, quantity=None):
placed.append(
{
"purpose": purpose,
"side": side,
"price": price,
"reduce_only": reduce_only,
"quantity": quantity,
"cell": cell.index,
}
)
return True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: 4.08)
monkeypatch.setattr("app.services.grid.engine.GridEngine._dedupe_open_exit_orders", lambda self, p: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine.sync_held_cell_exits", lambda self, px: 0)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._grid_base_qty",
lambda self, px: 0.059111,
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: (
generate_levels(588, 758, 23, "arithmetic"),
generate_cells(generate_levels(588, 758, 23, "arithmetic")),
),
)
class FakeOrders:
def list_open(self, strategy_id):
return []
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
n = engine.sync_exit_coverage(676.8)
assert n == 1
assert len(placed) == 1
assert placed[0]["purpose"] == "long_exit"
assert placed[0]["side"] == "sell"
assert placed[0]["reduce_only"] is True
assert placed[0]["quantity"] == pytest.approx(0.059111)
assert placed[0]["price"] > 676.8 - 20 # active cell upper near current price
def test_sync_exit_coverage_skips_when_exits_already_cover_position(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.resting_orders_repo import GridRestingOrder
tc = {
"initial_capital": 1000,
"leverage": 2,
"market_type": "swap",
"bot_params": {
"upperPrice": 758,
"lowerPrice": 588,
"gridCount": 23,
"amountPerGrid": 20,
"gridDirection": "long",
"initialPositionPct": 20,
},
}
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: 4.08)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: ([], []),
)
open_exit = GridRestingOrder(
id=1,
strategy_id=9,
symbol="BNB/USDT",
cell_index=11,
purpose="long_exit",
side="sell",
pos_side="long",
reduce_only=True,
price=676.7,
quantity=4.08,
quote_amount=20,
client_order_id="x",
exchange_order_id="y",
status="open",
filled_quantity=0,
processed_fill_qty=0,
)
class FakeOrders:
def list_open(self, strategy_id):
return [open_exit]
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return True
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
assert engine.sync_exit_coverage(676.8) == 0
def test_sync_exit_coverage_skips_when_target_cell_already_has_open_exit(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
from app.services.grid.resting_orders_repo import GridRestingOrder
tc = {
"initial_capital": 1000,
"leverage": 10,
"market_type": "swap",
"bot_params": {
"upperPrice": 758,
"lowerPrice": 588,
"gridCount": 24,
"amountPerGrid": 4,
"gridDirection": "long",
"initialPositionPct": 35,
},
}
placed = []
def fake_place(self, cell, purpose, side, price, *, reduce_only, pos_side, quantity=None):
placed.append({"cell": cell.index, "quantity": quantity})
return True
levels = generate_levels(588, 758, 24, "arithmetic")
cells = generate_cells(levels)
open_exit = GridRestingOrder(
id=1,
strategy_id=9,
symbol="BNB/USDT",
cell_index=13,
purpose="long_exit",
side="sell",
pos_side="long",
reduce_only=True,
price=691.47,
quantity=0.51,
quote_amount=4,
client_order_id="x",
exchange_order_id="y",
status="open",
filled_quantity=0,
processed_fill_qty=0,
)
class FakeOrders:
def list_open(self, strategy_id):
return [open_exit]
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return int(cell_index) == 13 and purpose == "long_exit"
target = next(c for c in cells if c.index == 13)
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: 0.62)
monkeypatch.setattr("app.services.grid.engine.GridEngine._dedupe_open_exit_orders", lambda self, p: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine.sync_held_cell_exits", lambda self, px: 0)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._active_cell_for_price",
lambda self, _cells, _price, _direction: target,
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: (levels, cells),
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._grid_base_qty",
lambda self, px: 0.059111,
)
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
assert engine.sync_exit_coverage(684.0) == 0
assert placed == []
def test_sync_exit_coverage_skips_when_position_below_one_grid(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
tc = {
"initial_capital": 100,
"leverage": 10,
"market_type": "swap",
"bot_params": {
"upperPrice": 758,
"lowerPrice": 588,
"gridCount": 24,
"amountPerGrid": 4,
"gridDirection": "long",
"initialPositionPct": 35,
},
}
placed = []
def fake_place(self, *args, **kwargs):
placed.append(1)
return True
levels = generate_levels(588, 758, 24, "arithmetic")
cells = generate_cells(levels)
class FakeOrders:
def list_open(self, strategy_id):
return []
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: 0.005)
monkeypatch.setattr("app.services.grid.engine.GridEngine._dedupe_open_exit_orders", lambda self, p: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine.sync_held_cell_exits", lambda self, px: 0)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._grid_base_qty",
lambda self, px: 0.059111,
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: (levels, cells),
)
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
assert engine.sync_exit_coverage(684.0) == 0
assert placed == []
def test_sync_exit_coverage_does_not_cover_held_cell_with_active_price_cell(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
from app.services.live_trading.grid_cells import GridCell, GridCellState
tc = {
"initial_capital": 1000,
"leverage": 2,
"market_type": "swap",
"bot_params": {
"upperPrice": 758,
"lowerPrice": 588,
"gridCount": 24,
"amountPerGrid": 20,
"gridDirection": "long",
"initialPositionPct": 20,
},
}
placed = []
levels = generate_levels(588, 758, 24, "arithmetic")
cells = generate_cells(levels)
held = GridCell(
strategy_id=9,
symbol="BNB/USDT",
cell_index=11,
lower_price=cells[11].lower_price,
upper_price=cells[11].upper_price,
state=GridCellState.LONG_HELD,
leg_size=0.05,
leg_entry_price=669.3,
)
class FakeOrders:
def list_open(self, strategy_id):
return []
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
class FakeCells:
def list_cells(self, strategy_id, symbol=None):
return [held]
def fake_place(self, *args, **kwargs):
placed.append((args, kwargs))
return True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: 0.05)
monkeypatch.setattr("app.services.grid.engine.GridEngine._dedupe_open_exit_orders", lambda self, p: None)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: (levels, cells),
)
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
engine._cells = FakeCells()
assert engine.sync_exit_coverage(669.3) == 1
assert len(placed) == 1
# The only allowed action is to repair the held cell's own TP, not to
# sell the position at the current price's lower active cell.
assert placed[0][0][1] == "long_exit"
assert placed[0][0][0].index == 11
assert placed[0][0][3] == pytest.approx(cells[11].upper_price)
def test_run_initial_market_stops_when_okx_net_position_exists(monkeypatch):
from app.services.grid.engine import GridEngine
tc = {
"initial_capital": 1000,
"leverage": 2,
"market_type": "swap",
"bot_params": {
"upperPrice": 758,
"lowerPrice": 588,
"gridCount": 23,
"amountPerGrid": 20,
"gridDirection": "long",
"initialPositionPct": 20,
},
}
recorded = {"calls": 0, "market": 0}
def fake_record(*args, **kwargs):
recorded["calls"] += 1
def fake_market(*args, **kwargs):
recorded["market"] += 1
return False, 0.0, 0.0
monkeypatch.setattr("app.services.grid.engine.record_grid_market_fill", fake_record)
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.persist_grid_resting_state", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._has_initial_market_trade", lambda self: False)
monkeypatch.setattr("app.services.grid.engine.execute_grid_market_order", fake_market)
engine = GridEngine(
11,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
target = engine._target_initial_base_qty(679.0)
monkeypatch.setattr("app.services.grid.engine.GridEngine._leg_position_qty", lambda self, side: target)
ok = engine.run_initial_market_position(679.0)
assert ok is True
assert engine._initial_done is True
assert recorded["calls"] == 1
assert recorded["market"] == 0
def test_sync_grid_orders_skips_non_idle_cell(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
from app.services.live_trading.grid_cells import GridCellState
tc = {
"initial_capital": 100,
"leverage": 10,
"market_type": "swap",
"bot_params": {
"upperPrice": 700,
"lowerPrice": 680,
"gridCount": 5,
"amountPerGrid": 4,
"gridDirection": "long",
},
}
placed = []
def fake_place(self, *args, **kwargs):
placed.append(args)
return True
levels = generate_levels(680, 700, 5, "arithmetic")
cells = generate_cells(levels)
class FakeOrders:
def list_open(self, strategy_id):
return []
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
def update_status(self, *a, **k):
return True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
monkeypatch.setattr("app.services.grid.engine.GridEngine._dedupe_open_entry_orders", lambda self, p: None)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: (levels, cells),
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._cell_state_by_index",
lambda self: {3: GridCellState.LONG_HELD},
)
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
n = engine.sync_grid_orders(691.5)
assert n == len(cells) - 1
assert all(int(p[0].index) != 3 for p in placed)
def test_sync_grid_orders_skips_when_exit_open(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import generate_cells, generate_levels
from app.services.live_trading.grid_cells import GridCellState
tc = {
"initial_capital": 100,
"leverage": 10,
"market_type": "swap",
"bot_params": {
"upperPrice": 700,
"lowerPrice": 680,
"gridCount": 5,
"amountPerGrid": 4,
"gridDirection": "long",
},
}
placed = []
def fake_place(self, *args, **kwargs):
placed.append(args)
return True
levels = generate_levels(680, 700, 5, "arithmetic")
cells = generate_cells(levels)
target_idx = 2
class FakeOrders:
def list_open(self, strategy_id):
return []
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return int(cell_index) == target_idx and purpose == "long_exit"
def update_status(self, *a, **k):
return True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
monkeypatch.setattr("app.services.grid.engine.GridEngine._dedupe_open_entry_orders", lambda self, p: None)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: (levels, cells),
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._cell_state_by_index",
lambda self: {target_idx: GridCellState.IDLE},
)
engine = GridEngine(
9,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._bootstrapped = True
engine._orders = FakeOrders()
engine.sync_grid_orders(691.5)
assert all(int(p[0].index) != target_idx for p in placed)
def test_on_order_filled_long_entry_marks_held_even_if_exit_hangs(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import GridCellSpec
from app.services.grid.resting_orders_repo import GridRestingOrder
from app.services.live_trading.grid_cells import GridCellState
tc = {"market_type": "swap", "bot_params": {"gridDirection": "long", "gridCount": 5}}
updates = []
class FakeOrders:
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
class FakeCells:
def update_state(self, *args, **kwargs):
updates.append(kwargs)
cell = GridCellSpec(index=1, lower_price=691.4, upper_price=691.5)
order = GridRestingOrder(
strategy_id=1,
symbol="BNB/USDT",
cell_index=1,
purpose="long_entry",
side="buy",
pos_side="long",
price=691.4,
quantity=0.05,
)
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr(
"app.services.grid.fill_handler.apply_grid_fill_to_local_state",
lambda *a, **k: None,
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: ([], [cell]),
)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", lambda *a, **k: False)
engine = GridEngine(
1,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._orders = FakeOrders()
engine._cells = FakeCells()
engine.on_order_filled(order, 0.05, 691.4)
assert len(updates) == 1
assert updates[0]["state"] == GridCellState.LONG_HELD
def test_on_order_filled_long_exit_rehangs_entry_immediately(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import GridCellSpec
from app.services.grid.resting_orders_repo import GridRestingOrder
from app.services.live_trading.grid_cells import GridCellState
tc = {"market_type": "swap", "bot_params": {"gridDirection": "long", "gridCount": 5}}
placed = []
state = {"value": GridCellState.LONG_HELD}
class FakeOrders:
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
class FakeCells:
def update_state(self, *args, **kwargs):
state["value"] = kwargs["state"]
cell = GridCellSpec(index=1, lower_price=691.4, upper_price=691.5)
order = GridRestingOrder(
strategy_id=1,
symbol="BNB/USDT",
cell_index=1,
purpose="long_exit",
side="sell",
pos_side="long",
price=691.5,
quantity=0.05,
)
def fake_place(self, cell, purpose, side, price, *, reduce_only, pos_side, quantity=None):
placed.append(
{
"purpose": purpose,
"side": side,
"price": price,
"reduce_only": reduce_only,
"pos_side": pos_side,
"quantity": quantity,
}
)
state["value"] = GridCellState.BUY_OPEN
return True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr(
"app.services.grid.fill_handler.apply_grid_fill_to_local_state",
lambda *a, **k: None,
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: ([], [cell]),
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._cell_state_by_index",
lambda self: {1: state["value"]},
)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
engine = GridEngine(
1,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._orders = FakeOrders()
engine._cells = FakeCells()
engine.on_order_filled(order, 0.05, 691.5)
assert placed == [
{
"purpose": "long_entry",
"side": "buy",
"price": 691.4,
"reduce_only": False,
"pos_side": "long",
"quantity": 0.05,
}
]
assert state["value"] == GridCellState.BUY_OPEN
def test_on_order_filled_short_exit_rehangs_entry_immediately(monkeypatch):
from app.services.grid.engine import GridEngine
from app.services.grid.levels import GridCellSpec
from app.services.grid.resting_orders_repo import GridRestingOrder
from app.services.live_trading.grid_cells import GridCellState
tc = {"market_type": "swap", "bot_params": {"gridDirection": "short", "gridCount": 5}}
placed = []
state = {"value": GridCellState.SHORT_HELD}
class FakeOrders:
def has_open_for_cell(self, strategy_id, cell_index, purpose):
return False
class FakeCells:
def update_state(self, *args, **kwargs):
state["value"] = kwargs["state"]
cell = GridCellSpec(index=1, lower_price=691.4, upper_price=691.5)
order = GridRestingOrder(
strategy_id=1,
symbol="BNB/USDT",
cell_index=1,
purpose="short_exit",
side="buy",
pos_side="short",
price=691.4,
quantity=0.05,
)
def fake_place(self, cell, purpose, side, price, *, reduce_only, pos_side, quantity=None):
placed.append(
{
"purpose": purpose,
"side": side,
"price": price,
"reduce_only": reduce_only,
"pos_side": pos_side,
"quantity": quantity,
}
)
state["value"] = GridCellState.SELL_OPEN
return True
monkeypatch.setattr("app.services.grid.engine.append_strategy_log", lambda *a, **k: None)
monkeypatch.setattr(
"app.services.grid.fill_handler.apply_grid_fill_to_local_state",
lambda *a, **k: None,
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._levels_and_cells",
lambda self: ([], [cell]),
)
monkeypatch.setattr(
"app.services.grid.engine.GridEngine._cell_state_by_index",
lambda self: {1: state["value"]},
)
monkeypatch.setattr("app.services.grid.engine.GridEngine._place_limit", fake_place)
engine = GridEngine(
1,
"BNB/USDT",
tc,
{},
create_client_fn=lambda: object(),
enqueue_market=lambda *a, **k: False,
)
engine._orders = FakeOrders()
engine._cells = FakeCells()
engine.on_order_filled(order, 0.05, 691.4)
assert placed == [
{
"purpose": "short_entry",
"side": "sell",
"price": 691.5,
"reduce_only": False,
"pos_side": "short",
"quantity": 0.05,
}
]
assert state["value"] == GridCellState.SELL_OPEN
def test_grid_fill_profit_uses_cell_entry_price(monkeypatch):
from app.services.grid import fill_handler
from app.services.grid.resting_orders_repo import GridRestingOrder
from app.services.live_trading.grid_cells import GridCell, GridCellState
cell = GridCell(
strategy_id=1,
symbol="BNB/USDT",
cell_index=11,
lower_price=669.3043,
upper_price=676.6957,
state=GridCellState.LONG_HELD,
leg_size=0.05,
leg_entry_price=669.3,
)
captured = {}
class FakeCellRepo:
def list_cells(self, strategy_id, symbol=None):
return [cell]
monkeypatch.setattr(fill_handler, "GridCellRepository", lambda: FakeCellRepo())
monkeypatch.setattr(fill_handler, "resolve_leg_context", lambda **kwargs: None)
monkeypatch.setattr(
fill_handler,
"apply_fill_to_local_position",
lambda **kwargs: (-0.99, None, 690.0),
)
monkeypatch.setattr(fill_handler, "record_trade", lambda **kwargs: captured.update(kwargs))
order = GridRestingOrder(
strategy_id=1,
symbol="BNB/USDT",
cell_index=11,
purpose="long_exit",
side="sell",
pos_side="long",
price=676.6957,
quantity=0.05,
)
fill_handler.apply_grid_fill_to_local_state(
1,
"BNB/USDT",
order,
0.05,
676.7,
{"market_type": "swap", "commission": 0},
)
expected = (676.7 - 669.3) * 0.05
assert captured["profit"] == pytest.approx(expected)
assert captured["grid_matched_profit"] == pytest.approx(expected)
assert captured["matched_entry_price"] == pytest.approx(669.3)