doribt 0.2.0__py3-none-any.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.
Files changed (53) hide show
  1. doribt/__init__.py +69 -0
  2. doribt/accounting/__init__.py +1 -0
  3. doribt/accounting/account.py +91 -0
  4. doribt/accounting/costs.py +32 -0
  5. doribt/accounting/orders.py +162 -0
  6. doribt/accounting/rights.py +239 -0
  7. doribt/accounting/taxes.py +170 -0
  8. doribt/kernels/__init__.py +1 -0
  9. doribt/kernels/compiled.py +93 -0
  10. doribt/kernels/execution.py +193 -0
  11. doribt/kernels/segments.py +79 -0
  12. doribt/market/__init__.py +1 -0
  13. doribt/market/actions.py +77 -0
  14. doribt/market/adjustments.py +88 -0
  15. doribt/market/bars.py +133 -0
  16. doribt/market/china.py +128 -0
  17. doribt/market/clock.py +56 -0
  18. doribt/market/data.py +269 -0
  19. doribt/market/instruments.py +24 -0
  20. doribt/market/rules.py +102 -0
  21. doribt/provenance.py +198 -0
  22. doribt/py.typed +0 -0
  23. doribt/reporting/__init__.py +1 -0
  24. doribt/reporting/analysis.py +159 -0
  25. doribt/reporting/benchmark.py +55 -0
  26. doribt/reporting/daily.py +94 -0
  27. doribt/reporting/export.py +224 -0
  28. doribt/reporting/plotting.py +97 -0
  29. doribt/reporting/publish.py +28 -0
  30. doribt/reporting/report.py +131 -0
  31. doribt/reporting/result.py +194 -0
  32. doribt/reporting/trades.py +186 -0
  33. doribt/research/__init__.py +1 -0
  34. doribt/research/config.py +128 -0
  35. doribt/research/outputs.py +176 -0
  36. doribt/research/parameters.py +122 -0
  37. doribt/runtime/__init__.py +1 -0
  38. doribt/runtime/bar_runtime.py +118 -0
  39. doribt/runtime/bar_targets.py +72 -0
  40. doribt/runtime/broker.py +297 -0
  41. doribt/runtime/context.py +213 -0
  42. doribt/runtime/engine.py +353 -0
  43. doribt/runtime/intents.py +75 -0
  44. doribt/runtime/matching.py +167 -0
  45. doribt/runtime/scheduled.py +131 -0
  46. doribt/runtime/slippage.py +53 -0
  47. doribt/runtime/targets.py +107 -0
  48. doribt/serialization.py +44 -0
  49. doribt/validation.py +50 -0
  50. doribt-0.2.0.dist-info/METADATA +107 -0
  51. doribt-0.2.0.dist-info/RECORD +53 -0
  52. doribt-0.2.0.dist-info/WHEEL +4 -0
  53. doribt-0.2.0.dist-info/licenses/LICENSE +202 -0
