Qubx 0.6.76__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.77__cp312-cp312-manylinux_2_39_x86_64.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.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- qubx/connectors/ccxt/account.py +18 -17
- qubx/connectors/ccxt/subscription_manager.py +51 -30
- qubx/core/account.py +7 -0
- qubx/core/context.py +15 -5
- qubx/core/interfaces.py +50 -10
- qubx/core/loggers.py +2 -0
- qubx/core/mixins/processing.py +55 -5
- qubx/core/mixins/trading.py +20 -14
- qubx/core/mixins/universe.py +13 -2
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/emitters/indicator.py +8 -3
- qubx/emitters/questdb.py +1 -1
- qubx/restarts/state_resolvers.py +4 -4
- qubx/restorers/signal.py +2 -2
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/runner/_jupyter_runner.pyt +9 -9
- qubx/utils/runner/runner.py +2 -1
- {qubx-0.6.76.dist-info → qubx-0.6.77.dist-info}/METADATA +1 -1
- {qubx-0.6.76.dist-info → qubx-0.6.77.dist-info}/RECORD +23 -23
- {qubx-0.6.76.dist-info → qubx-0.6.77.dist-info}/LICENSE +0 -0
- {qubx-0.6.76.dist-info → qubx-0.6.77.dist-info}/WHEEL +0 -0
- {qubx-0.6.76.dist-info → qubx-0.6.77.dist-info}/entry_points.txt +0 -0
qubx/connectors/ccxt/account.py
CHANGED
|
@@ -28,6 +28,7 @@ from qubx.utils.marketdata.ccxt import ccxt_symbol_to_instrument
|
|
|
28
28
|
from qubx.utils.misc import AsyncThreadLoop
|
|
29
29
|
|
|
30
30
|
from .exceptions import CcxtSymbolNotRecognized
|
|
31
|
+
from .exchange_manager import ExchangeManager
|
|
31
32
|
from .utils import (
|
|
32
33
|
ccxt_convert_balance,
|
|
33
34
|
ccxt_convert_deal_info,
|
|
@@ -46,7 +47,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
46
47
|
Subscribes to account information from the exchange.
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
exchange_manager: ExchangeManager
|
|
50
51
|
channel: CtrlChannel
|
|
51
52
|
base_currency: str
|
|
52
53
|
balance_interval: str
|
|
@@ -69,7 +70,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
69
70
|
def __init__(
|
|
70
71
|
self,
|
|
71
72
|
account_id: str,
|
|
72
|
-
|
|
73
|
+
exchange_manager: ExchangeManager,
|
|
73
74
|
channel: CtrlChannel,
|
|
74
75
|
time_provider: ITimeProvider,
|
|
75
76
|
base_currency: str,
|
|
@@ -90,7 +91,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
90
91
|
tcc=tcc,
|
|
91
92
|
initial_capital=0,
|
|
92
93
|
)
|
|
93
|
-
self.
|
|
94
|
+
self.exchange_manager = exchange_manager
|
|
94
95
|
self.channel = channel
|
|
95
96
|
self.max_retries = max_retries
|
|
96
97
|
self.balance_interval = balance_interval
|
|
@@ -99,7 +100,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
99
100
|
self.open_order_interval = open_order_interval
|
|
100
101
|
self.open_order_backoff = open_order_backoff
|
|
101
102
|
self.max_position_restore_days = max_position_restore_days
|
|
102
|
-
self._loop = AsyncThreadLoop(exchange.asyncio_loop)
|
|
103
|
+
self._loop = AsyncThreadLoop(exchange_manager.exchange.asyncio_loop)
|
|
103
104
|
self._is_running = False
|
|
104
105
|
self._polling_tasks = {}
|
|
105
106
|
self._polling_to_init = defaultdict(bool)
|
|
@@ -125,7 +126,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
125
126
|
|
|
126
127
|
self._is_running = True
|
|
127
128
|
|
|
128
|
-
if not self.exchange.isSandboxModeEnabled:
|
|
129
|
+
if not self.exchange_manager.exchange.isSandboxModeEnabled:
|
|
129
130
|
# - start polling tasks
|
|
130
131
|
self._polling_tasks["balance"] = self._loop.submit(
|
|
131
132
|
self._poller("balance", self._update_balance, self.balance_interval)
|
|
@@ -178,8 +179,8 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
178
179
|
|
|
179
180
|
def _get_instrument_for_currency(self, currency: str) -> Instrument:
|
|
180
181
|
symbol = f"{currency}/{self.base_currency}"
|
|
181
|
-
market = self.exchange.market(symbol)
|
|
182
|
-
exchange_name = self.exchange.name
|
|
182
|
+
market = self.exchange_manager.exchange.market(symbol)
|
|
183
|
+
exchange_name = self.exchange_manager.exchange.name
|
|
183
184
|
assert exchange_name is not None
|
|
184
185
|
return ccxt_symbol_to_instrument(exchange_name, market)
|
|
185
186
|
|
|
@@ -267,7 +268,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
267
268
|
|
|
268
269
|
async def _update_balance(self) -> None:
|
|
269
270
|
"""Fetch and update balances from exchange"""
|
|
270
|
-
balances_raw = await self.exchange.fetch_balance()
|
|
271
|
+
balances_raw = await self.exchange_manager.exchange.fetch_balance()
|
|
271
272
|
balances = ccxt_convert_balance(balances_raw)
|
|
272
273
|
current_balances = self.get_balances()
|
|
273
274
|
|
|
@@ -292,8 +293,8 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
292
293
|
|
|
293
294
|
async def _update_positions(self) -> None:
|
|
294
295
|
# fetch and update positions from exchange
|
|
295
|
-
ccxt_positions = await self.exchange.fetch_positions()
|
|
296
|
-
positions = ccxt_convert_positions(ccxt_positions, self.exchange.name, self.exchange.markets) # type: ignore
|
|
296
|
+
ccxt_positions = await self.exchange_manager.exchange.fetch_positions()
|
|
297
|
+
positions = ccxt_convert_positions(ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets) # type: ignore
|
|
297
298
|
# update required instruments that we need to subscribe to
|
|
298
299
|
self._required_instruments.update([p.instrument for p in positions])
|
|
299
300
|
# update positions
|
|
@@ -358,7 +359,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
358
359
|
if _fetch_instruments:
|
|
359
360
|
logger.debug(f"Fetching missing tickers for {_fetch_instruments}")
|
|
360
361
|
_fetch_symbols = [instrument_to_ccxt_symbol(instr) for instr in _fetch_instruments]
|
|
361
|
-
tickers: dict[str, dict] = await self.exchange.fetch_tickers(_fetch_symbols)
|
|
362
|
+
tickers: dict[str, dict] = await self.exchange_manager.exchange.fetch_tickers(_fetch_symbols)
|
|
362
363
|
for symbol, ticker in tickers.items():
|
|
363
364
|
instr = _symbol_to_instrument.get(symbol)
|
|
364
365
|
if instr is not None:
|
|
@@ -457,7 +458,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
457
458
|
|
|
458
459
|
async def _cancel_order(order: Order) -> None:
|
|
459
460
|
try:
|
|
460
|
-
await self.exchange.cancel_order(order.id, symbol=instrument_to_ccxt_symbol(order.instrument))
|
|
461
|
+
await self.exchange_manager.exchange.cancel_order(order.id, symbol=instrument_to_ccxt_symbol(order.instrument))
|
|
461
462
|
logger.debug(
|
|
462
463
|
f" :: [SYNC] Canceled {order.id} {order.instrument.symbol} {order.side} {order.quantity} @ {order.price} ({order.status})"
|
|
463
464
|
)
|
|
@@ -475,7 +476,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
475
476
|
) -> dict[str, Order]:
|
|
476
477
|
_start_ms = self._get_start_time_in_ms(days_before) if limit is None else None
|
|
477
478
|
_ccxt_symbol = instrument_to_ccxt_symbol(instrument)
|
|
478
|
-
_fetcher = self.exchange.fetch_open_orders if is_open else self.exchange.fetch_orders
|
|
479
|
+
_fetcher = self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
|
|
479
480
|
_raw_orders = await _fetcher(_ccxt_symbol, since=_start_ms, limit=limit)
|
|
480
481
|
_orders = [ccxt_convert_order_info(instrument, o) for o in _raw_orders]
|
|
481
482
|
_id_to_order = {o.id: o for o in _orders}
|
|
@@ -484,7 +485,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
484
485
|
async def _fetch_deals(self, instrument: Instrument, days_before: int = 30) -> list[Deal]:
|
|
485
486
|
_start_ms = self._get_start_time_in_ms(days_before)
|
|
486
487
|
_ccxt_symbol = instrument_to_ccxt_symbol(instrument)
|
|
487
|
-
deals_data = await self.exchange.fetch_my_trades(_ccxt_symbol, since=_start_ms)
|
|
488
|
+
deals_data = await self.exchange_manager.exchange.fetch_my_trades(_ccxt_symbol, since=_start_ms)
|
|
488
489
|
deals: list[Deal] = [ccxt_convert_deal_info(o) for o in deals_data]
|
|
489
490
|
return sorted(deals, key=lambda x: x.time) if deals else []
|
|
490
491
|
|
|
@@ -530,9 +531,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
530
531
|
_symbol_to_instrument = {}
|
|
531
532
|
|
|
532
533
|
async def _watch_executions():
|
|
533
|
-
exec = await self.exchange.watch_orders()
|
|
534
|
+
exec = await self.exchange_manager.exchange.watch_orders()
|
|
534
535
|
for report in exec:
|
|
535
|
-
instrument = ccxt_find_instrument(report["symbol"], self.exchange, _symbol_to_instrument)
|
|
536
|
+
instrument = ccxt_find_instrument(report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument)
|
|
536
537
|
order = ccxt_convert_order_info(instrument, report)
|
|
537
538
|
deals = ccxt_extract_deals_from_exec(report)
|
|
538
539
|
channel.send((instrument, "order", order, False))
|
|
@@ -541,7 +542,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
|
|
|
541
542
|
|
|
542
543
|
await self._listen_to_stream(
|
|
543
544
|
subscriber=_watch_executions,
|
|
544
|
-
exchange=self.exchange,
|
|
545
|
+
exchange=self.exchange_manager.exchange,
|
|
545
546
|
channel=channel,
|
|
546
547
|
name=name,
|
|
547
548
|
)
|
|
@@ -8,7 +8,7 @@ separating subscription concerns from connection management and data handling.
|
|
|
8
8
|
from collections import defaultdict
|
|
9
9
|
from typing import Dict, List, Set
|
|
10
10
|
|
|
11
|
-
from qubx.core.basics import Instrument
|
|
11
|
+
from qubx.core.basics import DataType, Instrument
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class SubscriptionManager:
|
|
@@ -37,7 +37,7 @@ class SubscriptionManager:
|
|
|
37
37
|
|
|
38
38
|
# Symbol to instrument mapping for quick lookups
|
|
39
39
|
self._symbol_to_instrument: dict[str, Instrument] = {}
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
# Individual stream mappings: {subscription_type: {instrument: stream_name}}
|
|
42
42
|
self._individual_streams: dict[str, dict[Instrument, str]] = defaultdict(dict)
|
|
43
43
|
|
|
@@ -125,7 +125,7 @@ class SubscriptionManager:
|
|
|
125
125
|
|
|
126
126
|
# Clean up name mapping
|
|
127
127
|
self._sub_to_name.pop(subscription_type, None)
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
# Clean up individual stream mappings
|
|
130
130
|
self._individual_streams.pop(subscription_type, None)
|
|
131
131
|
|
|
@@ -163,15 +163,21 @@ class SubscriptionManager:
|
|
|
163
163
|
"""
|
|
164
164
|
if instrument is not None:
|
|
165
165
|
# Return subscriptions (both active and pending) that contain this instrument
|
|
166
|
-
active = [
|
|
167
|
-
|
|
166
|
+
active = [
|
|
167
|
+
sub
|
|
168
|
+
for sub, instrs in self._subscriptions.items()
|
|
169
|
+
if instrument in instrs and self._sub_connection_ready.get(sub, False)
|
|
170
|
+
]
|
|
168
171
|
pending = [sub for sub, instrs in self._pending_subscriptions.items() if instrument in instrs]
|
|
169
172
|
return list(set(active + pending))
|
|
170
173
|
|
|
171
174
|
# Return all subscription types that have any instruments (both active and pending)
|
|
172
175
|
# Only include active subscriptions if connection is ready
|
|
173
|
-
active = [
|
|
174
|
-
|
|
176
|
+
active = [
|
|
177
|
+
sub
|
|
178
|
+
for sub, instruments in self._subscriptions.items()
|
|
179
|
+
if instruments and self._sub_connection_ready.get(sub, False)
|
|
180
|
+
]
|
|
175
181
|
pending = [sub for sub, instruments in self._pending_subscriptions.items() if instruments]
|
|
176
182
|
return list(set(active + pending))
|
|
177
183
|
|
|
@@ -211,17 +217,24 @@ class SubscriptionManager:
|
|
|
211
217
|
|
|
212
218
|
Args:
|
|
213
219
|
instrument: Instrument to check
|
|
214
|
-
subscription_type:
|
|
220
|
+
subscription_type: Base or full subscription type (e.g., "orderbook" or "orderbook(0.0, 20)")
|
|
215
221
|
|
|
216
222
|
Returns:
|
|
217
223
|
True if subscription is active (not just pending)
|
|
218
224
|
"""
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
# Get the base type for comparison
|
|
226
|
+
base_type = DataType.from_str(subscription_type)[0]
|
|
227
|
+
|
|
228
|
+
# Check if any subscription with matching base type contains the instrument and is ready
|
|
229
|
+
for stored_sub_type, instruments in self._subscriptions.items():
|
|
230
|
+
if (
|
|
231
|
+
DataType.from_str(stored_sub_type)[0] == base_type
|
|
232
|
+
and instrument in instruments
|
|
233
|
+
and self._sub_connection_ready.get(stored_sub_type, False)
|
|
234
|
+
):
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
return False
|
|
225
238
|
|
|
226
239
|
def has_pending_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
|
|
227
240
|
"""
|
|
@@ -229,16 +242,24 @@ class SubscriptionManager:
|
|
|
229
242
|
|
|
230
243
|
Args:
|
|
231
244
|
instrument: Instrument to check
|
|
232
|
-
subscription_type:
|
|
245
|
+
subscription_type: Base or full subscription type (e.g., "orderbook" or "orderbook(0.0, 20)")
|
|
233
246
|
|
|
234
247
|
Returns:
|
|
235
248
|
True if subscription is pending (connection being established)
|
|
236
249
|
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
250
|
+
# Get the base type for comparison
|
|
251
|
+
base_type = DataType.from_str(subscription_type)[0]
|
|
252
|
+
|
|
253
|
+
# Check if any pending subscription with matching base type contains the instrument and is not ready
|
|
254
|
+
for stored_sub_type, instruments in self._pending_subscriptions.items():
|
|
255
|
+
if (
|
|
256
|
+
DataType.from_str(stored_sub_type)[0] == base_type
|
|
257
|
+
and instrument in instruments
|
|
258
|
+
and not self._sub_connection_ready.get(stored_sub_type, False)
|
|
259
|
+
):
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
return False
|
|
242
263
|
|
|
243
264
|
def get_all_subscribed_instruments(self) -> Set[Instrument]:
|
|
244
265
|
"""
|
|
@@ -262,14 +283,14 @@ class SubscriptionManager:
|
|
|
262
283
|
True if connection is established and ready
|
|
263
284
|
"""
|
|
264
285
|
return self._sub_connection_ready.get(subscription_type, False)
|
|
265
|
-
|
|
286
|
+
|
|
266
287
|
def has_subscription_type(self, subscription_type: str) -> bool:
|
|
267
288
|
"""
|
|
268
289
|
Check if a subscription type exists (has any instruments).
|
|
269
|
-
|
|
290
|
+
|
|
270
291
|
Args:
|
|
271
292
|
subscription_type: Full subscription type (e.g., "ohlc(1m)")
|
|
272
|
-
|
|
293
|
+
|
|
273
294
|
Returns:
|
|
274
295
|
True if subscription type has any instruments
|
|
275
296
|
"""
|
|
@@ -283,33 +304,33 @@ class SubscriptionManager:
|
|
|
283
304
|
Dictionary mapping symbols to instruments
|
|
284
305
|
"""
|
|
285
306
|
return self._symbol_to_instrument.copy()
|
|
286
|
-
|
|
307
|
+
|
|
287
308
|
def set_individual_streams(self, subscription_type: str, streams: dict[Instrument, str]) -> None:
|
|
288
309
|
"""
|
|
289
310
|
Store individual stream mappings for a subscription type.
|
|
290
|
-
|
|
311
|
+
|
|
291
312
|
Args:
|
|
292
313
|
subscription_type: Full subscription type (e.g., "ohlc(1m)")
|
|
293
314
|
streams: Dictionary mapping instrument to stream name
|
|
294
315
|
"""
|
|
295
316
|
self._individual_streams[subscription_type] = streams
|
|
296
|
-
|
|
317
|
+
|
|
297
318
|
def get_individual_streams(self, subscription_type: str) -> dict[Instrument, str]:
|
|
298
319
|
"""
|
|
299
320
|
Get individual stream mappings for a subscription type.
|
|
300
|
-
|
|
321
|
+
|
|
301
322
|
Args:
|
|
302
323
|
subscription_type: Full subscription type (e.g., "ohlc(1m)")
|
|
303
|
-
|
|
324
|
+
|
|
304
325
|
Returns:
|
|
305
326
|
Dictionary mapping instrument to stream name
|
|
306
327
|
"""
|
|
307
328
|
return self._individual_streams.get(subscription_type, {})
|
|
308
|
-
|
|
329
|
+
|
|
309
330
|
def clear_individual_streams(self, subscription_type: str) -> None:
|
|
310
331
|
"""
|
|
311
332
|
Clear individual stream mappings for a subscription type.
|
|
312
|
-
|
|
333
|
+
|
|
313
334
|
Args:
|
|
314
335
|
subscription_type: Full subscription type (e.g., "ohlc(1m)")
|
|
315
336
|
"""
|
qubx/core/account.py
CHANGED
|
@@ -75,6 +75,9 @@ class BasicAccountProcessor(IAccountProcessor):
|
|
|
75
75
|
def get_positions(self, exchange: str | None = None) -> dict[Instrument, Position]:
|
|
76
76
|
return self._positions
|
|
77
77
|
|
|
78
|
+
def get_fees_calculator(self, exchange: str | None = None) -> TransactionCostsCalculator:
|
|
79
|
+
return self._tcc
|
|
80
|
+
|
|
78
81
|
def get_position(self, instrument: Instrument) -> Position:
|
|
79
82
|
_pos = self._positions.get(instrument)
|
|
80
83
|
if _pos is None:
|
|
@@ -443,6 +446,10 @@ class CompositeAccountProcessor(IAccountProcessor):
|
|
|
443
446
|
exch = self._get_exchange(exchange)
|
|
444
447
|
return self._account_processors[exch].position_report()
|
|
445
448
|
|
|
449
|
+
def get_fees_calculator(self, exchange: str | None = None) -> TransactionCostsCalculator:
|
|
450
|
+
exch = self._get_exchange(exchange)
|
|
451
|
+
return self._account_processors[exch].get_fees_calculator()
|
|
452
|
+
|
|
446
453
|
########################################################
|
|
447
454
|
# Leverage information
|
|
448
455
|
########################################################
|
qubx/core/context.py
CHANGED
|
@@ -15,6 +15,7 @@ from qubx.core.basics import (
|
|
|
15
15
|
Order,
|
|
16
16
|
OrderRequest,
|
|
17
17
|
Position,
|
|
18
|
+
RestoredState,
|
|
18
19
|
Signal,
|
|
19
20
|
TargetPosition,
|
|
20
21
|
Timestamped,
|
|
@@ -112,6 +113,7 @@ class StrategyContext(IStrategyContext):
|
|
|
112
113
|
strategy_name: str | None = None,
|
|
113
114
|
strategy_state: StrategyState | None = None,
|
|
114
115
|
health_monitor: IHealthMonitor | None = None,
|
|
116
|
+
restored_state: RestoredState | None = None,
|
|
115
117
|
) -> None:
|
|
116
118
|
self.account = account
|
|
117
119
|
self.strategy = self.__instantiate_strategy(strategy, config)
|
|
@@ -138,6 +140,7 @@ class StrategyContext(IStrategyContext):
|
|
|
138
140
|
self._lifecycle_notifier = lifecycle_notifier
|
|
139
141
|
self._strategy_state = strategy_state if strategy_state is not None else StrategyState()
|
|
140
142
|
self._strategy_name = strategy_name if strategy_name is not None else strategy.__class__.__name__
|
|
143
|
+
self._restored_state = restored_state
|
|
141
144
|
|
|
142
145
|
self._health_monitor = health_monitor or DummyHealthMonitor()
|
|
143
146
|
self.health = self._health_monitor
|
|
@@ -150,6 +153,8 @@ class StrategyContext(IStrategyContext):
|
|
|
150
153
|
if __position_gathering is None:
|
|
151
154
|
__position_gathering = position_gathering if position_gathering is not None else SimplePositionGatherer()
|
|
152
155
|
|
|
156
|
+
__warmup_position_gathering = SimplePositionGatherer()
|
|
157
|
+
|
|
153
158
|
self._subscription_manager = SubscriptionManager(
|
|
154
159
|
data_providers=self._data_providers,
|
|
155
160
|
default_base_subscription=DataType.ORDERBOOK
|
|
@@ -175,9 +180,10 @@ class StrategyContext(IStrategyContext):
|
|
|
175
180
|
time_provider=self,
|
|
176
181
|
account=self.account,
|
|
177
182
|
position_gathering=__position_gathering,
|
|
183
|
+
warmup_position_gathering=__warmup_position_gathering,
|
|
178
184
|
)
|
|
179
185
|
self._trading_manager = TradingManager(
|
|
180
|
-
|
|
186
|
+
context=self,
|
|
181
187
|
brokers=self._brokers,
|
|
182
188
|
account=self.account,
|
|
183
189
|
strategy_name=self._strategy_name,
|
|
@@ -192,6 +198,7 @@ class StrategyContext(IStrategyContext):
|
|
|
192
198
|
account=self.account,
|
|
193
199
|
position_tracker=__position_tracker,
|
|
194
200
|
position_gathering=__position_gathering,
|
|
201
|
+
warmup_position_gathering=__warmup_position_gathering,
|
|
195
202
|
universe_manager=self._universe_manager,
|
|
196
203
|
cache=self._cache,
|
|
197
204
|
scheduler=self._scheduler,
|
|
@@ -455,11 +462,11 @@ class StrategyContext(IStrategyContext):
|
|
|
455
462
|
) -> Order:
|
|
456
463
|
return self._trading_manager.set_target_position(instrument, target, price, **options)
|
|
457
464
|
|
|
458
|
-
def close_position(self, instrument: Instrument) -> None:
|
|
459
|
-
return self._trading_manager.close_position(instrument)
|
|
465
|
+
def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
|
|
466
|
+
return self._trading_manager.close_position(instrument, without_signals)
|
|
460
467
|
|
|
461
|
-
def close_positions(self, market_type: MarketType | None = None) -> None:
|
|
462
|
-
return self._trading_manager.close_positions(market_type)
|
|
468
|
+
def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
|
|
469
|
+
return self._trading_manager.close_positions(market_type, without_signals)
|
|
463
470
|
|
|
464
471
|
def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
|
|
465
472
|
return self._trading_manager.cancel_order(order_id, exchange)
|
|
@@ -585,6 +592,9 @@ class StrategyContext(IStrategyContext):
|
|
|
585
592
|
def get_warmup_orders(self) -> dict[Instrument, list[Order]]:
|
|
586
593
|
return self._warmup_orders if self._warmup_orders is not None else {}
|
|
587
594
|
|
|
595
|
+
def get_restored_state(self) -> RestoredState | None:
|
|
596
|
+
return self._restored_state
|
|
597
|
+
|
|
588
598
|
# private methods
|
|
589
599
|
def __process_incoming_data_loop(self, channel: CtrlChannel):
|
|
590
600
|
logger.info("[StrategyContext] :: Start processing market data")
|
qubx/core/interfaces.py
CHANGED
|
@@ -36,6 +36,7 @@ from qubx.core.basics import (
|
|
|
36
36
|
Signal,
|
|
37
37
|
TargetPosition,
|
|
38
38
|
Timestamped,
|
|
39
|
+
TransactionCostsCalculator,
|
|
39
40
|
TriggerEvent,
|
|
40
41
|
dt_64,
|
|
41
42
|
td_64,
|
|
@@ -153,6 +154,17 @@ class IAccountViewer:
|
|
|
153
154
|
"""
|
|
154
155
|
...
|
|
155
156
|
|
|
157
|
+
def get_fees_calculator(self, exchange: str | None = None) -> TransactionCostsCalculator:
|
|
158
|
+
"""Get the fees calculator.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
exchange: The exchange to get the fees calculator for
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
TransactionCostsCalculator: The transaction costs calculator
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
156
168
|
@property
|
|
157
169
|
def positions(self) -> dict[Instrument, Position]:
|
|
158
170
|
"""[Deprecated: Use get_positions()] Get all current positions.
|
|
@@ -673,15 +685,16 @@ class ITradingManager:
|
|
|
673
685
|
"""
|
|
674
686
|
...
|
|
675
687
|
|
|
676
|
-
def close_position(self, instrument: Instrument) -> None:
|
|
688
|
+
def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
|
|
677
689
|
"""Close position for an instrument.
|
|
678
690
|
|
|
679
691
|
Args:
|
|
680
692
|
instrument: The instrument to close position for
|
|
693
|
+
without_signals: If True, trade submitted instead of emitting signal
|
|
681
694
|
"""
|
|
682
695
|
...
|
|
683
696
|
|
|
684
|
-
def close_positions(self, market_type: MarketType | None = None,
|
|
697
|
+
def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
|
|
685
698
|
"""Close all positions."""
|
|
686
699
|
...
|
|
687
700
|
|
|
@@ -1200,6 +1213,10 @@ class IStrategyContext(
|
|
|
1200
1213
|
"""Get the list of exchanges."""
|
|
1201
1214
|
return []
|
|
1202
1215
|
|
|
1216
|
+
def get_restored_state(self) -> "RestoredState | None":
|
|
1217
|
+
"""Get the restored state."""
|
|
1218
|
+
return None
|
|
1219
|
+
|
|
1203
1220
|
|
|
1204
1221
|
class IPositionGathering:
|
|
1205
1222
|
"""
|
|
@@ -1226,6 +1243,28 @@ class IPositionGathering:
|
|
|
1226
1243
|
|
|
1227
1244
|
def on_execution_report(self, ctx: IStrategyContext, instrument: Instrument, deal: Deal): ...
|
|
1228
1245
|
|
|
1246
|
+
def update(self, ctx: IStrategyContext, instrument: Instrument, update: Timestamped) -> None:
|
|
1247
|
+
"""
|
|
1248
|
+
Position gatherer is being updated by new market data.
|
|
1249
|
+
|
|
1250
|
+
Args:
|
|
1251
|
+
ctx: Strategy context object
|
|
1252
|
+
instrument: The instrument for which market data was updated
|
|
1253
|
+
update: The market data update (Quote, Trade, Bar, etc.)
|
|
1254
|
+
"""
|
|
1255
|
+
pass
|
|
1256
|
+
|
|
1257
|
+
def restore_from_target_positions(self, ctx: IStrategyContext, target_positions: list[TargetPosition]) -> None:
|
|
1258
|
+
"""
|
|
1259
|
+
Restore gatherer state from target positions.
|
|
1260
|
+
|
|
1261
|
+
Args:
|
|
1262
|
+
ctx: Strategy context object
|
|
1263
|
+
target_positions: List of target positions to restore gatherer state from
|
|
1264
|
+
"""
|
|
1265
|
+
# Default implementation - subclasses can override if needed
|
|
1266
|
+
pass
|
|
1267
|
+
|
|
1229
1268
|
|
|
1230
1269
|
class IPositionSizer:
|
|
1231
1270
|
"""Interface for calculating target positions from signals."""
|
|
@@ -1307,15 +1346,16 @@ class PositionsTracker:
|
|
|
1307
1346
|
"""
|
|
1308
1347
|
...
|
|
1309
1348
|
|
|
1310
|
-
def
|
|
1349
|
+
def restore_position_from_signals(self, ctx: IStrategyContext, signals: list[Signal]) -> None:
|
|
1311
1350
|
"""
|
|
1312
|
-
Restore
|
|
1351
|
+
Restore tracker state from signals.
|
|
1313
1352
|
|
|
1314
1353
|
Args:
|
|
1315
|
-
|
|
1316
|
-
|
|
1354
|
+
ctx: Strategy context object
|
|
1355
|
+
signals: List of signals to restore tracker state from
|
|
1317
1356
|
"""
|
|
1318
|
-
|
|
1357
|
+
# Default implementation - subclasses can override
|
|
1358
|
+
pass
|
|
1319
1359
|
|
|
1320
1360
|
|
|
1321
1361
|
@dataclass
|
|
@@ -1350,12 +1390,12 @@ class HealthMetrics:
|
|
|
1350
1390
|
@runtime_checkable
|
|
1351
1391
|
class IDataArrivalListener(Protocol):
|
|
1352
1392
|
"""Interface for components that want to be notified of data arrivals."""
|
|
1353
|
-
|
|
1393
|
+
|
|
1354
1394
|
def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
|
|
1355
1395
|
"""Called when new data arrives.
|
|
1356
|
-
|
|
1396
|
+
|
|
1357
1397
|
Args:
|
|
1358
|
-
event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
|
|
1398
|
+
event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
|
|
1359
1399
|
event_time: Timestamp of the data event
|
|
1360
1400
|
"""
|
|
1361
1401
|
...
|
qubx/core/loggers.py
CHANGED
|
@@ -244,6 +244,7 @@ class SignalsAndTargetsLogger(_BaseIntervalDumper):
|
|
|
244
244
|
"entry_price": t.entry_price,
|
|
245
245
|
"take_price": t.take_price,
|
|
246
246
|
"stop_price": t.stop_price,
|
|
247
|
+
"options": t.options,
|
|
247
248
|
}
|
|
248
249
|
for t in self._targets
|
|
249
250
|
]
|
|
@@ -262,6 +263,7 @@ class SignalsAndTargetsLogger(_BaseIntervalDumper):
|
|
|
262
263
|
"group": s.group,
|
|
263
264
|
"comment": s.comment,
|
|
264
265
|
"service": s.is_service,
|
|
266
|
+
"options": s.options,
|
|
265
267
|
}
|
|
266
268
|
for s in self._signals
|
|
267
269
|
]
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -16,6 +16,7 @@ from qubx.core.basics import (
|
|
|
16
16
|
Instrument,
|
|
17
17
|
MarketEvent,
|
|
18
18
|
Order,
|
|
19
|
+
RestoredState,
|
|
19
20
|
Signal,
|
|
20
21
|
TargetPosition,
|
|
21
22
|
Timestamped,
|
|
@@ -59,6 +60,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
59
60
|
_account: IAccountProcessor
|
|
60
61
|
_position_tracker: PositionsTracker
|
|
61
62
|
_position_gathering: IPositionGathering
|
|
63
|
+
_warmup_position_gathering: IPositionGathering
|
|
62
64
|
_cache: CachedMarketDataHolder
|
|
63
65
|
_scheduler: BasicScheduler
|
|
64
66
|
_universe_manager: IUniverseManager
|
|
@@ -101,6 +103,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
101
103
|
account: IAccountProcessor,
|
|
102
104
|
position_tracker: PositionsTracker,
|
|
103
105
|
position_gathering: IPositionGathering,
|
|
106
|
+
warmup_position_gathering: IPositionGathering,
|
|
104
107
|
universe_manager: IUniverseManager,
|
|
105
108
|
cache: CachedMarketDataHolder,
|
|
106
109
|
scheduler: BasicScheduler,
|
|
@@ -152,6 +155,8 @@ class ProcessingManager(IProcessingManager):
|
|
|
152
155
|
self._active_targets = {}
|
|
153
156
|
self._custom_scheduled_methods = {}
|
|
154
157
|
|
|
158
|
+
self._warmup_position_gathering = warmup_position_gathering
|
|
159
|
+
|
|
155
160
|
# - schedule daily delisting check at 23:30 (end of day)
|
|
156
161
|
self._scheduler.schedule_event("30 23 * * *", "delisting_check")
|
|
157
162
|
|
|
@@ -254,6 +259,12 @@ class ProcessingManager(IProcessingManager):
|
|
|
254
259
|
):
|
|
255
260
|
if self._context.get_warmup_positions() or self._context.get_warmup_orders():
|
|
256
261
|
self._handle_state_resolution()
|
|
262
|
+
|
|
263
|
+
# Restore tracker and gatherer state if available
|
|
264
|
+
restored_state = self._context.get_restored_state()
|
|
265
|
+
if restored_state is not None:
|
|
266
|
+
self._restore_tracker_and_gatherer_state(restored_state)
|
|
267
|
+
|
|
257
268
|
self._handle_warmup_finished()
|
|
258
269
|
|
|
259
270
|
# - check if it still didn't call on_fit() for first time
|
|
@@ -350,6 +361,13 @@ class ProcessingManager(IProcessingManager):
|
|
|
350
361
|
else self._position_tracker
|
|
351
362
|
)
|
|
352
363
|
|
|
364
|
+
def _get_position_gatherer(self) -> IPositionGathering:
|
|
365
|
+
return (
|
|
366
|
+
self._position_gathering
|
|
367
|
+
if self._context._strategy_state.is_on_warmup_finished_called
|
|
368
|
+
else self._warmup_position_gathering
|
|
369
|
+
)
|
|
370
|
+
|
|
353
371
|
def __preprocess_signals_and_split_by_stage(
|
|
354
372
|
self, signals: list[Signal]
|
|
355
373
|
) -> tuple[list[Signal], list[Signal], set[Instrument]]:
|
|
@@ -428,7 +446,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
428
446
|
|
|
429
447
|
# - notify position gatherer for the new target positions
|
|
430
448
|
if _targets_from_trackers:
|
|
431
|
-
self.
|
|
449
|
+
self._get_position_gatherer().alter_positions(
|
|
432
450
|
self._context, self.__preprocess_and_log_target_positions(_targets_from_trackers)
|
|
433
451
|
)
|
|
434
452
|
|
|
@@ -643,15 +661,16 @@ class ProcessingManager(IProcessingManager):
|
|
|
643
661
|
# - update tracker
|
|
644
662
|
_targets_from_tracker = self._get_tracker_for(instrument).update(self._context, instrument, _update)
|
|
645
663
|
|
|
646
|
-
# TODO: add gatherer update
|
|
647
|
-
|
|
648
664
|
# - notify position gatherer for the new target positions
|
|
649
665
|
if _targets_from_tracker:
|
|
650
666
|
# - tracker generated new targets on update, notify position gatherer
|
|
651
|
-
self.
|
|
667
|
+
self._get_position_gatherer().alter_positions(
|
|
652
668
|
self._context, self.__preprocess_and_log_target_positions(self._as_list(_targets_from_tracker))
|
|
653
669
|
)
|
|
654
670
|
|
|
671
|
+
# - update position gatherer with market data
|
|
672
|
+
self._get_position_gatherer().update(self._context, instrument, _update)
|
|
673
|
+
|
|
655
674
|
# - check for stale data periodically (only for base data updates)
|
|
656
675
|
# This ensures we only check when we have new meaningful data
|
|
657
676
|
if self._stale_data_detection_enabled and self._context._strategy_state.is_on_start_called:
|
|
@@ -753,6 +772,37 @@ class ProcessingManager(IProcessingManager):
|
|
|
753
772
|
|
|
754
773
|
resolver(_ctx, _ctx.get_warmup_positions(), _ctx.get_warmup_orders(), _ctx.get_warmup_active_targets())
|
|
755
774
|
|
|
775
|
+
def _restore_tracker_and_gatherer_state(self, restored_state: RestoredState) -> None:
|
|
776
|
+
"""
|
|
777
|
+
Restore state for position tracker and gatherer.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
restored_state: The restored state containing signals and target positions
|
|
781
|
+
"""
|
|
782
|
+
if not self._is_data_ready():
|
|
783
|
+
return
|
|
784
|
+
|
|
785
|
+
# Restore tracker state from signals
|
|
786
|
+
all_signals = []
|
|
787
|
+
for instrument, signals in restored_state.instrument_to_signal_positions.items():
|
|
788
|
+
all_signals.extend(signals)
|
|
789
|
+
|
|
790
|
+
if all_signals:
|
|
791
|
+
logger.info(f"<yellow>Restoring tracker state from {len(all_signals)} signals</yellow>")
|
|
792
|
+
self._position_tracker.restore_position_from_signals(self._context, all_signals)
|
|
793
|
+
|
|
794
|
+
# Restore gatherer state from latest target positions only
|
|
795
|
+
latest_targets = []
|
|
796
|
+
for instrument, targets in restored_state.instrument_to_target_positions.items():
|
|
797
|
+
if targets: # Only if there are targets for this instrument
|
|
798
|
+
# Get the latest target position (assuming they are sorted by time)
|
|
799
|
+
latest_target = max(targets, key=lambda t: t.time)
|
|
800
|
+
latest_targets.append(latest_target)
|
|
801
|
+
|
|
802
|
+
if latest_targets:
|
|
803
|
+
logger.info(f"<yellow>Restoring gatherer state from {len(latest_targets)} latest target positions</yellow>")
|
|
804
|
+
self._position_gathering.restore_from_target_positions(self._context, latest_targets)
|
|
805
|
+
|
|
756
806
|
def _handle_warmup_finished(self) -> None:
|
|
757
807
|
if not self._is_data_ready():
|
|
758
808
|
return
|
|
@@ -856,7 +906,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
856
906
|
# - Process all deals first
|
|
857
907
|
for d in deals:
|
|
858
908
|
# - notify position gatherer and tracker
|
|
859
|
-
self.
|
|
909
|
+
self._get_position_gatherer().on_execution_report(self._context, instrument, d)
|
|
860
910
|
self._get_tracker_for(instrument).on_execution_report(self._context, instrument, d)
|
|
861
911
|
|
|
862
912
|
logger.debug(
|
qubx/core/mixins/trading.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Any
|
|
|
3
3
|
from qubx import logger
|
|
4
4
|
from qubx.core.basics import Instrument, MarketType, Order, OrderRequest, OrderSide
|
|
5
5
|
from qubx.core.exceptions import OrderNotFound
|
|
6
|
-
from qubx.core.interfaces import IAccountProcessor, IBroker, ITimeProvider, ITradingManager
|
|
6
|
+
from qubx.core.interfaces import IAccountProcessor, IBroker, IStrategyContext, ITimeProvider, ITradingManager
|
|
7
7
|
|
|
8
8
|
from .utils import EXCHANGE_MAPPINGS
|
|
9
9
|
|
|
@@ -60,7 +60,7 @@ class ClientIdStore:
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class TradingManager(ITradingManager):
|
|
63
|
-
|
|
63
|
+
_context: IStrategyContext
|
|
64
64
|
_brokers: list[IBroker]
|
|
65
65
|
_account: IAccountProcessor
|
|
66
66
|
_strategy_name: str
|
|
@@ -69,9 +69,9 @@ class TradingManager(ITradingManager):
|
|
|
69
69
|
_exchange_to_broker: dict[str, IBroker]
|
|
70
70
|
|
|
71
71
|
def __init__(
|
|
72
|
-
self,
|
|
72
|
+
self, context: IStrategyContext, brokers: list[IBroker], account: IAccountProcessor, strategy_name: str
|
|
73
73
|
) -> None:
|
|
74
|
-
self.
|
|
74
|
+
self._context = context
|
|
75
75
|
self._brokers = brokers
|
|
76
76
|
self._account = account
|
|
77
77
|
self._strategy_name = strategy_name
|
|
@@ -156,21 +156,27 @@ class TradingManager(ITradingManager):
|
|
|
156
156
|
) -> Order:
|
|
157
157
|
raise NotImplementedError("Not implemented yet")
|
|
158
158
|
|
|
159
|
-
def close_position(self, instrument: Instrument) -> None:
|
|
159
|
+
def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
|
|
160
160
|
position = self._account.get_position(instrument)
|
|
161
161
|
|
|
162
162
|
if not position.is_open():
|
|
163
163
|
logger.debug(f"[<g>{instrument.symbol}</g>] :: Position already closed or zero size")
|
|
164
164
|
return
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
166
|
+
if without_signals:
|
|
167
|
+
closing_amount = -position.quantity
|
|
168
|
+
logger.debug(
|
|
169
|
+
f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} with market order for {closing_amount}"
|
|
170
|
+
)
|
|
171
|
+
self.trade(instrument, closing_amount, reduceOnly=True)
|
|
172
|
+
else:
|
|
173
|
+
logger.debug(
|
|
174
|
+
f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} by emitting signal with 0 target"
|
|
175
|
+
)
|
|
176
|
+
signal = instrument.signal(self._context, 0, comment="Close position trade")
|
|
177
|
+
self._context.emit_signal(signal)
|
|
172
178
|
|
|
173
|
-
def close_positions(self, market_type: MarketType | None = None) -> None:
|
|
179
|
+
def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
|
|
174
180
|
positions = self._account.get_positions()
|
|
175
181
|
|
|
176
182
|
positions_to_close = []
|
|
@@ -188,7 +194,7 @@ class TradingManager(ITradingManager):
|
|
|
188
194
|
)
|
|
189
195
|
|
|
190
196
|
for instrument in positions_to_close:
|
|
191
|
-
self.close_position(instrument)
|
|
197
|
+
self.close_position(instrument, without_signals)
|
|
192
198
|
|
|
193
199
|
def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
|
|
194
200
|
if not order_id:
|
|
@@ -208,7 +214,7 @@ class TradingManager(ITradingManager):
|
|
|
208
214
|
self.cancel_order(o.id, instrument.exchange)
|
|
209
215
|
|
|
210
216
|
def _generate_order_client_id(self, symbol: str) -> str:
|
|
211
|
-
return self._client_id_store.generate_id(self.
|
|
217
|
+
return self._client_id_store.generate_id(self._context, symbol)
|
|
212
218
|
|
|
213
219
|
def exchanges(self) -> list[str]:
|
|
214
220
|
return list(self._exchange_to_broker.keys())
|
qubx/core/mixins/universe.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from qubx.core.basics import DataType, Instrument
|
|
1
|
+
from qubx.core.basics import DataType, Instrument
|
|
2
2
|
from qubx.core.helpers import CachedMarketDataHolder
|
|
3
3
|
from qubx.core.interfaces import (
|
|
4
4
|
IAccountProcessor,
|
|
@@ -24,6 +24,7 @@ class UniverseManager(IUniverseManager):
|
|
|
24
24
|
_time_provider: ITimeProvider
|
|
25
25
|
_account: IAccountProcessor
|
|
26
26
|
_position_gathering: IPositionGathering
|
|
27
|
+
_warmup_position_gathering: IPositionGathering
|
|
27
28
|
_removal_queue: dict[Instrument, tuple[RemovalPolicy, bool]]
|
|
28
29
|
|
|
29
30
|
def __init__(
|
|
@@ -37,6 +38,7 @@ class UniverseManager(IUniverseManager):
|
|
|
37
38
|
time_provider: ITimeProvider,
|
|
38
39
|
account: IAccountProcessor,
|
|
39
40
|
position_gathering: IPositionGathering,
|
|
41
|
+
warmup_position_gathering: IPositionGathering,
|
|
40
42
|
):
|
|
41
43
|
self._context = context
|
|
42
44
|
self._strategy = strategy
|
|
@@ -50,6 +52,8 @@ class UniverseManager(IUniverseManager):
|
|
|
50
52
|
self._instruments = set()
|
|
51
53
|
self._removal_queue = {}
|
|
52
54
|
|
|
55
|
+
self._warmup_position_gathering = warmup_position_gathering
|
|
56
|
+
|
|
53
57
|
def _has_position(self, instrument: Instrument) -> bool:
|
|
54
58
|
return (
|
|
55
59
|
instrument in self._account.positions
|
|
@@ -107,6 +111,13 @@ class UniverseManager(IUniverseManager):
|
|
|
107
111
|
to_remove.append(instr)
|
|
108
112
|
return to_remove, to_keep
|
|
109
113
|
|
|
114
|
+
def _get_position_gatherer(self) -> IPositionGathering:
|
|
115
|
+
return (
|
|
116
|
+
self._position_gathering
|
|
117
|
+
if self._context._strategy_state.is_on_warmup_finished_called
|
|
118
|
+
else self._warmup_position_gathering
|
|
119
|
+
)
|
|
120
|
+
|
|
110
121
|
def __cleanup_removal_queue(self, instruments: list[Instrument]):
|
|
111
122
|
for instr in instruments:
|
|
112
123
|
# - if it's still in the removal queue, remove it
|
|
@@ -181,7 +192,7 @@ class UniverseManager(IUniverseManager):
|
|
|
181
192
|
)
|
|
182
193
|
|
|
183
194
|
# - alter positions
|
|
184
|
-
self.
|
|
195
|
+
self._get_position_gatherer().alter_positions(self._context, exit_targets)
|
|
185
196
|
|
|
186
197
|
# - if still open positions close them manually
|
|
187
198
|
for instr in instruments:
|
|
Binary file
|
|
Binary file
|
qubx/emitters/indicator.py
CHANGED
|
@@ -113,11 +113,17 @@ class IndicatorEmitter(Indicator):
|
|
|
113
113
|
emission_tags = self._tags.copy()
|
|
114
114
|
|
|
115
115
|
# Emit the metric with the proper timestamp
|
|
116
|
+
# Convert time to numpy datetime64 if needed
|
|
117
|
+
if isinstance(time, int):
|
|
118
|
+
timestamp = np.datetime64(time, 'ns')
|
|
119
|
+
else:
|
|
120
|
+
timestamp = pd.Timestamp(time).to_datetime64()
|
|
121
|
+
|
|
116
122
|
self._metric_emitter.emit(
|
|
117
123
|
name=self._metric_name,
|
|
118
124
|
value=float(current_value),
|
|
119
125
|
tags=emission_tags,
|
|
120
|
-
timestamp=
|
|
126
|
+
timestamp=timestamp,
|
|
121
127
|
instrument=self._instrument,
|
|
122
128
|
)
|
|
123
129
|
|
|
@@ -134,8 +140,7 @@ class IndicatorEmitter(Indicator):
|
|
|
134
140
|
f"from '{self._wrapped_indicator.name}': {e}"
|
|
135
141
|
)
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
return current_value
|
|
143
|
+
return float(current_value)
|
|
139
144
|
|
|
140
145
|
@classmethod
|
|
141
146
|
def wrap_with_emitter(
|
qubx/emitters/questdb.py
CHANGED
|
@@ -146,7 +146,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
|
|
|
146
146
|
symbols = {"metric_name": name}
|
|
147
147
|
symbols.update(tags) # Add all tags as symbols
|
|
148
148
|
|
|
149
|
-
columns = {"value": round(value, 5)} # Add the value as a column
|
|
149
|
+
columns: dict = {"value": round(value, 5)} # Add the value as a column
|
|
150
150
|
|
|
151
151
|
# Use the provided timestamp if available, otherwise use current time
|
|
152
152
|
dt_timestamp = self._convert_timestamp(timestamp) if timestamp is not None else datetime.datetime.now()
|
qubx/restarts/state_resolvers.py
CHANGED
|
@@ -56,7 +56,7 @@ class StateResolver:
|
|
|
56
56
|
# If signs are opposite, close the live position
|
|
57
57
|
if live_qty * sim_qty < 0:
|
|
58
58
|
logger.info(f"Closing position for {instrument.symbol} due to opposite direction: {live_qty} -> 0")
|
|
59
|
-
ctx.
|
|
59
|
+
ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=0.0))
|
|
60
60
|
|
|
61
61
|
# If live position is larger than sim position (same direction), reduce it
|
|
62
62
|
elif abs(live_qty) > abs(sim_qty) and abs(live_qty) > instrument.lot_size:
|
|
@@ -64,7 +64,7 @@ class StateResolver:
|
|
|
64
64
|
logger.info(
|
|
65
65
|
f"Reducing position for {instrument.symbol}: {live_qty} -> {sim_qty} (diff: {qty_diff:.4f})"
|
|
66
66
|
)
|
|
67
|
-
ctx.
|
|
67
|
+
ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=sim_qty))
|
|
68
68
|
|
|
69
69
|
# If sim position is larger or equal (same direction), do nothing
|
|
70
70
|
else:
|
|
@@ -73,7 +73,7 @@ class StateResolver:
|
|
|
73
73
|
# If the instrument doesn't exist in simulation, close the position
|
|
74
74
|
else:
|
|
75
75
|
logger.info(f"Closing position for {instrument.symbol} not in simulation: {live_qty} -> 0")
|
|
76
|
-
ctx.
|
|
76
|
+
ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=0.0))
|
|
77
77
|
|
|
78
78
|
@staticmethod
|
|
79
79
|
def CLOSE_ALL(
|
|
@@ -108,7 +108,7 @@ class StateResolver:
|
|
|
108
108
|
for instrument, position in live_positions.items():
|
|
109
109
|
if abs(position.quantity) > instrument.lot_size:
|
|
110
110
|
logger.info(f"Closing position for {instrument.symbol}: {position.quantity} -> 0")
|
|
111
|
-
ctx.
|
|
111
|
+
ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=0.0))
|
|
112
112
|
|
|
113
113
|
@staticmethod
|
|
114
114
|
def SYNC_STATE(
|
qubx/restorers/signal.py
CHANGED
|
@@ -289,7 +289,7 @@ class MongoDBSignalRestorer(ISignalRestorer):
|
|
|
289
289
|
continue
|
|
290
290
|
|
|
291
291
|
price = log.get("price") or log.get("reference_price")
|
|
292
|
-
options =
|
|
292
|
+
options = log.get("options", {})
|
|
293
293
|
|
|
294
294
|
signal = Signal(
|
|
295
295
|
time=recognize_time(log["timestamp"]),
|
|
@@ -328,7 +328,7 @@ class MongoDBSignalRestorer(ISignalRestorer):
|
|
|
328
328
|
|
|
329
329
|
target_size = float(log["target_position"])
|
|
330
330
|
price = log.get("entry_price", None)
|
|
331
|
-
options =
|
|
331
|
+
options = log.get("options", {})
|
|
332
332
|
|
|
333
333
|
target = TargetPosition(
|
|
334
334
|
time=recognize_time(log["timestamp"]),
|
|
Binary file
|
|
@@ -85,13 +85,14 @@ for x in inspect.getmembers(S, (inspect.ismethod)):
|
|
|
85
85
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
86
86
|
|
|
87
87
|
def _pos_to_dict(p: Position):
|
|
88
|
-
mv = round(p.
|
|
88
|
+
mv = round(p.notional_value, 3)
|
|
89
89
|
return dict(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
MktValue=mv,
|
|
91
|
+
Position=round(p.quantity, p.instrument.size_precision),
|
|
92
|
+
PnL=p.total_pnl(),
|
|
93
|
+
AvgPrice=round(p.position_avg_price_funds, p.instrument.price_precision),
|
|
94
|
+
LastPrice=round(p.last_update_price, p.instrument.price_precision),
|
|
95
|
+
)
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
class ActiveInstrument:
|
|
@@ -195,7 +196,6 @@ def portfolio(all=True):
|
|
|
195
196
|
|
|
196
197
|
d = dict()
|
|
197
198
|
for s, p in ctx.get_positions().items():
|
|
198
|
-
mv = round(p.market_value_funds, 3)
|
|
199
199
|
if p.quantity != 0.0 or all:
|
|
200
200
|
d[dequotify(s.symbol, s.quote)] = _pos_to_dict(p)
|
|
201
201
|
|
|
@@ -205,10 +205,10 @@ def portfolio(all=True):
|
|
|
205
205
|
print('-(no open positions yet)-')
|
|
206
206
|
return
|
|
207
207
|
|
|
208
|
-
d = d.sort_values('
|
|
208
|
+
d = d.sort_values('MktValue' ,ascending=False)
|
|
209
209
|
# d = pd.concat((d, pd.Series(dict(TOTAL=d['PnL'].sum()), name='PnL'))).fillna('')
|
|
210
210
|
d = pd.concat((d, scols(pd.Series(dict(TOTAL=d['PnL'].sum()), name='PnL'), pd.Series(dict(TOTAL=d['MktValue'].sum()), name='MktValue')))).fillna('')
|
|
211
|
-
print(tabulate(d, ['Position', 'PnL', 'AvgPrice', 'LastPrice'
|
|
211
|
+
print(tabulate(d, ['MktValue', 'Position', 'PnL', 'AvgPrice', 'LastPrice'], tablefmt='rounded_grid'))
|
|
212
212
|
|
|
213
213
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
214
214
|
__exit = exit
|
qubx/utils/runner/runner.py
CHANGED
|
@@ -384,6 +384,7 @@ def create_strategy_context(
|
|
|
384
384
|
initializer=_initializer,
|
|
385
385
|
strategy_name=stg_name,
|
|
386
386
|
health_monitor=_health_monitor,
|
|
387
|
+
restored_state=restored_state,
|
|
387
388
|
)
|
|
388
389
|
|
|
389
390
|
return ctx
|
|
@@ -509,7 +510,7 @@ def _create_account_processor(
|
|
|
509
510
|
return get_ccxt_account(
|
|
510
511
|
exchange_name,
|
|
511
512
|
account_id=exchange_name,
|
|
512
|
-
|
|
513
|
+
exchange_manager=exchange_manager,
|
|
513
514
|
channel=channel,
|
|
514
515
|
time_provider=time_provider,
|
|
515
516
|
base_currency=creds.base_currency,
|
|
@@ -20,7 +20,7 @@ qubx/cli/misc.py,sha256=tP28QxLEzuP8R2xnt8g3JTs9Z7aYy4iVWY4g3VzKTsQ,14777
|
|
|
20
20
|
qubx/cli/release.py,sha256=7xaCcpUSm6aK_SC_F_YIZl-vYToKWnkaZW-Ik8oBcRs,40435
|
|
21
21
|
qubx/cli/tui.py,sha256=N15UiNEdnWOWYh8E9DNlQCDWdoyP6rMGMhEItogPW88,16491
|
|
22
22
|
qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
|
|
23
|
-
qubx/connectors/ccxt/account.py,sha256=
|
|
23
|
+
qubx/connectors/ccxt/account.py,sha256=f_Qa3ti5RY6CP3aww04CKvGgc8FYdSOi__FMdscG2y4,24664
|
|
24
24
|
qubx/connectors/ccxt/adapters/__init__.py,sha256=4qwWer4C9fTIZKUYmcgbMFEwFuYp34UKbb-AoQNvXjc,178
|
|
25
25
|
qubx/connectors/ccxt/adapters/polling_adapter.py,sha256=UrOAoIfgtoRw7gj6Bmk5lPs_UI8iOxr88DAfp5o5CF8,8962
|
|
26
26
|
qubx/connectors/ccxt/broker.py,sha256=I6I_BynhwHadKa62nBGmDoj5LhFiEzbUq-1ZDwE25aM,16512
|
|
@@ -51,38 +51,38 @@ qubx/connectors/ccxt/handlers/quote.py,sha256=JwQ8mXMpFMdFEpQTx3x_Xaj6VHZanC6_JI
|
|
|
51
51
|
qubx/connectors/ccxt/handlers/trade.py,sha256=VspCqw13r9SyvF0N3a31YKIVTzUx5IjFtMDaQeSxblM,4519
|
|
52
52
|
qubx/connectors/ccxt/reader.py,sha256=uUG1I_ejzTf0f4bCAHpLhBzTUqtNX-JvJGFA4bi7-WU,26602
|
|
53
53
|
qubx/connectors/ccxt/subscription_config.py,sha256=jbMZ_9US3nvrp6LCVmMXLQnAjXH0xIltzUSPqXJZvgs,3865
|
|
54
|
-
qubx/connectors/ccxt/subscription_manager.py,sha256=
|
|
54
|
+
qubx/connectors/ccxt/subscription_manager.py,sha256=9ZfA6bR6YwtlZqVs6yymKPvYSvy5x3yBuHA_LDbiKgc,13285
|
|
55
55
|
qubx/connectors/ccxt/subscription_orchestrator.py,sha256=CbZMTRhmgcJZd8cofQbyBDI__N2Lbo1loYfh9_-EkFA,16512
|
|
56
56
|
qubx/connectors/ccxt/utils.py,sha256=ygUHJdhuuSPCvPULEYK05ZuKExbOnDd-dsJ8mEhDDBA,16378
|
|
57
57
|
qubx/connectors/ccxt/warmup_service.py,sha256=a7qSFUmgUm6s7qP-ae9RP-j1bR9XyEsNy4SNOLbPk_c,4893
|
|
58
58
|
qubx/connectors/tardis/data.py,sha256=TlapY1dwc_aQxf4Na9sF620lK9drrg7E9E8gPTGD3FE,31004
|
|
59
59
|
qubx/connectors/tardis/utils.py,sha256=epThu9DwqbDb7BgScH6fHa_FVpKUaItOqp3JwtKGc5g,9092
|
|
60
60
|
qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
|
-
qubx/core/account.py,sha256=
|
|
61
|
+
qubx/core/account.py,sha256=EiyMYAiVkxxAv2PqjfqeKCVgJrFVVMIezXKgBkO-9rs,23278
|
|
62
62
|
qubx/core/basics.py,sha256=ggK75OsmpH-SCH_Q_2kJmmgFxgrgt4RgO7QipCqUCGk,41301
|
|
63
|
-
qubx/core/context.py,sha256=
|
|
63
|
+
qubx/core/context.py,sha256=Yq039nsKmf-IeyvcRdnMs_phVmehoCUqFvQuVnm-d4s,26074
|
|
64
64
|
qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
|
|
65
65
|
qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
|
|
66
66
|
qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
|
|
67
67
|
qubx/core/helpers.py,sha256=3rY7VkeL8WQ3hj7f8_rbSYNuQftlT0IDpTUARGP5ORM,21622
|
|
68
68
|
qubx/core/initializer.py,sha256=VVti9UJDJCg0125Mm09S6Tt1foHK4XOBL0mXgDmvXes,7724
|
|
69
|
-
qubx/core/interfaces.py,sha256=
|
|
70
|
-
qubx/core/loggers.py,sha256=
|
|
69
|
+
qubx/core/interfaces.py,sha256=AwSS92BbfUXw92TLvB75Wn5bxYByHAAXie2GAC70vUQ,67824
|
|
70
|
+
qubx/core/loggers.py,sha256=eYijsR02S5u1Hv21vjIk_dOUwOMv0fiBDYwEmFhAoWk,14344
|
|
71
71
|
qubx/core/lookups.py,sha256=Bed30kPZvbTGjZ8exojhIMOIVfB46j6741yF3fXGTiM,18313
|
|
72
72
|
qubx/core/metrics.py,sha256=eMsg9qaL86pkKVKrQOyF9u7yf27feLRxOvJj-6qwmj8,75551
|
|
73
73
|
qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
|
|
74
74
|
qubx/core/mixins/market.py,sha256=w9OEDfy0r9xnb4KdKA-PuFsCQugNo4pMLQivGFeyqGw,6356
|
|
75
|
-
qubx/core/mixins/processing.py,sha256=
|
|
75
|
+
qubx/core/mixins/processing.py,sha256=UqKwOzkDXvmcjvffrsR8vugq_XacwW9rcGM0TcBd9RM,44485
|
|
76
76
|
qubx/core/mixins/subscription.py,sha256=GcZKHHzjPyYFLAEpE7j4fpLDOlAhFKojQEYfFO3QarY,11064
|
|
77
|
-
qubx/core/mixins/trading.py,sha256=
|
|
78
|
-
qubx/core/mixins/universe.py,sha256=
|
|
77
|
+
qubx/core/mixins/trading.py,sha256=7KwxHiPWkXGnkHS4VLaxOZ7BHULkvvqPS8ooAG1NTxM,9477
|
|
78
|
+
qubx/core/mixins/universe.py,sha256=UBa3OIr2XvlK04O7YUG9c66CY8AZ5rQDSZov1rnUSjQ,10512
|
|
79
79
|
qubx/core/mixins/utils.py,sha256=P71cLuqKjId8989MwOL_BtvvCnnwOFMkZyB1SY-0Ork,147
|
|
80
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
80
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=twWpMjLnKkjeDumspgtLYvFpX2bs8E5qGp-7SDhpoGU,1019592
|
|
81
81
|
qubx/core/series.pxd,sha256=PvnUEupOsZg8u81U5Amd-nbfmWQ0-PwZwc7yUoaZpoQ,4739
|
|
82
82
|
qubx/core/series.pyi,sha256=RkM-F3AyrzT7m1H2UmOvZmmcOzU2eBeEWf2c0GUZe2o,5437
|
|
83
83
|
qubx/core/series.pyx,sha256=wAn7L9HIkvVl-1Tt7bgdWhec7xy4AiHSXyDsrA4a29U,51703
|
|
84
84
|
qubx/core/stale_data_detector.py,sha256=NHnnG9NkcivC93n8QMwJUzFVQv2ziUaN-fg76ppng_c,17118
|
|
85
|
-
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
85
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=aGkvYLRC0V3DmEX6vz_Qyw1SE1hlJyi6lybTXM03lnY,86568
|
|
86
86
|
qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
|
|
87
87
|
qubx/core/utils.pyx,sha256=UR9achMR-LARsztd2eelFsDsFH3n0gXACIKoGNPI9X4,1766
|
|
88
88
|
qubx/data/__init__.py,sha256=BlyZ99esHLDmFA6ktNkuIce9RZO99TA1IMKWe94aI8M,599
|
|
@@ -96,10 +96,10 @@ qubx/emitters/__init__.py,sha256=MPs7ZRZZnURljusiuvlO5g8M4H1UjEfg5fkyKeJmIBI,791
|
|
|
96
96
|
qubx/emitters/base.py,sha256=lxNmP81pXuRo0LKjjxkGqn0LCYjWDiqJ94dQdosGugg,8744
|
|
97
97
|
qubx/emitters/composite.py,sha256=JkFch4Tp5q6CaLU2nAmeZnRiVPGkFhGNvzhT255yJfI,3411
|
|
98
98
|
qubx/emitters/csv.py,sha256=S-oQ84rCgP-bb2_q-FWcegACg_Ej_Ik3tXE6aJBlqOk,4963
|
|
99
|
-
qubx/emitters/indicator.py,sha256=
|
|
99
|
+
qubx/emitters/indicator.py,sha256=NlhXJAZCboUDz7M7MOjfiR-ASM_L5qv0KgPJE-ekQCY,8206
|
|
100
100
|
qubx/emitters/inmemory.py,sha256=AsFpAGGTWQsv42H5-3tDeZ3XP9b5Ye7lFHis53qcdjs,8862
|
|
101
101
|
qubx/emitters/prometheus.py,sha256=lZJ_Hl-AlkeWJmktxhAiEMiTIc8dTQvBpf3Ih5Fy6pE,10516
|
|
102
|
-
qubx/emitters/questdb.py,sha256=
|
|
102
|
+
qubx/emitters/questdb.py,sha256=hGneKVEnkV82t7c9G3_uVEgAoN302nXBSjjCWHKnAv0,11532
|
|
103
103
|
qubx/exporters/__init__.py,sha256=7HeYHCZfKAaBVAByx9wE8DyGv6C55oeED9uUphcyjuc,360
|
|
104
104
|
qubx/exporters/composite.py,sha256=c45XcMC0dsIDwOyOxxCuiyYQjUNhqPjptAulbaSqttU,2973
|
|
105
105
|
qubx/exporters/formatters/__init__.py,sha256=La9rMsl3wyplza0xVyAFrUwhFyrGDIMJWmOB_boJyIg,488
|
|
@@ -145,18 +145,18 @@ qubx/resources/instruments/symbols-kraken-spot.json,sha256=3JLxi18nQAXE5J73hFY-m
|
|
|
145
145
|
qubx/resources/instruments/symbols-kraken.f-future.json,sha256=FzOg8KIcl4nBQdPqugc-dMHxXGvyiQncNAHs84Tf4Pg,247468
|
|
146
146
|
qubx/resources/instruments/symbols-kraken.f-perpetual.json,sha256=a1xXqbEcOyL1NLQO2JsSsseezPP7QCB9dub4IQhRviE,177233
|
|
147
147
|
qubx/restarts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
148
|
-
qubx/restarts/state_resolvers.py,sha256=
|
|
148
|
+
qubx/restarts/state_resolvers.py,sha256=_MpcCqHL0_NL1A3CC9iKIwVffkjMHj6QJir3jQGrG10,7755
|
|
149
149
|
qubx/restarts/time_finders.py,sha256=AX0Hb-RvB4YSot6L5m1ylkj09V7TYYXFvKgdb8gj0ok,4307
|
|
150
150
|
qubx/restorers/__init__.py,sha256=vrnZBPJHR0-6knAccj4bK0tkjUPNRl32qiLr5Mv4aR0,911
|
|
151
151
|
qubx/restorers/balance.py,sha256=yLV1vBki0XhBxrOhgaJBHuuL8VmIii82LAWgLxusbcE,6967
|
|
152
152
|
qubx/restorers/factory.py,sha256=hQz3MRI7OH3SyMK1RjSuneb2ImJ_oWX0WSpu0tgJUug,6743
|
|
153
153
|
qubx/restorers/interfaces.py,sha256=TsfdtcMUMncB6Cit_k66lEe5YKzVdVKpU68FXtZL-qY,1932
|
|
154
154
|
qubx/restorers/position.py,sha256=jMJjq2ZJwHpAlG45bMy49WvkYK5UylDiExt7nVpxCfg,8703
|
|
155
|
-
qubx/restorers/signal.py,sha256=
|
|
155
|
+
qubx/restorers/signal.py,sha256=5nK5ji8AucyWrFBK9uW619YCI_vPRGFnuDu8JnG3B_Y,14512
|
|
156
156
|
qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
|
|
157
157
|
qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
|
|
158
158
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
159
|
-
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
159
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=YSgLYvVD9kAtfj0fzdwZzWp-YRJP_LQ5Hr244Ia9dQ4,762760
|
|
160
160
|
qubx/ta/indicators.pxd,sha256=l4JgvNdlWBuLqMzqTZVaYYn4XyZ9-c222YCyBXVv8MA,4843
|
|
161
161
|
qubx/ta/indicators.pyi,sha256=kHoHONhzI7aq0qs-wP5cxyDPj96ZvQLlThEC8yQj6U4,2630
|
|
162
162
|
qubx/ta/indicators.pyx,sha256=rT6OJ7xygZdTF4-pT6vr6g9MRhvbi6nYBlkTzzZYA_U,35126
|
|
@@ -204,15 +204,15 @@ qubx/utils/plotting/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
204
204
|
qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
205
205
|
qubx/utils/questdb.py,sha256=bxlWiCyYf8IspsvXrs58tn5iXYBUtv6ojeYwOj8EXI0,5269
|
|
206
206
|
qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
207
|
-
qubx/utils/runner/_jupyter_runner.pyt,sha256=
|
|
207
|
+
qubx/utils/runner/_jupyter_runner.pyt,sha256=DHXhXkjHe8-HkOa4g5EkSb3qbz64TLyM3-c__cQDPjk,9973
|
|
208
208
|
qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
|
|
209
209
|
qubx/utils/runner/configs.py,sha256=X915N6wbRSPFBiZ3WZcNQBSeoiocy-wVxCTTSdM8IAo,5367
|
|
210
210
|
qubx/utils/runner/factory.py,sha256=hmtUDYNFQwVQffHEfxgrlmKwOGLcFQ6uJIH_ZLscpIY,16347
|
|
211
|
-
qubx/utils/runner/runner.py,sha256=
|
|
211
|
+
qubx/utils/runner/runner.py,sha256=gZvj-ScJkSnbl7Vj3VENfdiruc5eCTzUkKek3zPmXiM,33299
|
|
212
212
|
qubx/utils/time.py,sha256=xOWl_F6dOLFCmbB4xccLIx5yVt5HOH-I8ZcuowXjtBQ,11797
|
|
213
213
|
qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
|
|
214
|
-
qubx-0.6.
|
|
215
|
-
qubx-0.6.
|
|
216
|
-
qubx-0.6.
|
|
217
|
-
qubx-0.6.
|
|
218
|
-
qubx-0.6.
|
|
214
|
+
qubx-0.6.77.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
|
|
215
|
+
qubx-0.6.77.dist-info/METADATA,sha256=033S0kYQmIR1ionA0rSQuk57myJENFggwzw0Xqfy9-w,5836
|
|
216
|
+
qubx-0.6.77.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
|
|
217
|
+
qubx-0.6.77.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
218
|
+
qubx-0.6.77.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|