"""``Strategy.quote``: a strategy that quotes, written in the DSL and run by ``mbt.run`` on the order-level simulation. The venue's rules for quotes are pinned by the engine's own tests (hold, withdraw, keep, reprice, the maker of example 30 against its native port). This file checks the Python side: the builder and its JSON, the refusals by name, and that a run through ``mbt.run`` gives exactly what ``bt.sim.run`` gives the same maker written as a function, with the same columns. """ import json import math import pytest import manifoldbt as bt from manifoldbt import _native, book from manifoldbt.helpers import Interval from manifoldbt.result import QuoteResult MS = 1_000_000 CLIP = 0.01 MAX_INVENTORY = 0.05 def _layer_open(): try: bt.sim.Market.from_ladders("X", 0.1, [(0, [(1.0, 1.0)], [(2.0, 1.0)])]) except PermissionError: return False return True needs_layer = pytest.mark.skipif( not _layer_open(), reason="order-level simulation locked (runs in the engine's own debug builds)", ) # --------------------------------------------------------------------------- # The maker of example 30, both ways # --------------------------------------------------------------------------- def dsl_maker(): inventory = bt.position() clips = bt.round(inventory / CLIP) bids, asks = book.bid_levels(), book.ask_levels() book_ok = (bids > 0) & (asks > 0) bid = book.bid_price_at(bt.clip(clips, 0, bids - 1) + 1) ask = book.ask_price_at(bt.clip(-clips, 0, asks - 1) + 1) return ( bt.Strategy.create("dsl_maker") .quote("buy", bt.when(book_ok, bid), CLIP, tif="GTX", enabled=inventory < MAX_INVENTORY - 1e-12) .quote("sell", bt.when(book_ok, ask), CLIP, tif="GTX", enabled=inventory > -MAX_INVENTORY + 1e-12) ) def py_maker(sim): """Example 30's `maker`, verbatim but for the symbol and the clock.""" live_states = ("in_flight", "resting", "partially_filled") quotes = {"buy": None, "sell": None} while sim.elapse(Interval.millis(100)): bids, asks = sim.book("BTCUSDT").levels() if not bids or not asks: continue inventory = sim.position("BTCUSDT") clips = round(inventory / CLIP) bid_level = min(max(clips, 0), len(bids) - 1) ask_level = min(max(-clips, 0), len(asks) - 1) wanted = { "buy": bids[bid_level][0] if inventory < MAX_INVENTORY - 1e-12 else None, "sell": asks[ask_level][0] if inventory > -MAX_INVENTORY + 1e-12 else None, } for side, price in wanted.items(): oid = quotes[side] order = sim.order(oid) if oid is not None else None live = order is not None and order.status in live_states if live and price is not None and math.isclose(order.price, price): continue if live and not order.cancel_pending: sim.cancel(order.id) quotes[side] = None if price is not None: quotes[side] = sim.post("BTCUSDT", side, price, CLIP, tif="GTX") LATENCY = {"order": 5 * MS, "cancel": 5 * MS, "response": 5 * MS, "feed": 2 * MS} QUEUE = {"depth_source": "book", "assumed_queue": 2.0} def quote_config(**over): execution = dict(latency=LATENCY, fill_model={"queue": QUEUE}) execution.update(over.pop("execution", {})) kw = dict( universe=[1], time_range_start=0, time_range_end=86_400_000_000_000, bar_interval=Interval.millis(100), initial_capital=1_000.0, fees=bt.FeeConfig(maker_fee_bps=0.0, taker_fee_bps=4.0), execution=bt.ExecutionConfig(**execution), ) kw.update(over) return bt.BacktestConfig(**kw) def sim_config(): return bt.sim.Config( latency={"entry": 5 * MS, "cancel": 5 * MS, "response": 5 * MS, "feed": 2 * MS}, queue=QUEUE, taker_fee_bps=4.0, initial_cash=1_000.0, ) def walk_market(seed=7, n=2_000): """A book that walks a tick at a time, thins, empties a side now and then, and prints on both sides: a day in miniature.""" import random rng = random.Random(seed) mid, t, states, trades = 1_000_000, 0, [], [] for _ in range(n): t += (1 + rng.randrange(60)) * MS mid += rng.choice((-1, 0, 1)) bids = [((mid - 1 - k) / 10, 0.001 * (1 + rng.randrange(50))) for k in range(1 + rng.randrange(10))] asks = [((mid + 1 + k) / 10, 0.001 * (1 + rng.randrange(50))) for k in range(1 + rng.randrange(10))] r = rng.randrange(40) if r == 0: bids = [] elif r == 1: asks = [] states.append((t, bids, asks)) for _ in range(rng.randrange(3)): ts = t + rng.randrange(5) * MS q = 0.002 * (1 + rng.randrange(50)) if rng.random() < 0.5 and bids: trades.append((ts, bids[rng.randrange(min(2, len(bids)))][0], q, "sell")) elif asks: trades.append((ts, asks[rng.randrange(min(2, len(asks)))][0], q, "buy")) trades.sort(key=lambda p: p[0]) return bt.sim.Market.from_ladders("BTCUSDT", 0.1, states, trades) def run_on(market, strategy=None, config=None): strategy = strategy or dsl_maker() config = config or quote_config() raw, sim_raw = _native.run_quotes(strategy.to_json(), config.to_json(), None, [market]) sim = bt.sim.SimResult(sim_raw, bt.sim.Config(initial_cash=config.initial_capital)) return QuoteResult(raw, sim) def _bits(x): return None if x is None else float(x).hex() def _table(cols): return [tuple(_bits(v) if isinstance(v, float) else v for v in row) for row in zip(*cols.values())] @needs_layer def test_the_dsl_maker_gives_the_python_maker_s_journal_to_the_bit(): market = walk_market() res = run_on(market) ref = bt.sim.run(py_maker, sim_config(), markets=[market]) assert len(res.sim.raw.orders_columns()["order_id"]) > 50 for name in ("orders_columns", "fills_columns", "events_columns"): a = getattr(res.sim.raw, name)() b = getattr(ref.raw, name)() assert list(a) == list(b), name assert _table(a) == _table(b), name ts_a, eq_a = res.sim.raw.equity_columns() ts_b, eq_b = ref.raw.equity_columns() assert ts_a == ts_b assert [e.hex() for e in eq_a] == [e.hex() for e in eq_b] assert (res.sim.cash.hex(), res.sim.fees.hex()) == (ref.cash.hex(), ref.fees.hex()) def _squared_maker(square): """The maker above, its level pushed back by the SQUARE of the inventory in clips: `**` inside a quote, read at each wake-up.""" inventory = bt.position() clips = bt.round(inventory / CLIP) bids, asks = book.bid_levels(), book.ask_levels() book_ok = (bids > 0) & (asks > 0) depth = square(clips) bid = book.bid_price_at(bt.clip(depth, 0, bids - 1) + 1) ask = book.ask_price_at(bt.clip(depth, 0, asks - 1) + 1) return ( bt.Strategy.create("squared_maker") .quote("buy", bt.when(book_ok, bid), CLIP, tif="GTX", enabled=inventory < MAX_INVENTORY - 1e-12) .quote("sell", bt.when(book_ok, ask), CLIP, tif="GTX", enabled=inventory > -MAX_INVENTORY + 1e-12) ) @needs_layer def test_a_power_in_a_quote_runs_as_the_product_written_by_hand(): powered = _squared_maker(lambda c: c ** 2) by_hand = _squared_maker(lambda c: c * c) assert powered.to_json() == by_hand.to_json() market = walk_market() a, b = run_on(market, powered), run_on(market, by_hand) assert len(a.sim.raw.orders_columns()["order_id"]) > 50 for name in ("orders_columns", "fills_columns", "events_columns"): assert _table(getattr(a.sim.raw, name)()) == _table(getattr(b.sim.raw, name)()), name @needs_layer def test_the_result_is_a_result_with_the_venue_s_tables_beside_it(): res = run_on(walk_market()) ref = bt.sim.run(py_maker, sim_config(), markets=[walk_market()]) pd = pytest.importorskip("pandas") for name in ("orders_df", "fills_df", "events_df"): a, b = getattr(res, name)("pandas"), getattr(ref, name)("pandas") assert list(a.columns) == list(b.columns), name pd.testing.assert_frame_equal(a, b) # The same Result as bt.run: one trade per fill, metrics, the activity. fills = res.fills_df("pandas") trades = res.trades_df("pandas") assert len(trades) == len(fills) assert list(trades["fill_price"]) == list(fills["price"]) assert set(res.metrics) >= {"sharpe", "max_drawdown", "total_return", "trade_stats"} activity = res.order_activity assert activity["orders_posted"] == len(res.orders_df("pandas")) assert {"stale_fills", "overlapping_live_orders"} <= set(activity) assert res.fill_marks["anchor"] == "fill" # Equity at one second: the wake-ups are 100 ms apart. eq = res.equity_df("pandas") steps = eq["timestamp"].diff().dropna().dt.total_seconds() assert steps.median() == pytest.approx(1.0) assert eq["equity"].iloc[-1] == pytest.approx(ref.metrics["final_equity"]) # --------------------------------------------------------------------------- # The builder and its JSON # --------------------------------------------------------------------------- def test_a_strategy_without_quotes_serialises_as_it_always_did(): s = bt.Strategy.create("s").signal("x", bt.col("close") > 1).size(bt.col("x")) assert "quotes" not in s.to_json_dict() assert "quotes" not in json.loads(s.to_json()) def test_quote_writes_its_fields_and_collects_its_params(): s = bt.Strategy.create("q").quote( "Buy", book.bid_price_at(1) - bt.param("edge", default=0.1), 2, tif="gtc", enabled=bt.position() < 1, ) doc = s.to_json_dict() (q,) = doc["quotes"] assert (q["side"], q["tif"]) == ("buy", "GTC") assert q["size"] == {"Literal": {"Float64": 2.0}} assert "enabled" in q assert "edge" in doc["parameters"] s2 = bt.Strategy.create("q").quote("sell", 100.0, 1.0) assert "enabled" not in s2.to_json_dict()["quotes"][0] assert s2.to_json_dict()["quotes"][0]["tif"] == "GTX", "post-only by default" def test_a_cooldown_is_written_in_nanoseconds_and_only_when_there_is_one(): def q(**kw): return bt.Strategy.create("q").quote("buy", 100.0, 1.0, **kw) assert "cooldown_ns" not in q().to_json_dict()["quotes"][0] # Zero is no pause: the JSON of the quote without one, byte for byte. assert q(cooldown=0).to_json() == q().to_json() assert q(cooldown=Interval.millis(0)).to_json() == q().to_json() assert q(cooldown=Interval.millis(500)).to_json_dict()["quotes"][0]["cooldown_ns"] == 500 * MS assert q(cooldown=Interval.seconds(2)).to_json_dict()["quotes"][0]["cooldown_ns"] == 2_000 * MS assert q(cooldown=250_000).to_json_dict()["quotes"][0]["cooldown_ns"] == 250_000 with pytest.raises(ValueError, match="cooldown of quote 1 must be a duration of zero or more"): q(cooldown=-1) for bad in (True, 0.5, "500ms"): with pytest.raises(TypeError, match="cooldown of quote 1 takes a duration"): q(cooldown=bad) with pytest.raises(TypeError, match="simulation clock"): q(cooldown=Interval.trades()) def test_size_and_quotes_refuse_each_other(): with pytest.raises(ValueError, match="sizes each quote itself"): bt.Strategy.create("a").size(bt.lit(1.0)).quote("buy", 100.0, 1.0) with pytest.raises(ValueError, match="sizes each quote itself"): bt.Strategy.create("a").quote("buy", 100.0, 1.0).size(bt.lit(1.0)) def test_a_side_or_a_time_in_force_is_checked_where_it_is_written(): with pytest.raises(ValueError, match='side must be "buy" or "sell"'): bt.Strategy.create("a").quote("long", 100.0, 1.0) with pytest.raises(ValueError, match="tif must be"): bt.Strategy.create("a").quote("buy", 100.0, 1.0, tif="GTD") # --------------------------------------------------------------------------- # Refused by name # --------------------------------------------------------------------------- @pytest.fixture def empty_store(tmp_path): return bt.DataStore( data_root=str(tmp_path / "data"), metadata_db=str(tmp_path / "meta.sqlite") ) def _refused(strategy, config, store): with pytest.raises(Exception) as err: bt.run(strategy, config, store) return str(err.value) @needs_layer @pytest.mark.parametrize( "over, words", [ (dict(universe=[1, 2]), "runs on one symbol"), (dict(execution=dict(signal_delay=1)), "execution.signal_delay"), (dict(slippage={"FixedBps": {"bps": 1.0}}), "slippage"), (dict(execution=dict(position_sizing_mode="Units")), "execution.position_sizing_mode"), (dict(execution=dict(tick_size=0.1)), "execution.tick_size"), ( dict(execution=dict(fill_model={ "queue": QUEUE, "adverse": {"model": "snipe", "threshold_ticks": 2.0, "extra_cancel_latency": Interval.millis(5)}, })), "execution.fill_model.adverse (snipe)", ), (dict(bar_interval=Interval.trades()), "Interval.trades()"), (dict(resample_to=Interval.seconds(1)), "resample_to"), ], ) def test_a_setting_a_quoting_strategy_does_not_read_is_refused_by_name(over, words, empty_store): m = _refused(dsl_maker(), quote_config(**over), empty_store) assert words in m, m @needs_layer def test_brackets_are_refused_by_name(empty_store): s = dsl_maker().stop_loss(pct=2.0) assert "sets stop_loss" in _refused(s, quote_config(), empty_store) def test_every_bar_path_refuses_a_millisecond_clock(empty_store): s = bt.Strategy.create("s").signal("x", bt.col("close") > 1).size(bt.col("x")) m = _refused(s, quote_config(execution=dict(latency=None, fill_model=None)), empty_store) assert "bar_interval=Interval.millis(100) is the wake-up clock" in m, m def test_the_bar_engine_refuses_response_and_feed(empty_store): s = bt.Strategy.create("s").signal("x", bt.col("close") > 1).size(bt.col("x")) cfg = quote_config( bar_interval=Interval.minutes(1), execution=dict(latency={"order": MS, "feed": MS}, fill_model=None), ) m = _refused(s, cfg, empty_store) assert "execution.latency.feed is read only by a strategy that quotes" in m, m def test_the_bar_compiler_refuses_a_strategy_that_quotes_naming_where_it_runs(): with pytest.raises(Exception, match=r"quotes \(Strategy.quote\): it runs on the " r"order-level simulation, through mbt.run, run_sweep"): _native.compile_strategy_json(dsl_maker().to_json()) @pytest.mark.skipif(_layer_open(), reason="the order-level layer is open here") def test_a_strategy_that_quotes_is_behind_the_same_door_as_bt_sim(empty_store): with pytest.raises(PermissionError, match="Order-level simulation"): bt.run(dsl_maker(), quote_config(), empty_store) # --------------------------------------------------------------------------- # Prices on the tick grid, and what a quoting run counts # --------------------------------------------------------------------------- def _offered_at_116994_6(trades): """5 BTC offered at 116994.6, tick 0.1 (``repro/grille_1.py``).""" return bt.sim.Market.from_ladders( "BTCUSDT", tick_size=0.1, states=[(1 * MS, [(116994.3, 1.0)], [(116994.4, 1.0), (116994.6, 5.0), (116994.7, 1.0)])], trades=trades, ) @needs_layer def test_a_sim_price_a_few_ulps_under_the_grid_waits_in_its_queue(): computed = 116994.4 + 0.2 assert computed != 116994.6 seen = [] for price in (computed, 116994.6): def strat(sim, price=price): sim.elapse(1_500_000 - sim.now()) oid = sim.post("BTCUSDT", "sell", price, 0.005, tif="GTX") seen.append(sim.order(oid).price) sim.elapse(5 * MS) market = _offered_at_116994_6([(3 * MS, 116994.6, 0.01, "buy")]) res = bt.sim.run(strat, markets=[market]) assert res.fills_df(backend="dict")["qty"] == [], price assert res.levels_snapped == 0 assert res.fill_fragility["queue"]["traverse_fills"] == 0 assert seen == [116994.6, 116994.6] @needs_layer def test_a_sim_price_between_two_ticks_goes_to_the_passive_side(): def strat(sim): sim.elapse(1_500_000 - sim.now()) buy = sim.post("BTCUSDT", "buy", 116994.37, 0.005, tif="GTX") sell = sim.post("BTCUSDT", "sell", 116994.43, 0.005, tif="GTX") assert (sim.order(buy).price, sim.order(sell).price) == (116994.3, 116994.5) sim.elapse(MS) res = bt.sim.run(strat, markets=[_offered_at_116994_6([])]) assert res.levels_snapped == 2 def _ask_plus(ticks, rounded): price = book.ask_price_at(1) + ticks * 0.1 if rounded: price = price.round_to(0.1) return bt.Strategy.create("grille").quote("sell", price, 0.005, tif="GTX") @needs_layer def test_a_quote_computed_off_the_grid_runs_as_the_quote_rounded_onto_it(): market = walk_market(seed=3, n=1_500) a = run_on(market, _ask_plus(2, rounded=False)) b = run_on(market, _ask_plus(2, rounded=True)) assert len(a.sim.raw.orders_columns()["order_id"]) > 20 for name in ("orders_columns", "fills_columns", "events_columns"): assert _table(getattr(a.sim.raw, name)()) == _table(getattr(b.sim.raw, name)()), name assert a.order_activity == b.order_activity assert "levels_snapped" not in a.order_activity @needs_layer def test_a_quoting_run_reports_its_queue_and_its_refusals(): res = run_on(walk_market()) frag = res.fill_fragility assert frag is not None, "a quoting run has fill_fragility, as a bar run under a queue" q = frag["queue"] assert set(q) >= { "queue_decided_fills", "traverse_fills", "partial_fills", "book_unknown_at_post", "fills_from_book_cross", } channels = res.fills_df(backend="dict")["channel"] assert q["queue_decided_fills"] == channels.count("queue") assert q["fills_from_book_cross"] == channels.count("book_cross") assert q["traverse_fills"] == channels.count("traverse") + channels.count("book_cross") assert frag["maker_fills"] == len(channels) assert frag["touch_only_fills"] == 0 statuses = res.orders_df(backend="dict")["status"] activity = res.order_activity assert activity.get("post_only_rejected", 0) == statuses.count("rejected") > 0 # Per quote: this maker never posts a quote again before the cancellation # of its previous order came back, so every overlap is a requote. assert activity["overlapping_live_orders"] == activity["requotes"] # The execution columns of a sweep read them. cols = bt.sweep_columns([res], ["service_rate", "queue_decided_fills"]) assert cols["queue_decided_fills"][0] == q["queue_decided_fills"] assert cols["service_rate"][0] == pytest.approx( frag["maker_fills"] / activity["orders_posted"] ) @needs_layer def test_a_quote_deeper_than_the_levels_read_is_refused_as_such(): deep = bt.Strategy.create("profond").quote("buy", book.bid_price_at(1) - 3.0, 0.01) config = quote_config(execution={"fill_model": {"queue": {"depth_source": "book"}}}) with pytest.raises(Exception, match="levels read from the store") as err: run_on(walk_market(), deep, config) assert "ingest the book" not in str(err.value) res = run_on(walk_market(), deep, quote_config()) assert res.fill_fragility["queue"]["book_unknown_at_post"] > 0 # --------------------------------------------------------------------------- # The ages and the order's own price: a maker that pulls and pauses, a taker # that holds for a bounded time, each against its bt.sim twin # --------------------------------------------------------------------------- SEUIL, MAX_INV, EPS = 0.03, 0.025, 1e-12 PAUSE_S = 0.5 WAKE = Interval.millis(50) def dsl_file_courte(cooldown=False): """The maker of the HFT harness's ``file_courte``: a clip at each best price, pulled (never moved) when the queue ahead passes SEUIL, when the best price leaves it or when the inventory is full; a side pulled pauses PAUSE_S, written with ``last_cancel_age`` or, ``cooldown=True``, as the quote's ``cooldown``.""" from manifoldbt.indicators import abs_val pos = bt.position() bb, ba = book.bid_price_at(1), book.ask_price_at(1) book_ok = (bb > 0) & (ba > bb) strategy = bt.Strategy.create("file_courte") for side, qs, own, inv_ok in (("buy", "bid", bb, pos < MAX_INV - EPS), ("sell", "ask", ba, pos > -MAX_INV + EPS)): has = bt.live_qty(qs) > 0 pull = (~inv_ok | ~(own > 0) | (abs_val(bt.order_price(qs) - own) > 1e-6) | (bt.queue_ahead(qs) > SEUIL)) wanted = (has & ~pull) | (~has & book_ok & inv_ok) if cooldown: strategy = strategy.quote( side, bt.when(has, bt.order_price(qs), own), CLIP, tif="GTX", enabled=wanted, cooldown=Interval.millis(int(PAUSE_S * 1000))) else: paused = bt.last_cancel_age(qs) < PAUSE_S strategy = strategy.quote( side, bt.when(has, bt.order_price(qs), own), CLIP, tif="GTX", enabled=~paused & wanted) return strategy def py_file_courte(sim): """``run_mbt_sim`` of the harness, on the wake-up grid of the quotes.""" live_states = ("in_flight", "resting", "partially_filled") oid = {"buy": None, "sell": None} pulled_at = {"buy": None, "sell": None} while sim.elapse(WAKE): now = sim.now() bids, asks = sim.book("BTCUSDT").levels(1) bb = bids[0][0] if bids else None ba = asks[0][0] if asks else None pos = sim.position("BTCUSDT") for side, px, inv_ok in (("buy", bb, pos < MAX_INV - EPS), ("sell", ba, pos > -MAX_INV + EPS)): if oid[side] is not None: o = sim.order(oid[side]) if o.status in live_states: if o.cancel_pending: continue qa = o.queue_ahead if (not inv_ok or px is None or abs(o.price - px) > 1e-6 or (qa is not None and qa > SEUIL)): sim.cancel(o.id) pulled_at[side] = now continue oid[side] = None if px is None or bb is None or ba is None or ba <= bb or not inv_ok: continue if pulled_at[side] is not None and now - pulled_at[side] < PAUSE_S * 1e9: continue oid[side] = sim.post("BTCUSDT", side, px, CLIP, tif="GTX") HOLD_S, POS_MAX, SIG = 2.0, 0.03, 2 def dsl_preneur(): """A taker that leaves at the touch after HOLD_S, its holding time read from the state: no timer order.""" pos = bt.position() nb, na = book.bid_levels(), book.ask_levels() sig = nb - na book_ok = (nb > 0) & (na > 0) old = bt.position_age() >= HOLD_S - 1e-6 out_long = book_ok & (pos > 1e-9) & ((sig < -SIG) | old) out_short = book_ok & (pos < -1e-9) & ((sig > SIG) | old) free = ~out_long & ~out_short bid1, ask1 = book.bid_price_at(1), book.ask_price_at(1) return ( bt.Strategy.create("preneur") .quote("sell", bid1, pos, tif="IOC", enabled=out_long) .quote("buy", ask1, -pos, tif="IOC", enabled=out_short) .quote("buy", ask1, CLIP, tif="IOC", enabled=book_ok & (sig > SIG) & (pos < POS_MAX - 1e-9) & free) .quote("sell", bid1, CLIP, tif="IOC", enabled=book_ok & (sig < -SIG) & (pos > -POS_MAX + 1e-9) & free) ) def py_preneur(sim): """The same taker for bt.sim, its holding time counted as the harness counts it: from the first wake-up that sees the position.""" t_pos = None while sim.elapse(Interval.millis(100)): now = sim.now() pos = sim.position("BTCUSDT") if abs(pos) > 1e-9: t_pos = now if t_pos is None else t_pos else: t_pos = None bids, asks = sim.book("BTCUSDT").levels() if not bids or not asks: continue sig = len(bids) - len(asks) old = t_pos is not None and now - t_pos >= HOLD_S * 1e9 if pos > 1e-9 and (sig < -SIG or old): sim.post("BTCUSDT", "sell", bids[0][0], pos, tif="IOC") elif pos < -1e-9 and (sig > SIG or old): sim.post("BTCUSDT", "buy", asks[0][0], -pos, tif="IOC") elif sig > SIG and pos < POS_MAX - 1e-9: sim.post("BTCUSDT", "buy", asks[0][0], CLIP, tif="IOC") elif sig < -SIG and pos > -POS_MAX + 1e-9: sim.post("BTCUSDT", "sell", bids[0][0], CLIP, tif="IOC") def _same_journal(res, ref): for name in ("orders_columns", "fills_columns", "events_columns"): a = getattr(res.sim.raw, name)() b = getattr(ref.raw, name)() assert list(a) == list(b), name assert _table(a) == _table(b), name ts_a, eq_a = res.sim.raw.equity_columns() ts_b, eq_b = ref.raw.equity_columns() assert ts_a == ts_b assert [e.hex() for e in eq_a] == [e.hex() for e in eq_b] @needs_layer @pytest.mark.parametrize("cooldown", [False, True]) @pytest.mark.parametrize("seed", [3, 11]) def test_a_maker_that_pulls_and_pauses_gives_its_bt_sim_twin_s_journal(seed, cooldown): market = walk_market(seed, 3_000) res = run_on(market, dsl_file_courte(cooldown), quote_config(bar_interval=WAKE)) ref = bt.sim.run(py_file_courte, sim_config(), markets=[market]) events = res.sim.raw.events_columns()["event"] assert events.count("cancel_sent") > 100 assert res.order_activity["requotes"] == 0 _same_journal(res, ref) @needs_layer @pytest.mark.parametrize("seed", [5, 17]) def test_a_taker_that_leaves_on_the_position_age_gives_its_bt_sim_twin_s_journal(seed): market = walk_market(seed, 3_000) res = run_on(market, dsl_preneur()) ref = bt.sim.run(py_preneur, sim_config(), markets=[market]) assert len(res.sim.raw.orders_columns()["order_id"]) > 100 _same_journal(res, ref)