#!/usr/bin/env python3 """Generate ESC/POS ticker tape output from trade CSV data. 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. 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 import sys from escpos.printer import Dummy 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 = 4 EDGE = 1 # 1-char margin on each edge GAP = 1 # 1-char gap between columns # Layout: edge(1) + col(4) + gap(1) + col(4) + gap(1) + col(4) + gap(1) + col(4) + edge(1) = 21 COL_STARTS = [EDGE + i * (COL_WIDTH + GAP) for i in range(NUM_COLUMNS)] def format_price(lots, price): """Format price/lots string for a trade.""" frac = price_to_fraction(price) if lots > 1: return f"{lots}s{frac}" return frac 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 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_STARTS[col_idx] 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) p._raw(b"\x1b\x56\x01") # ESC 3 n: set line spacing to n/180 inch. Font A chars are 12 dots wide, # so n=12 makes rotated characters sit flush with no gap. p._raw(b"\x1b\x33\x0c") # Upload custom fraction glyphs (⅛ ¼ ⅜ ½ ⅝ ¾ ⅞) upload_fractions(p) print_all(p, trades) # Feed past the cutter (~20mm above print head) p.text("\n" * 16) p.cut() with open(output_file, "wb") as f: f.write(p.output) print(f"Wrote {len(p.output)} bytes to {output_file}", file=sys.stderr) if __name__ == "__main__": main()