quantex 0.6.0__tar.gz → 0.6.2__tar.gz
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.
- {quantex-0.6.0 → quantex-0.6.2}/PKG-INFO +1 -1
- {quantex-0.6.0 → quantex-0.6.2}/pyproject.toml +1 -1
- {quantex-0.6.0 → quantex-0.6.2}/src/quantex/backtester.py +23 -0
- {quantex-0.6.0 → quantex-0.6.2}/src/quantex/broker.py +105 -66
- {quantex-0.6.0 → quantex-0.6.2}/LICENSE.md +0 -0
- {quantex-0.6.0 → quantex-0.6.2}/README.md +0 -0
- {quantex-0.6.0 → quantex-0.6.2}/src/quantex/__init__.py +0 -0
- {quantex-0.6.0 → quantex-0.6.2}/src/quantex/commission.py +0 -0
- {quantex-0.6.0 → quantex-0.6.2}/src/quantex/datasource.py +0 -0
- {quantex-0.6.0 → quantex-0.6.2}/src/quantex/strategy.py +0 -0
|
@@ -36,6 +36,29 @@ class Result:
|
|
|
36
36
|
self.run_strategy = run_strategy
|
|
37
37
|
self.total_trades = total_trades
|
|
38
38
|
|
|
39
|
+
@property
|
|
40
|
+
def timedelta(self) -> pd.Timedelta:
|
|
41
|
+
"""
|
|
42
|
+
The amount of time elapsed between the start and end of the backtest.
|
|
43
|
+
"""
|
|
44
|
+
return (
|
|
45
|
+
self.run_strategy.data.Timestamp[-1] - self.run_strategy.data.Timestamp[0]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def start(self) -> pd.Timestamp:
|
|
50
|
+
"""
|
|
51
|
+
The start time of the backtest.
|
|
52
|
+
"""
|
|
53
|
+
return self.run_strategy.data.Timestamp[0]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def end(self) -> pd.Timestamp:
|
|
57
|
+
"""
|
|
58
|
+
The end time of the backtest.
|
|
59
|
+
"""
|
|
60
|
+
return self.run_strategy.data.Timestamp[-1]
|
|
61
|
+
|
|
39
62
|
@property
|
|
40
63
|
def total_return(self) -> float:
|
|
41
64
|
"""
|
|
@@ -16,9 +16,9 @@ class OrderDirection(Enum):
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
19
|
-
class
|
|
19
|
+
class NewOrder:
|
|
20
20
|
id: int
|
|
21
|
-
|
|
21
|
+
transmit_timestamp: pd.Timestamp
|
|
22
22
|
type: OrderType
|
|
23
23
|
direction: OrderDirection
|
|
24
24
|
amount: float
|
|
@@ -26,8 +26,15 @@ class Order:
|
|
|
26
26
|
parentId: int | None
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
@dataclass
|
|
30
|
+
class Order(NewOrder):
|
|
31
|
+
fill_timestamp: pd.Timestamp
|
|
32
|
+
amount_filled: float
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
class Broker:
|
|
30
|
-
orderQueue: dict[str, list[
|
|
36
|
+
orderQueue: dict[str, list[NewOrder]] ## Orders to be processed
|
|
37
|
+
cancelQueue: dict[str, list[NewOrder]] ## Orders that need to be cancelled
|
|
31
38
|
processedOrders: dict[str, list[Order]] ## Orders that have been processed
|
|
32
39
|
openPositions: dict[str, list[Order]] ## Orders that are currently open
|
|
33
40
|
__orderId__: int
|
|
@@ -38,15 +45,16 @@ class Broker:
|
|
|
38
45
|
self.orderQueue = {}
|
|
39
46
|
self.processedOrders = {}
|
|
40
47
|
self.openPositions = {}
|
|
48
|
+
self.cancelQueue = {}
|
|
41
49
|
self.__context__ = context
|
|
42
50
|
self.__orderId__ = 1
|
|
43
|
-
self.orderQueue = {}
|
|
44
51
|
for name in self.__context__.datas.keys():
|
|
45
52
|
self.orderQueue[name] = []
|
|
46
53
|
self.processedOrders[name] = []
|
|
47
54
|
self.openPositions[name] = []
|
|
55
|
+
self.cancelQueue[name] = []
|
|
48
56
|
|
|
49
|
-
def execute_condition(self, order:
|
|
57
|
+
def execute_condition(self, order: NewOrder, price: float) -> bool:
|
|
50
58
|
if order.type == OrderType.MARKET:
|
|
51
59
|
return True
|
|
52
60
|
elif order.type == OrderType.LIMIT and order.price:
|
|
@@ -61,38 +69,42 @@ class Broker:
|
|
|
61
69
|
return True
|
|
62
70
|
return False
|
|
63
71
|
|
|
64
|
-
def __process_order__(self, order:
|
|
72
|
+
def __process_order__(self, order: NewOrder, name: str):
|
|
65
73
|
openPositionsDirection = None
|
|
66
74
|
if self.openPositions[name] and len(self.openPositions[name]) > 0:
|
|
67
75
|
openPositionsDirection = self.openPositions[name][0].direction
|
|
76
|
+
curr_timestamp = self.__context__.datas[name].Timestamp[-1]
|
|
68
77
|
if openPositionsDirection and openPositionsDirection != order.direction:
|
|
69
|
-
price: float = self.__context__.datas[name].
|
|
78
|
+
price: float = self.__context__.datas[name].Open[-1]
|
|
70
79
|
if self.execute_condition(order, price):
|
|
71
|
-
total = order.amount * price
|
|
72
80
|
amount = order.amount
|
|
73
81
|
for i in range(len(self.openPositions[name])):
|
|
74
|
-
open_amount = self.openPositions[name][i].
|
|
82
|
+
open_amount = self.openPositions[name][i].amount_filled
|
|
75
83
|
closed_amount = min(open_amount, amount)
|
|
76
|
-
self.openPositions[name][i].
|
|
84
|
+
self.openPositions[name][i].amount_filled -= closed_amount
|
|
85
|
+
if self.openPositions[name][i].amount_filled == 0:
|
|
86
|
+
## Check if the order has any take profit or stop loss and if so, cancel it
|
|
87
|
+
queueParents = [i.parentId for i in self.orderQueue[name]]
|
|
88
|
+
if self.openPositions[name][i].id in queueParents:
|
|
89
|
+
item = self.orderQueue[name][
|
|
90
|
+
queueParents.index(self.openPositions[name][i].id)
|
|
91
|
+
]
|
|
92
|
+
self.cancelQueue[name].append(item)
|
|
77
93
|
amount -= closed_amount
|
|
78
94
|
if amount == 0:
|
|
79
95
|
if order.parentId is not None:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
elif (
|
|
85
|
-
self.orderQueue[name][i].parentId == order.parentId
|
|
96
|
+
for queued_order in self.orderQueue[name]:
|
|
97
|
+
if (
|
|
98
|
+
queued_order.id != order.id
|
|
99
|
+
and queued_order.parentId == order.parentId
|
|
86
100
|
):
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
idx
|
|
91
|
-
) ## OCO - One Cancels the Other
|
|
101
|
+
self.cancelQueue[name].append(
|
|
102
|
+
queued_order
|
|
103
|
+
) ## OCO - One Cancels the Other
|
|
92
104
|
break
|
|
93
105
|
positions = []
|
|
94
106
|
for openOrder in self.openPositions[name]:
|
|
95
|
-
if openOrder.
|
|
107
|
+
if openOrder.amount_filled > 0:
|
|
96
108
|
positions.append(
|
|
97
109
|
openOrder
|
|
98
110
|
) ## This order has not been fully executed
|
|
@@ -106,29 +118,54 @@ class Broker:
|
|
|
106
118
|
positions.append(
|
|
107
119
|
Order(
|
|
108
120
|
order.id,
|
|
109
|
-
order.
|
|
121
|
+
order.transmit_timestamp,
|
|
110
122
|
order.type,
|
|
111
123
|
order.direction,
|
|
112
|
-
amount,
|
|
124
|
+
order.amount,
|
|
113
125
|
order.price,
|
|
114
126
|
order.parentId,
|
|
127
|
+
curr_timestamp,
|
|
128
|
+
order.amount - amount, ## Partially filled
|
|
115
129
|
)
|
|
116
130
|
)
|
|
117
131
|
else:
|
|
118
132
|
self.processedOrders[name].append(
|
|
119
|
-
|
|
133
|
+
Order(
|
|
134
|
+
order.id,
|
|
135
|
+
order.transmit_timestamp,
|
|
136
|
+
order.type,
|
|
137
|
+
order.direction,
|
|
138
|
+
order.amount,
|
|
139
|
+
order.price,
|
|
140
|
+
order.parentId,
|
|
141
|
+
curr_timestamp,
|
|
142
|
+
order.amount, ## Fully filled
|
|
143
|
+
)
|
|
120
144
|
) ## This order has been fully executed
|
|
121
145
|
self.openPositions[name] = positions
|
|
146
|
+
total = (order.amount - amount) * price
|
|
122
147
|
if order.direction == OrderDirection.BUY:
|
|
123
148
|
self.cash -= total
|
|
124
149
|
else:
|
|
125
150
|
self.cash += total
|
|
126
151
|
return True
|
|
127
152
|
else:
|
|
128
|
-
price: float = self.__context__.datas[name].
|
|
153
|
+
price: float = self.__context__.datas[name].Open[-1]
|
|
129
154
|
if self.execute_condition(order, price):
|
|
130
155
|
total = order.amount * price
|
|
131
|
-
self.openPositions[name].append(
|
|
156
|
+
self.openPositions[name].append(
|
|
157
|
+
Order(
|
|
158
|
+
order.id,
|
|
159
|
+
order.transmit_timestamp,
|
|
160
|
+
order.type,
|
|
161
|
+
order.direction,
|
|
162
|
+
order.amount,
|
|
163
|
+
order.price,
|
|
164
|
+
order.parentId,
|
|
165
|
+
curr_timestamp,
|
|
166
|
+
order.amount, ## Fully filled
|
|
167
|
+
)
|
|
168
|
+
)
|
|
132
169
|
if order.direction == OrderDirection.BUY:
|
|
133
170
|
self.cash -= total
|
|
134
171
|
else:
|
|
@@ -140,6 +177,9 @@ class Broker:
|
|
|
140
177
|
for name in self.orderQueue.keys():
|
|
141
178
|
queue = []
|
|
142
179
|
for order in self.orderQueue[name]:
|
|
180
|
+
if order in self.cancelQueue[name]:
|
|
181
|
+
self.cancelQueue[name].remove(order)
|
|
182
|
+
continue
|
|
143
183
|
if not self.__process_order__(order, name):
|
|
144
184
|
queue.append(order)
|
|
145
185
|
self.orderQueue[name] = queue
|
|
@@ -156,9 +196,9 @@ class Broker:
|
|
|
156
196
|
for order in self.openPositions[name]:
|
|
157
197
|
prices = self.__context__.datas[name].Close
|
|
158
198
|
if order.direction == OrderDirection.BUY:
|
|
159
|
-
equity += order.
|
|
199
|
+
equity += order.amount_filled * prices[-1]
|
|
160
200
|
else:
|
|
161
|
-
equity -= order.
|
|
201
|
+
equity -= order.amount_filled * prices[-1]
|
|
162
202
|
return equity
|
|
163
203
|
|
|
164
204
|
def buy(
|
|
@@ -186,7 +226,7 @@ class Broker:
|
|
|
186
226
|
self.__orderId__ += 1
|
|
187
227
|
if limit is not None:
|
|
188
228
|
self.orderQueue[name].append(
|
|
189
|
-
|
|
229
|
+
NewOrder(
|
|
190
230
|
parentId,
|
|
191
231
|
data.Timestamp[-1],
|
|
192
232
|
OrderType.LIMIT,
|
|
@@ -198,7 +238,7 @@ class Broker:
|
|
|
198
238
|
)
|
|
199
239
|
else:
|
|
200
240
|
self.orderQueue[name].append(
|
|
201
|
-
|
|
241
|
+
NewOrder(
|
|
202
242
|
parentId,
|
|
203
243
|
data.Timestamp[-1],
|
|
204
244
|
OrderType.MARKET,
|
|
@@ -212,7 +252,7 @@ class Broker:
|
|
|
212
252
|
price = limit or data.Close[-1]
|
|
213
253
|
if stop_loss < price:
|
|
214
254
|
self.orderQueue[name].append(
|
|
215
|
-
|
|
255
|
+
NewOrder(
|
|
216
256
|
self.__orderId__,
|
|
217
257
|
data.Timestamp[-1],
|
|
218
258
|
OrderType.STOP,
|
|
@@ -231,10 +271,10 @@ class Broker:
|
|
|
231
271
|
price = limit or data.Close[-1]
|
|
232
272
|
if take_profit > price:
|
|
233
273
|
self.orderQueue[name].append(
|
|
234
|
-
|
|
274
|
+
NewOrder(
|
|
235
275
|
self.__orderId__,
|
|
236
276
|
data.Timestamp[-1],
|
|
237
|
-
OrderType.
|
|
277
|
+
OrderType.LIMIT,
|
|
238
278
|
OrderDirection.SELL,
|
|
239
279
|
amount,
|
|
240
280
|
take_profit,
|
|
@@ -265,6 +305,8 @@ class Broker:
|
|
|
265
305
|
stop_loss: The stop loss price to use. If None, no stop loss will be used.
|
|
266
306
|
take_profit: The take profit price to use. If None, no take profit will be used.
|
|
267
307
|
"""
|
|
308
|
+
if amount <= 0:
|
|
309
|
+
raise ValueError("Amount must be greater than 0")
|
|
268
310
|
if name is None:
|
|
269
311
|
name = list(self.__context__.datas.keys())[0]
|
|
270
312
|
data = self.__context__.datas[name]
|
|
@@ -272,7 +314,7 @@ class Broker:
|
|
|
272
314
|
self.__orderId__ += 1
|
|
273
315
|
if limit is not None:
|
|
274
316
|
self.orderQueue[name].append(
|
|
275
|
-
|
|
317
|
+
NewOrder(
|
|
276
318
|
parentId,
|
|
277
319
|
data.Timestamp[-1],
|
|
278
320
|
OrderType.LIMIT,
|
|
@@ -284,7 +326,7 @@ class Broker:
|
|
|
284
326
|
)
|
|
285
327
|
else:
|
|
286
328
|
self.orderQueue[name].append(
|
|
287
|
-
|
|
329
|
+
NewOrder(
|
|
288
330
|
parentId,
|
|
289
331
|
data.Timestamp[-1],
|
|
290
332
|
OrderType.MARKET,
|
|
@@ -298,7 +340,7 @@ class Broker:
|
|
|
298
340
|
price = limit or data.Close[-1]
|
|
299
341
|
if stop_loss > price:
|
|
300
342
|
self.orderQueue[name].append(
|
|
301
|
-
|
|
343
|
+
NewOrder(
|
|
302
344
|
self.__orderId__,
|
|
303
345
|
data.Timestamp[-1],
|
|
304
346
|
OrderType.STOP,
|
|
@@ -317,10 +359,10 @@ class Broker:
|
|
|
317
359
|
price = limit or data.Close[-1]
|
|
318
360
|
if take_profit < price:
|
|
319
361
|
self.orderQueue[name].append(
|
|
320
|
-
|
|
362
|
+
NewOrder(
|
|
321
363
|
self.__orderId__,
|
|
322
364
|
data.Timestamp[-1],
|
|
323
|
-
OrderType.
|
|
365
|
+
OrderType.LIMIT,
|
|
324
366
|
OrderDirection.BUY,
|
|
325
367
|
amount,
|
|
326
368
|
take_profit,
|
|
@@ -342,38 +384,35 @@ class Broker:
|
|
|
342
384
|
"""
|
|
343
385
|
if name is None:
|
|
344
386
|
name = list(self.__context__.datas.keys())[0]
|
|
345
|
-
totalPositionAmount = 0
|
|
346
387
|
if len(self.openPositions[name]) == 0:
|
|
347
388
|
return
|
|
348
|
-
direction = self.openPositions[name][0].direction
|
|
349
389
|
for order in self.openPositions[name]:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
390
|
+
if order.direction == OrderDirection.BUY:
|
|
391
|
+
self.orderQueue[name].append(
|
|
392
|
+
NewOrder(
|
|
393
|
+
self.__orderId__,
|
|
394
|
+
self.__context__.datas[name].Timestamp[-1],
|
|
395
|
+
OrderType.MARKET,
|
|
396
|
+
OrderDirection.SELL,
|
|
397
|
+
order.amount_filled,
|
|
398
|
+
None,
|
|
399
|
+
order.parentId or order.id,
|
|
400
|
+
)
|
|
361
401
|
)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
self.__orderId__ += 1
|
|
403
|
+
elif order.direction == OrderDirection.SELL:
|
|
404
|
+
self.orderQueue[name].append(
|
|
405
|
+
NewOrder(
|
|
406
|
+
self.__orderId__,
|
|
407
|
+
self.__context__.datas[name].Timestamp[-1],
|
|
408
|
+
OrderType.MARKET,
|
|
409
|
+
OrderDirection.BUY,
|
|
410
|
+
order.amount_filled,
|
|
411
|
+
None,
|
|
412
|
+
order.parentId or order.id,
|
|
413
|
+
)
|
|
374
414
|
)
|
|
375
|
-
|
|
376
|
-
self.__orderId__ += 1
|
|
415
|
+
self.__orderId__ += 1
|
|
377
416
|
|
|
378
417
|
def is_long(self, name: str | None = None) -> bool:
|
|
379
418
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|