doribt/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """DoriBT: A-share backtesting with explicit data, rules and accounting."""
2
+
3
+ from doribt.accounting.costs import Costs
4
+ from doribt.accounting.orders import Fill, IntentRecord, Order, Reason
5
+ from doribt.accounting.rights import CorporateEvent, EntitlementRecord, UnsupportedCorporateAction
6
+ from doribt.accounting.taxes import TaxLotRecord, TaxPayment, TaxRecord
7
+ from doribt.market.actions import CorporateAction
8
+ from doribt.market.adjustments import PriceAdjustment
9
+ from doribt.market.china import china_rules
10
+ from doribt.market.clock import MinuteClock
11
+ from doribt.market.data import MarketData
12
+ from doribt.market.instruments import Instrument, TradingStatus
13
+ from doribt.market.rules import RuleBook, RulePeriod, TradingRule
14
+ from doribt.provenance import RunInfo
15
+ from doribt.reporting.benchmark import Benchmark
16
+ from doribt.reporting.report import ResearchReport
17
+ from doribt.reporting.result import BacktestResult
18
+ from doribt.research.config import RunConfig
19
+ from doribt.research.outputs import Metric, ResearchOutput, Series, Table
20
+ from doribt.research.parameters import Parameter, ParameterSet
21
+ from doribt.runtime.context import Context
22
+ from doribt.runtime.engine import Backtest
23
+ from doribt.runtime.slippage import BarExecution, FixedBps, FixedTicks, VolumeImpact
24
+ from doribt.runtime.targets import PositionTargets, WeightTargets
25
+
26
+ __all__ = [
27
+ "MinuteClock",
28
+ "RunConfig",
29
+ "Parameter",
30
+ "ParameterSet",
31
+ "Metric",
32
+ "Series",
33
+ "Table",
34
+ "ResearchOutput",
35
+ "ResearchReport",
36
+ "Backtest",
37
+ "BarExecution",
38
+ "FixedBps",
39
+ "FixedTicks",
40
+ "VolumeImpact",
41
+ "BacktestResult",
42
+ "Benchmark",
43
+ "RunInfo",
44
+ "Context",
45
+ "Costs",
46
+ "WeightTargets",
47
+ "PositionTargets",
48
+ "CorporateAction",
49
+ "PriceAdjustment",
50
+ "china_rules",
51
+ "CorporateEvent",
52
+ "EntitlementRecord",
53
+ "UnsupportedCorporateAction",
54
+ "TaxLotRecord",
55
+ "TaxPayment",
56
+ "TaxRecord",
57
+ "Instrument",
58
+ "MarketData",
59
+ "RuleBook",
60
+ "RulePeriod",
61
+ "TradingRule",
62
+ "TradingStatus",
63
+ "Order",
64
+ "Fill",
65
+ "IntentRecord",
66
+ "Reason",
67
+ ]
68
+
69
+ __version__ = "0.2.0"
@@ -0,0 +1 @@
1
+ """DoriBT accounting implementation; public API is exported by doribt."""
@@ -0,0 +1,91 @@
1
+ """FIFO acquisition lots and trading-session settlement state."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date
5
+
6
+ import numpy as np
7
+
8
+ from doribt.accounting.orders import Order
9
+ from doribt.kernels.execution import IntArray
10
+ from doribt.validation import MAX_MONEY
11
+
12
+
13
+ @dataclass
14
+ class Lot:
15
+ symbol: str
16
+ quantity: int
17
+ acquired: date
18
+ available_session: int
19
+ price_units: int
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Position:
24
+ symbol: str
25
+ quantity: int
26
+ sellable: int
27
+ value: float
28
+ pending_quantity: int = 0
29
+ frozen_quantity: int = 0
30
+
31
+ @property
32
+ def available_quantity(self) -> int:
33
+ return max(0, self.sellable - self.frozen_quantity)
34
+
35
+
36
+ class Account:
37
+ def __init__(self, initial_cash: int, symbols: tuple[str, ...]) -> None:
38
+ self.cash = initial_cash
39
+ self.symbols = symbols
40
+ self.lots: list[Lot] = []
41
+
42
+ def quantities(self, index: int | None = None) -> IntArray:
43
+ totals = dict.fromkeys(self.symbols, 0)
44
+ for lot in self.lots:
45
+ if index is None or lot.available_session <= index:
46
+ totals[lot.symbol] += lot.quantity
47
+ if any(quantity > 1_000_000_000 for quantity in totals.values()):
48
+ raise OverflowError("registered share quantity exceeds supported bounds")
49
+ return np.array(list(totals.values()), dtype=np.int64)
50
+
51
+ def apply(self, order: Order, index: int, settlement: int) -> None:
52
+ if order.filled > 0:
53
+ self.lots.append(
54
+ Lot(
55
+ order.symbol, order.filled, order.session, index + settlement, order.price_units
56
+ )
57
+ )
58
+ elif order.filled < 0:
59
+ self._sell(order.symbol, -order.filled, index)
60
+
61
+ def _sell(self, symbol: str, quantity: int, index: int) -> None:
62
+ for lot in self.lots:
63
+ if lot.symbol != symbol or lot.available_session > index:
64
+ continue
65
+ consumed = min(quantity, lot.quantity)
66
+ lot.quantity -= consumed
67
+ quantity -= consumed
68
+ self.lots = [lot for lot in self.lots if lot.quantity]
69
+ if quantity:
70
+ raise RuntimeError("execution sold more than settled lots")
71
+
72
+ def value(
73
+ self,
74
+ closes: IntArray,
75
+ pending_shares: IntArray | None = None,
76
+ receivable: int = 0,
77
+ tax_payable: int = 0,
78
+ ) -> int:
79
+ equity = self.cash + receivable - tax_payable
80
+ quantities = self.quantities()
81
+ if pending_shares is not None:
82
+ quantities = quantities + pending_shares
83
+ if np.any(quantities > 1_000_000_000):
84
+ raise OverflowError("economic share quantity exceeds supported bounds")
85
+ for quantity, price in zip(quantities, closes, strict=True):
86
+ if quantity and price <= 0:
87
+ raise ValueError("held security has no active valuation; unsupported delisting")
88
+ equity += int(quantity) * int(price)
89
+ if not 0 <= equity <= MAX_MONEY:
90
+ raise OverflowError("account equity exceeds supported bounds")
91
+ return equity
@@ -0,0 +1,32 @@
1
+ """Account-specific execution assumptions, separate from historical market rules."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+
7
+ from doribt.kernels.execution import IntArray
8
+ from doribt.validation import Number, amount, integer, ratio
9
+
10
+
11
+ @dataclass(frozen=True, kw_only=True)
12
+ class Costs:
13
+ commission: Number = "0.0003"
14
+ minimum_commission: Number = "5.00"
15
+ slippage_ticks: int = 0
16
+
17
+ def __post_init__(self) -> None:
18
+ ratio(self.commission, "commission")
19
+ minimum = amount(self.minimum_commission, "minimum_commission")
20
+ if minimum % 100:
21
+ raise ValueError("minimum commission must use whole cents")
22
+ integer(self.slippage_ticks, 1, "slippage_ticks", 0, 1000)
23
+
24
+ def compile(self) -> IntArray:
25
+ return np.array(
26
+ [
27
+ ratio(self.commission),
28
+ amount(self.minimum_commission),
29
+ self.slippage_ticks,
30
+ ],
31
+ dtype=np.int64,
32
+ )
@@ -0,0 +1,162 @@
1
+ """Immutable public order history. All money fields are integer 1/10,000 yuan."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import date, datetime
5
+ from enum import StrEnum
6
+
7
+
8
+ class Reason(StrEnum):
9
+ NONE = "none"
10
+ SUSPENDED = "suspended"
11
+ INACTIVE = "inactive"
12
+ BUY_AT_UPPER_LIMIT = "buy_at_upper_limit"
13
+ SELL_AT_LOWER_LIMIT = "sell_at_lower_limit"
14
+ NO_VOLUME = "no_volume"
15
+ INVALID_QUANTITY = "invalid_quantity"
16
+ INSUFFICIENT_CASH = "insufficient_cash"
17
+ INSUFFICIENT_SELLABLE = "insufficient_sellable"
18
+ INSUFFICIENT_POSITION = "insufficient_position"
19
+ PRICE_OUT_OF_RANGE = "price_out_of_range"
20
+ PARTICIPATION_LIMIT = "participation_limit"
21
+ LIMIT_PRICE = "limit_price"
22
+ AUCTION = "auction"
23
+
24
+
25
+ REASONS = tuple(Reason)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Order:
30
+ order_id: int
31
+ intent_id: int
32
+ session: date
33
+ symbol: str
34
+ quantity: int
35
+ filled: int
36
+ price_units: int
37
+ commission_units: int
38
+ stamp_duty_units: int
39
+ transfer_fee_units: int
40
+ reason: Reason
41
+ created_at: date | None = None
42
+ updated_at: date | None = None
43
+ state: str = ""
44
+ notional_units: int = 0
45
+ valid_for: str = "next_bar"
46
+ limit_units: int = 0
47
+ frozen_cash_units: int = 0
48
+ frozen_quantity: int = 0
49
+
50
+ @property
51
+ def commission(self) -> float:
52
+ return self.commission_units / 10_000
53
+
54
+ @property
55
+ def stamp_duty(self) -> float:
56
+ return self.stamp_duty_units / 10_000
57
+
58
+ @property
59
+ def transfer_fee(self) -> float:
60
+ return self.transfer_fee_units / 10_000
61
+
62
+ @property
63
+ def events(self) -> tuple[str, ...]:
64
+ if self.state:
65
+ middle = ("partially_filled",) if self.filled and self.remaining else ()
66
+ return ("created", "accepted", *middle, self.state)
67
+ if self.reason == Reason.INVALID_QUANTITY:
68
+ return ("created", "rejected")
69
+ if self.filled == self.quantity:
70
+ return ("created", "accepted", "filled")
71
+ if self.filled:
72
+ return ("created", "accepted", "partially_filled", "expired")
73
+ return ("created", "accepted", "expired")
74
+
75
+ @property
76
+ def fees(self) -> float:
77
+ return (self.commission_units + self.stamp_duty_units + self.transfer_fee_units) / 10_000
78
+
79
+ @property
80
+ def status(self) -> str:
81
+ return self.events[-1]
82
+
83
+ @property
84
+ def remaining(self) -> int:
85
+ return self.quantity - self.filled
86
+
87
+ @property
88
+ def price(self) -> float | None:
89
+ if self.state and self.filled:
90
+ return self.notional_units / abs(self.filled) / 10_000
91
+ return self.price_units / 10_000 if self.filled else None
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class Fill:
96
+ fill_id: int
97
+ order_id: int
98
+ intent_id: int
99
+ session: date
100
+ symbol: str
101
+ quantity: int
102
+ price_units: int
103
+ commission_units: int
104
+ stamp_duty_units: int
105
+ transfer_fee_units: int
106
+ timestamp: datetime | None = None
107
+
108
+ @property
109
+ def commission(self) -> float:
110
+ return self.commission_units / 10_000
111
+
112
+ @property
113
+ def stamp_duty(self) -> float:
114
+ return self.stamp_duty_units / 10_000
115
+
116
+ @property
117
+ def transfer_fee(self) -> float:
118
+ return self.transfer_fee_units / 10_000
119
+
120
+ @property
121
+ def price(self) -> float:
122
+ return self.price_units / 10_000
123
+
124
+ @property
125
+ def fees(self) -> float:
126
+ return (self.commission_units + self.stamp_duty_units + self.transfer_fee_units) / 10_000
127
+
128
+ @classmethod
129
+ def from_order(cls, fill_id: int, order: Order) -> "Fill":
130
+ return cls(
131
+ fill_id,
132
+ order.order_id,
133
+ order.intent_id,
134
+ order.session,
135
+ order.symbol,
136
+ order.filled,
137
+ order.price_units,
138
+ order.commission_units,
139
+ order.stamp_duty_units,
140
+ order.transfer_fee_units,
141
+ )
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class TargetAdjustment:
146
+ action_id: str
147
+ session: date
148
+ before: int
149
+ after: int
150
+
151
+
152
+ @dataclass(frozen=True)
153
+ class IntentRecord:
154
+ intent_id: int
155
+ created: date
156
+ symbol: str
157
+ kind: str
158
+ quantity: int
159
+ status: str
160
+ closed: date | None
161
+ reason: str
162
+ adjustments: tuple[TargetAdjustment, ...] = ()
@@ -0,0 +1,239 @@
1
+ """Record-date ownership, receivables, share credit and subsequent availability."""
2
+
3
+ from bisect import bisect_left
4
+ from dataclasses import dataclass
5
+ from datetime import date
6
+
7
+ import numpy as np
8
+
9
+ from doribt.accounting.account import Account, Lot
10
+ from doribt.accounting.taxes import TaxBook
11
+ from doribt.kernels.execution import IntArray
12
+ from doribt.market.actions import CorporateAction
13
+ from doribt.market.data import MarketData
14
+ from doribt.runtime.intents import IntentBook
15
+ from doribt.validation import MAX_MONEY, day, integer
16
+
17
+
18
+ class UnsupportedCorporateAction(ValueError):
19
+ pass
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CorporateEvent:
24
+ session: date
25
+ action_id: str
26
+ symbol: str
27
+ kind: str
28
+ quantity: int = 0
29
+ amount_units: int = 0
30
+
31
+
32
+ @dataclass
33
+ class Entitlement:
34
+ action: CorporateAction
35
+ quantity: int
36
+ cash_units: int
37
+ bonus_quantity: int
38
+ accrued: bool = False
39
+ cash_paid: bool = False
40
+ shares_credited: bool = False
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class EntitlementRecord:
45
+ action_id: str
46
+ symbol: str
47
+ record_date: date
48
+ ex_date: date
49
+ pay_date: date | None
50
+ share_credit_date: date | None
51
+ share_listing_date: date | None
52
+ eligible_quantity: int
53
+ cash_units: int
54
+ bonus_quantity: int
55
+ accrued: bool
56
+ cash_paid: bool
57
+ shares_credited: bool
58
+
59
+
60
+ class RightsBook:
61
+ def __init__(self, data: MarketData) -> None:
62
+ self.data = data
63
+ self.kinds = {item.symbol: item.kind for item in data.instruments}
64
+ self.tax = TaxBook(self.kinds)
65
+ self.entitlements: dict[str, Entitlement] = {}
66
+ self.events: list[CorporateEvent] = []
67
+ self._record: dict[date, list[CorporateAction]] = {}
68
+ self._ex: dict[date, list[CorporateAction]] = {}
69
+ for action in data.actions:
70
+ self._record.setdefault(day(action.record_date), []).append(action)
71
+ self._ex.setdefault(day(action.ex_date), []).append(action)
72
+
73
+ def start(self, index: int, account: Account, intents: IntentBook) -> None:
74
+ session = self.data.sessions[index]
75
+ for action in self._ex.get(session, []):
76
+ if action.kind != "distribution" and action.symbol in intents.pending:
77
+ raise UnsupportedCorporateAction(f"unsupported {action.kind}: {action.action_id}")
78
+ self._ex_date(action, session, account)
79
+ bonus = integer(action.bonus_per_share, 1_000_000, "bonus", 0, 100_000_000)
80
+ intents.adjust(action.symbol, action.action_id, session, bonus)
81
+ for entitlement in self.entitlements.values():
82
+ if not entitlement.accrued:
83
+ continue
84
+ self._pay(entitlement, session, account)
85
+ self._credit(entitlement, index, account)
86
+ account.cash = self.tax.settle(session, account.cash)
87
+
88
+ def close(self, index: int, account: Account) -> None:
89
+ session = self.data.sessions[index]
90
+ quantities = dict(zip(self.data.symbols, map(int, account.quantities()), strict=True))
91
+ self.tax.reconcile(session, quantities)
92
+ for action in self._record.get(session, []):
93
+ quantity = quantities[action.symbol]
94
+ if quantity:
95
+ self._capture(action, quantity, session)
96
+
97
+ def _capture(self, action: CorporateAction, quantity: int, session: date) -> None:
98
+ cash_micros = integer(action.cash_per_share, 1_000_000, "cash per share", 0, MAX_MONEY)
99
+ bonus = integer(action.bonus_per_share, 1_000_000, "bonus per share", 0, 100_000_000)
100
+ if quantity * bonus % 1_000_000:
101
+ raise UnsupportedCorporateAction(
102
+ f"fractional share allocation unavailable: {action.action_id}"
103
+ )
104
+ cash = (quantity * cash_micros + 5000) // 10_000 * 100
105
+ if cash > MAX_MONEY:
106
+ raise OverflowError("dividend entitlement exceeds supported bounds")
107
+ entitlement = Entitlement(
108
+ action,
109
+ quantity,
110
+ cash,
111
+ quantity * bonus // 1_000_000,
112
+ cash_paid=not bool(cash),
113
+ shares_credited=not bool(bonus),
114
+ )
115
+ self.entitlements[action.action_id] = entitlement
116
+ if action.kind == "distribution":
117
+ taxable_bonus = self._taxable_bonus(action, bonus)
118
+ self.tax.attach(action.symbol, action.action_id, cash_micros + taxable_bonus, session)
119
+ self.events.append(
120
+ CorporateEvent(session, action.action_id, action.symbol, "recorded", quantity)
121
+ )
122
+
123
+ def _taxable_bonus(self, action: CorporateAction, bonus: int) -> int:
124
+ value = action.taxable_bonus_amount_per_share
125
+ if bonus and value is None and self.kinds[action.symbol] == "stock":
126
+ raise ValueError(f"taxable bonus amount is required: {action.action_id}")
127
+ return integer(value or 0, 1_000_000, "taxable bonus", 0, MAX_MONEY)
128
+
129
+ def _ex_date(self, action: CorporateAction, session: date, account: Account) -> None:
130
+ entitlement = self.entitlements.get(action.action_id)
131
+ held = int(account.quantities()[self.data.symbols.index(action.symbol)])
132
+ pending = any(
133
+ e.action.symbol == action.symbol
134
+ and e.accrued
135
+ and (not e.shares_credited or not e.cash_paid)
136
+ for e in self.entitlements.values()
137
+ )
138
+ if action.kind != "distribution" and (entitlement is not None or held or pending):
139
+ raise UnsupportedCorporateAction(f"unsupported {action.kind}: {action.action_id}")
140
+ if entitlement is not None:
141
+ entitlement.accrued = True
142
+ self.events.append(
143
+ CorporateEvent(
144
+ session,
145
+ action.action_id,
146
+ action.symbol,
147
+ "accrued",
148
+ entitlement.bonus_quantity,
149
+ entitlement.cash_units,
150
+ )
151
+ )
152
+
153
+ def _pay(self, entitlement: Entitlement, session: date, account: Account) -> None:
154
+ action = entitlement.action
155
+ if entitlement.cash_paid or action.pay_date is None or session < day(action.pay_date):
156
+ return
157
+ account.cash += entitlement.cash_units
158
+ if account.cash > MAX_MONEY:
159
+ raise OverflowError("dividend payment exceeds supported cash bounds")
160
+ entitlement.cash_paid = True
161
+ self.events.append(
162
+ CorporateEvent(
163
+ session,
164
+ action.action_id,
165
+ action.symbol,
166
+ "cash_paid",
167
+ amount_units=entitlement.cash_units,
168
+ )
169
+ )
170
+
171
+ def _credit(self, entitlement: Entitlement, index: int, account: Account) -> None:
172
+ action, session = entitlement.action, self.data.sessions[index]
173
+ if entitlement.shares_credited or action.share_credit_date is None:
174
+ return
175
+ if session < day(action.share_credit_date):
176
+ return
177
+ if action.share_listing_date is None:
178
+ raise RuntimeError("validated bonus shares require a listing date")
179
+ available = bisect_left(self.data.sessions, day(action.share_listing_date))
180
+ account.lots.append(
181
+ Lot(
182
+ action.symbol,
183
+ entitlement.bonus_quantity,
184
+ day(action.share_credit_date),
185
+ available,
186
+ 0,
187
+ )
188
+ )
189
+ entitlement.shares_credited = True
190
+ self.events.append(
191
+ CorporateEvent(
192
+ session,
193
+ action.action_id,
194
+ action.symbol,
195
+ "shares_credited",
196
+ entitlement.bonus_quantity,
197
+ )
198
+ )
199
+
200
+ @property
201
+ def receivable(self) -> int:
202
+ total = sum(
203
+ e.cash_units for e in self.entitlements.values() if e.accrued and not e.cash_paid
204
+ )
205
+ if total > MAX_MONEY:
206
+ raise OverflowError("dividend receivables exceed supported bounds")
207
+ return total
208
+
209
+ def pending_shares(self) -> IntArray:
210
+ pending = dict.fromkeys(self.data.symbols, 0)
211
+ for entitlement in self.entitlements.values():
212
+ if entitlement.accrued and not entitlement.shares_credited:
213
+ pending[entitlement.action.symbol] += entitlement.bonus_quantity
214
+ if any(quantity > 1_000_000_000 for quantity in pending.values()):
215
+ raise OverflowError("pending share quantity exceeds supported bounds")
216
+ return np.array(list(pending.values()), dtype=np.int64)
217
+
218
+ def records(self) -> tuple[EntitlementRecord, ...]:
219
+ result = []
220
+ for e in self.entitlements.values():
221
+ action = e.action
222
+ result.append(
223
+ EntitlementRecord(
224
+ action.action_id,
225
+ action.symbol,
226
+ day(action.record_date),
227
+ day(action.ex_date),
228
+ None if action.pay_date is None else day(action.pay_date),
229
+ None if action.share_credit_date is None else day(action.share_credit_date),
230
+ None if action.share_listing_date is None else day(action.share_listing_date),
231
+ e.quantity,
232
+ e.cash_units,
233
+ e.bonus_quantity,
234
+ e.accrued,
235
+ e.cash_paid,
236
+ e.shares_credited,
237
+ )
238
+ )
239
+ return tuple(result)