ticker-tape/print_tape.py

123 lines
3.9 KiB
Python

#!/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 = LINE_WIDTH // NUM_COLUMNS # 5
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_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)
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()