diff --git a/print_tape.py b/print_tape.py index d149029..c9dfc4a 100644 --- a/print_tape.py +++ b/print_tape.py @@ -4,9 +4,9 @@ Reads a CSV of trades (timestamp, symbol, lots, price) and generates ESC/POS commands with 90° rotated text to emulate an old stock ticker tape. -Each character gets its own printed row. When the receipt paper is turned -sideways, symbol characters read across the top and price characters read -across the bottom — just like a real ticker tape. +Trades are distributed across 4 parallel columns. Each column independently +prints symbol characters at the top of the tape and price/lot characters at +the bottom — just like a real multi-track ticker tape. """ import csv @@ -19,32 +19,85 @@ from glyphs import price_to_fraction, upload_fractions # TM-T88V with 90° rotation: Font A chars are 24 dots wide (instead of 12), # so 512 printable dots / 24 = 21 chars fit across the 80mm paper width. LINE_WIDTH = 21 +NUM_COLUMNS = 4 +COL_WIDTH = LINE_WIDTH // NUM_COLUMNS # 5 -def print_trade(p, symbol, lots, price): - """Print a single trade as ticker tape characters, one per row.""" +def format_price(lots, price): + """Format price/lots string for a trade.""" frac = price_to_fraction(price) if lots > 1: - price_str = f"{lots}s{frac}" - else: - price_str = frac + return f"{lots}s{frac}" + return frac - # Symbol characters at the top (right-aligned) - for ch in symbol: - p.text(" " * (LINE_WIDTH - 1) + ch + "\n") - # Price characters at the bottom (left-aligned) - for ch in price_str: - p.text(ch + "\n") +def trade_char_count(symbol, price_str): + """Total characters a trade occupies in a column (symbol + price + separator).""" + return len(symbol) + len(price_str) + 1 - # Blank line separator between trades - p.text("\n") + +def assign_to_columns(trades): + """Assign trades to columns, always picking the column with the fewest chars.""" + columns = [[] for _ in range(NUM_COLUMNS)] + lengths = [0] * NUM_COLUMNS + + for symbol, lots, price in trades: + price_str = format_price(lots, price) + count = trade_char_count(symbol, price_str) + shortest = lengths.index(min(lengths)) + columns[shortest].append((symbol, price_str)) + lengths[shortest] += count + + return columns + + +def build_column_chars(trades_in_column): + """Build the character sequence for one column. + + Each entry is ('top', ch), ('bottom', ch), or ('blank',) controlling + where the character is placed within the column width. + """ + chars = [] + for symbol, price_str in trades_in_column: + for ch in symbol: + chars.append(("top", ch)) + for ch in price_str: + chars.append(("bottom", ch)) + chars.append(("blank",)) + return chars + + +def print_all(p, trades): + """Print all trades across multiple columns, one row at a time.""" + columns = assign_to_columns(trades) + col_chars = [build_column_chars(col) for col in columns] + max_len = max(len(cc) for cc in col_chars) + + for row_idx in range(max_len): + row = [" "] * LINE_WIDTH + for col_idx in range(NUM_COLUMNS): + col_start = col_idx * COL_WIDTH + if row_idx < len(col_chars[col_idx]): + entry = col_chars[col_idx][row_idx] + if entry[0] == "top": + row[col_start + COL_WIDTH - 1] = entry[1] + elif entry[0] == "bottom": + row[col_start] = entry[1] + p.text("".join(row) + "\n") def main(): csv_file = sys.argv[1] if len(sys.argv) > 1 else "trades_sample_sorted.csv" output_file = sys.argv[2] if len(sys.argv) > 2 else "ticker_tape.bin" + # Read all trades + trades = [] + with open(csv_file) as f: + reader = csv.reader(f) + for row in reader: + _timestamp, symbol, lots_str, price_str = row + trades.append((symbol, int(lots_str), float(price_str))) + p = Dummy(profile="TM-T88V") p.hw("INIT") # ESC V 1: 90° clockwise character rotation (not exposed by python-escpos) @@ -55,15 +108,9 @@ def main(): # Upload custom fraction glyphs (⅛ ¼ ⅜ ½ ⅝ ¾ ⅞) upload_fractions(p) - with open(csv_file) as f: - reader = csv.reader(f) - for row in reader: - _timestamp, symbol, lots_str, price_str = row - lots = int(lots_str) - price = float(price_str) - print_trade(p, symbol, lots, price) + print_all(p, trades) - # Feed past the cutter (~20mm above print head, ~12 lines at 12-dot spacing) + # Feed past the cutter (~20mm above print head) p.text("\n" * 16) p.cut()