pytalsim 0.4.0__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.
- pytalsim-0.4.0/LICENSE +21 -0
- pytalsim-0.4.0/PKG-INFO +293 -0
- pytalsim-0.4.0/README.md +255 -0
- pytalsim-0.4.0/pyproject.toml +62 -0
- pytalsim-0.4.0/pytalsim.egg-info/PKG-INFO +293 -0
- pytalsim-0.4.0/pytalsim.egg-info/SOURCES.txt +24 -0
- pytalsim-0.4.0/pytalsim.egg-info/dependency_links.txt +1 -0
- pytalsim-0.4.0/pytalsim.egg-info/entry_points.txt +2 -0
- pytalsim-0.4.0/pytalsim.egg-info/requires.txt +12 -0
- pytalsim-0.4.0/pytalsim.egg-info/top_level.txt +1 -0
- pytalsim-0.4.0/setup.cfg +4 -0
- pytalsim-0.4.0/talsim/__init__.py +23 -0
- pytalsim-0.4.0/talsim/cli.py +281 -0
- pytalsim-0.4.0/talsim/config.py +190 -0
- pytalsim-0.4.0/talsim/lots.py +377 -0
- pytalsim-0.4.0/talsim/market.py +51 -0
- pytalsim-0.4.0/talsim/optimize.py +297 -0
- pytalsim-0.4.0/talsim/plotting.py +162 -0
- pytalsim-0.4.0/talsim/risk.py +78 -0
- pytalsim-0.4.0/talsim/simulation.py +484 -0
- pytalsim-0.4.0/talsim/tax.py +113 -0
- pytalsim-0.4.0/tests/test_lots.py +254 -0
- pytalsim-0.4.0/tests/test_properties.py +97 -0
- pytalsim-0.4.0/tests/test_risk_and_optimize.py +71 -0
- pytalsim-0.4.0/tests/test_simulation.py +338 -0
- pytalsim-0.4.0/tests/test_tax.py +94 -0
pytalsim-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Engineer Investor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pytalsim-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytalsim
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Research simulator for tax-aware long-short (TALS) portfolio strategies: lot-level tax accounting, leverage, costs, and after-tax outcome distributions on synthetic markets.
|
|
5
|
+
Author: Engineer Investor (@egr_investor)
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/engineerinvestor/talsim
|
|
8
|
+
Project-URL: Repository, https://github.com/engineerinvestor/talsim
|
|
9
|
+
Project-URL: Documentation, https://engineerinvestor.github.io/talsim/
|
|
10
|
+
Project-URL: Issues, https://github.com/engineerinvestor/talsim/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/engineerinvestor/talsim#changelog
|
|
12
|
+
Keywords: tax-loss-harvesting,long-short,portfolio,simulation,after-tax
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.26
|
|
28
|
+
Requires-Dist: pandas>=2.0
|
|
29
|
+
Provides-Extra: plot
|
|
30
|
+
Requires-Dist: matplotlib>=3.8; extra == "plot"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
34
|
+
Requires-Dist: matplotlib>=3.8; extra == "dev"
|
|
35
|
+
Requires-Dist: hypothesis>=6.100; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# talsim
|
|
40
|
+
|
|
41
|
+
[](https://github.com/engineerinvestor/talsim/actions/workflows/ci.yml)
|
|
42
|
+
[](https://engineerinvestor.github.io/talsim/)
|
|
43
|
+
[](https://pypi.org/project/pytalsim/)
|
|
44
|
+
[](https://pypi.org/project/pytalsim/)
|
|
45
|
+
[](https://github.com/engineerinvestor/talsim/blob/master/LICENSE)
|
|
46
|
+
[](https://colab.research.google.com/github/engineerinvestor/talsim/blob/master/examples/talsim_tutorial.ipynb)
|
|
47
|
+
|
|
48
|
+
A research simulator for **tax-aware long-short (TALS)** portfolio strategies: lot-level tax accounting with enforced wash sales, long/short financing costs, leverage, margin response, full liquidation, and Monte Carlo outcome distributions on a synthetic market.
|
|
49
|
+
|
|
50
|
+
The question it exists to answer: **when does additional long-short leverage create usable after-tax value, and when does it merely create more turnover, risk, cost, and deferred tax?**
|
|
51
|
+
|
|
52
|
+
> **Status: v0.4.0, experimental research software.** The engine is synthetic
|
|
53
|
+
> and its tax accounting is a documented approximation. Results are
|
|
54
|
+
> conditional on stated assumptions and are not evidence about any real
|
|
55
|
+
> strategy. Do not use this for personal financial decisions.
|
|
56
|
+
|
|
57
|
+
## Results at a glance
|
|
58
|
+
|
|
59
|
+
The headline experiment: five books from long-only to 250/150 traded on the
|
|
60
|
+
same 200 simulated market paths, zero manager alpha, $1M for 10 years, full
|
|
61
|
+
liquidation at the end. Leverage multiplies harvested losses and still loses
|
|
62
|
+
the race after netting, costs, risk, and the terminal tax bill:
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
| Book | Median after-tax wealth | Paired diff vs 100/0 | Paths beating 100/0 | Gross losses | Tax benefit used |
|
|
67
|
+
|---|---:|---:|---:|---:|---:|
|
|
68
|
+
| 100/0 | $1.62M | — | — | $0.77M | $111k |
|
|
69
|
+
| 130/30 | $1.50M | −$118k | 29% | $2.43M | $157k |
|
|
70
|
+
| 150/50 | $1.40M | −$165k | 26% | $3.15M | $185k |
|
|
71
|
+
| 200/100 | $1.26M | −$324k | 26% | $4.70M | $235k |
|
|
72
|
+
| 250/150 | $1.13M | −$427k | 19% | $5.54M | $263k |
|
|
73
|
+
|
|
74
|
+
Medians across 200 common-random-number paths, seed 7 (250/150 is
|
|
75
|
+
infeasible at FINRA percentage floors and runs net-preserving at roughly
|
|
76
|
+
233/133). 7.2x the gross losses buy 2.4x the usable tax benefit. Every number regenerates from
|
|
77
|
+
`python -m talsim.cli sweep --paths 200 --seed 7` on the same platform; the
|
|
78
|
+
summary, path-level results, and manifest behind this table are committed
|
|
79
|
+
under [`docs/results/`](https://github.com/engineerinvestor/talsim/tree/master/docs/results) and regenerated in pinned CI, and the
|
|
80
|
+
figure rebuilds with
|
|
81
|
+
`python examples/make_readme_figure.py docs/results/leverage_sweep.csv`.
|
|
82
|
+
The 200-path probabilities are demonstration-scale, not inferential
|
|
83
|
+
evidence; paired p10/p90 ranges ship in the summary CSV.
|
|
84
|
+
These are synthetic research results conditional on stated assumptions, not
|
|
85
|
+
evidence about any real strategy.
|
|
86
|
+
|
|
87
|
+
## What it is
|
|
88
|
+
|
|
89
|
+
- A deterministic research engine: same config + seed + environment = same result. Floating-point behavior varies across platforms and BLAS builds and can cross discrete trade thresholds, so official artifacts are generated only in pinned CI (runner image, CPython patch version, and numeric stack fixed in `.github/workflows/artifacts.yml` and `requirements-artifacts.txt`), and every manifest records the commit, worktree state, source-tree hash, platform, and full installed-package list that produced it.
|
|
90
|
+
- An accounting-first design: the `Ledger` is independent of the trading policy and enforces wash-sale disallowance itself, so any trade list, compliant or not, is accounted correctly.
|
|
91
|
+
- Zero-alpha by default. With any positive alpha assumption a leverage comparison silently becomes an alpha study; here alpha is an explicit input, defaulted to zero.
|
|
92
|
+
|
|
93
|
+
## What it is not
|
|
94
|
+
|
|
95
|
+
- Not a tax-return calculator. Rules are simplified federal approximations (see below).
|
|
96
|
+
- Not an execution or advice system. It never touches real accounts, holdings, or personal data.
|
|
97
|
+
- Not empirical validation. The market is synthetic; results are conditional on the configured process.
|
|
98
|
+
|
|
99
|
+
## Install
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install pytalsim # import talsim; CLI: talsim
|
|
103
|
+
pip install "pytalsim[plot]" # adds matplotlib for the report charts
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The distribution is named `pytalsim` because PyPI rejects `talsim` as too
|
|
107
|
+
similar to an unrelated existing project; the import name and the command
|
|
108
|
+
are still `talsim`.
|
|
109
|
+
|
|
110
|
+
For development, from a clone:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install -e ".[dev]"
|
|
114
|
+
pytest # 56 tests: unit, regression, and property-based (hypothesis)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Quick start
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from talsim import ScenarioConfig, run_sweep
|
|
121
|
+
|
|
122
|
+
cfg = ScenarioConfig() # $1M, 10y, quarterly, zero alpha, top 2026 federal rates
|
|
123
|
+
sweeps = run_sweep(cfg, ["100/0", "130/30"], n_paths=50)
|
|
124
|
+
for s in sweeps:
|
|
125
|
+
print(
|
|
126
|
+
s.book,
|
|
127
|
+
f"median wealth ${s.median('ending_after_tax_wealth'):,.0f}",
|
|
128
|
+
f"gross losses ${s.median('gross_losses_realized'):,.0f}",
|
|
129
|
+
f"benefit used ${s.median('tax_benefit_used'):,.0f}",
|
|
130
|
+
)
|
|
131
|
+
# 100/0 median wealth $1,722,303 gross losses $751,035 benefit used $109,473
|
|
132
|
+
# 130/30 median wealth $1,585,744 gross losses $2,562,042 benefit used $157,310
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Single-path inspection, with every assumption in one config object:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from talsim import ScenarioConfig, run_path
|
|
139
|
+
|
|
140
|
+
cfg = ScenarioConfig(long_exposure=1.5, short_exposure=0.5, alpha_annual=0.0)
|
|
141
|
+
r = run_path(cfg, seed=7)
|
|
142
|
+
print(
|
|
143
|
+
f"wealth ${r.ending_after_tax_wealth:,.0f}, TE {r.tracking_error:.1%}, "
|
|
144
|
+
f"turnover {r.annual_turnover:.1f}x, washed ${r.disallowed_wash_losses:,.0f}"
|
|
145
|
+
)
|
|
146
|
+
# wealth $2,393,151, TE 9.7%, turnover 3.0x, washed $0
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or from the command line:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
talsim sweep --paths 200 --seed 7 --out results/
|
|
153
|
+
talsim scenarios --paths 100 --seed 7 --out results/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
(`python -m talsim.cli` is equivalent to the `talsim` command.)
|
|
157
|
+
|
|
158
|
+
Each run writes a summary CSV, a **path-level CSV** (every path, with its seed, so any statistic can be recomputed), and a manifest recording the package version, git commit, Python and NumPy versions, the full config of every scenario, and SHA-256 checksums of the outputs. The sweep summary includes **paired differences versus 100/0 on common random numbers** (median difference and probability of beating the baseline), which are far more informative than medians alone.
|
|
159
|
+
|
|
160
|
+
## Tutorial
|
|
161
|
+
|
|
162
|
+
A short notebook walks through the API end to end: one path, the five
|
|
163
|
+
accounting quantities, a leverage sweep on common random numbers, the report
|
|
164
|
+
figure, an outside-gain what-if, margin feasibility, and reproducibility. It
|
|
165
|
+
runs in about a minute, and CI executes it on every push. Its path counts are
|
|
166
|
+
small, so its numbers are illustrative; the official results above come from
|
|
167
|
+
pinned CI.
|
|
168
|
+
|
|
169
|
+
- Open in Colab: https://colab.research.google.com/github/engineerinvestor/talsim/blob/master/examples/talsim_tutorial.ipynb
|
|
170
|
+
- Source: https://github.com/engineerinvestor/talsim/blob/master/examples/talsim_tutorial.ipynb
|
|
171
|
+
|
|
172
|
+
## The accounting the reports keep separate
|
|
173
|
+
|
|
174
|
+
More harvested losses are not more wealth. Every report distinguishes:
|
|
175
|
+
|
|
176
|
+
1. **Gross losses realized (pre-liquidation)**: deductible realized losses before the terminal unwind, net of wash disallowance.
|
|
177
|
+
2. **Disallowed wash losses**: losses the ledger disallowed; their value moved into replacement basis (with holding-period tacking) rather than vanishing.
|
|
178
|
+
3. **Net realized result**: what survives netting against the portfolio's own realized gains.
|
|
179
|
+
4. **Tax benefit used**: the household tax actually saved against outside gains plus the $3,000 ordinary offset; the only number that deserves to be called a benefit.
|
|
180
|
+
5. **Liquidation tax**: the incremental household tax caused by the terminal unwind, measured against settling the final year without liquidating.
|
|
181
|
+
|
|
182
|
+
## Model mechanics (v0.4.0)
|
|
183
|
+
|
|
184
|
+
- **Wash sales are enforced in the ledger**, both directions of the window, share-matched **in acquisition order with lot splitting**: when only part of a replacement lot matches, the matched shares become their own sublot carrying the transferred basis and a tacked TAX holding clock, while their actual acquisition date (which drives the wash window, the PIL 45-day test, and dividend qualification) is preserved separately. Short-side replacements have the deferred loss subtracted from their basis (sale proceeds), never added. **The window is an exact elapsed-day comparison**: at quarterly cadence a same-step repurchase washes and the next quarter, 91 days later, legally does not. Long-term character requires MORE than 365 days, per Pub 550. The policy layer independently avoids washes: it will not harvest a freshly bought name, it waits out the window before re-entering, redistributes blocked exposure to substitute names (capped at 2x each name's own target), and risk-driven reductions of recent buys sell gain lots first.
|
|
185
|
+
- **Exposure is constructed from post-trade state per side**, never signed drift, so short-to-long transitions land on target. A harvest floor prevents a side from flattening itself when every position is at a loss at once. Realized net exposure error is recorded per path.
|
|
186
|
+
- **Dividends are ordinary income**, split qualified/non-qualified by a day-based holding test (61 days, a proxy for the statutory 60-days-in-121 rule, correct at any cadence), taxed annually in their own buckets; capital losses never absorb them beyond the statutory ordinary offset. **Payments in lieu accrue per short lot** and are capitalized into cover basis only when the short is closed within 45 days (Pub 550); longer-held PIL gets no tax benefit, a deliberate conservatism until an investment-interest bucket exists.
|
|
187
|
+
- **Negative cash accrues debit interest** (default 6%); positive cash earns a configurable rate (default zero, deliberately conservative).
|
|
188
|
+
- **Margin** is a strategy-level maintenance test at FINRA Rule 4210 percentage floors (25% long / 30% short; the rule's per-share short minima for low-priced stocks are not modeled). Feasibility scaling **preserves net exposure**: an infeasible book keeps its long-only core and shrinks the long/short extension equally, so 250/150 at floor requirements runs as roughly 233/133 (`extension_scale` reports the shrinkage) and every book in a sweep compares at the same market exposure. A deficiency during the path is cured by trading back to the compliant target fractions, with transaction costs and tax consequences; nonpositive equity ends the path in an explicit insolvent state. A "flag" mode records deficiencies without responding; its results should never be described as implementable. Actual average long and short exposures are reported per path.
|
|
189
|
+
- **Alpha**, when configured, enters as signal-proportional return drift calibrated at inception; the equal-weight 100/0 baseline has no active positions and receives none.
|
|
190
|
+
- **Tracking error** is measured against an investable equal-weight portfolio of the same universe, and includes cost and tax drag. **Turnover** is one-sided (traded dollars / 2) over average NAV per year, excluding initial construction and terminal liquidation.
|
|
191
|
+
|
|
192
|
+
## Remaining simplifications (read before citing any number)
|
|
193
|
+
|
|
194
|
+
- One wash group per (side, asset). Household scope (spouse, IRA, controlled entities), where a washed loss can be permanently destroyed rather than deferred, is out of scope.
|
|
195
|
+
- Short-sale gains/losses are treated as short-term; long-term short edge cases are not modeled.
|
|
196
|
+
- No delistings, corporate actions, borrow recalls, hard-to-borrow spikes, jumps, volatility clustering, intraperiod margin events, or capacity limits. Returns are Gaussian per step, floored at -90%.
|
|
197
|
+
- The trading policy is a transparent heuristic (rank tilts, bands, deferral), not a risk-model-constrained optimizer; `risk.py`'s estimators are provided for analysis and are not wired into construction.
|
|
198
|
+
- Federal only, top 2026 rates including NIIT by default; no state tax.
|
|
199
|
+
- Tax savings accrue to a zero-return side account rather than compounding.
|
|
200
|
+
|
|
201
|
+
## Layout
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
talsim/
|
|
205
|
+
config.py # every assumption, validated; presets 100/0 .. 250/150
|
|
206
|
+
lots.py # lot ledger, HIFO closes, enforced wash sales, basis transfer
|
|
207
|
+
tax.py # netting, dividend buckets, $3k offset, carryforwards
|
|
208
|
+
market.py # synthetic factor market + persistent signal
|
|
209
|
+
risk.py # sample/EWMA/Ledoit-Wolf/OAS covariance, PSD repair
|
|
210
|
+
optimize.py # per-side state targets, harvest floor, substitute redistribution
|
|
211
|
+
simulation.py # lifecycle loop, costs, margin response, liquidation, Monte Carlo
|
|
212
|
+
plotting.py # report charts (optional matplotlib extra)
|
|
213
|
+
cli.py # reproducible runs, path-level output, provenance manifests
|
|
214
|
+
examples/
|
|
215
|
+
talsim_tutorial.ipynb # end-to-end tutorial (Colab link in the first cell)
|
|
216
|
+
make_readme_figure.py # rebuilds docs/leverage_sweep.png from the summary CSV
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Documentation
|
|
220
|
+
|
|
221
|
+
API documentation is published from the module docstrings at
|
|
222
|
+
**https://engineerinvestor.github.io/talsim/** on every push to master.
|
|
223
|
+
|
|
224
|
+
## Changelog
|
|
225
|
+
|
|
226
|
+
**0.4.0** — Third correctness release. The wash-sale window is now an
|
|
227
|
+
exact elapsed-day comparison (the previous step-rounded window disallowed
|
|
228
|
+
legal 91-day repurchases at quarterly cadence, materially suppressing
|
|
229
|
+
harvests and inflating the leverage penalty); actual acquisition, tacked
|
|
230
|
+
tax holding, and PIL clocks are separate fields; long-term character
|
|
231
|
+
requires more than 365 days; early insolvency liquidates at its actual
|
|
232
|
+
step and settles its actual year (with a real regression test replacing a
|
|
233
|
+
vacuous one); configurations whose net core is infeasible at maintenance
|
|
234
|
+
floors are rejected in deleverage mode; ledger operations validate inputs
|
|
235
|
+
before mutating and reject unknown sides; all config values, including
|
|
236
|
+
every outside-gain event, must be finite and the offset limit
|
|
237
|
+
non-negative; the terminal unwind shares the final step (it was stamped
|
|
238
|
+
one step later, granting every lot an extra period of holding time, so an
|
|
239
|
+
inception lot on an exactly-one-year horizon counted as long term);
|
|
240
|
+
manifests record worktree state, source hash, platform, and full package
|
|
241
|
+
versions; official artifacts move to pinned CI; first PyPI release, as
|
|
242
|
+
`pytalsim`. Results produced by 0.3.0 should be discarded.
|
|
243
|
+
|
|
244
|
+
**0.3.0** — Second correctness release following a follow-up external
|
|
245
|
+
review. Partial wash-sale matches now SPLIT replacement lots (matched
|
|
246
|
+
shares get the basis transfer and tacked holding period; unmatched shares
|
|
247
|
+
keep their own), matching walks purchases chronologically instead of the
|
|
248
|
+
HIFO-sorted view, and a property-based test suite caught and fixed a
|
|
249
|
+
short-side sign error in basis transfer (deferred losses now reduce a
|
|
250
|
+
replacement short's basis). Payments in lieu accrue per lot and respect
|
|
251
|
+
the 45-day capitalization boundary; dividend qualification and holding
|
|
252
|
+
periods are day-based at any cadence; margin feasibility scaling preserves
|
|
253
|
+
net exposure (250/150 runs as ~233/133); nonpositive equity is an explicit
|
|
254
|
+
insolvency state; configuration and CLI inputs are validated; mypy runs in
|
|
255
|
+
CI. Results produced by 0.2.0 should be discarded.
|
|
256
|
+
|
|
257
|
+
**0.2.0** — Correctness release following external review. Wash-sale
|
|
258
|
+
enforcement moved into the ledger (the previous policy-only check allowed
|
|
259
|
+
same-step harvest-and-rebuy, overstating harvested losses); trade
|
|
260
|
+
construction rebuilt from per-side state (short-to-long transitions
|
|
261
|
+
previously overshot and created free leverage, now debit interest accrues);
|
|
262
|
+
dividends moved out of the capital-gain buckets (they were nettable against
|
|
263
|
+
losses without limit); payments in lieu now adjust cover basis; metric
|
|
264
|
+
definitions corrected (pre-liquidation snapshots, direct-comparison
|
|
265
|
+
liquidation tax); margin deficiencies now force deleveraging with a
|
|
266
|
+
persistent exposure scale. Results produced by 0.1.0 should be discarded.
|
|
267
|
+
|
|
268
|
+
**0.1.0** — Initial release.
|
|
269
|
+
|
|
270
|
+
## Citation
|
|
271
|
+
|
|
272
|
+
If you use talsim in academic work, please cite it:
|
|
273
|
+
|
|
274
|
+
```bibtex
|
|
275
|
+
@software{talsim,
|
|
276
|
+
author = {{Engineer Investor}},
|
|
277
|
+
title = {talsim: a research simulator for tax-aware long-short
|
|
278
|
+
portfolio strategies},
|
|
279
|
+
year = {2026},
|
|
280
|
+
version = {0.4.0},
|
|
281
|
+
url = {https://github.com/engineerinvestor/talsim},
|
|
282
|
+
license = {MIT},
|
|
283
|
+
note = {Synthetic-market research software; results are conditional
|
|
284
|
+
on configured assumptions}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A machine-readable [`CITATION.cff`](https://github.com/engineerinvestor/talsim/blob/master/CITATION.cff) is included, so GitHub's
|
|
289
|
+
"Cite this repository" button produces the same reference.
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT. This is educational research software, not tax, legal, accounting, or investment advice.
|
pytalsim-0.4.0/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# talsim
|
|
2
|
+
|
|
3
|
+
[](https://github.com/engineerinvestor/talsim/actions/workflows/ci.yml)
|
|
4
|
+
[](https://engineerinvestor.github.io/talsim/)
|
|
5
|
+
[](https://pypi.org/project/pytalsim/)
|
|
6
|
+
[](https://pypi.org/project/pytalsim/)
|
|
7
|
+
[](https://github.com/engineerinvestor/talsim/blob/master/LICENSE)
|
|
8
|
+
[](https://colab.research.google.com/github/engineerinvestor/talsim/blob/master/examples/talsim_tutorial.ipynb)
|
|
9
|
+
|
|
10
|
+
A research simulator for **tax-aware long-short (TALS)** portfolio strategies: lot-level tax accounting with enforced wash sales, long/short financing costs, leverage, margin response, full liquidation, and Monte Carlo outcome distributions on a synthetic market.
|
|
11
|
+
|
|
12
|
+
The question it exists to answer: **when does additional long-short leverage create usable after-tax value, and when does it merely create more turnover, risk, cost, and deferred tax?**
|
|
13
|
+
|
|
14
|
+
> **Status: v0.4.0, experimental research software.** The engine is synthetic
|
|
15
|
+
> and its tax accounting is a documented approximation. Results are
|
|
16
|
+
> conditional on stated assumptions and are not evidence about any real
|
|
17
|
+
> strategy. Do not use this for personal financial decisions.
|
|
18
|
+
|
|
19
|
+
## Results at a glance
|
|
20
|
+
|
|
21
|
+
The headline experiment: five books from long-only to 250/150 traded on the
|
|
22
|
+
same 200 simulated market paths, zero manager alpha, $1M for 10 years, full
|
|
23
|
+
liquidation at the end. Leverage multiplies harvested losses and still loses
|
|
24
|
+
the race after netting, costs, risk, and the terminal tax bill:
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
| Book | Median after-tax wealth | Paired diff vs 100/0 | Paths beating 100/0 | Gross losses | Tax benefit used |
|
|
29
|
+
|---|---:|---:|---:|---:|---:|
|
|
30
|
+
| 100/0 | $1.62M | — | — | $0.77M | $111k |
|
|
31
|
+
| 130/30 | $1.50M | −$118k | 29% | $2.43M | $157k |
|
|
32
|
+
| 150/50 | $1.40M | −$165k | 26% | $3.15M | $185k |
|
|
33
|
+
| 200/100 | $1.26M | −$324k | 26% | $4.70M | $235k |
|
|
34
|
+
| 250/150 | $1.13M | −$427k | 19% | $5.54M | $263k |
|
|
35
|
+
|
|
36
|
+
Medians across 200 common-random-number paths, seed 7 (250/150 is
|
|
37
|
+
infeasible at FINRA percentage floors and runs net-preserving at roughly
|
|
38
|
+
233/133). 7.2x the gross losses buy 2.4x the usable tax benefit. Every number regenerates from
|
|
39
|
+
`python -m talsim.cli sweep --paths 200 --seed 7` on the same platform; the
|
|
40
|
+
summary, path-level results, and manifest behind this table are committed
|
|
41
|
+
under [`docs/results/`](https://github.com/engineerinvestor/talsim/tree/master/docs/results) and regenerated in pinned CI, and the
|
|
42
|
+
figure rebuilds with
|
|
43
|
+
`python examples/make_readme_figure.py docs/results/leverage_sweep.csv`.
|
|
44
|
+
The 200-path probabilities are demonstration-scale, not inferential
|
|
45
|
+
evidence; paired p10/p90 ranges ship in the summary CSV.
|
|
46
|
+
These are synthetic research results conditional on stated assumptions, not
|
|
47
|
+
evidence about any real strategy.
|
|
48
|
+
|
|
49
|
+
## What it is
|
|
50
|
+
|
|
51
|
+
- A deterministic research engine: same config + seed + environment = same result. Floating-point behavior varies across platforms and BLAS builds and can cross discrete trade thresholds, so official artifacts are generated only in pinned CI (runner image, CPython patch version, and numeric stack fixed in `.github/workflows/artifacts.yml` and `requirements-artifacts.txt`), and every manifest records the commit, worktree state, source-tree hash, platform, and full installed-package list that produced it.
|
|
52
|
+
- An accounting-first design: the `Ledger` is independent of the trading policy and enforces wash-sale disallowance itself, so any trade list, compliant or not, is accounted correctly.
|
|
53
|
+
- Zero-alpha by default. With any positive alpha assumption a leverage comparison silently becomes an alpha study; here alpha is an explicit input, defaulted to zero.
|
|
54
|
+
|
|
55
|
+
## What it is not
|
|
56
|
+
|
|
57
|
+
- Not a tax-return calculator. Rules are simplified federal approximations (see below).
|
|
58
|
+
- Not an execution or advice system. It never touches real accounts, holdings, or personal data.
|
|
59
|
+
- Not empirical validation. The market is synthetic; results are conditional on the configured process.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install pytalsim # import talsim; CLI: talsim
|
|
65
|
+
pip install "pytalsim[plot]" # adds matplotlib for the report charts
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The distribution is named `pytalsim` because PyPI rejects `talsim` as too
|
|
69
|
+
similar to an unrelated existing project; the import name and the command
|
|
70
|
+
are still `talsim`.
|
|
71
|
+
|
|
72
|
+
For development, from a clone:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e ".[dev]"
|
|
76
|
+
pytest # 56 tests: unit, regression, and property-based (hypothesis)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from talsim import ScenarioConfig, run_sweep
|
|
83
|
+
|
|
84
|
+
cfg = ScenarioConfig() # $1M, 10y, quarterly, zero alpha, top 2026 federal rates
|
|
85
|
+
sweeps = run_sweep(cfg, ["100/0", "130/30"], n_paths=50)
|
|
86
|
+
for s in sweeps:
|
|
87
|
+
print(
|
|
88
|
+
s.book,
|
|
89
|
+
f"median wealth ${s.median('ending_after_tax_wealth'):,.0f}",
|
|
90
|
+
f"gross losses ${s.median('gross_losses_realized'):,.0f}",
|
|
91
|
+
f"benefit used ${s.median('tax_benefit_used'):,.0f}",
|
|
92
|
+
)
|
|
93
|
+
# 100/0 median wealth $1,722,303 gross losses $751,035 benefit used $109,473
|
|
94
|
+
# 130/30 median wealth $1,585,744 gross losses $2,562,042 benefit used $157,310
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Single-path inspection, with every assumption in one config object:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from talsim import ScenarioConfig, run_path
|
|
101
|
+
|
|
102
|
+
cfg = ScenarioConfig(long_exposure=1.5, short_exposure=0.5, alpha_annual=0.0)
|
|
103
|
+
r = run_path(cfg, seed=7)
|
|
104
|
+
print(
|
|
105
|
+
f"wealth ${r.ending_after_tax_wealth:,.0f}, TE {r.tracking_error:.1%}, "
|
|
106
|
+
f"turnover {r.annual_turnover:.1f}x, washed ${r.disallowed_wash_losses:,.0f}"
|
|
107
|
+
)
|
|
108
|
+
# wealth $2,393,151, TE 9.7%, turnover 3.0x, washed $0
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or from the command line:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
talsim sweep --paths 200 --seed 7 --out results/
|
|
115
|
+
talsim scenarios --paths 100 --seed 7 --out results/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
(`python -m talsim.cli` is equivalent to the `talsim` command.)
|
|
119
|
+
|
|
120
|
+
Each run writes a summary CSV, a **path-level CSV** (every path, with its seed, so any statistic can be recomputed), and a manifest recording the package version, git commit, Python and NumPy versions, the full config of every scenario, and SHA-256 checksums of the outputs. The sweep summary includes **paired differences versus 100/0 on common random numbers** (median difference and probability of beating the baseline), which are far more informative than medians alone.
|
|
121
|
+
|
|
122
|
+
## Tutorial
|
|
123
|
+
|
|
124
|
+
A short notebook walks through the API end to end: one path, the five
|
|
125
|
+
accounting quantities, a leverage sweep on common random numbers, the report
|
|
126
|
+
figure, an outside-gain what-if, margin feasibility, and reproducibility. It
|
|
127
|
+
runs in about a minute, and CI executes it on every push. Its path counts are
|
|
128
|
+
small, so its numbers are illustrative; the official results above come from
|
|
129
|
+
pinned CI.
|
|
130
|
+
|
|
131
|
+
- Open in Colab: https://colab.research.google.com/github/engineerinvestor/talsim/blob/master/examples/talsim_tutorial.ipynb
|
|
132
|
+
- Source: https://github.com/engineerinvestor/talsim/blob/master/examples/talsim_tutorial.ipynb
|
|
133
|
+
|
|
134
|
+
## The accounting the reports keep separate
|
|
135
|
+
|
|
136
|
+
More harvested losses are not more wealth. Every report distinguishes:
|
|
137
|
+
|
|
138
|
+
1. **Gross losses realized (pre-liquidation)**: deductible realized losses before the terminal unwind, net of wash disallowance.
|
|
139
|
+
2. **Disallowed wash losses**: losses the ledger disallowed; their value moved into replacement basis (with holding-period tacking) rather than vanishing.
|
|
140
|
+
3. **Net realized result**: what survives netting against the portfolio's own realized gains.
|
|
141
|
+
4. **Tax benefit used**: the household tax actually saved against outside gains plus the $3,000 ordinary offset; the only number that deserves to be called a benefit.
|
|
142
|
+
5. **Liquidation tax**: the incremental household tax caused by the terminal unwind, measured against settling the final year without liquidating.
|
|
143
|
+
|
|
144
|
+
## Model mechanics (v0.4.0)
|
|
145
|
+
|
|
146
|
+
- **Wash sales are enforced in the ledger**, both directions of the window, share-matched **in acquisition order with lot splitting**: when only part of a replacement lot matches, the matched shares become their own sublot carrying the transferred basis and a tacked TAX holding clock, while their actual acquisition date (which drives the wash window, the PIL 45-day test, and dividend qualification) is preserved separately. Short-side replacements have the deferred loss subtracted from their basis (sale proceeds), never added. **The window is an exact elapsed-day comparison**: at quarterly cadence a same-step repurchase washes and the next quarter, 91 days later, legally does not. Long-term character requires MORE than 365 days, per Pub 550. The policy layer independently avoids washes: it will not harvest a freshly bought name, it waits out the window before re-entering, redistributes blocked exposure to substitute names (capped at 2x each name's own target), and risk-driven reductions of recent buys sell gain lots first.
|
|
147
|
+
- **Exposure is constructed from post-trade state per side**, never signed drift, so short-to-long transitions land on target. A harvest floor prevents a side from flattening itself when every position is at a loss at once. Realized net exposure error is recorded per path.
|
|
148
|
+
- **Dividends are ordinary income**, split qualified/non-qualified by a day-based holding test (61 days, a proxy for the statutory 60-days-in-121 rule, correct at any cadence), taxed annually in their own buckets; capital losses never absorb them beyond the statutory ordinary offset. **Payments in lieu accrue per short lot** and are capitalized into cover basis only when the short is closed within 45 days (Pub 550); longer-held PIL gets no tax benefit, a deliberate conservatism until an investment-interest bucket exists.
|
|
149
|
+
- **Negative cash accrues debit interest** (default 6%); positive cash earns a configurable rate (default zero, deliberately conservative).
|
|
150
|
+
- **Margin** is a strategy-level maintenance test at FINRA Rule 4210 percentage floors (25% long / 30% short; the rule's per-share short minima for low-priced stocks are not modeled). Feasibility scaling **preserves net exposure**: an infeasible book keeps its long-only core and shrinks the long/short extension equally, so 250/150 at floor requirements runs as roughly 233/133 (`extension_scale` reports the shrinkage) and every book in a sweep compares at the same market exposure. A deficiency during the path is cured by trading back to the compliant target fractions, with transaction costs and tax consequences; nonpositive equity ends the path in an explicit insolvent state. A "flag" mode records deficiencies without responding; its results should never be described as implementable. Actual average long and short exposures are reported per path.
|
|
151
|
+
- **Alpha**, when configured, enters as signal-proportional return drift calibrated at inception; the equal-weight 100/0 baseline has no active positions and receives none.
|
|
152
|
+
- **Tracking error** is measured against an investable equal-weight portfolio of the same universe, and includes cost and tax drag. **Turnover** is one-sided (traded dollars / 2) over average NAV per year, excluding initial construction and terminal liquidation.
|
|
153
|
+
|
|
154
|
+
## Remaining simplifications (read before citing any number)
|
|
155
|
+
|
|
156
|
+
- One wash group per (side, asset). Household scope (spouse, IRA, controlled entities), where a washed loss can be permanently destroyed rather than deferred, is out of scope.
|
|
157
|
+
- Short-sale gains/losses are treated as short-term; long-term short edge cases are not modeled.
|
|
158
|
+
- No delistings, corporate actions, borrow recalls, hard-to-borrow spikes, jumps, volatility clustering, intraperiod margin events, or capacity limits. Returns are Gaussian per step, floored at -90%.
|
|
159
|
+
- The trading policy is a transparent heuristic (rank tilts, bands, deferral), not a risk-model-constrained optimizer; `risk.py`'s estimators are provided for analysis and are not wired into construction.
|
|
160
|
+
- Federal only, top 2026 rates including NIIT by default; no state tax.
|
|
161
|
+
- Tax savings accrue to a zero-return side account rather than compounding.
|
|
162
|
+
|
|
163
|
+
## Layout
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
talsim/
|
|
167
|
+
config.py # every assumption, validated; presets 100/0 .. 250/150
|
|
168
|
+
lots.py # lot ledger, HIFO closes, enforced wash sales, basis transfer
|
|
169
|
+
tax.py # netting, dividend buckets, $3k offset, carryforwards
|
|
170
|
+
market.py # synthetic factor market + persistent signal
|
|
171
|
+
risk.py # sample/EWMA/Ledoit-Wolf/OAS covariance, PSD repair
|
|
172
|
+
optimize.py # per-side state targets, harvest floor, substitute redistribution
|
|
173
|
+
simulation.py # lifecycle loop, costs, margin response, liquidation, Monte Carlo
|
|
174
|
+
plotting.py # report charts (optional matplotlib extra)
|
|
175
|
+
cli.py # reproducible runs, path-level output, provenance manifests
|
|
176
|
+
examples/
|
|
177
|
+
talsim_tutorial.ipynb # end-to-end tutorial (Colab link in the first cell)
|
|
178
|
+
make_readme_figure.py # rebuilds docs/leverage_sweep.png from the summary CSV
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Documentation
|
|
182
|
+
|
|
183
|
+
API documentation is published from the module docstrings at
|
|
184
|
+
**https://engineerinvestor.github.io/talsim/** on every push to master.
|
|
185
|
+
|
|
186
|
+
## Changelog
|
|
187
|
+
|
|
188
|
+
**0.4.0** — Third correctness release. The wash-sale window is now an
|
|
189
|
+
exact elapsed-day comparison (the previous step-rounded window disallowed
|
|
190
|
+
legal 91-day repurchases at quarterly cadence, materially suppressing
|
|
191
|
+
harvests and inflating the leverage penalty); actual acquisition, tacked
|
|
192
|
+
tax holding, and PIL clocks are separate fields; long-term character
|
|
193
|
+
requires more than 365 days; early insolvency liquidates at its actual
|
|
194
|
+
step and settles its actual year (with a real regression test replacing a
|
|
195
|
+
vacuous one); configurations whose net core is infeasible at maintenance
|
|
196
|
+
floors are rejected in deleverage mode; ledger operations validate inputs
|
|
197
|
+
before mutating and reject unknown sides; all config values, including
|
|
198
|
+
every outside-gain event, must be finite and the offset limit
|
|
199
|
+
non-negative; the terminal unwind shares the final step (it was stamped
|
|
200
|
+
one step later, granting every lot an extra period of holding time, so an
|
|
201
|
+
inception lot on an exactly-one-year horizon counted as long term);
|
|
202
|
+
manifests record worktree state, source hash, platform, and full package
|
|
203
|
+
versions; official artifacts move to pinned CI; first PyPI release, as
|
|
204
|
+
`pytalsim`. Results produced by 0.3.0 should be discarded.
|
|
205
|
+
|
|
206
|
+
**0.3.0** — Second correctness release following a follow-up external
|
|
207
|
+
review. Partial wash-sale matches now SPLIT replacement lots (matched
|
|
208
|
+
shares get the basis transfer and tacked holding period; unmatched shares
|
|
209
|
+
keep their own), matching walks purchases chronologically instead of the
|
|
210
|
+
HIFO-sorted view, and a property-based test suite caught and fixed a
|
|
211
|
+
short-side sign error in basis transfer (deferred losses now reduce a
|
|
212
|
+
replacement short's basis). Payments in lieu accrue per lot and respect
|
|
213
|
+
the 45-day capitalization boundary; dividend qualification and holding
|
|
214
|
+
periods are day-based at any cadence; margin feasibility scaling preserves
|
|
215
|
+
net exposure (250/150 runs as ~233/133); nonpositive equity is an explicit
|
|
216
|
+
insolvency state; configuration and CLI inputs are validated; mypy runs in
|
|
217
|
+
CI. Results produced by 0.2.0 should be discarded.
|
|
218
|
+
|
|
219
|
+
**0.2.0** — Correctness release following external review. Wash-sale
|
|
220
|
+
enforcement moved into the ledger (the previous policy-only check allowed
|
|
221
|
+
same-step harvest-and-rebuy, overstating harvested losses); trade
|
|
222
|
+
construction rebuilt from per-side state (short-to-long transitions
|
|
223
|
+
previously overshot and created free leverage, now debit interest accrues);
|
|
224
|
+
dividends moved out of the capital-gain buckets (they were nettable against
|
|
225
|
+
losses without limit); payments in lieu now adjust cover basis; metric
|
|
226
|
+
definitions corrected (pre-liquidation snapshots, direct-comparison
|
|
227
|
+
liquidation tax); margin deficiencies now force deleveraging with a
|
|
228
|
+
persistent exposure scale. Results produced by 0.1.0 should be discarded.
|
|
229
|
+
|
|
230
|
+
**0.1.0** — Initial release.
|
|
231
|
+
|
|
232
|
+
## Citation
|
|
233
|
+
|
|
234
|
+
If you use talsim in academic work, please cite it:
|
|
235
|
+
|
|
236
|
+
```bibtex
|
|
237
|
+
@software{talsim,
|
|
238
|
+
author = {{Engineer Investor}},
|
|
239
|
+
title = {talsim: a research simulator for tax-aware long-short
|
|
240
|
+
portfolio strategies},
|
|
241
|
+
year = {2026},
|
|
242
|
+
version = {0.4.0},
|
|
243
|
+
url = {https://github.com/engineerinvestor/talsim},
|
|
244
|
+
license = {MIT},
|
|
245
|
+
note = {Synthetic-market research software; results are conditional
|
|
246
|
+
on configured assumptions}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
A machine-readable [`CITATION.cff`](https://github.com/engineerinvestor/talsim/blob/master/CITATION.cff) is included, so GitHub's
|
|
251
|
+
"Cite this repository" button produces the same reference.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT. This is educational research software, not tax, legal, accounting, or investment advice.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# Distribution name differs from the import name: PyPI rejects "talsim" as
|
|
7
|
+
# too similar to the unrelated existing project "taisim".
|
|
8
|
+
name = "pytalsim"
|
|
9
|
+
version = "0.4.0"
|
|
10
|
+
description = "Research simulator for tax-aware long-short (TALS) portfolio strategies: lot-level tax accounting, leverage, costs, and after-tax outcome distributions on synthetic markets."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [{ name = "Engineer Investor (@egr_investor)" }]
|
|
16
|
+
keywords = ["tax-loss-harvesting", "long-short", "portfolio", "simulation", "after-tax"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"numpy>=1.26",
|
|
32
|
+
"pandas>=2.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
plot = ["matplotlib>=3.8"]
|
|
37
|
+
dev = ["pytest>=8.0", "ruff>=0.4", "matplotlib>=3.8", "hypothesis>=6.100", "mypy>=1.10"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/engineerinvestor/talsim"
|
|
41
|
+
Repository = "https://github.com/engineerinvestor/talsim"
|
|
42
|
+
Documentation = "https://engineerinvestor.github.io/talsim/"
|
|
43
|
+
Issues = "https://github.com/engineerinvestor/talsim/issues"
|
|
44
|
+
Changelog = "https://github.com/engineerinvestor/talsim#changelog"
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
talsim = "talsim.cli:main"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
include = ["talsim*"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py310"
|
|
55
|
+
# The tutorial notebook ends cells with bare expressions for display.
|
|
56
|
+
extend-exclude = ["*.ipynb"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|