Qubx 0.6.36__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.38__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/backtester/runner.py +2 -1
- qubx/connectors/ccxt/broker.py +68 -44
- qubx/connectors/ccxt/exchanges/__init__.py +1 -1
- qubx/connectors/ccxt/exchanges/binance/exchange.py +7 -2
- qubx/core/context.py +13 -2
- qubx/core/errors.py +19 -0
- qubx/core/interfaces.py +11 -2
- qubx/core/loggers.py +3 -160
- qubx/core/metrics.py +26 -4
- 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/data/hft.py +37 -9
- qubx/data/readers.py +22 -22
- qubx/features/core.py +8 -7
- qubx/loggers/__init__.py +17 -0
- qubx/loggers/csv.py +100 -0
- qubx/loggers/factory.py +55 -0
- qubx/loggers/inmemory.py +68 -0
- qubx/loggers/mongo.py +80 -0
- qubx/restorers/balance.py +76 -0
- qubx/restorers/factory.py +8 -4
- qubx/restorers/position.py +95 -0
- qubx/restorers/signal.py +115 -0
- qubx/restorers/state.py +89 -3
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/runner/_jupyter_runner.pyt +8 -1
- qubx/utils/runner/runner.py +6 -8
- {qubx-0.6.36.dist-info → qubx-0.6.38.dist-info}/METADATA +1 -1
- {qubx-0.6.36.dist-info → qubx-0.6.38.dist-info}/RECORD +32 -27
- {qubx-0.6.36.dist-info → qubx-0.6.38.dist-info}/LICENSE +0 -0
- {qubx-0.6.36.dist-info → qubx-0.6.38.dist-info}/WHEEL +0 -0
- {qubx-0.6.36.dist-info → qubx-0.6.38.dist-info}/entry_points.txt +0 -0
qubx/backtester/runner.py
CHANGED
|
@@ -20,8 +20,9 @@ from qubx.core.interfaces import (
|
|
|
20
20
|
ITimeProvider,
|
|
21
21
|
StrategyState,
|
|
22
22
|
)
|
|
23
|
-
from qubx.core.loggers import
|
|
23
|
+
from qubx.core.loggers import StrategyLogging
|
|
24
24
|
from qubx.core.lookups import lookup
|
|
25
|
+
from qubx.loggers.inmemory import InMemoryLogsWriter
|
|
25
26
|
from qubx.pandaz.utils import _frame_to_str
|
|
26
27
|
|
|
27
28
|
from .account import SimulatedAccountProcessor
|
qubx/connectors/ccxt/broker.py
CHANGED
|
@@ -14,7 +14,7 @@ from qubx.core.basics import (
|
|
|
14
14
|
Order,
|
|
15
15
|
OrderSide,
|
|
16
16
|
)
|
|
17
|
-
from qubx.core.errors import OrderCancellationError, OrderCreationError, create_error_event
|
|
17
|
+
from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
|
|
18
18
|
from qubx.core.exceptions import BadRequest, InvalidOrderParameters
|
|
19
19
|
from qubx.core.interfaces import (
|
|
20
20
|
IAccountProcessor,
|
|
@@ -61,6 +61,57 @@ class CcxtBroker(IBroker):
|
|
|
61
61
|
def is_simulated_trading(self) -> bool:
|
|
62
62
|
return False
|
|
63
63
|
|
|
64
|
+
def _post_order_error_to_databus(
|
|
65
|
+
self,
|
|
66
|
+
error: Exception,
|
|
67
|
+
instrument: Instrument,
|
|
68
|
+
order_side: OrderSide,
|
|
69
|
+
order_type: str,
|
|
70
|
+
amount: float,
|
|
71
|
+
price: float | None,
|
|
72
|
+
client_id: str | None,
|
|
73
|
+
time_in_force: str,
|
|
74
|
+
**options,
|
|
75
|
+
):
|
|
76
|
+
level = ErrorLevel.LOW
|
|
77
|
+
match error:
|
|
78
|
+
case ccxt.InsufficientFunds():
|
|
79
|
+
level = ErrorLevel.HIGH
|
|
80
|
+
logger.error(
|
|
81
|
+
f"(::create_order) INSUFFICIENT FUNDS for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
82
|
+
)
|
|
83
|
+
case ccxt.OrderNotFillable():
|
|
84
|
+
level = ErrorLevel.LOW
|
|
85
|
+
logger.error(
|
|
86
|
+
f"(::create_order) ORDER NOT FILLEABLE for {order_side} {amount} {order_type} for [{instrument.symbol}] : {error}"
|
|
87
|
+
)
|
|
88
|
+
case ccxt.InvalidOrder():
|
|
89
|
+
level = ErrorLevel.LOW
|
|
90
|
+
logger.error(
|
|
91
|
+
f"(::create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
92
|
+
)
|
|
93
|
+
case ccxt.BadRequest():
|
|
94
|
+
level = ErrorLevel.LOW
|
|
95
|
+
logger.error(
|
|
96
|
+
f"(::create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
97
|
+
)
|
|
98
|
+
case _:
|
|
99
|
+
level = ErrorLevel.MEDIUM
|
|
100
|
+
logger.error(f"(::create_order) Unexpected error: {error}")
|
|
101
|
+
|
|
102
|
+
error_event = OrderCreationError(
|
|
103
|
+
timestamp=self.time_provider.time(),
|
|
104
|
+
message=f"Error message: {str(error)}",
|
|
105
|
+
level=level,
|
|
106
|
+
instrument=instrument,
|
|
107
|
+
amount=amount,
|
|
108
|
+
price=price,
|
|
109
|
+
order_type=order_type,
|
|
110
|
+
side=order_side,
|
|
111
|
+
error=error,
|
|
112
|
+
)
|
|
113
|
+
self.channel.send(create_error_event(error_event))
|
|
114
|
+
|
|
64
115
|
def send_order_async(
|
|
65
116
|
self,
|
|
66
117
|
instrument: Instrument,
|
|
@@ -93,33 +144,20 @@ class CcxtBroker(IBroker):
|
|
|
93
144
|
)
|
|
94
145
|
|
|
95
146
|
if error:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
timestamp=self.time_provider.time(),
|
|
99
|
-
message=str(error),
|
|
100
|
-
instrument=instrument,
|
|
101
|
-
amount=amount,
|
|
102
|
-
price=price,
|
|
103
|
-
order_type=order_type,
|
|
104
|
-
side=order_side,
|
|
147
|
+
self._post_order_error_to_databus(
|
|
148
|
+
error, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
105
149
|
)
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
order = None
|
|
151
|
+
|
|
108
152
|
return order
|
|
153
|
+
|
|
109
154
|
except Exception as err:
|
|
110
155
|
# Catch any unexpected errors and send them through the channel as well
|
|
111
|
-
logger.error(f"Unexpected error in async order creation: {err}")
|
|
156
|
+
logger.error(f"{self.__class__.__name__} :: Unexpected error in async order creation: {err}")
|
|
112
157
|
logger.error(traceback.format_exc())
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
message=f"Unexpected error: {str(err)}",
|
|
116
|
-
instrument=instrument,
|
|
117
|
-
amount=amount,
|
|
118
|
-
price=price,
|
|
119
|
-
order_type=order_type,
|
|
120
|
-
side=order_side,
|
|
158
|
+
self._post_order_error_to_databus(
|
|
159
|
+
err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
121
160
|
)
|
|
122
|
-
self.channel.send(create_error_event(error_event))
|
|
123
161
|
return None
|
|
124
162
|
|
|
125
163
|
# Submit the task to the async loop
|
|
@@ -135,7 +173,7 @@ class CcxtBroker(IBroker):
|
|
|
135
173
|
client_id: str | None = None,
|
|
136
174
|
time_in_force: str = "gtc",
|
|
137
175
|
**options,
|
|
138
|
-
) -> Order:
|
|
176
|
+
) -> Order | None:
|
|
139
177
|
"""
|
|
140
178
|
Submit an order and wait for the result. Exceptions will be raised on errors.
|
|
141
179
|
|
|
@@ -169,14 +207,16 @@ class CcxtBroker(IBroker):
|
|
|
169
207
|
|
|
170
208
|
# If there was no error but also no order, something went wrong
|
|
171
209
|
if not order and not self.enable_create_order_ws:
|
|
172
|
-
raise ExchangeError("Order creation failed with no specific error")
|
|
210
|
+
raise ExchangeError(f"{self.__class__.__name__} :: Order creation failed with no specific error")
|
|
173
211
|
|
|
174
212
|
return order
|
|
175
213
|
|
|
176
214
|
except Exception as err:
|
|
177
215
|
# This will catch any errors from future.result() or if we explicitly raise an error
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
self._post_order_error_to_databus(
|
|
217
|
+
err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
218
|
+
)
|
|
219
|
+
return None
|
|
180
220
|
|
|
181
221
|
def cancel_order(self, order_id: str) -> Order | None:
|
|
182
222
|
orders = self.account.get_orders()
|
|
@@ -231,25 +271,7 @@ class CcxtBroker(IBroker):
|
|
|
231
271
|
logger.info(f"New order {order}")
|
|
232
272
|
return order, None
|
|
233
273
|
|
|
234
|
-
except ccxt.OrderNotFillable as exc:
|
|
235
|
-
logger.error(
|
|
236
|
-
f"(::_create_order) [{instrument.symbol}] ORDER NOT FILLEABLE for {order_side} {amount} {order_type} : {exc}"
|
|
237
|
-
)
|
|
238
|
-
return None, exc
|
|
239
|
-
except ccxt.InvalidOrder as exc:
|
|
240
|
-
logger.error(
|
|
241
|
-
f"(::_create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
|
|
242
|
-
)
|
|
243
|
-
return None, exc
|
|
244
|
-
except ccxt.BadRequest as exc:
|
|
245
|
-
logger.error(
|
|
246
|
-
f"(::_create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
|
|
247
|
-
)
|
|
248
|
-
return None, exc
|
|
249
274
|
except Exception as err:
|
|
250
|
-
logger.error(
|
|
251
|
-
f"(::_create_order) {order_side} {amount} {order_type} for {instrument.symbol} exception : {err}"
|
|
252
|
-
)
|
|
253
275
|
return None, err
|
|
254
276
|
|
|
255
277
|
def _prepare_order_payload(
|
|
@@ -371,6 +393,8 @@ class CcxtBroker(IBroker):
|
|
|
371
393
|
order_id=order_id,
|
|
372
394
|
message=f"Timeout reached for canceling order {order_id}",
|
|
373
395
|
instrument=instrument,
|
|
396
|
+
level=ErrorLevel.LOW,
|
|
397
|
+
error=None,
|
|
374
398
|
)
|
|
375
399
|
)
|
|
376
400
|
)
|
|
@@ -25,7 +25,7 @@ EXCHANGE_ALIASES = {
|
|
|
25
25
|
|
|
26
26
|
CUSTOM_BROKERS = {
|
|
27
27
|
"binance": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
28
|
-
"binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=
|
|
28
|
+
"binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
29
29
|
"binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
30
30
|
"binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
|
|
31
31
|
"bitfinex.f": partial(CcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
@@ -3,7 +3,7 @@ from typing import Dict, List
|
|
|
3
3
|
import ccxt.pro as cxp
|
|
4
4
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp
|
|
5
5
|
from ccxt.async_support.base.ws.client import Client
|
|
6
|
-
from ccxt.base.errors import ArgumentsRequired, BadRequest, NotSupported
|
|
6
|
+
from ccxt.base.errors import ArgumentsRequired, BadRequest, InsufficientFunds, NotSupported
|
|
7
7
|
from ccxt.base.precise import Precise
|
|
8
8
|
from ccxt.base.types import (
|
|
9
9
|
Any,
|
|
@@ -34,7 +34,12 @@ class BinanceQV(cxp.binance):
|
|
|
34
34
|
"name": "aggTrade",
|
|
35
35
|
},
|
|
36
36
|
"localOrderBookLimit": 10_000, # set a large limit to avoid cutting off the orderbook
|
|
37
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"exceptions": {
|
|
39
|
+
"exact": {
|
|
40
|
+
"-2019": InsufficientFunds, # ccxt doesn't have this code for some weird reason !!
|
|
41
|
+
},
|
|
42
|
+
},
|
|
38
43
|
},
|
|
39
44
|
)
|
|
40
45
|
|
qubx/core/context.py
CHANGED
|
@@ -16,6 +16,7 @@ from qubx.core.basics import (
|
|
|
16
16
|
Timestamped,
|
|
17
17
|
dt_64,
|
|
18
18
|
)
|
|
19
|
+
from qubx.core.errors import BaseErrorEvent, ErrorLevel
|
|
19
20
|
from qubx.core.exceptions import StrategyExceededMaxNumberOfRuntimeFailuresError
|
|
20
21
|
from qubx.core.helpers import (
|
|
21
22
|
BasicScheduler,
|
|
@@ -286,7 +287,7 @@ class StrategyContext(IStrategyContext):
|
|
|
286
287
|
|
|
287
288
|
# - invoke strategy's stop code
|
|
288
289
|
try:
|
|
289
|
-
if not self.
|
|
290
|
+
if not self.is_warmup_in_progress:
|
|
290
291
|
self.strategy.on_stop(self)
|
|
291
292
|
except Exception as strat_error:
|
|
292
293
|
logger.error(
|
|
@@ -327,7 +328,7 @@ class StrategyContext(IStrategyContext):
|
|
|
327
328
|
return self._data_providers[0].is_simulation
|
|
328
329
|
|
|
329
330
|
@property
|
|
330
|
-
def
|
|
331
|
+
def is_paper_trading(self) -> bool:
|
|
331
332
|
return self._brokers[0].is_simulated_trading
|
|
332
333
|
|
|
333
334
|
# IAccountViewer delegation
|
|
@@ -536,6 +537,16 @@ class StrategyContext(IStrategyContext):
|
|
|
536
537
|
if _should_record:
|
|
537
538
|
self._health_monitor.record_start_processing(d_type, dt_64(data.time, "ns"))
|
|
538
539
|
|
|
540
|
+
# - notify error if error level is medium or higher
|
|
541
|
+
if (
|
|
542
|
+
self._lifecycle_notifier
|
|
543
|
+
and isinstance(data, BaseErrorEvent)
|
|
544
|
+
and data.level.value >= ErrorLevel.MEDIUM.value
|
|
545
|
+
):
|
|
546
|
+
self._lifecycle_notifier.notify_error(
|
|
547
|
+
self._strategy_name, data.error or Exception("Unknown error"), {"message": str(data)}
|
|
548
|
+
)
|
|
549
|
+
|
|
539
550
|
if self.process_data(instrument, d_type, data, hist):
|
|
540
551
|
channel.stop()
|
|
541
552
|
break
|
qubx/core/errors.py
CHANGED
|
@@ -3,14 +3,27 @@ Error types that are sent through the event channel.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
6
7
|
|
|
7
8
|
from qubx.core.basics import Instrument, dt_64
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
class ErrorLevel(Enum):
|
|
12
|
+
LOW = 1 # continue trading
|
|
13
|
+
MEDIUM = 2 # send notifications and continue trading
|
|
14
|
+
HIGH = 3 # send notification and cancel orders and close positions
|
|
15
|
+
CRITICAL = 4 # send notification and shutdown strategy
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
@dataclass
|
|
11
19
|
class BaseErrorEvent:
|
|
12
20
|
timestamp: dt_64
|
|
13
21
|
message: str
|
|
22
|
+
level: ErrorLevel
|
|
23
|
+
error: Exception | None
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error}"
|
|
14
27
|
|
|
15
28
|
|
|
16
29
|
def create_error_event(error: BaseErrorEvent) -> tuple[None, str, BaseErrorEvent, bool]:
|
|
@@ -25,8 +38,14 @@ class OrderCreationError(BaseErrorEvent):
|
|
|
25
38
|
order_type: str
|
|
26
39
|
side: str
|
|
27
40
|
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order creation error for {self.order_type} {self.side} {self.instrument} {self.amount}"
|
|
43
|
+
|
|
28
44
|
|
|
29
45
|
@dataclass
|
|
30
46
|
class OrderCancellationError(BaseErrorEvent):
|
|
31
47
|
instrument: Instrument
|
|
32
48
|
order_id: str
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order cancellation error for {self.order_id} {self.instrument}"
|
qubx/core/interfaces.py
CHANGED
|
@@ -1068,7 +1068,6 @@ class IStrategyContext(
|
|
|
1068
1068
|
IProcessingManager,
|
|
1069
1069
|
IAccountViewer,
|
|
1070
1070
|
IWarmupStateSaver,
|
|
1071
|
-
StrategyState,
|
|
1072
1071
|
):
|
|
1073
1072
|
strategy: "IStrategy"
|
|
1074
1073
|
initializer: "IStrategyInitializer"
|
|
@@ -1086,17 +1085,27 @@ class IStrategyContext(
|
|
|
1086
1085
|
"""Stop the strategy context."""
|
|
1087
1086
|
pass
|
|
1088
1087
|
|
|
1088
|
+
@property
|
|
1089
|
+
def state(self) -> StrategyState:
|
|
1090
|
+
"""Get the strategy state."""
|
|
1091
|
+
return StrategyState(**self._strategy_state.__dict__)
|
|
1092
|
+
|
|
1089
1093
|
def is_running(self) -> bool:
|
|
1090
1094
|
"""Check if the strategy context is running."""
|
|
1091
1095
|
return False
|
|
1092
1096
|
|
|
1097
|
+
@property
|
|
1098
|
+
def is_warmup_in_progress(self) -> bool:
|
|
1099
|
+
"""Check if the warmup is in progress."""
|
|
1100
|
+
return self._strategy_state.is_warmup_in_progress
|
|
1101
|
+
|
|
1093
1102
|
@property
|
|
1094
1103
|
def is_simulation(self) -> bool:
|
|
1095
1104
|
"""Check if the strategy context is running in simulation mode."""
|
|
1096
1105
|
return False
|
|
1097
1106
|
|
|
1098
1107
|
@property
|
|
1099
|
-
def
|
|
1108
|
+
def is_paper_trading(self) -> bool:
|
|
1100
1109
|
"""Check if the strategy context is running in simulated trading mode."""
|
|
1101
1110
|
return False
|
|
1102
1111
|
|
qubx/core/loggers.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import csv
|
|
2
|
-
import os
|
|
3
|
-
from multiprocessing.pool import ThreadPool
|
|
4
1
|
from typing import Any, Dict, List, Tuple
|
|
5
2
|
|
|
6
3
|
import numpy as np
|
|
7
|
-
import pandas as pd
|
|
8
4
|
|
|
9
5
|
from qubx import logger
|
|
10
6
|
from qubx.core.basics import (
|
|
@@ -14,11 +10,11 @@ from qubx.core.basics import (
|
|
|
14
10
|
Position,
|
|
15
11
|
TargetPosition,
|
|
16
12
|
)
|
|
17
|
-
|
|
13
|
+
|
|
18
14
|
from qubx.core.series import time_as_nsec
|
|
19
15
|
from qubx.core.utils import recognize_timeframe
|
|
20
|
-
|
|
21
|
-
from qubx.utils.misc import Stopwatch
|
|
16
|
+
|
|
17
|
+
from qubx.utils.misc import Stopwatch
|
|
22
18
|
from qubx.utils.time import convert_tf_str_td64, floor_t64
|
|
23
19
|
|
|
24
20
|
_SW = Stopwatch()
|
|
@@ -48,159 +44,6 @@ class LogsWriter:
|
|
|
48
44
|
pass
|
|
49
45
|
|
|
50
46
|
|
|
51
|
-
class InMemoryLogsWriter(LogsWriter):
|
|
52
|
-
_portfolio: List
|
|
53
|
-
_execs: List
|
|
54
|
-
_signals: List
|
|
55
|
-
|
|
56
|
-
def __init__(self, account_id: str, strategy_id: str, run_id: str) -> None:
|
|
57
|
-
super().__init__(account_id, strategy_id, run_id)
|
|
58
|
-
self._portfolio = []
|
|
59
|
-
self._execs = []
|
|
60
|
-
self._signals = []
|
|
61
|
-
|
|
62
|
-
def write_data(self, log_type: str, data: List[Dict[str, Any]]):
|
|
63
|
-
if len(data) > 0:
|
|
64
|
-
if log_type == "portfolio":
|
|
65
|
-
self._portfolio.extend(data)
|
|
66
|
-
elif log_type == "executions":
|
|
67
|
-
self._execs.extend(data)
|
|
68
|
-
elif log_type == "signals":
|
|
69
|
-
self._signals.extend(data)
|
|
70
|
-
|
|
71
|
-
def get_portfolio(self, as_plain_dataframe=True) -> pd.DataFrame:
|
|
72
|
-
pfl = pd.DataFrame.from_records(self._portfolio, index="timestamp")
|
|
73
|
-
pfl.index = pd.DatetimeIndex(pfl.index)
|
|
74
|
-
if as_plain_dataframe:
|
|
75
|
-
# - convert to Qube presentation (TODO: temporary)
|
|
76
|
-
pis = []
|
|
77
|
-
for s in set(pfl["symbol"]):
|
|
78
|
-
pi = pfl[pfl["symbol"] == s]
|
|
79
|
-
pi = pi.drop(columns=["symbol", "realized_pnl_quoted", "current_price", "exchange_time"])
|
|
80
|
-
pi = pi.rename(
|
|
81
|
-
{
|
|
82
|
-
"pnl_quoted": "PnL",
|
|
83
|
-
"quantity": "Pos",
|
|
84
|
-
"avg_position_price": "Price",
|
|
85
|
-
"market_value_quoted": "Value",
|
|
86
|
-
"commissions_quoted": "Commissions",
|
|
87
|
-
},
|
|
88
|
-
axis=1,
|
|
89
|
-
)
|
|
90
|
-
# We want to convert the value to just price * quantity
|
|
91
|
-
# in reality value of perps is just the unrealized pnl but
|
|
92
|
-
# it's not important after simulation for metric calculations
|
|
93
|
-
pi["Value"] = pi["Pos"] * pi["Price"] + pi["Value"]
|
|
94
|
-
pis.append(pi.rename(lambda x: s + "_" + x, axis=1))
|
|
95
|
-
return split_cumulative_pnl(scols(*pis))
|
|
96
|
-
return pfl
|
|
97
|
-
|
|
98
|
-
def get_executions(self) -> pd.DataFrame:
|
|
99
|
-
p = pd.DataFrame()
|
|
100
|
-
if self._execs:
|
|
101
|
-
p = pd.DataFrame.from_records(self._execs, index="timestamp")
|
|
102
|
-
p.index = pd.DatetimeIndex(p.index)
|
|
103
|
-
return p
|
|
104
|
-
|
|
105
|
-
def get_signals(self) -> pd.DataFrame:
|
|
106
|
-
p = pd.DataFrame()
|
|
107
|
-
if self._signals:
|
|
108
|
-
p = pd.DataFrame.from_records(self._signals, index="timestamp")
|
|
109
|
-
p.index = pd.DatetimeIndex(p.index)
|
|
110
|
-
return p
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class CsvFileLogsWriter(LogsWriter):
|
|
114
|
-
"""
|
|
115
|
-
Simple CSV strategy log data writer. It does data writing in separate thread.
|
|
116
|
-
"""
|
|
117
|
-
|
|
118
|
-
def __init__(self, account_id: str, strategy_id: str, run_id: str, log_folder="logs") -> None:
|
|
119
|
-
super().__init__(account_id, strategy_id, run_id)
|
|
120
|
-
|
|
121
|
-
path = makedirs(log_folder)
|
|
122
|
-
# - it rewrites positions every time
|
|
123
|
-
self._pos_file_path = f"{path}/{self.strategy_id}_{self.account_id}_positions.csv"
|
|
124
|
-
self._balance_file_path = f"{path}/{self.strategy_id}_{self.account_id}_balance.csv"
|
|
125
|
-
|
|
126
|
-
_pfl_path = f"{path}/{strategy_id}_{account_id}_portfolio.csv"
|
|
127
|
-
_exe_path = f"{path}/{strategy_id}_{account_id}_executions.csv"
|
|
128
|
-
_sig_path = f"{path}/{strategy_id}_{account_id}_signals.csv"
|
|
129
|
-
self._hdr_pfl = not os.path.exists(_pfl_path)
|
|
130
|
-
self._hdr_exe = not os.path.exists(_exe_path)
|
|
131
|
-
self._hdr_sig = not os.path.exists(_sig_path)
|
|
132
|
-
|
|
133
|
-
self._pfl_file_ = open(_pfl_path, "+a", newline="")
|
|
134
|
-
self._execs_file_ = open(_exe_path, "+a", newline="")
|
|
135
|
-
self._sig_file_ = open(_sig_path, "+a", newline="")
|
|
136
|
-
self._pfl_writer = csv.writer(self._pfl_file_)
|
|
137
|
-
self._exe_writer = csv.writer(self._execs_file_)
|
|
138
|
-
self._sig_writer = csv.writer(self._sig_file_)
|
|
139
|
-
self.pool = ThreadPool(3)
|
|
140
|
-
|
|
141
|
-
@staticmethod
|
|
142
|
-
def _header(d: dict) -> List[str]:
|
|
143
|
-
return list(d.keys()) + ["run_id"]
|
|
144
|
-
|
|
145
|
-
def _values(self, data: List[Dict[str, Any]]) -> List[List[str]]:
|
|
146
|
-
# - attach run_id (last column)
|
|
147
|
-
return [list((d | {"run_id": self.run_id}).values()) for d in data]
|
|
148
|
-
|
|
149
|
-
def _do_write(self, log_type, data):
|
|
150
|
-
match log_type:
|
|
151
|
-
case "positions":
|
|
152
|
-
with open(self._pos_file_path, "w", newline="") as f:
|
|
153
|
-
w = csv.writer(f)
|
|
154
|
-
w.writerow(self._header(data[0]))
|
|
155
|
-
w.writerows(self._values(data))
|
|
156
|
-
|
|
157
|
-
case "portfolio":
|
|
158
|
-
if self._hdr_pfl:
|
|
159
|
-
self._pfl_writer.writerow(self._header(data[0]))
|
|
160
|
-
self._hdr_pfl = False
|
|
161
|
-
self._pfl_writer.writerows(self._values(data))
|
|
162
|
-
self._pfl_file_.flush()
|
|
163
|
-
|
|
164
|
-
case "executions":
|
|
165
|
-
if self._hdr_exe:
|
|
166
|
-
self._exe_writer.writerow(self._header(data[0]))
|
|
167
|
-
self._hdr_exe = False
|
|
168
|
-
self._exe_writer.writerows(self._values(data))
|
|
169
|
-
self._execs_file_.flush()
|
|
170
|
-
|
|
171
|
-
case "signals":
|
|
172
|
-
if self._hdr_sig:
|
|
173
|
-
self._sig_writer.writerow(self._header(data[0]))
|
|
174
|
-
self._hdr_sig = False
|
|
175
|
-
self._sig_writer.writerows(self._values(data))
|
|
176
|
-
self._sig_file_.flush()
|
|
177
|
-
|
|
178
|
-
case "balance":
|
|
179
|
-
with open(self._balance_file_path, "w", newline="") as f:
|
|
180
|
-
w = csv.writer(f)
|
|
181
|
-
w.writerow(self._header(data[0]))
|
|
182
|
-
w.writerows(self._values(data))
|
|
183
|
-
|
|
184
|
-
def write_data(self, log_type: str, data: List[Dict[str, Any]]):
|
|
185
|
-
if len(data) > 0:
|
|
186
|
-
self.pool.apply_async(self._do_write, (log_type, data))
|
|
187
|
-
|
|
188
|
-
def flush_data(self):
|
|
189
|
-
try:
|
|
190
|
-
self._pfl_file_.flush()
|
|
191
|
-
self._execs_file_.flush()
|
|
192
|
-
self._sig_file_.flush()
|
|
193
|
-
except Exception as e:
|
|
194
|
-
logger.warning(f"Error flushing log writer: {str(e)}")
|
|
195
|
-
|
|
196
|
-
def close(self):
|
|
197
|
-
self._pfl_file_.close()
|
|
198
|
-
self._execs_file_.close()
|
|
199
|
-
self._sig_file_.close()
|
|
200
|
-
self.pool.close()
|
|
201
|
-
self.pool.join()
|
|
202
|
-
|
|
203
|
-
|
|
204
47
|
class _BaseIntervalDumper:
|
|
205
48
|
"""
|
|
206
49
|
Basic functionality for all interval based dumpers
|
qubx/core/metrics.py
CHANGED
|
@@ -717,7 +717,7 @@ class TradingSessionResult:
|
|
|
717
717
|
"name": self.name,
|
|
718
718
|
"start": pd.Timestamp(self.start).isoformat(),
|
|
719
719
|
"stop": pd.Timestamp(self.stop).isoformat(),
|
|
720
|
-
"
|
|
720
|
+
"exchanges": self.exchanges,
|
|
721
721
|
"capital": self.capital,
|
|
722
722
|
"base_currency": self.base_currency,
|
|
723
723
|
"commissions": self.commissions,
|
|
@@ -824,6 +824,12 @@ class TradingSessionResult:
|
|
|
824
824
|
info = self.info()
|
|
825
825
|
if description:
|
|
826
826
|
info["description"] = description
|
|
827
|
+
# - set name if not specified
|
|
828
|
+
if info.get("name") is None:
|
|
829
|
+
info["name"] = name
|
|
830
|
+
|
|
831
|
+
# - add numpy array representer
|
|
832
|
+
yaml.SafeDumper.add_representer(np.ndarray, lambda dumper, data: dumper.represent_list(data.tolist()))
|
|
827
833
|
yaml.safe_dump(info, f, sort_keys=False, indent=4)
|
|
828
834
|
|
|
829
835
|
# - save logs
|
|
@@ -855,15 +861,31 @@ class TradingSessionResult:
|
|
|
855
861
|
|
|
856
862
|
with zipfile.ZipFile(path, "r") as zip_ref:
|
|
857
863
|
info = yaml.safe_load(zip_ref.read("info.yml"))
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
864
|
+
try:
|
|
865
|
+
portfolio = pd.read_csv(
|
|
866
|
+
zip_ref.open("portfolio.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
|
|
867
|
+
)
|
|
868
|
+
except:
|
|
869
|
+
portfolio = pd.DataFrame()
|
|
870
|
+
try:
|
|
871
|
+
executions = pd.read_csv(
|
|
872
|
+
zip_ref.open("executions.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
|
|
873
|
+
)
|
|
874
|
+
except:
|
|
875
|
+
executions = pd.DataFrame()
|
|
876
|
+
try:
|
|
877
|
+
signals = pd.read_csv(zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
|
|
878
|
+
except:
|
|
879
|
+
signals = pd.DataFrame()
|
|
861
880
|
|
|
862
881
|
# load result
|
|
863
882
|
_qbx_version = info.pop("qubx_version")
|
|
864
883
|
_decr = info.pop("description", None)
|
|
865
884
|
_perf = info.pop("performance", None)
|
|
866
885
|
info["instruments"] = info.pop("symbols")
|
|
886
|
+
# - fix for old versions
|
|
887
|
+
_exch = info.pop("exchange")
|
|
888
|
+
info["exchanges"] = _exch if isinstance(_exch, list) else [_exch]
|
|
867
889
|
tsr = TradingSessionResult(**info, portfolio_log=portfolio, executions_log=executions, signals_log=signals)
|
|
868
890
|
tsr.qubx_version = _qbx_version
|
|
869
891
|
tsr._metrics = _perf
|
|
Binary file
|
|
Binary file
|