kissbt 0.1.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
kissbt/broker.py ADDED
@@ -0,0 +1,590 @@
1
+ from datetime import datetime
2
+ from typing import Dict, List, Optional
3
+
4
+ import pandas as pd
5
+
6
+ from kissbt.entities import ClosedPosition, OpenPosition, Order, OrderType
7
+
8
+
9
+ class Broker:
10
+ """
11
+ Manages the portfolio's cash, open and closed positions, and pending orders.
12
+
13
+ This broker class is responsible for tracking capital, applying trading fees, and
14
+ handling order execution for both opening and closing positions. It maintains
15
+ consistency between current and previous market data bars, ensuring correct updates
16
+ for position values, cash, and any accrued expenses such as short fees.
17
+
18
+ The broker interfaces with several other components:
19
+ - Manages OpenPosition and ClosedPosition objects to track trade lifecycles
20
+ - Processes Order objects from Strategy class based on market data
21
+ - Records transaction history for analysis by Analyzer
22
+ - Receives updates from Engine for each market data bar
23
+
24
+ Typical usage:
25
+ broker = Broker(start_capital=100000, fees=0.001)
26
+ broker.place_order(Order("AAPL", 100, OrderType.OPEN))
27
+ broker.update(next_bar, next_datetime)
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ start_capital: float = 100000,
33
+ fees: float = 0.001,
34
+ long_only: bool = True,
35
+ short_fee_rate: float = 0.0050,
36
+ benchmark: Optional[str] = None,
37
+ ):
38
+ """
39
+ Initialize the Broker.
40
+
41
+ Sets up the initial capital, commission fees, and optional benchmark tracking.
42
+ Configures whether broker operates in long-only mode or allows short selling.
43
+ Initializes all internal data structures for managing positions, orders, and
44
+ history.
45
+
46
+ Args:
47
+ start_capital: Initial amount of cash available for trading, defaults to
48
+ 100000.
49
+ fees: Commission or transaction fee applied to each trade, defaults to
50
+ 0.0050.
51
+ long_only: If True, restricts trading to long positions only.
52
+ Defaults to True.
53
+ short_fee_rate: Annual fee rate for maintaining short positions.
54
+ Defaults to 0.0050. Daily rate derived automatically.
55
+ benchmark: Symbol used for performance comparison.
56
+ If provided, broker tracks performance against this symbol.
57
+ """
58
+ self._cash = start_capital
59
+ self._start_capital = start_capital
60
+ self._fees = fees
61
+
62
+ self._open_positions: Dict[str, OpenPosition] = dict()
63
+ self._closed_positions: List[ClosedPosition] = []
64
+ self._open_orders: List[Order] = []
65
+
66
+ self._current_bar: pd.DataFrame = pd.DataFrame()
67
+ self._current_datetime = None
68
+ self._previous_bar: pd.DataFrame = pd.DataFrame()
69
+ self._previous_datetime = None
70
+
71
+ self._long_only = long_only
72
+ self._short_fee_rate = short_fee_rate
73
+ self._daily_short_fee_rate = -1.0 + (1.0 + short_fee_rate) ** (1.0 / 252.0)
74
+
75
+ self._benchmark = benchmark
76
+ self._benchmark_size = 0.0
77
+
78
+ self._history: Dict[str, List[float]] = {
79
+ "date": [],
80
+ "cash": [],
81
+ "long_position_value": [],
82
+ "short_position_value": [],
83
+ "total_value": [],
84
+ "positions": [],
85
+ }
86
+ if benchmark is not None:
87
+ self._history["benchmark"] = []
88
+
89
+ def _update_history(self):
90
+ """
91
+ Updates the history dictionary with the current portfolio state.
92
+ """
93
+ self._history["date"].append(self._current_datetime)
94
+ self._history["cash"].append(self._cash)
95
+ self._history["long_position_value"].append(self.long_position_value)
96
+ self._history["short_position_value"].append(self.short_position_value)
97
+ self._history["total_value"].append(
98
+ self.long_position_value + self.short_position_value + self._cash
99
+ )
100
+ self._history["positions"].append(len(self._open_positions))
101
+ if self._benchmark is not None:
102
+ if len(self._history["benchmark"]) == 0:
103
+ self.benchmark_size = (
104
+ self._start_capital
105
+ / self._current_bar.loc[self._benchmark, "close"]
106
+ * (1.0 + self._fees)
107
+ )
108
+ self._history["benchmark"].append(
109
+ self._current_bar.loc[self._benchmark, "close"]
110
+ * self.benchmark_size
111
+ * (1.0 - self._fees)
112
+ )
113
+
114
+ def _get_price_for_order(self, order: Order, bar: pd.DataFrame) -> float | None:
115
+ """
116
+ Determines the execution price for a given order based on the current market
117
+ data.
118
+
119
+ Args:
120
+ order (Order): The order to be executed, containing information about
121
+ ticker, order type, size, and limit price if applicable.
122
+ bar (pd.DataFrame): Current market data frame containing OHLC prices for the
123
+ ticker.
124
+
125
+ Returns:
126
+ float | None: The execution price for the order. None is returned if:
127
+ - For limit orders: the limit price condition is not met
128
+ - For open/close orders with limits: the limit price condition is not
129
+ met
130
+
131
+ Raises:
132
+ ValueError: If the order type is not recognized (not OPEN, CLOSE, or LIMIT)
133
+
134
+ Notes:
135
+ Price determination rules:
136
+ - For OPEN orders: Uses the opening price
137
+ - For CLOSE orders: Uses the closing price
138
+ - For LIMIT orders:
139
+ - Buy orders: Uses min(open price, limit price) if
140
+ low price <= limit
141
+ - Sell orders: Uses max(open price, limit price) if
142
+ high price >= limit
143
+ """
144
+ ticker = order.ticker
145
+ if order.order_type == OrderType.OPEN or order.order_type == OrderType.CLOSE:
146
+ col = "open" if order.order_type == OrderType.OPEN else "close"
147
+ if order.limit is None:
148
+ return bar.loc[ticker, col]
149
+ else:
150
+ if order.size > 0.0 and bar.loc[ticker, col] <= order.limit:
151
+ return bar.loc[ticker, col]
152
+ elif order.size < 0.0 and bar.loc[ticker, col] >= order.limit:
153
+ return bar.loc[ticker, col]
154
+ else:
155
+ return None
156
+ elif order.order_type == OrderType.LIMIT:
157
+ if order.size > 0.0 and bar.loc[ticker, "low"] <= order.limit:
158
+ return min(bar.loc[ticker, "open"], order.limit)
159
+ elif order.size < 0.0 and bar.loc[ticker, "high"] >= order.limit:
160
+ return max(bar.loc[ticker, "open"], order.limit)
161
+ else:
162
+ return None
163
+ else:
164
+ raise ValueError(f"Unknown order type {order.order_type}")
165
+
166
+ def _update_closed_positions(
167
+ self, ticker: str, size: float, price: float, datetime: datetime
168
+ ):
169
+ """
170
+ Updates the list of closed positions for a given trade.
171
+
172
+ Updates closed positions tracking when a position is fully or partially closed.
173
+ For long positions being closed, records the entry price from open position and
174
+ exit at current price. For short positions being closed, records entry at
175
+ current price and exit at open position price.
176
+
177
+ Args:
178
+ ticker (str): The ticker symbol of the position
179
+ size (float): Position size (positive for long, negative for short)
180
+ price (float): The current closing/reduction price
181
+ datetime (datetime): Timestamp of the closing/reduction
182
+ """
183
+ if (
184
+ ticker in self._open_positions
185
+ and size * self._open_positions[ticker].size < 0.0
186
+ ):
187
+ # if long position is closed/reduced
188
+ if self._open_positions[ticker].size > 0.0:
189
+ self._closed_positions.append(
190
+ ClosedPosition(
191
+ self._open_positions[ticker].ticker,
192
+ min(self._open_positions[ticker].size, abs(size)),
193
+ self._open_positions[ticker].price,
194
+ self._open_positions[ticker].datetime,
195
+ price,
196
+ datetime,
197
+ ),
198
+ )
199
+ # if short position is closed/reduced
200
+ else:
201
+ self._closed_positions.append(
202
+ ClosedPosition(
203
+ self._open_positions[ticker].ticker,
204
+ max(self._open_positions[ticker].size, -size),
205
+ price,
206
+ datetime,
207
+ self._open_positions[ticker].price,
208
+ self._open_positions[ticker].datetime,
209
+ ),
210
+ )
211
+
212
+ def _update_open_positions(
213
+ self, ticker: str, size: float, price: float, datetime: datetime
214
+ ):
215
+ """
216
+ Updates the open positions for a given ticker.
217
+
218
+ If the ticker already exists in the open positions, it updates the size, price,
219
+ and datetime based on the new transaction. If the size of the position becomes
220
+ zero, the position is removed. If the ticker does not exist, a new open position
221
+ is created.
222
+
223
+ Args:
224
+ ticker (str): The ticker symbol of the asset.
225
+ size (float): The size of the position.
226
+ price (float): The price at which the position was opened or updated.
227
+ datetime (datetime): The datetime when the position was opened or updated.
228
+ """
229
+ if ticker in self._open_positions:
230
+ if size + self._open_positions[ticker].size == 0.0:
231
+ self._open_positions.pop(ticker)
232
+ else:
233
+ open_position_size = self._open_positions[ticker].size + size
234
+ open_position_price = price
235
+ open_position_datetime = datetime
236
+
237
+ if size * self._open_positions[ticker].size > 0.0:
238
+ open_position_price = (
239
+ self._open_positions[ticker].size
240
+ * self._open_positions[ticker].price
241
+ + size * price
242
+ ) / (self._open_positions[ticker].size + size)
243
+ open_position_datetime = self._open_positions[ticker].datetime
244
+ elif abs(self._open_positions[ticker].size) > abs(size):
245
+ open_position_datetime = self._open_positions[ticker].datetime
246
+ open_position_price = self._open_positions[ticker].price
247
+ self._open_positions[ticker] = OpenPosition(
248
+ ticker,
249
+ open_position_size,
250
+ open_position_price,
251
+ open_position_datetime,
252
+ )
253
+ else:
254
+ self._open_positions[ticker] = OpenPosition(
255
+ ticker,
256
+ size,
257
+ price,
258
+ datetime,
259
+ )
260
+
261
+ def _update_cash(self, order: Order, price: float):
262
+ """
263
+ Updates the cash balance based on the given order and price, accounting for the
264
+ order size, price, and fees.
265
+
266
+ Args:
267
+ order (Order): The order object containing the size of the order.
268
+ price (float): The price at which the order is executed.
269
+ """
270
+ if order.size > 0.0:
271
+ self._cash -= order.size * price * (1.0 + self._fees)
272
+ else:
273
+ self._cash -= order.size * price * (1.0 - self._fees)
274
+
275
+ def _check_long_only_condition(self, order: Order, datetime: datetime):
276
+ size = order.size
277
+ if order.ticker in self._open_positions:
278
+ size += self._open_positions[order.ticker].size
279
+
280
+ if size < 0.0:
281
+ raise ValueError(
282
+ f"Short selling is not allowed for {order.ticker} on {datetime}."
283
+ )
284
+
285
+ def _execute_order(
286
+ self,
287
+ order: Order,
288
+ bar: pd.DataFrame,
289
+ datetime: datetime,
290
+ ) -> bool:
291
+ """
292
+ Executes an order based on the provided bar data and datetime.
293
+
294
+ Args:
295
+ order (Order): The order to be executed.
296
+ bar (pd.DataFrame): The bar data containing price information.
297
+ datetime (datetime): The datetime at which the order is executed.
298
+
299
+ Returns:
300
+ bool: True if the order was successfully executed, False otherwise.
301
+
302
+ Raises:
303
+ ValueError: If the long-only condition is violated.
304
+ """
305
+ ticker = order.ticker
306
+
307
+ if order.size == 0.0:
308
+ return False
309
+
310
+ if self._long_only:
311
+ self._check_long_only_condition(order, datetime)
312
+
313
+ price = self._get_price_for_order(order, bar)
314
+
315
+ # if the order is a limit order and cannot be filled, return
316
+ if price is None:
317
+ return False
318
+
319
+ # update cash for long and short positions
320
+ self._update_cash(order, price)
321
+
322
+ self._update_closed_positions(ticker, order.size, price, datetime)
323
+
324
+ self._update_open_positions(ticker, order.size, price, datetime)
325
+
326
+ return True
327
+
328
+ def update(
329
+ self,
330
+ next_bar: pd.DataFrame,
331
+ next_datetime: pd.Timestamp,
332
+ ):
333
+ """
334
+ Updates the broker's state with the next trading bar and executes pending
335
+ orders.
336
+
337
+ This method performs several key operations:
338
+ 1. Updates current and previous bar/date references
339
+ 2. Applies short fees for short positions if not in long-only mode
340
+ 3. Sells assets that are no longer in the universe
341
+ 4. Processes pending buy/sell orders
342
+ 5. Updates trading history statistics
343
+
344
+ Args:
345
+ next_bar (pd.DataFrame): The next trading bar data containing at minimum
346
+ 'close' prices for assets
347
+ next_datetime (pd.Timestamp): The timestamp for the next trading bar
348
+
349
+ Notes:
350
+ - Short fees are calculated using the current bar's closing price
351
+ - Assets outside the universe are sold at the previous bar's closing price
352
+ - Orders that cannot be executed due to missing data are skipped with a
353
+ warning
354
+ - Good-till-cancel orders that aren't filled are retained for the next bar
355
+ """
356
+ self._previous_bar = self._current_bar
357
+ self._previous_datetime = self._current_datetime
358
+ self._current_bar = next_bar
359
+ self._current_datetime = next_datetime
360
+
361
+ # consider short fees
362
+ if not self._long_only:
363
+ for ticker in self._open_positions.keys():
364
+ if self._open_positions[ticker].size < 0.0:
365
+ price = (
366
+ self._current_bar.loc[ticker, "close"]
367
+ if ticker in self._current_bar.index
368
+ else self._previous_bar.loc[ticker, "close"]
369
+ )
370
+ self._cash += (
371
+ self._open_positions[ticker].size
372
+ * price
373
+ * self._daily_short_fee_rate
374
+ )
375
+
376
+ # sell assets out of universe, we use close price of previous bar, since this is
377
+ # the last price we know
378
+ ticker_out_of_universe = set()
379
+ if self._previous_bar.empty:
380
+ ticker_out_of_universe = set(self._open_positions.keys()) - set(
381
+ self._current_bar.index
382
+ )
383
+ for ticker in ticker_out_of_universe:
384
+ self._execute_order(
385
+ Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
386
+ self._previous_bar,
387
+ self._previous_datetime,
388
+ )
389
+
390
+ # buy and sell assets
391
+ remaining_open_orders = []
392
+ ticker_not_available = set(
393
+ [open_order.ticker for open_order in self._open_orders]
394
+ ) - set(self._current_bar.index)
395
+ for open_order in self._open_orders:
396
+ if open_order.ticker in ticker_not_available:
397
+ if open_order.size > 0:
398
+ print(
399
+ f"{open_order.ticker} could not be bought on {self._current_datetime}." # noqa: E501
400
+ )
401
+ else:
402
+ print(
403
+ f"{open_order.ticker} could not be sold on {self._current_datetime}." # noqa: E501
404
+ )
405
+ continue
406
+ if (
407
+ not self._execute_order(
408
+ open_order, self._current_bar, self._current_datetime
409
+ )
410
+ and open_order.good_till_cancel
411
+ ):
412
+ remaining_open_orders.append(open_order)
413
+
414
+ # Retain orders that are good till cancel and were not filled for the next bar
415
+ self._open_orders = remaining_open_orders
416
+
417
+ # update stats
418
+ self._update_history()
419
+
420
+ def liquidate_positions(self):
421
+ """
422
+ Close all open positions in the broker by executing CLOSE orders.
423
+
424
+ The method iterates through all open positions and creates close orders with
425
+ opposite size to the current position size, effectively liquidating all
426
+ holdings.
427
+ All orders are executed at the current bar's timestamp.
428
+
429
+ No parameters are needed as it operates on the broker's internal state.
430
+
431
+ Returns:
432
+ None
433
+ """
434
+ for ticker in [
435
+ ticker for ticker in self._open_positions.keys()
436
+ ]: # open_positions is modified during iteration
437
+ self._execute_order(
438
+ Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
439
+ self._current_bar,
440
+ self._current_datetime,
441
+ )
442
+
443
+ def place_order(self, order: Order):
444
+ """
445
+ Places a new order in the broker's open orders list.
446
+
447
+ Args:
448
+ order (Order): The order object to be placed in the open orders.
449
+
450
+ Note:
451
+ The order is appended to the internal _open_orders list and remains there
452
+ until it is either executed or cancelled.
453
+ """
454
+ self._open_orders.append(order)
455
+
456
+ @property
457
+ def short_position_value(self) -> float:
458
+ """
459
+ Gets the total market value of all short positions including fees.
460
+
461
+ Calculates the sum of (price * quantity) for all positions with negative size,
462
+ using the most recent closing price available. Transaction fees are included
463
+ in the final value.
464
+
465
+ Returns:
466
+ float: Total value of short positions (negative number), including fees.
467
+ """
468
+ value = 0.0
469
+ for ticker in self._open_positions.keys():
470
+ if self._open_positions[ticker].size < 0.0:
471
+ price = (
472
+ self._current_bar.loc[ticker, "close"]
473
+ if ticker in self._current_bar.index
474
+ else self._previous_bar.loc[ticker, "close"]
475
+ )
476
+ value += price * self._open_positions[ticker].size * (1.0 + self._fees)
477
+ return value
478
+
479
+ @property
480
+ def long_position_value(self) -> float:
481
+ """
482
+ Gets the total market value of all long positions after fees.
483
+
484
+ Calculates the sum of (price * quantity) for all positions with positive size,
485
+ using the most recent closing price available. Transaction fees are deducted
486
+ from the final value to reflect net liquidation value.
487
+
488
+ Returns:
489
+ float: Total value of long positions (positive number), net of fees.
490
+ """
491
+ value = 0.0
492
+ for ticker in self._open_positions.keys():
493
+ if self._open_positions[ticker].size > 0.0:
494
+ price = (
495
+ self._current_bar.loc[ticker, "close"]
496
+ if ticker in self._current_bar.index
497
+ else self._previous_bar.loc[ticker, "close"]
498
+ )
499
+ value += price * self._open_positions[ticker].size * (1.0 - self._fees)
500
+ return value
501
+
502
+ @property
503
+ def portfolio_value(self) -> float:
504
+ """
505
+ Gets the total portfolio value including cash and all positions.
506
+
507
+ Calculates net portfolio value by summing:
508
+ - Available cash balance
509
+ - Long positions market value (less fees)
510
+ - Short positions market value (including fees)
511
+
512
+ Returns:
513
+ float: Total portfolio value in base currency.
514
+ """
515
+ return self._cash + self.long_position_value + self.short_position_value
516
+
517
+ @property
518
+ def open_positions(self) -> Dict[str, OpenPosition]:
519
+ """Gets a dictionary of currently active trading positions.
520
+
521
+ Maps ticker symbols to OpenPosition objects containing:
522
+ - ticker: Financial instrument identifier
523
+ - size: Position size (positive=long, negative=short)
524
+ - price: Average entry price
525
+ - datetime: Position opening timestamp
526
+
527
+ Returns:
528
+ Dict[str, OpenPosition]: Dictionary mapping ticker symbols to positions.
529
+ Returns a defensive copy to prevent external modifications.
530
+ """
531
+ return self._open_positions.copy()
532
+
533
+ @property
534
+ def closed_positions(self) -> List[ClosedPosition]:
535
+ """
536
+ Gets a list of all completed trades.
537
+
538
+ Each ClosedPosition contains the complete trade lifecycle:
539
+ - Entry price and timestamp
540
+ - Exit price and timestamp
541
+ - Position size and direction
542
+ - Ticker symbol
543
+
544
+ Returns a defensive copy to prevent external modifications.
545
+
546
+ Returns:
547
+ List[ClosedPosition]: Chronological list of completed trades.
548
+ """
549
+ return self._closed_positions.copy()
550
+
551
+ @property
552
+ def history(self) -> Dict[str, List[float]]:
553
+ """Gets the historical performance metrics dictionary.
554
+
555
+ Contains time series data tracking portfolio metrics:
556
+ - 'cash': Available cash balance
557
+ - 'portfolio': Total portfolio value
558
+ - 'short_value': Value of short positions
559
+ - 'long_value': Value of long positions
560
+ - 'benchmark': Benchmark performance if specified
561
+
562
+ Returns:
563
+ Dict[str, List[float]]: Dictionary mapping metric names to value histories.
564
+ Returns a defensive copy to prevent external modifications.
565
+ """
566
+ return self._history.copy()
567
+
568
+ @property
569
+ def cash(self) -> float:
570
+ """Gets the current available cash balance.
571
+
572
+ Represents uninvested capital that can be used for new positions. Updated after
573
+ each trade to reflect fees and position changes.
574
+
575
+ Returns:
576
+ float: Available cash balance in base currency.
577
+ """
578
+ return self._cash
579
+
580
+ @property
581
+ def benchmark(self) -> str:
582
+ """Gets the benchmark symbol used for performance comparison.
583
+
584
+ The benchmark tracks a reference asset (e.g., market index) to evaluate relative
585
+ strategy performance. Returns None if no benchmark was specified.
586
+
587
+ Returns:
588
+ str: Ticker symbol of the benchmark instrument.
589
+ """
590
+ return self._benchmark
kissbt/engine.py ADDED
@@ -0,0 +1,29 @@
1
+ import pandas as pd
2
+
3
+ from kissbt.broker import Broker
4
+ from kissbt.strategy import Strategy
5
+
6
+
7
+ class Engine:
8
+ """Coordinates execution of a trading strategy using broker actions.
9
+
10
+ This class drives the main loop that processes market data, updates the
11
+ broker's state, and calls the strategy logic for each segment of data.
12
+
13
+ Args:
14
+ broker (Broker): The Broker instance for managing trades and positions.
15
+ strategy (Strategy): The trading strategy to be applied to the data.
16
+ """
17
+
18
+ def __init__(self, broker: Broker, strategy: Strategy) -> None:
19
+ self.broker = broker
20
+ self.strategy = strategy
21
+
22
+ def run(self, data: pd.DataFrame) -> None:
23
+ for current_date, current_data in data.groupby("date"):
24
+ current_data.index = current_data.index.droplevel("date")
25
+
26
+ self.broker.update(current_data, current_date)
27
+ self.strategy(current_data, current_date)
28
+
29
+ self.broker.liquidate_positions()
kissbt/entities.py ADDED
@@ -0,0 +1,70 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ import pandas as pd
5
+
6
+
7
+ class OrderType(Enum):
8
+ OPEN = "open"
9
+ CLOSE = "close"
10
+ LIMIT = "limit"
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Order:
15
+ """
16
+ Trading order representation. Immutable to ensure data integrity and thread safety.
17
+
18
+ Args:
19
+ ticker (str): Ticker symbol of the traded asset
20
+ size (float): Order size (positive for buy, negative for sell)
21
+ order_type (OrderType, optional): Order type, defaults to OPEN
22
+ limit (float | None, optional): Limit price if applicable, defaults to None
23
+ good_till_cancel (bool, optional): Order validity flag, defaults to False
24
+ """
25
+
26
+ ticker: str
27
+ size: float
28
+ order_type: OrderType = OrderType.OPEN
29
+ limit: float | None = None
30
+ good_till_cancel: bool = False
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class OpenPosition:
35
+ """
36
+ Immutable representation of an open trading position.
37
+
38
+ Args:
39
+ ticker (str): Financial instrument identifier
40
+ size (float): Position size (positive for long, negative for short)
41
+ price (float): Opening price of the position
42
+ datetime (datetime): Position opening timestamp
43
+ """
44
+
45
+ ticker: str
46
+ size: float
47
+ price: float
48
+ datetime: pd.Timestamp
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ClosedPosition:
53
+ """
54
+ Immutable representation of a completed trading transaction.
55
+
56
+ Args:
57
+ ticker (str): Financial instrument identifier
58
+ size (float): Position size (positive for long, negative for short)
59
+ purchase_price (float): Entry price of the position
60
+ purchase_datetime (pd.Timestamp): Position entry timestamp
61
+ selling_price (float): Exit price of the position
62
+ selling_datetime (pd.Timestamp): Position exit timestamp
63
+ """
64
+
65
+ ticker: str
66
+ size: float
67
+ purchase_price: float
68
+ purchase_datetime: pd.Timestamp
69
+ selling_price: float
70
+ selling_datetime: pd.Timestamp