Compare commits
4 commits
abb935f739
...
0c5c181506
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c5c181506 | |||
| ef75e3e5f0 | |||
| 4c70fbd27a | |||
| 75dbaf176f |
7 changed files with 189 additions and 22 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -49,3 +49,4 @@ ENV/
|
|||
|
||||
# Environment variables
|
||||
.env
|
||||
u*-p*.sh
|
||||
|
|
|
|||
55
README.md
55
README.md
|
|
@ -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
|
||||
====================================================
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = "0.1.0"
|
||||
__version__ = "0.1.1"
|
||||
|
||||
from .parser import KalshiCSV
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue