0.1.2 - Add support for acquired and sold dates on IRS Form 8949
This commit is contained in:
parent
0c5c181506
commit
ebeaa8fdc4
7 changed files with 58 additions and 2 deletions
|
|
@ -76,6 +76,8 @@ Internal Tracked Net P&L: $+0.26
|
||||||
Use these exact aggregates for a single-line summary entry:
|
Use these exact aggregates for a single-line summary entry:
|
||||||
* Box to Check: Box C (Short-term, not reported on Form 1099-B)
|
* Box to Check: Box C (Short-term, not reported on Form 1099-B)
|
||||||
* (a) Description: Kalshi Event Contracts (Aggregate Summary)
|
* (a) Description: Kalshi Event Contracts (Aggregate Summary)
|
||||||
|
* (b) Date Acquired: 07/07/2026
|
||||||
|
* (c) Date Sold: 07/08/2026
|
||||||
* (d) Gross Proceeds: $+2.50
|
* (d) Gross Proceeds: $+2.50
|
||||||
* (e) Cost or Other Basis: $2.24
|
* (e) Cost or Other Basis: $2.24
|
||||||
* (h) Gain or (Loss): $+0.26
|
* (h) Gain or (Loss): $+0.26
|
||||||
|
|
@ -100,6 +102,8 @@ Internal Tracked Net P&L: $+0.26
|
||||||
Use these exact aggregates for a single-line summary entry:
|
Use these exact aggregates for a single-line summary entry:
|
||||||
* Box to Check: Box C (Short-term, not reported on Form 1099-B)
|
* Box to Check: Box C (Short-term, not reported on Form 1099-B)
|
||||||
* (a) Description: Kalshi Event Contracts (Aggregate Summary)
|
* (a) Description: Kalshi Event Contracts (Aggregate Summary)
|
||||||
|
* (b) Date Acquired: 07/07/2026
|
||||||
|
* (c) Date Sold: 07/08/2026
|
||||||
* (d) Gross Proceeds: $+2.50
|
* (d) Gross Proceeds: $+2.50
|
||||||
* (e) Cost or Other Basis: $2.24
|
* (e) Cost or Other Basis: $2.24
|
||||||
* (h) Gain or (Loss): $+0.26
|
* (h) Gain or (Loss): $+0.26
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "kalshi-csv"
|
name = "kalshi-csv"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
description = "Parse Kalshi transaction CSV files and generate IRS Form 8949 tax summaries"
|
description = "Parse Kalshi transaction CSV files and generate IRS Form 8949 tax summaries"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
__version__ = "0.1.1"
|
__version__ = "0.1.2"
|
||||||
|
|
||||||
from .parser import KalshiCSV
|
from .parser import KalshiCSV
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,8 @@ def main():
|
||||||
print("Use these exact aggregates for a single-line summary entry:")
|
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" * 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" * (a) Description: {irs['description']}")
|
||||||
|
print(f" * (b) Date Acquired: {irs['date_acquired']}")
|
||||||
|
print(f" * (c) Date Sold: {irs['date_sold']}")
|
||||||
print(f" * (d) Gross Proceeds: {format_currency_color(irs['gross_proceeds'], no_color)}")
|
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" * (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(f" * (h) Gain or (Loss): {format_currency_color(irs['gain_or_loss'], no_color)}")
|
||||||
|
|
@ -94,6 +96,8 @@ def main():
|
||||||
f.write("Use these exact aggregates for a single-line summary entry:\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" * 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" * (a) Description: {irs['description']}\n")
|
||||||
|
f.write(f" * (b) Date Acquired: {irs['date_acquired']}\n")
|
||||||
|
f.write(f" * (c) Date Sold: {irs['date_sold']}\n")
|
||||||
f.write(f" * (d) Gross Proceeds: ${irs['gross_proceeds']:.2f}\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" * (e) Cost or Other Basis: ${irs['cost_basis']:.2f}\n")
|
||||||
f.write(f" * (h) Gain or (Loss): ${irs['gain_or_loss']:.2f}\n")
|
f.write(f" * (h) Gain or (Loss): ${irs['gain_or_loss']:.2f}\n")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
class KalshiCSV:
|
class KalshiCSV:
|
||||||
|
|
@ -15,6 +16,8 @@ class KalshiCSV:
|
||||||
"total_pnl_with_fees": 0.0,
|
"total_pnl_with_fees": 0.0,
|
||||||
"total_tax_basis": 0.0,
|
"total_tax_basis": 0.0,
|
||||||
"total_tax_proceeds": 0.0,
|
"total_tax_proceeds": 0.0,
|
||||||
|
"earliest_open_date": None,
|
||||||
|
"latest_close_date": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
|
|
@ -57,13 +60,43 @@ class KalshiCSV:
|
||||||
self.summary["total_pnl_with_fees"] += pnl_with_fees
|
self.summary["total_pnl_with_fees"] += pnl_with_fees
|
||||||
self.summary["total_fees"] += open_fees + close_fees
|
self.summary["total_fees"] += open_fees + close_fees
|
||||||
|
|
||||||
|
if row.get("open_timestamp"):
|
||||||
|
try:
|
||||||
|
open_dt = datetime.fromisoformat(row["open_timestamp"])
|
||||||
|
if (
|
||||||
|
self.summary["earliest_open_date"] is None
|
||||||
|
or open_dt < self.summary["earliest_open_date"]
|
||||||
|
):
|
||||||
|
self.summary["earliest_open_date"] = open_dt
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if row.get("close_timestamp"):
|
||||||
|
try:
|
||||||
|
close_dt = datetime.fromisoformat(row["close_timestamp"])
|
||||||
|
if (
|
||||||
|
self.summary["latest_close_date"] is None
|
||||||
|
or close_dt > self.summary["latest_close_date"]
|
||||||
|
):
|
||||||
|
self.summary["latest_close_date"] = close_dt
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def _format_date(self, dt):
|
||||||
|
"""Formats a datetime object as MM/DD/YYYY for IRS Form 8949."""
|
||||||
|
if dt is None:
|
||||||
|
return ""
|
||||||
|
return dt.strftime("%m/%d/%Y")
|
||||||
|
|
||||||
def irs_summary(self):
|
def irs_summary(self):
|
||||||
"""Returns a dict with IRS Form 8949 aggregate fields."""
|
"""Returns a dict with IRS Form 8949 aggregate fields."""
|
||||||
return {
|
return {
|
||||||
"box": "C",
|
"box": "C",
|
||||||
"description": "Kalshi Event Contracts (Aggregate Summary)",
|
"description": "Kalshi Event Contracts (Aggregate Summary)",
|
||||||
|
"date_acquired": self._format_date(self.summary["earliest_open_date"]),
|
||||||
|
"date_sold": self._format_date(self.summary["latest_close_date"]),
|
||||||
"gross_proceeds": self.summary["total_tax_proceeds"],
|
"gross_proceeds": self.summary["total_tax_proceeds"],
|
||||||
"cost_basis": self.summary["total_tax_basis"],
|
"cost_basis": self.summary["total_tax_basis"],
|
||||||
"gain_or_loss": self.summary["total_pnl_with_fees"],
|
"gain_or_loss": self.summary["total_pnl_with_fees"],
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ def test_cli_irs_summary_output(sample_csv):
|
||||||
)
|
)
|
||||||
assert "IRS FORM 8949" in result.stdout
|
assert "IRS FORM 8949" in result.stdout
|
||||||
assert "Box C" in result.stdout
|
assert "Box C" in result.stdout
|
||||||
|
assert "Date Acquired:" in result.stdout
|
||||||
|
assert "Date Sold:" in result.stdout
|
||||||
assert "Gross Proceeds:" in result.stdout
|
assert "Gross Proceeds:" in result.stdout
|
||||||
assert "Cost or Other Basis:" in result.stdout
|
assert "Cost or Other Basis:" in result.stdout
|
||||||
assert "Gain or (Loss):" in result.stdout
|
assert "Gain or (Loss):" in result.stdout
|
||||||
|
|
@ -42,6 +44,8 @@ def test_cli_irs_file_creation(sample_csv, tmp_path):
|
||||||
content = irs_file.read_text()
|
content = irs_file.read_text()
|
||||||
assert "IRS FORM 8949" in content
|
assert "IRS FORM 8949" in content
|
||||||
assert "Box C" in content
|
assert "Box C" in content
|
||||||
|
assert "Date Acquired:" in content
|
||||||
|
assert "Date Sold:" in content
|
||||||
assert "Gross Proceeds:" in content
|
assert "Gross Proceeds:" in content
|
||||||
assert "Cost or Other Basis:" in content
|
assert "Cost or Other Basis:" in content
|
||||||
assert "Gain or (Loss):" in content
|
assert "Gain or (Loss):" in content
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,22 @@ def test_irs_summary(sample_csv):
|
||||||
irs = kalshi.irs_summary()
|
irs = kalshi.irs_summary()
|
||||||
assert irs["box"] == "C"
|
assert irs["box"] == "C"
|
||||||
assert irs["description"] == "Kalshi Event Contracts (Aggregate Summary)"
|
assert irs["description"] == "Kalshi Event Contracts (Aggregate Summary)"
|
||||||
|
assert irs["date_acquired"] == "07/07/2026"
|
||||||
|
assert irs["date_sold"] == "07/07/2026"
|
||||||
assert abs(irs["gross_proceeds"] - 1.37) < 1e-6
|
assert abs(irs["gross_proceeds"] - 1.37) < 1e-6
|
||||||
assert abs(irs["cost_basis"] - 1.64) < 1e-6
|
assert abs(irs["cost_basis"] - 1.64) < 1e-6
|
||||||
assert abs(irs["gain_or_loss"] - (-0.27)) < 1e-6
|
assert abs(irs["gain_or_loss"] - (-0.27)) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_tracking(sample_csv):
|
||||||
|
kalshi = KalshiCSV(sample_csv)
|
||||||
|
kalshi.parse()
|
||||||
|
assert kalshi.summary["earliest_open_date"] is not None
|
||||||
|
assert kalshi.summary["latest_close_date"] is not None
|
||||||
|
assert kalshi.summary["earliest_open_date"].strftime("%m/%d/%Y") == "07/07/2026"
|
||||||
|
assert kalshi.summary["latest_close_date"].strftime("%m/%d/%Y") == "07/07/2026"
|
||||||
|
|
||||||
|
|
||||||
def test_file_not_found():
|
def test_file_not_found():
|
||||||
kalshi = KalshiCSV("/nonexistent/path.csv")
|
kalshi = KalshiCSV("/nonexistent/path.csv")
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue