From ef75e3e5f0de6dfaaa95db70116166a2bd07f2f5 Mon Sep 17 00:00:00 2001 From: mark Date: Wed, 22 Jul 2026 12:48:16 -0400 Subject: [PATCH] Change table to box-style, supports unicode by default and ASCII as fallback mode --- src/kalshi_csv/cli.py | 36 +++++++++++++++++----- src/kalshi_csv/formatter.py | 61 +++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 14 +++++++++ 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/kalshi_csv/cli.py b/src/kalshi_csv/cli.py index 78b8f8b..fd0efca 100644 --- a/src/kalshi_csv/cli.py +++ b/src/kalshi_csv/cli.py @@ -9,6 +9,10 @@ from .formatter import ( format_currency_color, format_currency_color_padded, truncate_ticker, + format_table_header, + format_table_separator, + format_table_row, + get_box_chars, ) @@ -26,29 +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} | {'Fees':<6}" - ) - print("-" * 90) + 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_padded(trade["pnl_no_fees"], 14, no_color) fees = trade["open_fees"] + trade["close_fees"] ticker_display = truncate_ticker(trade["ticker"]) - print( - f"{ticker_display:<32} | {trade['side']:<4} | {trade['qty']:<6.2f} | " - f"${trade['entry']:<5.2f} | ${trade['exit']:<5.2f} | {pnl_str} | ${fees:<5.2f}" + + 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("-" * 90) + 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( diff --git a/src/kalshi_csv/formatter.py b/src/kalshi_csv/formatter.py index 9b6449f..7aa8c59 100644 --- a/src/kalshi_csv/formatter.py +++ b/src/kalshi_csv/formatter.py @@ -51,3 +51,64 @@ def truncate_ticker(ticker, max_len=32): 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"] diff --git a/tests/test_cli.py b/tests/test_cli.py index 7b34915..16d5bc4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -85,3 +85,17 @@ def test_cli_ticker_truncation(tmp_path): 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