Start of work on 0.2.0, add legacy compatible web view
This commit is contained in:
parent
ebeaa8fdc4
commit
f7171c025c
10 changed files with 642 additions and 23 deletions
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "kalshi-csv"
|
||||
version = "0.1.2"
|
||||
version = "0.2.0"
|
||||
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.2"
|
||||
__version__ = "0.2.0"
|
||||
|
||||
from .parser import KalshiCSV
|
||||
|
||||
|
|
|
|||
44
src/kalshi_csv/categories.py
Normal file
44
src/kalshi_csv/categories.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
TICKER_CATEGORY_MAP = {
|
||||
"KXMLB": "MLB Baseball",
|
||||
"KXMLBHR": "MLB Baseball",
|
||||
"KXMLBMEN": "MLB Baseball",
|
||||
"KXNPB": "NPB Baseball (Japan)",
|
||||
"KXNBASUMMER": "NBA Summer League",
|
||||
"KXNEXTTEAMNBA": "NBA Summer League",
|
||||
"KXWNBA": "WNBA Basketball",
|
||||
"KXINXU": "S&P 500 (INXU Intraday)",
|
||||
"KXINX": "S&P 500 (INXU Intraday)",
|
||||
"KXMVESPORTS": "Esports & Gaming",
|
||||
}
|
||||
|
||||
SOCCER_PREFIXES = [
|
||||
"KXWC",
|
||||
"KXUCL",
|
||||
"KXUECL",
|
||||
"KXBRASILEIRO",
|
||||
"KXALLSVENSKAN",
|
||||
"KXELITESERIEN",
|
||||
"KXECULP",
|
||||
"KXLIGAMX",
|
||||
"KXLIGAEXP",
|
||||
"KXKLEAGUE",
|
||||
"KXCLUBF",
|
||||
"KXSCOCUP",
|
||||
"KXURYPD",
|
||||
"KXDIMAYOR",
|
||||
"KXARGPREM",
|
||||
"KXBOLP",
|
||||
]
|
||||
|
||||
|
||||
def categorize_ticker(ticker):
|
||||
"""Maps a Kalshi market ticker to a human-readable category."""
|
||||
for prefix, category in TICKER_CATEGORY_MAP.items():
|
||||
if ticker.startswith(prefix):
|
||||
return category
|
||||
|
||||
for prefix in SOCCER_PREFIXES:
|
||||
if ticker.startswith(prefix):
|
||||
return "Global Soccer / Football"
|
||||
|
||||
return "Other Markets"
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .parser import KalshiCSV
|
||||
|
|
@ -35,6 +36,17 @@ def main():
|
|||
action="store_true",
|
||||
help="Use ASCII characters instead of Unicode box-drawing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--legacy-web",
|
||||
action="store_true",
|
||||
help="Start a legacy web server (HTML 4.01) to view portfolio in browser",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--legacy-web-port",
|
||||
type=int,
|
||||
default=8080,
|
||||
help="Port for legacy web server (default: 8080)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
no_color = args.no_color
|
||||
|
|
@ -43,6 +55,13 @@ def main():
|
|||
kalshi = KalshiCSV(args.csv_path)
|
||||
kalshi.parse()
|
||||
|
||||
if args.legacy_web:
|
||||
from .web import LegacyWebServer
|
||||
csv_filename = os.path.basename(args.csv_path)
|
||||
server = LegacyWebServer(kalshi, csv_filename, port=args.legacy_web_port)
|
||||
server.serve()
|
||||
return
|
||||
|
||||
headers = ["Ticker", "Side", "Qty", "Entry", "Exit", "P&L (No Fees)", "Fees"]
|
||||
widths = [32, 4, 6, 6, 6, 14, 6]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import csv
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
from .categories import categorize_ticker
|
||||
|
||||
|
||||
class KalshiCSV:
|
||||
"""Parses Kalshi transaction CSV data and calculates tax-relevant aggregates."""
|
||||
|
|
@ -18,6 +21,11 @@ class KalshiCSV:
|
|||
"total_tax_proceeds": 0.0,
|
||||
"earliest_open_date": None,
|
||||
"latest_close_date": None,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"pushes": 0,
|
||||
"best_trade": None,
|
||||
"worst_trade": None,
|
||||
}
|
||||
|
||||
def parse(self):
|
||||
|
|
@ -40,8 +48,22 @@ class KalshiCSV:
|
|||
open_fees = float(row["open_fees_dollars"])
|
||||
close_fees = float(row["close_fees_dollars"])
|
||||
|
||||
open_dt = None
|
||||
close_dt = None
|
||||
if row.get("open_timestamp"):
|
||||
try:
|
||||
open_dt = datetime.fromisoformat(row["open_timestamp"])
|
||||
except ValueError:
|
||||
pass
|
||||
if row.get("close_timestamp"):
|
||||
try:
|
||||
close_dt = datetime.fromisoformat(row["close_timestamp"])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
ticker = row["market_ticker"]
|
||||
trade = {
|
||||
"ticker": row["market_ticker"],
|
||||
"ticker": ticker,
|
||||
"side": row["side"].upper(),
|
||||
"qty": qty,
|
||||
"entry": entry,
|
||||
|
|
@ -50,6 +72,9 @@ class KalshiCSV:
|
|||
"pnl_with_fees": pnl_with_fees,
|
||||
"open_fees": open_fees,
|
||||
"close_fees": close_fees,
|
||||
"open_timestamp": open_dt,
|
||||
"close_timestamp": close_dt,
|
||||
"market_category": categorize_ticker(ticker),
|
||||
}
|
||||
self.trades.append(trade)
|
||||
|
||||
|
|
@ -60,27 +85,37 @@ class KalshiCSV:
|
|||
self.summary["total_pnl_with_fees"] += pnl_with_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 pnl_with_fees > 0:
|
||||
self.summary["wins"] += 1
|
||||
elif pnl_with_fees < 0:
|
||||
self.summary["losses"] += 1
|
||||
else:
|
||||
self.summary["pushes"] += 1
|
||||
|
||||
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
|
||||
if (
|
||||
self.summary["best_trade"] is None
|
||||
or pnl_with_fees > self.summary["best_trade"]["pnl_with_fees"]
|
||||
):
|
||||
self.summary["best_trade"] = trade
|
||||
if (
|
||||
self.summary["worst_trade"] is None
|
||||
or pnl_with_fees < self.summary["worst_trade"]["pnl_with_fees"]
|
||||
):
|
||||
self.summary["worst_trade"] = trade
|
||||
|
||||
if open_dt is not None:
|
||||
if (
|
||||
self.summary["earliest_open_date"] is None
|
||||
or open_dt < self.summary["earliest_open_date"]
|
||||
):
|
||||
self.summary["earliest_open_date"] = open_dt
|
||||
|
||||
if close_dt is not None:
|
||||
if (
|
||||
self.summary["latest_close_date"] is None
|
||||
or close_dt > self.summary["latest_close_date"]
|
||||
):
|
||||
self.summary["latest_close_date"] = close_dt
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -101,3 +136,36 @@ class KalshiCSV:
|
|||
"cost_basis": self.summary["total_tax_basis"],
|
||||
"gain_or_loss": self.summary["total_pnl_with_fees"],
|
||||
}
|
||||
|
||||
def market_breakdown(self):
|
||||
"""Returns a list of dicts with market category breakdown sorted by trade count."""
|
||||
categories = defaultdict(lambda: {"trades": 0, "wins": 0, "net_pnl": 0.0})
|
||||
|
||||
for trade in self.trades:
|
||||
cat = trade["market_category"]
|
||||
categories[cat]["trades"] += 1
|
||||
categories[cat]["net_pnl"] += trade["pnl_with_fees"]
|
||||
if trade["pnl_with_fees"] > 0:
|
||||
categories[cat]["wins"] += 1
|
||||
|
||||
breakdown = []
|
||||
for cat, data in categories.items():
|
||||
win_rate = (data["wins"] / data["trades"] * 100) if data["trades"] > 0 else 0
|
||||
breakdown.append({
|
||||
"category": cat,
|
||||
"trades": data["trades"],
|
||||
"win_rate": win_rate,
|
||||
"net_pnl": data["net_pnl"],
|
||||
})
|
||||
|
||||
return sorted(breakdown, key=lambda x: x["trades"], reverse=True)
|
||||
|
||||
def recent_closed_positions(self, n=20):
|
||||
"""Returns the last n trades sorted by close_timestamp descending."""
|
||||
trades_with_close = [t for t in self.trades if t["close_timestamp"] is not None]
|
||||
sorted_trades = sorted(
|
||||
trades_with_close,
|
||||
key=lambda t: t["close_timestamp"],
|
||||
reverse=True,
|
||||
)
|
||||
return sorted_trades[:n]
|
||||
|
|
|
|||
239
src/kalshi_csv/web.py
Normal file
239
src/kalshi_csv/web.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import html
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
from . import __version__
|
||||
|
||||
|
||||
def render_portfolio_html(kalshi, csv_filename):
|
||||
"""Renders the full HTML 4.01 portfolio page from parsed Kalshi data."""
|
||||
summary = kalshi.summary
|
||||
market_breakdown = kalshi.market_breakdown()
|
||||
recent_positions = kalshi.recent_closed_positions(20)
|
||||
|
||||
period_end = summary["latest_close_date"]
|
||||
if period_end:
|
||||
period_end_str = period_end.strftime("%B %d, %Y").upper()
|
||||
else:
|
||||
period_end_str = "N/A"
|
||||
|
||||
net_pnl = summary["total_pnl_with_fees"]
|
||||
net_pnl_color = "#006600" if net_pnl >= 0 else "#990000"
|
||||
net_pnl_str = f"${net_pnl:+.2f}"
|
||||
|
||||
wins = summary["wins"]
|
||||
losses = summary["losses"]
|
||||
pushes = summary["pushes"]
|
||||
total = summary["trade_count"]
|
||||
win_pct = (wins / total * 100) if total > 0 else 0
|
||||
push_note = f" ({pushes} Push{'s' if pushes != 1 else ''})" if pushes > 0 else ""
|
||||
|
||||
best_trade = summary["best_trade"]
|
||||
worst_trade = summary["worst_trade"]
|
||||
if best_trade and worst_trade:
|
||||
best_pnl = best_trade["pnl_with_fees"]
|
||||
worst_pnl = worst_trade["pnl_with_fees"]
|
||||
best_cat = best_trade["market_category"]
|
||||
worst_cat = worst_trade["market_category"]
|
||||
best_color = "#006600" if best_pnl >= 0 else "#990000"
|
||||
worst_color = "#006600" if worst_pnl >= 0 else "#990000"
|
||||
best_str = f"${best_pnl:+.2f}"
|
||||
worst_str = f"${worst_pnl:+.2f}"
|
||||
best_worst_subtext = f"{best_cat} / {worst_cat}"
|
||||
else:
|
||||
best_str = "$0.00"
|
||||
worst_str = "$0.00"
|
||||
best_color = "#006600"
|
||||
worst_color = "#990000"
|
||||
best_worst_subtext = "N/A"
|
||||
|
||||
rows_html = ""
|
||||
for i, item in enumerate(market_breakdown):
|
||||
pnl = item["net_pnl"]
|
||||
pnl_color = "#006600" if pnl >= 0 else "#990000"
|
||||
pnl_str = f"${pnl:+.2f}"
|
||||
win_rate_str = f"{item['win_rate']:.1f}%"
|
||||
rows_html += f"""
|
||||
<tr>
|
||||
<td align="left"><font face="Geneva, Verdana, sans-serif" size="2">{html.escape(item['category'])}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="2">{item['trades']}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="2">{win_rate_str}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="2" color="{pnl_color}"><b>{pnl_str}</b></font></td>
|
||||
</tr>"""
|
||||
if i < len(market_breakdown) - 1:
|
||||
rows_html += """
|
||||
<tr><td colspan="4"><hr size="1" color="#E0E0E0" noshade></td></tr>"""
|
||||
|
||||
positions_html = ""
|
||||
for trade in recent_positions:
|
||||
close_dt = trade["close_timestamp"]
|
||||
date_str = close_dt.strftime("%m/%d %H:%M") if close_dt else "N/A"
|
||||
ticker = html.escape(trade["ticker"])
|
||||
side = trade["side"]
|
||||
qty = f"{trade['qty']:.2f}"
|
||||
entry = f"${trade['entry']:.2f}"
|
||||
exit_val = f"${trade['exit']:.2f}"
|
||||
pnl = trade["pnl_with_fees"]
|
||||
pnl_color = "#006600" if pnl >= 0 else "#990000"
|
||||
pnl_str = f"${pnl:+.2f}"
|
||||
positions_html += f"""
|
||||
<tr>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="1">{date_str}</font></td>
|
||||
<td align="left"><font face="Courier New, Courier, monospace" size="1">{ticker}</font></td>
|
||||
<td align="center"><font face="Courier New, Courier, monospace" size="1">{side}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="1">{qty}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="1">{entry}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="1">{exit_val}</font></td>
|
||||
<td align="right"><font face="Courier New, Courier, monospace" size="1" color="{pnl_color}"><b>{pnl_str}</b></font></td>
|
||||
</tr>"""
|
||||
|
||||
page = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<title>Kalshi Portfolio Statement</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
|
||||
</head>
|
||||
<body bgcolor="#FFFFFF" text="#111111" link="#111111" vlink="#444444" alink="#000000" topmargin="20" leftmargin="20" marginwidth="20" marginheight="20">
|
||||
|
||||
<center>
|
||||
<table width="720" border="0" cellspacing="0" cellpadding="0">
|
||||
|
||||
<tr>
|
||||
<td align="left">
|
||||
<font face="Courier New, Courier, monospace" size="2"><b>KALSHI DERIVATIVES / ACCOUNT AUDIT</b></font><br>
|
||||
<font face="Georgia, Times New Roman, serif" size="5"><b>Year-End Performance Summary</b></font><br>
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#666666">PERIOD ENDING: {html.escape(period_end_str)} | SOURCE: {html.escape(csv_filename)}</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td padding="10">
|
||||
<hr size="2" color="#111111" noshade>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="6">
|
||||
<tr valign="top">
|
||||
<td width="25%">
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#666666">NET REALIZED P&L</font><br>
|
||||
<font face="Courier New, Courier, monospace" size="4" color="{net_pnl_color}"><b>{net_pnl_str}</b></font><br>
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#888888">Includes ${summary['total_fees']:.2f} fees</font>
|
||||
</td>
|
||||
<td width="25%">
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#666666">WIN / LOSS RECORD</font><br>
|
||||
<font face="Courier New, Courier, monospace" size="4"><b>{wins} - {losses}</b></font><br>
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#888888">{pushes} Push{'s' if pushes != 1 else ''} ({win_pct:.1f}% Win)</font>
|
||||
</td>
|
||||
<td width="25%">
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#666666">TOTAL VOLUME</font><br>
|
||||
<font face="Courier New, Courier, monospace" size="4"><b>{total}</b></font><br>
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#888888">Executed Contracts</font>
|
||||
</td>
|
||||
<td width="25%">
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#666666">BEST/WORST SINGLE</font><br>
|
||||
<font face="Courier New, Courier, monospace" size="2"><font color="{best_color}"><b>{best_str}</b></font> / <font color="{worst_color}"><b>{worst_str}</b></font></font><br>
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#888888">{html.escape(best_worst_subtext)}</font>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td><br><hr size="1" color="#CCCCCC" noshade><br></td></tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<font face="Georgia, Times New Roman, serif" size="3"><b>Market Breakdown</b></font>
|
||||
<br><br>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="4">
|
||||
<tr bgcolor="#EEEEEE">
|
||||
<td width="45%" align="left"><font face="Geneva, Verdana, sans-serif" size="1"><b>ASSET CLASS / MARKET</b></font></td>
|
||||
<td width="15%" align="right"><font face="Geneva, Verdana, sans-serif" size="1"><b>TRADES</b></font></td>
|
||||
<td width="20%" align="right"><font face="Geneva, Verdana, sans-serif" size="1"><b>WIN RATE</b></font></td>
|
||||
<td width="20%" align="right"><font face="Geneva, Verdana, sans-serif" size="1"><b>NET P&L</b></font></td>
|
||||
</tr>
|
||||
{rows_html}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td><br><hr size="1" color="#CCCCCC" noshade><br></td></tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<font face="Georgia, Times New Roman, serif" size="3"><b>Recent Closed Positions</b></font>
|
||||
<br><br>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="3">
|
||||
<tr bgcolor="#EEEEEE">
|
||||
<th align="left"><font face="Geneva, Verdana, sans-serif" size="1">DATE/TIME</font></th>
|
||||
<th align="left"><font face="Geneva, Verdana, sans-serif" size="1">TICKER</font></th>
|
||||
<th align="center"><font face="Geneva, Verdana, sans-serif" size="1">SIDE</font></th>
|
||||
<th align="right"><font face="Geneva, Verdana, sans-serif" size="1">QTY</font></th>
|
||||
<th align="right"><font face="Geneva, Verdana, sans-serif" size="1">ENTRY</font></th>
|
||||
<th align="right"><font face="Geneva, Verdana, sans-serif" size="1">EXIT</font></th>
|
||||
<th align="right"><font face="Geneva, Verdana, sans-serif" size="1">P&L</font></th>
|
||||
</tr>
|
||||
{positions_html}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td><br><hr size="2" color="#111111" noshade></td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<font face="Geneva, Verdana, sans-serif" size="1" color="#666666">
|
||||
{html.escape(csv_filename)} • rendered with vanilla HTML 4.01 strict table markup • kalshi-csv v{__version__}
|
||||
</font>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</center>
|
||||
|
||||
</body>
|
||||
</html>"""
|
||||
return page
|
||||
|
||||
|
||||
class LegacyWebHandler(BaseHTTPRequestHandler):
|
||||
"""HTTP request handler that serves the legacy portfolio page."""
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/" or self.path == "/index.html":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html; charset=iso-8859-1")
|
||||
self.end_headers()
|
||||
html_content = self.server.html_content
|
||||
self.wfile.write(html_content.encode("iso-8859-1"))
|
||||
else:
|
||||
self.send_error(404, "Not Found")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
|
||||
class LegacyWebServer:
|
||||
"""HTTP server for the legacy portfolio view."""
|
||||
|
||||
def __init__(self, kalshi, csv_filename, host="0.0.0.0", port=8080):
|
||||
self.kalshi = kalshi
|
||||
self.csv_filename = csv_filename
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.html_content = render_portfolio_html(kalshi, csv_filename)
|
||||
|
||||
def serve(self):
|
||||
"""Starts the HTTP server and blocks until interrupted."""
|
||||
server = HTTPServer((self.host, self.port), LegacyWebHandler)
|
||||
server.html_content = self.html_content
|
||||
print(f"Serving legacy portfolio view at http://{self.host}:{self.port}/")
|
||||
print("Press Ctrl+C to stop.")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down server.")
|
||||
server.server_close()
|
||||
47
tests/test_categories.py
Normal file
47
tests/test_categories.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from kalshi_csv.categories import categorize_ticker
|
||||
|
||||
|
||||
def test_mlb_categorization():
|
||||
assert categorize_ticker("KXMLBGAME-26JUL081940BOSCWS-BOS") == "MLB Baseball"
|
||||
assert categorize_ticker("KXMLBHRDERBY-26-KSCHWARBER12") == "MLB Baseball"
|
||||
|
||||
|
||||
def test_npb_categorization():
|
||||
assert categorize_ticker("KXNPBGAME-26JUL150500YOMYAK-YAK") == "NPB Baseball (Japan)"
|
||||
|
||||
|
||||
def test_nba_summer_categorization():
|
||||
assert categorize_ticker("KXNBASUMMERGAME-26JUL14MEMGSW-GSW") == "NBA Summer League"
|
||||
|
||||
|
||||
def test_wnba_categorization():
|
||||
assert categorize_ticker("KXWNBAGAME-26JUL13PHXMIN-PHX") == "WNBA Basketball"
|
||||
|
||||
|
||||
def test_sp500_categorization():
|
||||
assert categorize_ticker("KXINXU-26JUL08H1400-T7479.9999") == "S&P 500 (INXU Intraday)"
|
||||
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_soccer_categorization():
|
||||
assert categorize_ticker("KXWCADVANCE-26JUL07ARGEGY-ARG") == "Global Soccer / Football"
|
||||
assert categorize_ticker("KXUCLADVANCE-26JUL14KUPSVAR-VAR") == "Global Soccer / Football"
|
||||
assert categorize_ticker("KXBRASILEIROBGAME-26JUL13AMGLON-LON") == "Global Soccer / Football"
|
||||
assert categorize_ticker("KXECULPGAME-26JUL14MACMUR-MUR") == "Global Soccer / Football"
|
||||
assert categorize_ticker("KXALLSVENSKANGAME-26JUL12BROSIR-SIR") == "Global Soccer / Football"
|
||||
assert categorize_ticker("KXCLUBFGAME-26JUL27GALVEN-VEN") == "Global Soccer / Football"
|
||||
|
||||
|
||||
def test_other_markets_categorization():
|
||||
assert categorize_ticker("KXRAIN-26JUL15-ATL") == "Other Markets"
|
||||
assert categorize_ticker("KXTRUMPMENTION-26JUL15") == "Other Markets"
|
||||
assert categorize_ticker("KXTEMPNYCH-26JUL15") == "Other Markets"
|
||||
assert categorize_ticker("KXHIGHCHI-26JUL15") == "Other Markets"
|
||||
|
||||
|
||||
def test_unknown_ticker_defaults_to_other():
|
||||
assert categorize_ticker("UNKNOWN-TICKER-123") == "Other Markets"
|
||||
|
|
@ -103,3 +103,24 @@ def test_cli_ascii_flag(sample_csv):
|
|||
assert "|" in result.stdout
|
||||
assert "┌" not in result.stdout
|
||||
assert "│" not in result.stdout
|
||||
|
||||
|
||||
def test_cli_legacy_web_flag_in_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "kalshi_csv.cli", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "--legacy-web" in result.stdout
|
||||
assert "--legacy-web-port" in result.stdout
|
||||
|
||||
|
||||
def test_cli_legacy_web_port_default_in_help():
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "kalshi_csv.cli", "--help"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert "8080" in result.stdout
|
||||
|
|
|
|||
|
|
@ -89,3 +89,88 @@ def test_file_not_found():
|
|||
kalshi = KalshiCSV("/nonexistent/path.csv")
|
||||
with pytest.raises(FileNotFoundError):
|
||||
kalshi.parse()
|
||||
|
||||
|
||||
def test_market_breakdown_returns_list(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
breakdown = kalshi.market_breakdown()
|
||||
assert isinstance(breakdown, list)
|
||||
assert len(breakdown) > 0
|
||||
|
||||
|
||||
def test_market_breakdown_structure(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
breakdown = kalshi.market_breakdown()
|
||||
for item in breakdown:
|
||||
assert "category" in item
|
||||
assert "trades" in item
|
||||
assert "win_rate" in item
|
||||
assert "net_pnl" in item
|
||||
|
||||
|
||||
def test_market_breakdown_sorted_by_trades(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
breakdown = kalshi.market_breakdown()
|
||||
trade_counts = [item["trades"] for item in breakdown]
|
||||
assert trade_counts == sorted(trade_counts, reverse=True)
|
||||
|
||||
|
||||
def test_recent_closed_positions_returns_list(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
positions = kalshi.recent_closed_positions()
|
||||
assert isinstance(positions, list)
|
||||
|
||||
|
||||
def test_recent_closed_positions_limit(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
positions = kalshi.recent_closed_positions(n=2)
|
||||
assert len(positions) <= 2
|
||||
|
||||
|
||||
def test_recent_closed_positions_sorted_by_date(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
positions = kalshi.recent_closed_positions()
|
||||
if len(positions) > 1:
|
||||
timestamps = [p["close_timestamp"] for p in positions]
|
||||
assert timestamps == sorted(timestamps, reverse=True)
|
||||
|
||||
|
||||
def test_summary_tracks_wins_losses_pushes(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
assert kalshi.summary["wins"] >= 0
|
||||
assert kalshi.summary["losses"] >= 0
|
||||
assert kalshi.summary["pushes"] >= 0
|
||||
assert kalshi.summary["wins"] + kalshi.summary["losses"] + kalshi.summary["pushes"] == kalshi.summary["trade_count"]
|
||||
|
||||
|
||||
def test_summary_tracks_best_worst_trade(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
assert kalshi.summary["best_trade"] is not None
|
||||
assert kalshi.summary["worst_trade"] is not None
|
||||
assert "pnl_with_fees" in kalshi.summary["best_trade"]
|
||||
assert "pnl_with_fees" in kalshi.summary["worst_trade"]
|
||||
assert kalshi.summary["best_trade"]["pnl_with_fees"] >= kalshi.summary["worst_trade"]["pnl_with_fees"]
|
||||
|
||||
|
||||
def test_trade_has_market_category(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
for trade in kalshi.trades:
|
||||
assert "market_category" in trade
|
||||
assert isinstance(trade["market_category"], str)
|
||||
|
||||
|
||||
def test_trade_has_timestamps(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
for trade in kalshi.trades:
|
||||
assert "open_timestamp" in trade
|
||||
assert "close_timestamp" in trade
|
||||
|
|
|
|||
96
tests/test_web.py
Normal file
96
tests/test_web.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
from kalshi_csv import KalshiCSV
|
||||
from kalshi_csv.web import render_portfolio_html
|
||||
|
||||
|
||||
def test_render_html_contains_header(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "KALSHI DERIVATIVES / ACCOUNT AUDIT" in html_content
|
||||
assert "Year-End Performance Summary" in html_content
|
||||
|
||||
|
||||
def test_render_html_contains_summary_metrics(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "NET REALIZED P&L" in html_content
|
||||
assert "WIN / LOSS RECORD" in html_content
|
||||
assert "TOTAL VOLUME" in html_content
|
||||
assert "BEST/WORST SINGLE" in html_content
|
||||
|
||||
|
||||
def test_render_html_contains_market_breakdown(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "Market Breakdown" in html_content
|
||||
assert "ASSET CLASS / MARKET" in html_content
|
||||
assert "TRADES" in html_content
|
||||
assert "WIN RATE" in html_content
|
||||
|
||||
|
||||
def test_render_html_contains_recent_positions(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "Recent Closed Positions" in html_content
|
||||
assert "DATE/TIME" in html_content
|
||||
assert "TICKER" in html_content
|
||||
assert "SIDE" in html_content
|
||||
assert "ENTRY" in html_content
|
||||
assert "EXIT" in html_content
|
||||
|
||||
|
||||
def test_render_html_contains_trade_data(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "TESTMARKET-WIN" in html_content
|
||||
assert "TESTMARKET-LOSS" in html_content
|
||||
assert "TESTMARKET-SMALL" in html_content
|
||||
|
||||
|
||||
def test_render_html_html401_doctype(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"' in html_content
|
||||
|
||||
|
||||
def test_render_html_no_css(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "<style" not in html_content
|
||||
assert "style=" not in html_content
|
||||
|
||||
|
||||
def test_render_html_shows_win_loss_record(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "2 - 1" in html_content
|
||||
|
||||
|
||||
def test_render_html_shows_total_volume(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert ">3<" in html_content
|
||||
|
||||
|
||||
def test_render_html_shows_csv_filename(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
html_content = render_portfolio_html(kalshi, "my-kalshi-data.csv")
|
||||
assert "my-kalshi-data.csv" in html_content
|
||||
|
||||
|
||||
def test_render_html_escapes_html_in_tickers(sample_csv):
|
||||
kalshi = KalshiCSV(sample_csv)
|
||||
kalshi.parse()
|
||||
kalshi.trades[0]["ticker"] = "<script>alert('xss')</script>"
|
||||
html_content = render_portfolio_html(kalshi, "test.csv")
|
||||
assert "<script>alert('xss')</script>" not in html_content
|
||||
assert "<script>" in html_content
|
||||
Loading…
Add table
Add a link
Reference in a new issue