Finalizing of 0.2.0 changes
This commit is contained in:
parent
f7171c025c
commit
932fd06f71
9 changed files with 381 additions and 4 deletions
123
README.md
123
README.md
|
|
@ -8,6 +8,13 @@ Parse Kalshi transaction CSV files and generate IRS Form 8949 tax summaries for
|
|||
pip install kalshi-csv
|
||||
```
|
||||
|
||||
## What's New in 0.2.0
|
||||
|
||||
- **Summary Cards**: View key metrics at a glance - Net Realized P&L, Win/Loss Record, Total Volume, and Best/Worst Single Trade
|
||||
- **Market Breakdown**: See performance by market category with trade counts, win rates, and net P&L
|
||||
- **Legacy Web Mode**: Browse your portfolio in a retro HTML 4.01 web interface compatible with older browsers (Netscape Navigator, IE 4+)
|
||||
- **Market Categorization**: Automatic categorization of tickers into 7 market types (Global Soccer, MLB, NPB, NBA Summer League, WNBA, S&P 500, Multivariate Events, Other Markets)
|
||||
|
||||
## Getting Your Transactions CSV
|
||||
|
||||
Download your transaction history from Kalshi:
|
||||
|
|
@ -32,7 +39,7 @@ Rows without `realized_pnl_without_fees_dollars` are automatically skipped.
|
|||
|
||||
## CLI Usage
|
||||
|
||||
Parse a Kalshi transactions CSV and display the trade matrix with IRS summary:
|
||||
Parse a Kalshi transactions CSV and display the trade matrix with summary cards, market breakdown, and IRS summary:
|
||||
|
||||
```bash
|
||||
kalshi-csv Kalshi-Transactions-2026.csv
|
||||
|
|
@ -56,6 +63,18 @@ Use ASCII characters instead of Unicode box-drawing (for terminals without UTF-8
|
|||
kalshi-csv Kalshi-Transactions-2026.csv --ascii
|
||||
```
|
||||
|
||||
Start a legacy web server to view your portfolio in a browser (HTML 4.01 compatible with older browsers):
|
||||
|
||||
```bash
|
||||
kalshi-csv Kalshi-Transactions-2026.csv --legacy-web
|
||||
```
|
||||
|
||||
Specify a custom port for the legacy web server:
|
||||
|
||||
```bash
|
||||
kalshi-csv Kalshi-Transactions-2026.csv --legacy-web --legacy-web-port 3000
|
||||
```
|
||||
|
||||
### Sample Output
|
||||
|
||||
Default mode (Unicode box-drawing):
|
||||
|
|
@ -72,6 +91,27 @@ Total Transactions Parsed: 3
|
|||
Total Exchange Fees Paid: $0.02
|
||||
Internal Tracked Net P&L: $+0.26
|
||||
-----------------------------------------------------------------------------------
|
||||
|
||||
┌────────────────────────────┬────────────────────────────┬────────────────────────────┬────────────────────────────┐
|
||||
│ NET REALIZED P&L │ WIN / LOSS RECORD │ TOTAL VOLUME │ BEST/WORST SINGLE │
|
||||
├────────────────────────────┼────────────────────────────┼────────────────────────────┼────────────────────────────┤
|
||||
│ $+0.26 │ 2 - 1 │ 3 │ $+0.39 / $-0.10 │
|
||||
│ Includes $0.02 fees │ 0 Pushes (66.7% Win) │ Executed Contracts │ MLB Baseball / S&P 500 │
|
||||
└────────────────────────────┴────────────────────────────┴────────────────────────────┴────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────┬────────────┬──────────────┬──────────────────┐
|
||||
│ ASSET CLASS / MARKET │ TRADES │ WIN RATE │ NET P&L │
|
||||
├─────────────────────────────────────┼────────────┼──────────────┼──────────────────┤
|
||||
│ Global Soccer / Football │ 168 │ 54.8% │ $+15.15 │
|
||||
│ S&P 500 (INXU Intraday) │ 110 │ 50.9% │ $-32.21 │
|
||||
│ Other Markets │ 87 │ 35.6% │ $-28.63 │
|
||||
│ MLB Baseball │ 44 │ 40.9% │ $-13.06 │
|
||||
│ NBA Summer League │ 43 │ 46.5% │ $-7.09 │
|
||||
│ NPB Baseball (Japan) │ 40 │ 47.5% │ $-10.68 │
|
||||
│ Multivariate Events │ 23 │ 8.7% │ $-13.48 │
|
||||
│ WNBA Basketball │ 6 │ 0.0% │ $-5.87 │
|
||||
└─────────────────────────────────────┴────────────┴──────────────┴──────────────────┘
|
||||
|
||||
=== 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)
|
||||
|
|
@ -98,6 +138,27 @@ Total Transactions Parsed: 3
|
|||
Total Exchange Fees Paid: $0.02
|
||||
Internal Tracked Net P&L: $+0.26
|
||||
-----------------------------------------------------------------------------------
|
||||
|
||||
+----------------------------+----------------------------+----------------------------+----------------------------+
|
||||
| NET REALIZED P&L | WIN / LOSS RECORD | TOTAL VOLUME | BEST/WORST SINGLE |
|
||||
+----------------------------+----------------------------+----------------------------+----------------------------+
|
||||
| $+0.26 | 2 - 1 | 3 | $+0.39 / $-0.10 |
|
||||
| Includes $0.02 fees | 0 Pushes (66.7% Win) | Executed Contracts | MLB Baseball / S&P 500 |
|
||||
+----------------------------+----------------------------+----------------------------+----------------------------+
|
||||
|
||||
+-------------------------------------+------------+--------------+------------------+
|
||||
| ASSET CLASS / MARKET | TRADES | WIN RATE | NET P&L |
|
||||
+-------------------------------------+------------+--------------+------------------+
|
||||
| Global Soccer / Football | 168 | 54.8% | $+15.15 |
|
||||
| S&P 500 (INXU Intraday) | 110 | 50.9% | $-32.21 |
|
||||
| Other Markets | 87 | 35.6% | $-28.63 |
|
||||
| MLB Baseball | 44 | 40.9% | $-13.06 |
|
||||
| NBA Summer League | 43 | 46.5% | $-7.09 |
|
||||
| NPB Baseball (Japan) | 40 | 47.5% | $-10.68 |
|
||||
| Multivariate Events | 23 | 8.7% | $-13.48 |
|
||||
| WNBA Basketball | 6 | 0.0% | $-5.87 |
|
||||
+-------------------------------------+------------+--------------+------------------+
|
||||
|
||||
=== 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)
|
||||
|
|
@ -110,6 +171,48 @@ Use these exact aggregates for a single-line summary entry:
|
|||
====================================================
|
||||
```
|
||||
|
||||
## Legacy Web Mode
|
||||
|
||||
View your portfolio in a web browser with a retro HTML 4.01 interface compatible with older browsers (Netscape Navigator, IE 4+):
|
||||
|
||||
```bash
|
||||
kalshi-csv Kalshi-Transactions-2026.csv --legacy-web
|
||||
```
|
||||
|
||||
This starts an HTTP server on `0.0.0.0:8080` by default. Access it from any machine on your network by navigating to `http://<your-ip>:8080`.
|
||||
|
||||
To use a different port:
|
||||
|
||||
```bash
|
||||
kalshi-csv Kalshi-Transactions-2026.csv --legacy-web --legacy-web-port 3000
|
||||
```
|
||||
|
||||
### What's Displayed
|
||||
|
||||
The web interface shows:
|
||||
|
||||
- **Summary Cards**: Net Realized P&L, Win/Loss Record, Total Volume, Best/Worst Single Trade
|
||||
- **Market Breakdown**: Performance by category with trade counts, win rates, and net P&L
|
||||
- **Recent Closed Positions**: Last 20 trades with timestamps, tickers, sides, quantities, entry/exit prices, and P&L
|
||||
- **IRS Form 8949 Summary**: Tax reporting data including gross proceeds, cost basis, and gain/loss
|
||||
|
||||
The interface uses pure HTML 4.01 table layout with no CSS or JavaScript, ensuring compatibility with legacy browsers.
|
||||
|
||||
## Market Categorization
|
||||
|
||||
The tool automatically categorizes market tickers into the following categories:
|
||||
|
||||
- **Global Soccer / Football**: World Cup, Champions League, Europa League, Brasileirão, Argentino, Liga MX, and other soccer leagues
|
||||
- **MLB Baseball**: Major League Baseball games and derivatives
|
||||
- **NPB Baseball (Japan)**: Nippon Professional Baseball
|
||||
- **NBA Summer League**: NBA Summer League games
|
||||
- **WNBA Basketball**: Women's National Basketball Association
|
||||
- **S&P 500 (INXU Intraday)**: S&P 500 index intraday contracts
|
||||
- **Multivariate Events**: Multivariate Event (MVE) markets - parlay-style markets linking multiple individual event outcomes together
|
||||
- **Other Markets**: Weather, politics, crypto, and all other markets
|
||||
|
||||
Categories are determined by analyzing ticker prefixes (e.g., `KXMLBGAME` → MLB Baseball, `KXINXU` → S&P 500).
|
||||
|
||||
## Library API
|
||||
|
||||
Use `kalshi-csv` as a Python library in your own scripts:
|
||||
|
|
@ -135,6 +238,16 @@ 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}")
|
||||
|
||||
# Get market breakdown by category
|
||||
breakdown = kalshi.market_breakdown()
|
||||
for item in breakdown:
|
||||
print(f"{item['category']}: {item['trades']} trades, {item['win_rate']:.1f}% win, ${item['net_pnl']:+.2f}")
|
||||
|
||||
# Get recent closed positions
|
||||
recent = kalshi.recent_closed_positions(10)
|
||||
for trade in recent:
|
||||
print(f"{trade['close_timestamp']}: {trade['ticker']} ${trade['pnl_with_fees']:+.2f}")
|
||||
```
|
||||
|
||||
### Data Structures
|
||||
|
|
@ -149,6 +262,9 @@ print(f"Gain/Loss: ${irs['gain_or_loss']:.2f}")
|
|||
- `pnl_with_fees`: P&L including fees
|
||||
- `open_fees`: Opening fees
|
||||
- `close_fees`: Closing fees
|
||||
- `open_timestamp`: When the position was opened (datetime object or None)
|
||||
- `close_timestamp`: When the position was closed (datetime object or None)
|
||||
- `market_category`: Categorized market type (e.g., "MLB Baseball", "Global Soccer / Football")
|
||||
|
||||
**Summary dict** (`kalshi.summary`):
|
||||
- `trade_count`: Number of trades parsed
|
||||
|
|
@ -157,6 +273,11 @@ print(f"Gain/Loss: ${irs['gain_or_loss']:.2f}")
|
|||
- `total_pnl_with_fees`: Total P&L including fees
|
||||
- `total_tax_basis`: Total cost basis for IRS reporting
|
||||
- `total_tax_proceeds`: Total proceeds for IRS reporting
|
||||
- `wins`: Number of winning trades (pnl_with_fees > 0)
|
||||
- `losses`: Number of losing trades (pnl_with_fees < 0)
|
||||
- `pushes`: Number of break-even trades (pnl_with_fees == 0)
|
||||
- `best_trade`: Trade dict with highest pnl_with_fees (or None)
|
||||
- `worst_trade`: Trade dict with lowest pnl_with_fees (or None)
|
||||
|
||||
**IRS summary dict** (`kalshi.irs_summary()`):
|
||||
- `box`: "C" (for Form 8949 Box C)
|
||||
|
|
|
|||
BIN
kalshi-csv.jpg
Normal file
BIN
kalshi-csv.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 994 B |
|
|
@ -8,7 +8,7 @@ TICKER_CATEGORY_MAP = {
|
|||
"KXWNBA": "WNBA Basketball",
|
||||
"KXINXU": "S&P 500 (INXU Intraday)",
|
||||
"KXINX": "S&P 500 (INXU Intraday)",
|
||||
"KXMVESPORTS": "Esports & Gaming",
|
||||
"KXMVE": "Multivariate Events",
|
||||
}
|
||||
|
||||
SOCCER_PREFIXES = [
|
||||
|
|
|
|||
|
|
@ -14,9 +14,17 @@ from .formatter import (
|
|||
format_table_separator,
|
||||
format_table_row,
|
||||
get_box_chars,
|
||||
pad_colored_text,
|
||||
)
|
||||
|
||||
|
||||
def truncate_text(text, max_len):
|
||||
"""Truncates text to max_len, using ellipsis if longer than max_len-3 chars."""
|
||||
if len(text) > max_len:
|
||||
return text[:max_len-3] + "..."
|
||||
return text
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse Kalshi transaction CSV and generate IRS tax summary."
|
||||
|
|
@ -96,6 +104,72 @@ def main():
|
|||
)
|
||||
print("-" * 83)
|
||||
|
||||
# Summary cards section
|
||||
print()
|
||||
card_widths = [25, 25, 25, 25]
|
||||
|
||||
best_pnl = kalshi.summary["best_trade"]["pnl_with_fees"] if kalshi.summary["best_trade"] else 0
|
||||
worst_pnl = kalshi.summary["worst_trade"]["pnl_with_fees"] if kalshi.summary["worst_trade"] else 0
|
||||
best_cat = kalshi.summary["best_trade"]["market_category"] if kalshi.summary["best_trade"] else "N/A"
|
||||
worst_cat = kalshi.summary["worst_trade"]["market_category"] if kalshi.summary["worst_trade"] else "N/A"
|
||||
|
||||
card_labels = ["NET REALIZED P&L", "WIN / LOSS RECORD", "TOTAL VOLUME", "BEST/WORST SINGLE"]
|
||||
|
||||
best_worst_combined = format_currency_color(best_pnl, no_color) + " / " + format_currency_color(worst_pnl, no_color)
|
||||
|
||||
card_values = [
|
||||
format_currency_color_padded(kalshi.summary["total_pnl_with_fees"], 25, no_color),
|
||||
truncate_text(f"{kalshi.summary['wins']} - {kalshi.summary['losses']}", 25),
|
||||
truncate_text(str(kalshi.summary['trade_count']), 25),
|
||||
pad_colored_text(best_worst_combined, 25, no_color),
|
||||
]
|
||||
card_subtext = [
|
||||
truncate_text(f"Includes ${kalshi.summary['total_fees']:.2f} fees", 25),
|
||||
truncate_text(f"{kalshi.summary['pushes']} Push{'s' if kalshi.summary['pushes'] != 1 else ''} ({kalshi.summary['wins'] / kalshi.summary['trade_count'] * 100 if kalshi.summary['trade_count'] > 0 else 0:.1f}% Win)", 25),
|
||||
truncate_text("Executed Contracts", 25),
|
||||
truncate_text(f"{best_cat} / {worst_cat}", 25),
|
||||
]
|
||||
|
||||
print(format_table_separator(card_widths, ascii_mode, "top"))
|
||||
print(format_table_row(card_labels, card_widths, ascii_mode))
|
||||
print(format_table_separator(card_widths, ascii_mode, "middle"))
|
||||
|
||||
box = get_box_chars(ascii_mode)
|
||||
value_row = (
|
||||
f" {card_values[0]} "
|
||||
f"{box['vertical']} {card_values[1]:<25} "
|
||||
f"{box['vertical']} {card_values[2]:<25} "
|
||||
f"{box['vertical']} {card_values[3]} "
|
||||
)
|
||||
print(box["vertical"] + value_row + box["vertical"])
|
||||
|
||||
print(format_table_row(card_subtext, card_widths, ascii_mode))
|
||||
print(format_table_separator(card_widths, ascii_mode, "bottom"))
|
||||
print()
|
||||
|
||||
# Market breakdown section
|
||||
market_headers = ["ASSET CLASS / MARKET", "TRADES", "WIN RATE", "NET P&L"]
|
||||
market_widths = [35, 10, 12, 16]
|
||||
print(format_table_separator(market_widths, ascii_mode, "top"))
|
||||
print(format_table_header(market_headers, market_widths, ascii_mode))
|
||||
print(format_table_separator(market_widths, ascii_mode, "middle"))
|
||||
|
||||
for item in kalshi.market_breakdown():
|
||||
category = item["category"][:33] + ".." if len(item["category"]) > 35 else item["category"]
|
||||
trades = str(item["trades"])
|
||||
win_rate = f"{item['win_rate']:.1f}%"
|
||||
net_pnl = format_currency_color_padded(item["net_pnl"], 16, no_color)
|
||||
row = (
|
||||
f" {category:<35} "
|
||||
f"{get_box_chars(ascii_mode)['vertical']} {trades:<10} "
|
||||
f"{get_box_chars(ascii_mode)['vertical']} {win_rate:<12} "
|
||||
f"{get_box_chars(ascii_mode)['vertical']} {net_pnl} "
|
||||
)
|
||||
print(get_box_chars(ascii_mode)['vertical'] + row + get_box_chars(ascii_mode)['vertical'])
|
||||
|
||||
print(format_table_separator(market_widths, ascii_mode, "bottom"))
|
||||
print()
|
||||
|
||||
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:")
|
||||
|
|
|
|||
|
|
@ -46,6 +46,28 @@ def format_currency_color_padded(value, width, no_color=False):
|
|||
return color_green(padded_str, no_color) if value >= 0 else color_red(padded_str, no_color)
|
||||
|
||||
|
||||
def pad_colored_text(text, width, no_color=False):
|
||||
"""Pads a colored string to a specific visible width, accounting for ANSI codes."""
|
||||
if no_color:
|
||||
return f"{text:<{width}}"
|
||||
|
||||
# Strip ANSI codes to get visible length
|
||||
import re
|
||||
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
||||
visible_text = ansi_escape.sub('', text)
|
||||
visible_len = len(visible_text)
|
||||
|
||||
if visible_len >= width:
|
||||
return text
|
||||
|
||||
# Add padding to the end (before the final reset code if present)
|
||||
padding = ' ' * (width - visible_len)
|
||||
if text.endswith('\x1B[0m'):
|
||||
return text[:-4] + padding + '\x1B[0m'
|
||||
else:
|
||||
return text + padding
|
||||
|
||||
|
||||
def truncate_ticker(ticker, max_len=32):
|
||||
"""Truncates ticker to max_len, using ellipsis if longer than 29 chars."""
|
||||
if len(ticker) > 29:
|
||||
|
|
|
|||
|
|
@ -87,6 +87,59 @@ def render_portfolio_html(kalshi, csv_filename):
|
|||
<td align="right"><font face="Courier New, Courier, monospace" size="1" color="{pnl_color}"><b>{pnl_str}</b></font></td>
|
||||
</tr>"""
|
||||
|
||||
# IRS summary section
|
||||
irs = kalshi.irs_summary()
|
||||
gross_proceeds_color = "#006600" if irs["gross_proceeds"] >= 0 else "#990000"
|
||||
cost_basis_color = "#006600" if irs["cost_basis"] >= 0 else "#990000"
|
||||
gain_loss_color = "#006600" if irs["gain_or_loss"] >= 0 else "#990000"
|
||||
irs_html = f"""
|
||||
<tr><td><br><hr size="1" color="#CCCCCC" noshade><br></td></tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<font face="Georgia, Times New Roman, serif" size="3"><b>IRS Form 8949 / Schedule D Summary</b></font>
|
||||
<br><br>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="4">
|
||||
<tr>
|
||||
<td width="40%" align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Box to Check:</font></td>
|
||||
<td width="60%" align="left"><font face="Courier New, Courier, monospace" size="2"><b>{html.escape(irs['box'])}</b></font></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><hr size="1" color="#E0E0E0" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Description:</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="2">{html.escape(irs['description'])}</font></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><hr size="1" color="#E0E0E0" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Date Acquired:</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="2">{html.escape(irs['date_acquired'])}</font></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><hr size="1" color="#E0E0E0" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Date Sold:</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="2">{html.escape(irs['date_sold'])}</font></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><hr size="1" color="#E0E0E0" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Gross Proceeds:</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="2" color="{gross_proceeds_color}"><b>${irs['gross_proceeds']:.2f}</b></font></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><hr size="1" color="#E0E0E0" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Cost or Other Basis:</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="2" color="{cost_basis_color}"><b>${irs['cost_basis']:.2f}</b></font></td>
|
||||
</tr>
|
||||
<tr><td colspan="2"><hr size="1" color="#E0E0E0" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="1" color="#666666">Gain or (Loss):</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="2" color="{gain_loss_color}"><b>${irs['gain_or_loss']:+.2f}</b></font></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
page = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -182,6 +235,8 @@ def render_portfolio_html(kalshi, csv_filename):
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
{irs_html}
|
||||
|
||||
<tr><td><br><hr size="2" color="#111111" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ def test_sp500_categorization():
|
|||
assert categorize_ticker("KXINX-26JUL08H1400-T7479.9999") == "S&P 500 (INXU Intraday)"
|
||||
|
||||
|
||||
def test_esports_categorization():
|
||||
assert categorize_ticker("KXMVESPORTSMULTIGAMEEXTENDED-S2026769CE3FA3F9-6D4DB2E2128") == "Esports & Gaming"
|
||||
def test_multivariate_events_categorization():
|
||||
assert categorize_ticker("KXMVESPORTSMULTIGAMEEXTENDED-S2026769CE3FA3F9-6D4DB2E2128") == "Multivariate Events"
|
||||
assert categorize_ticker("KXMVECROSSCATEGORY-S2026AC77F3A8C7A-6D4DB2E2128") == "Multivariate Events"
|
||||
|
||||
|
||||
def test_soccer_categorization():
|
||||
|
|
|
|||
|
|
@ -124,3 +124,73 @@ def test_cli_legacy_web_port_default_in_help():
|
|||
)
|
||||
assert result.returncode == 0
|
||||
assert "8080" in result.stdout
|
||||
|
||||
|
||||
def test_cli_summary_cards_output(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 "NET REALIZED P&L" in result.stdout
|
||||
assert "WIN / LOSS RECORD" in result.stdout
|
||||
assert "TOTAL VOLUME" in result.stdout
|
||||
assert "BEST/WORST SINGLE" in result.stdout
|
||||
|
||||
|
||||
def test_cli_market_breakdown_output(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 "ASSET CLASS / MARKET" in result.stdout
|
||||
assert "TRADES" in result.stdout
|
||||
assert "WIN RATE" in result.stdout
|
||||
assert "NET P&L" in result.stdout
|
||||
assert "Other Markets" in result.stdout
|
||||
|
||||
|
||||
def test_cli_summary_cards_box_drawing(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 "┌" in result.stdout
|
||||
assert "└" in result.stdout
|
||||
assert "┬" in result.stdout
|
||||
assert "┴" in result.stdout
|
||||
|
||||
|
||||
def test_cli_summary_cards_before_irs(sample_csv):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "kalshi_csv.cli", sample_csv, "--no-color"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
output = result.stdout
|
||||
summary_pos = output.find("NET REALIZED P&L")
|
||||
irs_pos = output.find("IRS FORM 8949")
|
||||
assert summary_pos > 0
|
||||
assert irs_pos > 0
|
||||
assert summary_pos < irs_pos
|
||||
|
||||
|
||||
def test_cli_market_breakdown_before_irs(sample_csv):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "kalshi_csv.cli", sample_csv, "--no-color"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
output = result.stdout
|
||||
market_pos = output.find("ASSET CLASS / MARKET")
|
||||
irs_pos = output.find("IRS FORM 8949")
|
||||
assert market_pos > 0
|
||||
assert irs_pos > 0
|
||||
assert market_pos < irs_pos
|
||||
|
|
|
|||
|
|
@ -94,3 +94,37 @@ def test_render_html_escapes_html_in_tickers(sample_csv):
|
|||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "<script>alert('xss')</script>" not in html_content
|
||||
assert "<script>" in html_content
|
||||
|
||||
|
||||
def test_render_html_contains_irs_section(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "IRS Form 8949 / Schedule D Summary" in html_content
|
||||
assert "Box to Check:" in html_content
|
||||
assert "Description:" in html_content
|
||||
assert "Date Acquired:" in html_content
|
||||
assert "Date Sold:" in html_content
|
||||
assert "Gross Proceeds:" in html_content
|
||||
assert "Cost or Other Basis:" in html_content
|
||||
assert "Gain or (Loss):" in html_content
|
||||
|
||||
|
||||
def test_render_html_irs_values(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert ">C<" in html_content
|
||||
assert "Kalshi Event Contracts (Aggregate Summary)" in html_content
|
||||
assert "07/07/2026" in html_content
|
||||
|
||||
|
||||
def test_render_html_irs_after_positions(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
positions_pos = html_content.find("Recent Closed Positions")
|
||||
irs_pos = html_content.find("IRS Form 8949")
|
||||
assert positions_pos > 0
|
||||
assert irs_pos > 0
|
||||
assert positions_pos < irs_pos
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue