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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantex
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A simple quant strategy creation and backtesting package.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantex"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -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 Order:
19
+ class NewOrder:
20
20
  id: int
21
- timestamp: pd.Timestamp
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[Order]] ## Orders to be processed
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: Order, price: float) -> bool:
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: Order, name: str):
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].Close[-1]
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].amount
82
+ open_amount = self.openPositions[name][i].amount_filled
75
83
  closed_amount = min(open_amount, amount)
76
- self.openPositions[name][i].amount -= closed_amount
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
- idx = 0
81
- for i in range(len(self.orderQueue[name])):
82
- if self.orderQueue[name][i].id == order.id:
83
- continue
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
- idx = i
88
- break
89
- self.orderQueue[name].pop(
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.amount > 0:
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.timestamp,
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
- order
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].Close[-1]
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(order)
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.amount * prices[-1]
199
+ equity += order.amount_filled * prices[-1]
160
200
  else:
161
- equity -= order.amount * prices[-1]
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
- Order(
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
- Order(
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
- Order(
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
- Order(
274
+ NewOrder(
235
275
  self.__orderId__,
236
276
  data.Timestamp[-1],
237
- OrderType.STOP,
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
- Order(
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
- Order(
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
- Order(
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
- Order(
362
+ NewOrder(
321
363
  self.__orderId__,
322
364
  data.Timestamp[-1],
323
- OrderType.STOP,
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
- totalPositionAmount += order.amount
351
- if direction == OrderDirection.BUY:
352
- self.orderQueue[name].append(
353
- Order(
354
- self.__orderId__,
355
- self.__context__.datas[name].Timestamp[-1],
356
- OrderType.MARKET,
357
- OrderDirection.SELL,
358
- totalPositionAmount,
359
- None,
360
- None,
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
- self.__orderId__ += 1
364
- else:
365
- self.orderQueue[name].append(
366
- Order(
367
- self.__orderId__,
368
- self.__context__.datas[name].Timestamp[-1],
369
- OrderType.MARKET,
370
- OrderDirection.BUY,
371
- totalPositionAmount,
372
- None,
373
- None,
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