Structure into library for pip
This commit is contained in:
parent
87f8621fde
commit
25ad16dac2
12 changed files with 540 additions and 99 deletions
49
.gitignore
vendored
49
.gitignore
vendored
|
|
@ -1,2 +1,51 @@
|
||||||
# Private Kalshi csvs
|
# Private Kalshi csvs
|
||||||
*.csv
|
*.csv
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Pytest
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Mark Robillard Jr
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
88
README.md
Normal file
88
README.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# kalshi-csv
|
||||||
|
|
||||||
|
Parse Kalshi transaction CSV files and generate IRS Form 8949 tax summaries for event contract trading.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install kalshi-csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
Parse a Kalshi transactions CSV and display the trade matrix with IRS summary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kalshi-csv Kalshi-Transactions-2026.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
Export the IRS summary to a file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kalshi-csv Kalshi-Transactions-2026.csv --irs-file irs-summary.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Disable colored output (useful for piping or redirecting):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kalshi-csv Kalshi-Transactions-2026.csv --no-color
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Ticker | Side | Qty | Entry | Exit | P&L (No Fees)
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
KXWCADVANCE-26JUL07ARGEGY-ARG | YES | 0.17 | $0.86 | $0.69 | -$0.03
|
||||||
|
KXWC1H-26JUL07ARGEGY-TIE | YES | 0.34 | $0.28 | $0.00 | -$0.10
|
||||||
|
KXMLBGAME-26JUL081940BOSCWS-BOS | YES | 0.96 | $0.50 | $0.91 | $0.39
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Total Transactions Parsed: 3
|
||||||
|
Total Exchange Fees Paid: $0.05
|
||||||
|
Internal Tracked Net P&L: $0.26
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
=== IRS FORM 8949 / SCHEDULE D AGGREGATE SUMMARY ===
|
||||||
|
Use these exact aggregates for a single-line summary entry:
|
||||||
|
* Box to Check: Box C (Short-term, not reported on Form 1099-B)
|
||||||
|
* (a) Description: Kalshi Event Contracts (Aggregate Summary)
|
||||||
|
* (d) Gross Proceeds: $2.50
|
||||||
|
* (e) Cost or Other Basis: $2.24
|
||||||
|
* (h) Gain or (Loss): $0.26
|
||||||
|
====================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Library API
|
||||||
|
|
||||||
|
Use `kalshi-csv` as a Python library in your own scripts:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kalshi_csv import KalshiCSV
|
||||||
|
|
||||||
|
kalshi = KalshiCSV("Kalshi-Transactions-2026.csv")
|
||||||
|
kalshi.parse()
|
||||||
|
|
||||||
|
print(f"Total trades: {kalshi.summary['trade_count']}")
|
||||||
|
print(f"Total P&L: ${kalshi.summary['total_pnl_with_fees']:.2f}")
|
||||||
|
|
||||||
|
irs = kalshi.irs_summary()
|
||||||
|
print(f"Gross Proceeds: ${irs['gross_proceeds']:.2f}")
|
||||||
|
print(f"Cost Basis: ${irs['cost_basis']:.2f}")
|
||||||
|
print(f"Gain/Loss: ${irs['gain_or_loss']:.2f}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## IRS Form 8949
|
||||||
|
|
||||||
|
Kalshi event contracts are typically reported on **IRS Form 8949, Box C** (short-term transactions not reported on Form 1099-B). The tool calculates:
|
||||||
|
|
||||||
|
- **Gross Proceeds**: Total exit value minus close fees
|
||||||
|
- **Cost Basis**: Total entry value plus open fees
|
||||||
|
- **Gain/Loss**: Realized P&L including all fees
|
||||||
|
|
||||||
|
Use the aggregate summary for a single-line entry on Form 8949, or export to a file for your records.
|
||||||
|
|
||||||
|
**Disclaimer**: This tool provides calculations based on Kalshi transaction data. Consult a tax professional for specific tax advice.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import csv
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
class KalshiCSV:
|
|
||||||
"""Parses Kalshi transaction CSV data and handles terminal color-coding
|
|
||||||
and tax summary generation.
|
|
||||||
"""
|
|
||||||
def __init__(self, file_path):
|
|
||||||
self.file_path = file_path
|
|
||||||
self.trade_count = 0
|
|
||||||
self.total_tax_basis = 0.0
|
|
||||||
self.total_tax_proceeds = 0.0
|
|
||||||
self.total_pnl_without_fees = 0.0
|
|
||||||
self.total_pnl_with_fees = 0.0
|
|
||||||
self.total_fees = 0.0
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
"""Processes the CSV file row-by-row and prints the live trade matrix."""
|
|
||||||
if not os.path.exists(self.file_path):
|
|
||||||
print(f"Error: File '{self.file_path}' not found.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(f"\n{'Ticker':<32} | {'Side':<4} | {'Qty':<6} | {'Entry':<6} | {'Exit':<6} | {'P&L (No Fees)':<14}")
|
|
||||||
print("-" * 83)
|
|
||||||
|
|
||||||
with open(self.file_path, mode='r', newline='', encoding='utf-8') as f:
|
|
||||||
reader = csv.DictReader(f)
|
|
||||||
|
|
||||||
for row in reader:
|
|
||||||
if not row.get('realized_pnl_without_fees_dollars'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
ticker = row['market_ticker']
|
|
||||||
side = row['side'].upper()
|
|
||||||
qty = float(row['quantity_fp'])
|
|
||||||
entry = float(row['entry_price_dollars'])
|
|
||||||
exit_val = float(row['exit_price_dollars'])
|
|
||||||
|
|
||||||
pnl_no_fees = float(row['realized_pnl_without_fees_dollars'])
|
|
||||||
pnl_with_fees = float(row['realized_pnl_with_fees_dollars'])
|
|
||||||
|
|
||||||
open_fees = float(row['open_fees_dollars'])
|
|
||||||
close_fees = float(row['close_fees_dollars'])
|
|
||||||
|
|
||||||
# --- Aggregate Calculations ---
|
|
||||||
self.total_tax_basis += (qty * entry) + open_fees
|
|
||||||
self.total_tax_proceeds += (qty * exit_val) - close_fees
|
|
||||||
self.total_pnl_without_fees += pnl_no_fees
|
|
||||||
self.total_pnl_with_fees += pnl_with_fees
|
|
||||||
self.total_fees += (open_fees + close_fees)
|
|
||||||
self.trade_count += 1
|
|
||||||
|
|
||||||
# Format row P&L using the color methods
|
|
||||||
pnl_str = self.format_currency_color(pnl_no_fees)
|
|
||||||
print(f"{ticker:<32} | {side:<4} | {qty:<6.2f} | ${entry:<5.2f} | ${exit_val:<5.2f} | {pnl_str:<14}")
|
|
||||||
|
|
||||||
print("-" * 83)
|
|
||||||
print(f"Total Transactions Parsed: {self.trade_count}")
|
|
||||||
print(f"Total Exchange Fees Paid: ${self.total_fees:.2f}")
|
|
||||||
print(f"Internal Tracked Net P&L: " + self.format_currency_color(self.total_pnl_without_fees))
|
|
||||||
print("-" * 83)
|
|
||||||
|
|
||||||
def color_green(self, text):
|
|
||||||
"""Wraps text in ANSI green."""
|
|
||||||
return f"\033[1;32m{text}\033[0m"
|
|
||||||
|
|
||||||
def color_red(self, text):
|
|
||||||
"""Wraps text in ANSI red."""
|
|
||||||
return f"\033[1;31m{text}\033[0m"
|
|
||||||
|
|
||||||
def format_currency_color(self, value):
|
|
||||||
"""Returns a signed, colorized string based on profit or loss status."""
|
|
||||||
val_str = f"${value:+.2f}"
|
|
||||||
return self.color_green(val_str) if value >= 0 else self.color_red(val_str)
|
|
||||||
|
|
||||||
def summarizeIRS(self):
|
|
||||||
"""Outputs the structured block needed for a single-line entry on IRS Form 8949."""
|
|
||||||
print("\033[1;33m=== IRS FORM 8949 / SCHEDULE D AGGREGATE SUMMARY ===\033[0m")
|
|
||||||
print("Use these exact aggregates for a single-line summary entry:")
|
|
||||||
print(f" * Box to Check: \033[1;37mBox C\033[0m (Short-term, not reported on Form 1099-B)")
|
|
||||||
print(f" * (a) Description: Kalshi Event Contracts (Aggregate Summary)")
|
|
||||||
print(f" * (d) Gross Proceeds: " + self.color_green(f"${self.total_tax_proceeds:.2f}"))
|
|
||||||
print(f" * (e) Cost or Other Basis: \033[1;36m${self.total_tax_basis:.2f}\033[0m")
|
|
||||||
print(f" * (h) Gain or (Loss): " + self.format_currency_color(self.total_pnl_with_fees))
|
|
||||||
print("\033[1;33m====================================================\033[0m\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python3 kalshi_parser.py <path_to_transactions_csv>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Instantiate, parse the matrix, and drop the IRS aggregate summary
|
|
||||||
parser = KalshiCSV(sys.argv[1])
|
|
||||||
parser.parse()
|
|
||||||
parser.summarizeIRS()
|
|
||||||
|
|
||||||
33
pyproject.toml
Normal file
33
pyproject.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "kalshi-csv"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Parse Kalshi transaction CSV files and generate IRS Form 8949 tax summaries"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
authors = [
|
||||||
|
{ name = "markmental", email = "marky611@gmail.com" }
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Office/Business :: Financial",
|
||||||
|
]
|
||||||
|
keywords = ["kalshi", "csv", "tax", "irs", "form-8949"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
kalshi-csv = "kalshi_csv.cli:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/kalshi_csv"]
|
||||||
5
src/kalshi_csv/__init__.py
Normal file
5
src/kalshi_csv/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
from .parser import KalshiCSV
|
||||||
|
|
||||||
|
__all__ = ["KalshiCSV", "__version__"]
|
||||||
80
src/kalshi_csv/cli.py
Normal file
80
src/kalshi_csv/cli.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .parser import KalshiCSV
|
||||||
|
from .formatter import (
|
||||||
|
color_white,
|
||||||
|
color_yellow,
|
||||||
|
color_cyan,
|
||||||
|
format_currency_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Parse Kalshi transaction CSV and generate IRS tax summary."
|
||||||
|
)
|
||||||
|
parser.add_argument("csv_path", help="Path to Kalshi transactions CSV file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--irs-file",
|
||||||
|
help="Write IRS Form 8949 summary to this file",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-color",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable ANSI color output",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
no_color = args.no_color
|
||||||
|
|
||||||
|
kalshi = KalshiCSV(args.csv_path)
|
||||||
|
kalshi.parse()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
f"{'Ticker':<32} | {'Side':<4} | {'Qty':<6} | {'Entry':<6} | {'Exit':<6} | {'P&L (No Fees)':<14}"
|
||||||
|
)
|
||||||
|
print("-" * 83)
|
||||||
|
|
||||||
|
for trade in kalshi.trades:
|
||||||
|
pnl_str = format_currency_color(trade["pnl_no_fees"], no_color)
|
||||||
|
print(
|
||||||
|
f"{trade['ticker']:<32} | {trade['side']:<4} | {trade['qty']:<6.2f} | "
|
||||||
|
f"${trade['entry']:<5.2f} | ${trade['exit']:<5.2f} | {pnl_str:<14}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 83)
|
||||||
|
print(f"Total Transactions Parsed: {kalshi.summary['trade_count']}")
|
||||||
|
print(f"Total Exchange Fees Paid: ${kalshi.summary['total_fees']:.2f}")
|
||||||
|
print(
|
||||||
|
f"Internal Tracked Net P&L: "
|
||||||
|
+ format_currency_color(kalshi.summary["total_pnl_without_fees"], no_color)
|
||||||
|
)
|
||||||
|
print("-" * 83)
|
||||||
|
|
||||||
|
irs = kalshi.irs_summary()
|
||||||
|
print(color_yellow("=== IRS FORM 8949 / SCHEDULE D AGGREGATE SUMMARY ===", no_color))
|
||||||
|
print("Use these exact aggregates for a single-line summary entry:")
|
||||||
|
print(f" * Box to Check: {color_white('Box C', no_color)} (Short-term, not reported on Form 1099-B)")
|
||||||
|
print(f" * (a) Description: {irs['description']}")
|
||||||
|
print(f" * (d) Gross Proceeds: {format_currency_color(irs['gross_proceeds'], no_color)}")
|
||||||
|
print(f" * (e) Cost or Other Basis: {color_cyan(f'${irs['cost_basis']:.2f}', no_color)}")
|
||||||
|
print(f" * (h) Gain or (Loss): {format_currency_color(irs['gain_or_loss'], no_color)}")
|
||||||
|
print(color_yellow("====================================================", no_color))
|
||||||
|
print()
|
||||||
|
|
||||||
|
if args.irs_file:
|
||||||
|
with open(args.irs_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write("IRS FORM 8949 / SCHEDULE D AGGREGATE SUMMARY\n")
|
||||||
|
f.write("Use these exact aggregates for a single-line summary entry:\n")
|
||||||
|
f.write(f" * Box to Check: Box C (Short-term, not reported on Form 1099-B)\n")
|
||||||
|
f.write(f" * (a) Description: {irs['description']}\n")
|
||||||
|
f.write(f" * (d) Gross Proceeds: ${irs['gross_proceeds']:.2f}\n")
|
||||||
|
f.write(f" * (e) Cost or Other Basis: ${irs['cost_basis']:.2f}\n")
|
||||||
|
f.write(f" * (h) Gain or (Loss): ${irs['gain_or_loss']:.2f}\n")
|
||||||
|
print(f"IRS summary written to: {args.irs_file}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
39
src/kalshi_csv/formatter.py
Normal file
39
src/kalshi_csv/formatter.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
def color_green(text, no_color=False):
|
||||||
|
"""Wraps text in ANSI green."""
|
||||||
|
if no_color:
|
||||||
|
return text
|
||||||
|
return f"\033[1;32m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def color_red(text, no_color=False):
|
||||||
|
"""Wraps text in ANSI red."""
|
||||||
|
if no_color:
|
||||||
|
return text
|
||||||
|
return f"\033[1;31m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def color_yellow(text, no_color=False):
|
||||||
|
"""Wraps text in ANSI yellow."""
|
||||||
|
if no_color:
|
||||||
|
return text
|
||||||
|
return f"\033[1;33m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def color_cyan(text, no_color=False):
|
||||||
|
"""Wraps text in ANSI cyan."""
|
||||||
|
if no_color:
|
||||||
|
return text
|
||||||
|
return f"\033[1;36m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def color_white(text, no_color=False):
|
||||||
|
"""Wraps text in ANSI white."""
|
||||||
|
if no_color:
|
||||||
|
return text
|
||||||
|
return f"\033[1;37m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def format_currency_color(value, no_color=False):
|
||||||
|
"""Returns a signed, colorized string based on profit or loss status."""
|
||||||
|
val_str = f"${value:+.2f}"
|
||||||
|
return color_green(val_str, no_color) if value >= 0 else color_red(val_str, no_color)
|
||||||
70
src/kalshi_csv/parser.py
Normal file
70
src/kalshi_csv/parser.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class KalshiCSV:
|
||||||
|
"""Parses Kalshi transaction CSV data and calculates tax-relevant aggregates."""
|
||||||
|
|
||||||
|
def __init__(self, file_path):
|
||||||
|
self.file_path = file_path
|
||||||
|
self.trades = []
|
||||||
|
self.summary = {
|
||||||
|
"trade_count": 0,
|
||||||
|
"total_fees": 0.0,
|
||||||
|
"total_pnl_without_fees": 0.0,
|
||||||
|
"total_pnl_with_fees": 0.0,
|
||||||
|
"total_tax_basis": 0.0,
|
||||||
|
"total_tax_proceeds": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
"""Processes the CSV file row-by-row and populates trades and summary."""
|
||||||
|
if not os.path.exists(self.file_path):
|
||||||
|
raise FileNotFoundError(f"File '{self.file_path}' not found.")
|
||||||
|
|
||||||
|
with open(self.file_path, mode="r", newline="", encoding="utf-8") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
if not row.get("realized_pnl_without_fees_dollars"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
qty = float(row["quantity_fp"])
|
||||||
|
entry = float(row["entry_price_dollars"])
|
||||||
|
exit_val = float(row["exit_price_dollars"])
|
||||||
|
pnl_no_fees = float(row["realized_pnl_without_fees_dollars"])
|
||||||
|
pnl_with_fees = float(row["realized_pnl_with_fees_dollars"])
|
||||||
|
open_fees = float(row["open_fees_dollars"])
|
||||||
|
close_fees = float(row["close_fees_dollars"])
|
||||||
|
|
||||||
|
trade = {
|
||||||
|
"ticker": row["market_ticker"],
|
||||||
|
"side": row["side"].upper(),
|
||||||
|
"qty": qty,
|
||||||
|
"entry": entry,
|
||||||
|
"exit": exit_val,
|
||||||
|
"pnl_no_fees": pnl_no_fees,
|
||||||
|
"pnl_with_fees": pnl_with_fees,
|
||||||
|
"open_fees": open_fees,
|
||||||
|
"close_fees": close_fees,
|
||||||
|
}
|
||||||
|
self.trades.append(trade)
|
||||||
|
|
||||||
|
self.summary["trade_count"] += 1
|
||||||
|
self.summary["total_tax_basis"] += (qty * entry) + open_fees
|
||||||
|
self.summary["total_tax_proceeds"] += (qty * exit_val) - close_fees
|
||||||
|
self.summary["total_pnl_without_fees"] += pnl_no_fees
|
||||||
|
self.summary["total_pnl_with_fees"] += pnl_with_fees
|
||||||
|
self.summary["total_fees"] += open_fees + close_fees
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def irs_summary(self):
|
||||||
|
"""Returns a dict with IRS Form 8949 aggregate fields."""
|
||||||
|
return {
|
||||||
|
"box": "C",
|
||||||
|
"description": "Kalshi Event Contracts (Aggregate Summary)",
|
||||||
|
"gross_proceeds": self.summary["total_tax_proceeds"],
|
||||||
|
"cost_basis": self.summary["total_tax_basis"],
|
||||||
|
"gain_or_loss": self.summary["total_pnl_with_fees"],
|
||||||
|
}
|
||||||
8
tests/conftest.py
Normal file
8
tests/conftest.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_csv():
|
||||||
|
"""Returns the path to the synthetic test CSV fixture."""
|
||||||
|
return str(Path(__file__).parent / "fixtures" / "sample.csv")
|
||||||
67
tests/test_cli.py
Normal file
67
tests/test_cli.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_runs_successfully(sample_csv):
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "kalshi_csv.cli", sample_csv],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "TESTMARKET-WIN" in result.stdout
|
||||||
|
assert "TESTMARKET-LOSS" in result.stdout
|
||||||
|
assert "Total Transactions Parsed: 3" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_irs_summary_output(sample_csv):
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "kalshi_csv.cli", sample_csv],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert "IRS FORM 8949" in result.stdout
|
||||||
|
assert "Box C" in result.stdout
|
||||||
|
assert "Gross Proceeds:" in result.stdout
|
||||||
|
assert "Cost or Other Basis:" in result.stdout
|
||||||
|
assert "Gain or (Loss):" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_irs_file_creation(sample_csv, tmp_path):
|
||||||
|
irs_file = tmp_path / "irs-output.txt"
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "kalshi_csv.cli", sample_csv, "--irs-file", str(irs_file)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert irs_file.exists()
|
||||||
|
|
||||||
|
content = irs_file.read_text()
|
||||||
|
assert "IRS FORM 8949" in content
|
||||||
|
assert "Box C" in content
|
||||||
|
assert "Gross Proceeds:" in content
|
||||||
|
assert "Cost or Other Basis:" in content
|
||||||
|
assert "Gain or (Loss):" in content
|
||||||
|
assert "\033[" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_no_color_flag(sample_csv):
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "kalshi_csv.cli", sample_csv, "--no-color"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "\033[" not in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_missing_file():
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "kalshi_csv.cli", "/nonexistent/file.csv"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
assert "not found" in result.stderr.lower() or "error" in result.stderr.lower()
|
||||||
80
tests/test_parser.py
Normal file
80
tests/test_parser.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import pytest
|
||||||
|
from kalshi_csv import KalshiCSV
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_returns_self(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
result = kalshi.parse()
|
||||||
|
assert result is kalshi
|
||||||
|
|
||||||
|
|
||||||
|
def test_trade_count(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert kalshi.summary["trade_count"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_fees(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert abs(kalshi.summary["total_fees"] - 0.07) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_pnl_without_fees(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert abs(kalshi.summary["total_pnl_without_fees"] - (-0.20)) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_pnl_with_fees(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert abs(kalshi.summary["total_pnl_with_fees"] - (-0.27)) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_tax_basis(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert abs(kalshi.summary["total_tax_basis"] - 1.64) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_tax_proceeds(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert abs(kalshi.summary["total_tax_proceeds"] - 1.37) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_trades_list_length(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert len(kalshi.trades) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_trade_data(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
trade = kalshi.trades[0]
|
||||||
|
assert trade["ticker"] == "TESTMARKET-WIN"
|
||||||
|
assert trade["side"] == "YES"
|
||||||
|
assert trade["qty"] == 1.0
|
||||||
|
assert trade["entry"] == 0.50
|
||||||
|
assert trade["exit"] == 1.00
|
||||||
|
assert abs(trade["pnl_no_fees"] - 0.50) < 1e-6
|
||||||
|
assert abs(trade["pnl_with_fees"] - 0.47) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_irs_summary(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
irs = kalshi.irs_summary()
|
||||||
|
assert irs["box"] == "C"
|
||||||
|
assert irs["description"] == "Kalshi Event Contracts (Aggregate Summary)"
|
||||||
|
assert abs(irs["gross_proceeds"] - 1.37) < 1e-6
|
||||||
|
assert abs(irs["cost_basis"] - 1.64) < 1e-6
|
||||||
|
assert abs(irs["gain_or_loss"] - (-0.27)) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_not_found():
|
||||||
|
kalshi = KalshiCSV("/nonexistent/path.csv")
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
kalshi.parse()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue