nyrobrain 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. nyrobrain/__init__.py +7 -0
  2. nyrobrain/execution/CONTEXT.md +109 -0
  3. nyrobrain/execution/__init__.py +78 -0
  4. nyrobrain/execution/aegis.py +92 -0
  5. nyrobrain/execution/backtest.py +184 -0
  6. nyrobrain/execution/candles.py +218 -0
  7. nyrobrain/execution/credentials.py +203 -0
  8. nyrobrain/execution/data.py +95 -0
  9. nyrobrain/execution/driver.py +219 -0
  10. nyrobrain/execution/emitter.py +210 -0
  11. nyrobrain/execution/examples/README.md +173 -0
  12. nyrobrain/execution/examples/__init__.py +17 -0
  13. nyrobrain/execution/examples/oms.py +804 -0
  14. nyrobrain/execution/examples/over_delivery_guard.py +83 -0
  15. nyrobrain/execution/examples/sizing_a_target.py +70 -0
  16. nyrobrain/execution/examples/your_own_placement.py +117 -0
  17. nyrobrain/execution/executor.py +837 -0
  18. nyrobrain/execution/freshness.py +108 -0
  19. nyrobrain/execution/ingest.py +171 -0
  20. nyrobrain/execution/intake.py +194 -0
  21. nyrobrain/execution/ladder.py +256 -0
  22. nyrobrain/execution/metrics.py +463 -0
  23. nyrobrain/execution/pending.py +96 -0
  24. nyrobrain/execution/placement.py +177 -0
  25. nyrobrain/execution/prime.py +232 -0
  26. nyrobrain/execution/ratelimit.py +97 -0
  27. nyrobrain/execution/reconciliation.py +213 -0
  28. nyrobrain/execution/report.py +649 -0
  29. nyrobrain/execution/rules.py +72 -0
  30. nyrobrain/execution/runner.py +328 -0
  31. nyrobrain/execution/sandbox.py +109 -0
  32. nyrobrain/execution/signal.py +236 -0
  33. nyrobrain/execution/soak.py +761 -0
  34. nyrobrain/execution/store.py +95 -0
  35. nyrobrain/execution/target.py +169 -0
  36. nyrobrain/execution/testing.py +273 -0
  37. nyrobrain/execution/venue.py +335 -0
  38. nyrobrain/py.typed +0 -0
  39. nyrobrain-0.1.0.dist-info/METADATA +100 -0
  40. nyrobrain-0.1.0.dist-info/RECORD +41 -0
  41. nyrobrain-0.1.0.dist-info/WHEEL +4 -0
