Compare commits

...

4 commits

7 changed files with 189 additions and 22 deletions

1
.gitignore vendored
View file

@ -49,3 +49,4 @@ ENV/
# Environment variables
.env
u*-p*.sh

View file

@ -50,26 +50,59 @@ Disable colored output (useful for piping or redirecting):
kalshi-csv Kalshi-Transactions-2026.csv --no-color
```
Use ASCII characters instead of Unicode box-drawing (for terminals without UTF-8 support):
```bash
kalshi-csv Kalshi-Transactions-2026.csv --ascii
```
### Sample Output
Default mode (Unicode box-drawing):
```
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
-------------------------------------------------------------------------------
┌──────────────────────────────────┬──────┬────────┬────────┬────────┬────────────────┬────────┐
│ Ticker │ Side │ Qty │ Entry │ Exit │ P&L (No Fees) │ Fees │
├──────────────────────────────────┼──────┼────────┼────────┼────────┼────────────────┼────────┤
│ KXWCADVANCE-26JUL07ARGEGY-ARG │ YES │ 0.17 │ $0.86 │ $0.69 │ $-0.03 │ $0.00 │
│ KXWC1H-26JUL07ARGEGY-TIE │ YES │ 0.34 │ $0.28 │ $0.00 │ $-0.10 │ $0.00 │
│ KXMLBGAME-26JUL081940BOSCWS-BOS │ YES │ 0.96 │ $0.50 │ $0.91 │ $+0.39 │ $0.02 │
└──────────────────────────────────┴──────┴────────┴────────┴────────┴────────────────┴────────┘
Total Transactions Parsed: 3
Total Exchange Fees Paid: $0.05
Internal Tracked Net P&L: $0.26
-------------------------------------------------------------------------------
Total Exchange Fees Paid: $0.02
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
* (d) Gross Proceeds: $+2.50
* (e) Cost or Other Basis: $2.24
* (h) Gain or (Loss): $0.26
* (h) Gain or (Loss): $+0.26
====================================================
```
ASCII mode (`--ascii`):
```
+----------------------------------+------+--------+--------+--------+----------------+--------+
| Ticker | Side | Qty | Entry | Exit | P&L (No Fees) | Fees |
+----------------------------------+------+--------+--------+--------+----------------+--------+
| KXWCADVANCE-26JUL07ARGEGY-ARG | YES | 0.17 | $0.86 | $0.69 | $-0.03 | $0.00 |
| KXWC1H-26JUL07ARGEGY-TIE | YES | 0.34 | $0.28 | $0.00 | $-0.10 | $0.00 |
| KXMLBGAME-26JUL081940BOSCWS-BOS | YES | 0.96 | $0.50 | $0.91 | $+0.39 | $0.02 |
+----------------------------------+------+--------+--------+--------+----------------+--------+
Total Transactions Parsed: 3
Total Exchange Fees Paid: $0.02
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
====================================================
```

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "kalshi-csv"
version = "0.1.0"
version = "0.1.1"
description = "Parse Kalshi transaction CSV files and generate IRS Form 8949 tax summaries"
readme = "README.md"
license = "MIT"

View file

@ -1,4 +1,4 @@
__version__ = "0.1.0"
__version__ = "0.1.1"
from .parser import KalshiCSV

View file

@ -7,6 +7,12 @@ from .formatter import (
color_yellow,
color_cyan,
format_currency_color,
format_currency_color_padded,
truncate_ticker,
format_table_header,
format_table_separator,
format_table_row,
get_box_chars,
)
@ -24,27 +30,45 @@ def main():
action="store_true",
help="Disable ANSI color output",
)
parser.add_argument(
"--ascii",
action="store_true",
help="Use ASCII characters instead of Unicode box-drawing",
)
args = parser.parse_args()
no_color = args.no_color
ascii_mode = args.ascii
kalshi = KalshiCSV(args.csv_path)
kalshi.parse()
headers = ["Ticker", "Side", "Qty", "Entry", "Exit", "P&L (No Fees)", "Fees"]
widths = [32, 4, 6, 6, 6, 14, 6]
print()
print(
f"{'Ticker':<32} | {'Side':<4} | {'Qty':<6} | {'Entry':<6} | {'Exit':<6} | {'P&L (No Fees)':<14}"
)
print("-" * 83)
print(format_table_separator(widths, ascii_mode, "top"))
print(format_table_header(headers, widths, ascii_mode))
print(format_table_separator(widths, ascii_mode, "middle"))
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}"
pnl_str = format_currency_color_padded(trade["pnl_no_fees"], 14, no_color)
fees = trade["open_fees"] + trade["close_fees"]
ticker_display = truncate_ticker(trade["ticker"])
box = get_box_chars(ascii_mode)
row = (
f" {ticker_display:<32} "
f"{box['vertical']} {trade['side']:<4} "
f"{box['vertical']} {trade['qty']:<6.2f} "
f"{box['vertical']} ${trade['entry']:<5.2f} "
f"{box['vertical']} ${trade['exit']:<5.2f} "
f"{box['vertical']} {pnl_str} "
f"{box['vertical']} ${fees:<5.2f} "
)
print(box["vertical"] + row + box["vertical"])
print("-" * 83)
print(format_table_separator(widths, ascii_mode, "bottom"))
print(f"Total Transactions Parsed: {kalshi.summary['trade_count']}")
print(f"Total Exchange Fees Paid: ${kalshi.summary['total_fees']:.2f}")
print(

View file

@ -37,3 +37,78 @@ 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)
def format_currency_color_padded(value, width, no_color=False):
"""Returns a signed, colorized string padded to specified width before coloring."""
val_str = f"${value:+.2f}"
padded_str = f"{val_str:<{width}}"
return color_green(padded_str, no_color) if value >= 0 else color_red(padded_str, no_color)
def truncate_ticker(ticker, max_len=32):
"""Truncates ticker to max_len, using ellipsis if longer than 29 chars."""
if len(ticker) > 29:
return ticker[:29] + "..."
return ticker
UNICODE_BOX = {
"top_left": "",
"top_right": "",
"bottom_left": "",
"bottom_right": "",
"horizontal": "",
"vertical": "",
"top_tee": "",
"bottom_tee": "",
"left_tee": "",
"right_tee": "",
"cross": "",
}
ASCII_BOX = {
"top_left": "+",
"top_right": "+",
"bottom_left": "+",
"bottom_right": "+",
"horizontal": "-",
"vertical": "|",
"top_tee": "+",
"bottom_tee": "+",
"left_tee": "+",
"right_tee": "+",
"cross": "+",
}
def get_box_chars(ascii_mode=False):
"""Returns the appropriate box-drawing characters."""
return ASCII_BOX if ascii_mode else UNICODE_BOX
def format_table_header(headers, widths, ascii_mode=False):
"""Formats a table header row with box-drawing characters."""
box = get_box_chars(ascii_mode)
cells = [f" {h:<{w}} " for h, w in zip(headers, widths)]
return box["vertical"] + box["vertical"].join(cells) + box["vertical"]
def format_table_separator(widths, ascii_mode=False, position="middle"):
"""Formats a table separator line with box-drawing characters."""
box = get_box_chars(ascii_mode)
segments = [box["horizontal"] * (w + 2) for w in widths]
if position == "top":
return box["top_left"] + box["top_tee"].join(segments) + box["top_right"]
elif position == "bottom":
return box["bottom_left"] + box["bottom_tee"].join(segments) + box["bottom_right"]
else:
return box["left_tee"] + box["cross"].join(segments) + box["right_tee"]
def format_table_row(values, widths, ascii_mode=False):
"""Formats a table data row with box-drawing characters."""
box = get_box_chars(ascii_mode)
cells = [f" {str(v):<{w}} " for v, w in zip(values, widths)]
return box["vertical"] + box["vertical"].join(cells) + box["vertical"]

View file

@ -13,6 +13,7 @@ def test_cli_runs_successfully(sample_csv):
assert "TESTMARKET-WIN" in result.stdout
assert "TESTMARKET-LOSS" in result.stdout
assert "Total Transactions Parsed: 3" in result.stdout
assert "Fees" in result.stdout
def test_cli_irs_summary_output(sample_csv):
@ -65,3 +66,36 @@ def test_cli_missing_file():
)
assert result.returncode != 0
assert "not found" in result.stderr.lower() or "error" in result.stderr.lower()
def test_cli_ticker_truncation(tmp_path):
csv_file = tmp_path / "long_ticker.csv"
csv_file.write_text(
"type,quantity_fp,market_ticker,side,entry_price_dollars,exit_price_dollars,"
"open_fees_dollars,close_fees_dollars,realized_pnl_without_fees_dollars,"
"realized_pnl_with_fees_dollars,close_timestamp,open_timestamp\n"
"trade,1.00,VERYLONGTICKERNAME-THAT-EXCEEDS-THIRTY-CHARS,yes,0.50,1.00,"
"0.01,0.02,0.50,0.47,2026-07-07T12:19:57-04:00,2026-07-07T09:48:19-04:00\n"
)
result = subprocess.run(
[sys.executable, "-m", "kalshi_csv.cli", str(csv_file), "--no-color"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "VERYLONGTICKERNAME-THAT-EXCEE..." in result.stdout
assert "VERYLONGTICKERNAME-THAT-EXCEEDS-THIRTY-CHARS" not in result.stdout
def test_cli_ascii_flag(sample_csv):
result = subprocess.run(
[sys.executable, "-m", "kalshi_csv.cli", sample_csv, "--no-color", "--ascii"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "+" in result.stdout
assert "-" in result.stdout
assert "|" in result.stdout
assert "" not in result.stdout
assert "" not in result.stdout