nyrobrain/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Nyrobrain Python libraries.
2
+
3
+ This package is a namespace and deliberately exports nothing. Siblings are
4
+ expected alongside `nyrobrain.execution` — adrs migrates in later under the same
5
+ brand — so importing `nyrobrain` must never pull an engine, a transport or a
6
+ research stack into the process. Import the subpackage you want.
7
+ """
@@ -0,0 +1,109 @@
1
+ # nyrobrain.execution
2
+
3
+ The shared language of the execution library. A glossary, not a spec: it fixes what each word means so that the library, the portfolio that feeds it, and the dashboards that watch it all use it the same way.
4
+
5
+ ## Language
6
+
7
+ ### The instruction
8
+
9
+ **Signal**:
10
+ One portfolio's complete target state at one instant: a target weight for every asset it holds. The whole truth, never a change set — so applying the same signal twice leaves the portfolio where applying it once did.
11
+ _Avoid_: Order, instruction, update, delta, rebalance
12
+
13
+ **Target weight**:
14
+ The share of a portfolio's notional budget an asset should occupy, signed for direction. Positive is long, negative is short. It says what to hold, never what to trade.
15
+ _Avoid_: Allocation, size, position size, exposure
16
+
17
+ **Gross exposure**:
18
+ The sum of a signal's target weights ignoring their signs — the total notional a portfolio wants deployed. A signal is invalid above 1. Distinct from net exposure, which cancels longs against shorts and therefore bounds direction rather than leverage.
19
+ _Avoid_: Total exposure, leverage, sum of weights
20
+
21
+ **Flat**:
22
+ Holding nothing in an asset. A target weight of zero and an asset's absence from a signal both mean flat; a correct publisher states the zero, and an absence is a defect worth alerting on.
23
+ _Avoid_: Closed, exited, zeroed, neutral
24
+
25
+ **Target**:
26
+ The position an asset should hold, in base units, implied by its target weight and the portfolio's budget, leverage and the current price. What we want; never what we do about it.
27
+ _Avoid_: Desired, goal, wanted position, ideal
28
+
29
+ **Actual**:
30
+ The position held at the venue. Read, never remembered — a figure the library keeps for itself between ticks is a figure that can go stale, and stale actuals are what made an OMS buy without end.
31
+ _Avoid_: Current, held, exchange position, real position
32
+
33
+ **Working**:
34
+ Every order of ours that could still fill, counted toward the position we are already committed to. Includes orders sent but not yet acknowledged: a quantity in flight has been committed even though nothing has filled and the venue has not replied.
35
+ _Avoid_: Open orders, pending, resting, outstanding, in-flight
36
+
37
+ **Delta**:
38
+ What remains to be traded for an asset: its target less its actual less its working. Derived afresh each tick, never carried forward, so an under-traded tick corrects itself and a repeated tick does nothing.
39
+ _Avoid_: Diff, gap, remainder, adjustment, order size
40
+
41
+ **Placement**:
42
+ Deciding how to work a delta into the book — how many orders, at what prices, amended or cancelled when. Owned by the user, who may substitute their own. It is told the quantity to trade and never the position, so that position arithmetic cannot re-enter through it.
43
+ _Avoid_: Execution, order strategy, routing, slicing
44
+
45
+ **Driver**:
46
+ The script that assembles an engine or a node, attaches the executor, runs it for a while and writes the report. Owned by the user, like placement — the library ships examples of one, never one of its own. What distinguishes it from placement is scope rather than ownership: placement decides how a single delta reaches the book, a driver decides which venue, which mode, which symbols and for how long.
47
+ _Avoid_: Runner, entrypoint, harness, main
48
+
49
+ **Committed**:
50
+ The quantity we already stand behind for an asset — what our live orders would add to the position if every one of them filled. Placement is judged on how much it changes this, never on the size of the orders it returns: repricing an order changes its price and commits nothing, which is what makes it legal when there is nothing left to trade.
51
+ _Avoid_: Exposure, pending, allocated, reserved
52
+
53
+ **Over-delivery**:
54
+ Placement returning orders that would trade further than the delta, or in the opposite direction. Always a defect: under-trading is bounded by doing nothing and corrects next tick, while over-trading has no ceiling and compounds.
55
+ _Avoid_: Overshoot, overfill, excess
56
+
57
+ **Base asset**:
58
+ What a signal names — the underlying an alpha has a view on, such as `BTC`. Independent of any venue.
59
+ _Avoid_: Ticker, symbol, instrument, coin
60
+
61
+ **Symbol**:
62
+ What a venue trades, such as `BTCUSDT`. One base asset maps to a different symbol on each venue; the mapping is configuration, never part of a signal.
63
+ _Avoid_: Pair, instrument, market, asset
64
+
65
+ **Sandbox**:
66
+ Paper trading against live market data with simulated fills. Needs no exchange credentials, and is therefore not a venue — no order it produces exists anywhere, and its results are a simulation whose realism is a setting rather than a fact.
67
+ _Avoid_: Paper, demo, testnet, simulation, dry run
68
+
69
+ ### Provenance and ordering
70
+
71
+ **Publisher**:
72
+ The process that computes a portfolio's target state and emits signals. It owns the portfolio's alphas and their aggregation; the library is only ever its consumer.
73
+ _Avoid_: Producer, sender, portfolio process, upstream
74
+
75
+ **Epoch**:
76
+ A publisher process's identity — fixed for its lifetime and different after every restart. It exists so a restarted publisher, whose sequence has returned to zero, still outranks the stream it replaced.
77
+ _Avoid_: Run id, generation, boot id, version
78
+
79
+ **Sequence**:
80
+ A signal's position within one epoch, increasing with each signal that publisher emits. Meaningless across epochs on its own.
81
+ _Avoid_: Index, counter, offset, id
82
+
83
+ **Supersede**:
84
+ What a signal does to the last one applied when it is genuinely newer, judged on epoch and sequence together. A signal that does not supersede is inert rather than erroneous: redelivery is expected traffic.
85
+ _Avoid_: Override, replace, win, update
86
+
87
+ ### Time and health
88
+
89
+ **Expected gap**:
90
+ How long a publisher says it will be before it speaks again, taken from its own aggregation cadence. Not the interval of the alphas it aggregates — those are usually far longer, and a deadline built on them would never fire.
91
+ _Avoid_: Interval, period, frequency, cadence, alpha interval
92
+
93
+ **Missed deadline**:
94
+ The expected gap elapsing, plus grace, without a signal arriving. It describes the health of the feed, never the validity of a signal: one that arrives afterwards is still applied, and applying it is what clears the condition.
95
+ _Avoid_: Timeout, stale signal, late signal, expired
96
+
97
+ **Escalated**:
98
+ The state a portfolio enters after several consecutive missed deadlines — its publisher is presumed gone rather than slow. Escalation stops new orders and cancels resting ones; it never exits positions, because a crash-looping process would then exit and rebuy in a loop, paying the spread each cycle.
99
+ _Avoid_: Halted, stopped, paused, killed, panicked
100
+
101
+ **Actionable**:
102
+ Young enough to trade on. A signal can be the newest ever seen and still too old to act on, in which case it is not traded — a signal that stale means the publisher was broken, and guessing is worse than stopping.
103
+ _Avoid_: Fresh, valid, current, live
104
+
105
+ ### Rejection
106
+
107
+ **Invalid**:
108
+ Untrustworthy, and therefore not applied at all. Rejection is always whole-signal: applying the sound part of a target state would leave a position the strategy never asked for and cannot reason about.
109
+ _Avoid_: Malformed, bad, partial, unparseable
@@ -0,0 +1,78 @@
1
+ """Order execution: portfolio signal in, exchange orders out.
2
+
3
+ The library's job is target-position reconciliation. A signal names a target
4
+ weight per symbol; this package decides what orders that implies against the
5
+ position actually held at the venue, and places them.
6
+
7
+ Nothing nautilus-shaped crosses this package's public boundary. Callers work in
8
+ plain types — symbols as `str`, quantities and weights as `Decimal` — so the
9
+ engine underneath stays an implementation detail and hooks stay testable without
10
+ constructing framework objects.
11
+
12
+ Public so far: the input type, and the sizing and guard arithmetic the executor
13
+ is built on. The executor itself — the nautilus Strategy that drives these on a
14
+ timer — arrives with the hook-surface and testing-surface tickets.
15
+ """
16
+
17
+ from nyrobrain.execution.intake import Fetch, SignalSource
18
+ from nyrobrain.execution.ladder import LadderConfig, TouchLadder
19
+ from nyrobrain.execution.metrics import AegisMetrics, Fanout, Metric, MetricSink
20
+ from nyrobrain.execution.placement import (
21
+ Action,
22
+ Amend,
23
+ Cancel,
24
+ Context,
25
+ Place,
26
+ Placement,
27
+ Resting,
28
+ UnknownOrder,
29
+ committed_change,
30
+ validate,
31
+ )
32
+ from nyrobrain.execution.signal import (
33
+ MAX_GROSS_EXPOSURE,
34
+ SCHEMA_VERSION,
35
+ InvalidSignal,
36
+ Signal,
37
+ parse_signal,
38
+ )
39
+ from nyrobrain.execution.target import (
40
+ OverDelivery,
41
+ check_intent,
42
+ round_price,
43
+ round_quantity,
44
+ target_quantity,
45
+ trade_delta,
46
+ )
47
+
48
+ __all__ = [
49
+ "MAX_GROSS_EXPOSURE",
50
+ "SCHEMA_VERSION",
51
+ "Action",
52
+ "AegisMetrics",
53
+ "Amend",
54
+ "Cancel",
55
+ "Context",
56
+ "Fanout",
57
+ "Fetch",
58
+ "InvalidSignal",
59
+ "LadderConfig",
60
+ "Metric",
61
+ "MetricSink",
62
+ "OverDelivery",
63
+ "Place",
64
+ "Placement",
65
+ "Resting",
66
+ "Signal",
67
+ "SignalSource",
68
+ "TouchLadder",
69
+ "UnknownOrder",
70
+ "check_intent",
71
+ "committed_change",
72
+ "parse_signal",
73
+ "round_price",
74
+ "round_quantity",
75
+ "target_quantity",
76
+ "trade_delta",
77
+ "validate",
78
+ ]
@@ -0,0 +1,92 @@
1
+ """Publishing metrics to Aegis over the JetStream sidecar.
2
+
3
+ Requires the ``aegis`` extra, which carries the gRPC client. The payloads — and
4
+ every decision about what they contain — live in `nyrobrain.execution.metrics`,
5
+ which needs nothing installed at all. A second transport replaces this file and
6
+ inherits the rest.
7
+
8
+ **This is not a NATS client.** `bq-nats-client` depends only on `grpcio` and
9
+ `protobuf`: publishers reach JetStream through a sidecar authenticated by an API
10
+ key and hold no NATS credentials. That is convenient here, and it is also why
11
+ portfolio targets deliberately live in a different account — the sidecar applies
12
+ no per-subject authorization, so anything publishable through it is publishable
13
+ by any holder of a key.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable
19
+ from typing import Protocol
20
+
21
+ from nyrobrain.execution.metrics import Metric
22
+
23
+
24
+ class JetStreamClient(Protocol):
25
+ """The part of `nats_client.NATSClient` this uses.
26
+
27
+ Stated as a protocol so a test can drive the real sink without a sidecar,
28
+ and so the dependency is visible at a glance rather than implied by an
29
+ import.
30
+ """
31
+
32
+ async def jetstream(self) -> None: ...
33
+
34
+ async def js_publish(
35
+ self,
36
+ subject: str,
37
+ payload: bytes = b"",
38
+ headers: dict[str, str] | None = None,
39
+ ) -> object: ...
40
+
41
+
42
+ class JetStreamSink:
43
+ """A `MetricSink` that publishes through the sidecar.
44
+
45
+ Deliberately thin, and deliberately not defensive: a publish that fails
46
+ raises. The emitter counts the failure and the report shows it, so a sink
47
+ that swallowed exceptions would turn a dead metrics pipeline into one that
48
+ looks healthy — the exact failure this map exists to prevent, and the one
49
+ nothing downstream detects.
50
+ """
51
+
52
+ def __init__(self, *, open_client: Callable[[], JetStreamClient]) -> None:
53
+ #: A factory, not a client, and that is the whole point. `NATSClient`
54
+ #: binds its gRPC channel to an event loop when it is **constructed**,
55
+ #: and a driver builds its publisher before the node has a loop — so a
56
+ #: ready-made client binds to a loop that is closed by the time the
57
+ #: first heartbeat fires, and every publish dies with "attached to a
58
+ #: different loop". Deferring construction to the first publish means
59
+ #: the channel is built in whichever loop is actually going to use it.
60
+ #:
61
+ #: Measured the hard way: a live run reported `published 0, failures 3`
62
+ #: and nothing else until the round started recording the reason.
63
+ self._open_client = open_client
64
+ self._client: JetStreamClient | None = None
65
+
66
+ async def connect(self) -> None:
67
+ """Build the client and configure JetStream, once.
68
+
69
+ Call this only from the loop that will publish — or not at all, since
70
+ `publish` does it. `js_publish` refuses with "call jetstream() first"
71
+ until it has run, and doing it per publish would add a round trip to
72
+ every row.
73
+ """
74
+ if self._client is not None:
75
+ return
76
+ client = self._open_client()
77
+ await client.jetstream()
78
+ self._client = client
79
+
80
+ async def publish(self, metric: Metric) -> None:
81
+ # Lazily, so the channel is built in whichever loop is publishing.
82
+ await self.connect()
83
+ assert self._client is not None
84
+ # `msg_id` becomes `Nats-Msg-Id` here and nowhere else. The client does
85
+ # `headers.setdefault("Nats-Msg-Id", uuid4())`, so omitting it would
86
+ # publish successfully while silently losing every cross-publish
87
+ # collapse — retries would still be idempotent, and nothing else would.
88
+ await self._client.js_publish(
89
+ metric.subject,
90
+ metric.payload,
91
+ headers={"Nats-Msg-Id": metric.msg_id},
92
+ )
@@ -0,0 +1,184 @@
1
+ """Assembling a backtest over a recorded signal timeseries.
2
+
3
+ Requires the ``execution`` extra.
4
+
5
+ `backtest_engine` hands the engine back **unrun**, so the caller decides when
6
+ and whether to run it. That is not ceremony: this function has more callers
7
+ inside the test suite than out of it — the property-based fuzz drives it at 500
8
+ examples with deliberately-broken placements — and those callers need the
9
+ assembly without the run.
10
+
11
+ For a driver that uses it, see `nyrobrain.execution.examples.oms`.
12
+
13
+ Signals and quotes go into `BacktestEngine` as **one timestamp-ordered stream**,
14
+ so they interleave exactly as they would live. That is the whole reason signals
15
+ are a `Data` subclass rather than something handed to the strategy directly —
16
+ hand-sequencing them against the quote timeline is where an off-by-one becomes a
17
+ backtest that looks fine and means nothing.
18
+
19
+ `BacktestEngine` gives the strategy a `TestClock`, so timers fire as the data
20
+ advances and a week of replay does not take a week.
21
+
22
+ **What a run here can and cannot tell you** is set out in `candles`: with quotes
23
+ synthesized from 1m bars, this proves the wiring, the convergence and the
24
+ invariants. It is not evidence about fill quality, and the report withholds
25
+ those numbers rather than inviting the mistake.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import dataclasses
31
+ from collections.abc import Callable, Sequence
32
+ from pathlib import Path
33
+
34
+ from nautilus_trader.backtest import BacktestEngine
35
+ from nautilus_trader.config import BacktestEngineConfig, LoggerConfig
36
+ from nautilus_trader.model import (
37
+ AccountType,
38
+ ClientId,
39
+ CryptoPerpetual,
40
+ Currency,
41
+ Money,
42
+ OmsType,
43
+ TraderId,
44
+ Venue,
45
+ )
46
+
47
+ from nyrobrain.execution.candles import QuoteSource
48
+ from nyrobrain.execution.data import as_custom_data
49
+ from nyrobrain.execution.executor import TargetPositionExecutor
50
+ from nyrobrain.execution.placement import Placement
51
+ from nyrobrain.execution.report import (
52
+ RunRecorder,
53
+ )
54
+ from nyrobrain.execution.store import read_signals
55
+ from nyrobrain.execution.venue import InstrumentsNotReady, VenueConfig
56
+
57
+ #: The cache holds 1m bars, and the quote cadence follows from that.
58
+ CANDLE_INTERVAL_NS = 60 * 1_000_000_000
59
+
60
+
61
+ def stale_quote_threshold_for(quote_interval_ns: int) -> int:
62
+ """How old a quote may get before a candle-fed run stops believing it.
63
+
64
+ Three intervals — the same rule the signal deadline uses, so the number
65
+ follows the data rather than the other way round.
66
+
67
+ This exists because the live default is 30 seconds and a candle source is
68
+ three orders of magnitude slower. `candles._expand` stamps all four quotes
69
+ of a bar with the bar's own timestamp, so a 1m source emits four quotes and
70
+ then goes quiet for 59 seconds; against 30s that reads as a dead feed for
71
+ half of every minute. Measured before the override existed: **50,395 of
72
+ 70,553 ticks skipped** across a seven-day replay, with the whole suite
73
+ green, because skipping a tick breaks no invariant. It only stops the run
74
+ meaning anything.
75
+
76
+ Named and returned rather than inlined so the rule can be tested directly,
77
+ without standing up an engine to observe it.
78
+ """
79
+ return 3 * quote_interval_ns
80
+
81
+
82
+ def backtest_engine(
83
+ *,
84
+ signals: Path,
85
+ config: VenueConfig,
86
+ instruments: Sequence[CryptoPerpetual],
87
+ quotes: QuoteSource,
88
+ placement_for: Callable[[str], Placement] | None = None,
89
+ quote_interval_ns: int = CANDLE_INTERVAL_NS,
90
+ ) -> tuple[BacktestEngine, RunRecorder]:
91
+ """Assemble the engine, run it, and hand back what happened.
92
+
93
+ `instruments` is yours to supply, and there is no default. A backtest has no
94
+ venue to ask, so the tick size, the lot size and the fee schedule are
95
+ modelling inputs — they belong in your project where they can be reviewed,
96
+ not in a file inside this package that is silently authoritative and dated
97
+ to whenever someone last refreshed it. Snapshot them from a live connection
98
+ with `write_instruments(path, cache.instruments())`, or describe them with
99
+ `venue.perpetual(...)`.
100
+
101
+ `quotes` is a `QuoteSource` — a function from instrument to quotes — rather
102
+ than a directory, because a directory is not a format. This used to be
103
+ `candles: Path`, which meant "laid out the way we lay ours out": sharded
104
+ parquet with a date in the filename, produced by nothing outside this
105
+ project. A user whose candles are a CSV, a database, or nautilus's own
106
+ catalog had no way in. `candles.parquet_shards(dir)` is ours, named where
107
+ it is used; anything matching the signature is equally welcome.
108
+
109
+ The date window and the synthetic spread went with it, which is why this
110
+ signature is shorter than it was. Both are properties of a particular
111
+ source — a window matched against a filename, a spread invented because
112
+ candles have no book — and neither means anything to a loader reading real
113
+ quotes, which would have been handed them regardless.
114
+
115
+ `placement_for` is the executor's own resolver, exposed here because a
116
+ driver that can only run the default ladder cannot exercise the guard that
117
+ exists to contain a *misbehaving* placement — and that guard is the one
118
+ protecting users who write their own.
119
+ """
120
+ # The stale-quote threshold has to match the cadence of the quotes actually
121
+ # being fed, and this driver's are three orders of magnitude slower than a
122
+ # live ticker's. `QuoteTickDataWrangler` stamps all four quotes of a bar with
123
+ # the bar's own timestamp, so a 1m candle source emits four quotes and then
124
+ # goes quiet for 59 seconds — and the live default of 30s reads that as a
125
+ # dead feed for half of every minute. Measured before this existed: 50,395 of
126
+ # 70,553 ticks skipped across a seven-day replay, with the whole suite green,
127
+ # because skipping a tick breaks no invariant. It only stops the run meaning
128
+ # anything.
129
+ #
130
+ # Three intervals, the same rule the signal deadline uses, so the number
131
+ # follows the data rather than the other way round.
132
+ config = dataclasses.replace(
133
+ config,
134
+ freshness=dataclasses.replace(
135
+ config.freshness,
136
+ stale_quote_ns=stale_quote_threshold_for(quote_interval_ns),
137
+ ),
138
+ )
139
+
140
+ # Two lists that have to agree, so check it here rather than let it surface
141
+ # as a run that quietly trades nothing. A backtest has no excuse for a
142
+ # missing definition — there is no venue still connecting, only a driver
143
+ # that named a symbol and did not describe it. Live and paper keep the
144
+ # retry, because there "not yet" is a real answer.
145
+ described = {i.raw_symbol.value for i in instruments}
146
+ undescribed = [s for s in config.symbols if s not in described]
147
+ if undescribed:
148
+ raise InstrumentsNotReady(
149
+ f"no instrument described for {', '.join(undescribed)}. A backtest "
150
+ "has no venue to ask, so every configured symbol needs one: add it "
151
+ "with `venue.perpetual(...)`, or snapshot the real definitions with "
152
+ "`write_instruments(path, cache.instruments())` and load that. "
153
+ f"Described: {', '.join(sorted(described)) or '(none)'}"
154
+ )
155
+
156
+ engine = BacktestEngine(
157
+ config=BacktestEngineConfig(
158
+ trader_id=TraderId("NYRO-001"), logging=LoggerConfig(bypass_logging=True)
159
+ )
160
+ )
161
+ engine.add_venue(
162
+ venue=Venue(config.venue),
163
+ oms_type=OmsType.NETTING,
164
+ account_type=AccountType.MARGIN,
165
+ base_currency=Currency.from_str(config.currency),
166
+ starting_balances=[
167
+ Money(config.starting_balance, Currency.from_str(config.currency)) # type: ignore[arg-type] # v2 stubs say float; the runtime takes Decimal exactly (verified) and float would lose it
168
+ ],
169
+ )
170
+
171
+ for instrument in instruments:
172
+ engine.add_instrument(instrument)
173
+ engine.add_data(list(quotes(instrument)))
174
+
175
+ engine.add_data(
176
+ [as_custom_data(s) for s in read_signals(signals)],
177
+ client_id=ClientId("NYRO"),
178
+ )
179
+
180
+ recorder = RunRecorder()
181
+ engine.add_strategy(
182
+ TargetPositionExecutor(config, recorder=recorder, placement_for=placement_for)
183
+ )
184
+ return engine, recorder