ticker-tape/ticker_tape.py

211 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Fetch NYSE trade data from Alpaca and display like an old stock ticker tape."""
import argparse
import os
import sys
import time
from datetime import datetime, timedelta
import requests
# NYSE-listed stocks (tape A). AAPL, MSFT, INTC etc are Nasdaq-listed (tape C).
DEFAULT_SYMBOLS = "JPM,JNJ,V,PG,UNH,HD,BAC,XOM,WMT,DIS,KO,PFE,VZ,MRK,T,GE,F,GM,CVX,CL"
BASE_URL = "https://data.alpaca.markets/v2/stocks/trades"
def fetch_trades(api_key, api_secret, symbols, start, end, limit=1000, page_token=None, debug=False):
headers = {
"APCA-API-KEY-ID": api_key,
"APCA-API-SECRET-KEY": api_secret,
}
params = {
"symbols": symbols,
"start": start,
"end": end,
"feed": "sip",
"limit": limit,
"sort": "asc",
}
if page_token:
params["page_token"] = page_token
if debug:
print(f"[DEBUG] GET {BASE_URL}", file=sys.stderr)
print(f"[DEBUG] params: {params}", file=sys.stderr)
time.sleep(0.3) # stay under 200 req/min
resp = requests.get(BASE_URL, headers=headers, params=params)
# retry once on rate limit
if resp.status_code == 429:
print("[RATE LIMITED] sleeping 5s...", file=sys.stderr)
time.sleep(5)
resp = requests.get(BASE_URL, headers=headers, params=params)
if debug:
print(f"[DEBUG] HTTP {resp.status_code}", file=sys.stderr)
if not resp.ok:
print(f"[DEBUG] response body: {resp.text[:500]}", file=sys.stderr)
resp.raise_for_status()
data = resp.json()
if debug:
top_keys = list(data.keys())
symbols_returned = list(data.get("trades", {}).keys()) if isinstance(data.get("trades"), dict) else "N/A"
trade_count = sum(len(v) for v in data.get("trades", {}).values()) if isinstance(data.get("trades"), dict) else 0
print(f"[DEBUG] response keys: {top_keys}", file=sys.stderr)
print(f"[DEBUG] symbols in response: {symbols_returned}", file=sys.stderr)
print(f"[DEBUG] total trades returned: {trade_count}", file=sys.stderr)
print(f"[DEBUG] next_page_token: {data.get('next_page_token', 'none')}", file=sys.stderr)
return data
def flatten_and_sort(data):
"""Flatten the per-symbol trade dict into a single time-sorted list."""
trades = []
for symbol, symbol_trades in data.get("trades", {}).items():
for t in symbol_trades:
trades.append({"sym": symbol, **t})
trades.sort(key=lambda t: t["t"])
return trades
def format_tape_line(sym, lots, price):
"""Format a trade like an old ticker tape: SYM lots price.
A bare price means 1 round lot. '2s' means 2 lots (200 shares)."""
vol = f"{lots}s" if lots > 1 else ""
parts = [sym]
if vol:
parts.append(vol)
parts.append(f"{price:.2f}")
return " ".join(parts)
def main():
parser = argparse.ArgumentParser(description="NYSE ticker tape via Alpaca market data")
parser.add_argument("--symbols", default=DEFAULT_SYMBOLS,
help=f"Comma-separated symbols (default: {DEFAULT_SYMBOLS})")
parser.add_argument("--symbol-file", metavar="FILE",
help="Read symbols from FILE (one per line) and batch requests")
parser.add_argument("--start", help="Start datetime, YYYY-MM-DD or RFC-3339 (default: previous trading day)")
parser.add_argument("--end", help="End datetime (default: same day as start)")
parser.add_argument("--limit", type=int, default=10000,
help="Max trades to fetch per API call (default: 10000, max: 10000)")
parser.add_argument("--all", action="store_true",
help="Paginate through ALL trades in the range (many API calls)")
parser.add_argument("--tape", default="A",
help="Filter by SIP tape: A=NYSE-listed, B=regional/Arca, C=Nasdaq-listed, all=no filter (default: A)")
parser.add_argument("--no-filter", action="store_true",
help="Show all trades regardless of tape/exchange")
parser.add_argument("--raw", action="store_true",
help="Output raw CSV instead of tape format: time,symbol,lots,price")
parser.add_argument("--debug", action="store_true",
help="Print debug info: request URL/params, response structure, exchange codes seen")
args = parser.parse_args()
api_key = os.environ.get("APCA_API_KEY_ID", "")
api_secret = os.environ.get("APCA_API_SECRET_KEY", "")
if not api_key or not api_secret:
print("Set APCA_API_KEY_ID and APCA_API_SECRET_KEY environment variables.", file=sys.stderr)
sys.exit(1)
# Default to last Friday if today is weekend, else yesterday
if not args.start:
today = datetime.now()
days_back = 1 if today.weekday() > 0 else 3 # Monday->Friday
if today.weekday() == 6: # Sunday
days_back = 2
args.start = (today - timedelta(days=days_back)).strftime("%Y-%m-%d")
# Default start to market open (9:30 ET = 13:30 UTC) if only date given
if "T" not in args.start:
args.start = args.start + "T13:30:00Z"
if not args.end:
# Default end = start day at market close (4pm ET = 20:00 UTC)
args.end = args.start[:10] + "T20:00:00Z"
tape_filter = None if args.no_filter or args.tape.lower() == "all" else args.tape.upper()
# Build symbol batches
if args.symbol_file:
with open(args.symbol_file) as f:
all_symbols = [line.strip() for line in f if line.strip()]
# Batch into groups of 500 to stay under URL length limits
batch_size = 500
batches = []
for i in range(0, len(all_symbols), batch_size):
batches.append(",".join(all_symbols[i:i + batch_size]))
print(f"{len(all_symbols)} symbols in {len(batches)} batches", file=sys.stderr)
# --all-nyse implies --all (paginate everything)
args.all = True
else:
batches = [args.symbols]
if args.debug:
print(f"[DEBUG] start={args.start} end={args.end}", file=sys.stderr)
print(f"[DEBUG] tape_filter={tape_filter} (A=NYSE-listed, B=regional, C=Nasdaq)", file=sys.stderr)
total = 0
api_calls = 0
exchanges_seen = {}
for batch_num, symbols in enumerate(batches, 1):
if len(batches) > 1:
print(f"Batch {batch_num}/{len(batches)}...", file=sys.stderr)
page_token = None
while True:
data = fetch_trades(api_key, api_secret, symbols,
args.start, args.end, args.limit, page_token, debug=args.debug)
api_calls += 1
trades = flatten_and_sort(data)
if args.debug:
for trade in trades:
ex = trade.get("x", "?")
tape = trade.get("z", "?")
exchanges_seen[ex] = exchanges_seen.get(ex, 0) + 1
exchanges_seen[f"tape:{tape}"] = exchanges_seen.get(f"tape:{tape}", 0) + 1
for trade in trades:
if tape_filter and trade.get("z") != tape_filter:
continue
if trade["s"] < 100:
continue
lots = trade["s"] // 100
if args.raw:
print(f"{trade['t']},{trade['sym']},{lots},{trade['p']:.2f}")
else:
ts = trade["t"][:19].replace("T", " ")
line = format_tape_line(trade["sym"], lots, trade["p"])
print(f"{ts} {line}")
total += 1
page_token = data.get("next_page_token")
if not page_token or not args.all:
break
if api_calls % 100 == 0:
print(f" {api_calls} API calls, {total} trades so far...", file=sys.stderr)
if args.debug and exchanges_seen:
print(f"[DEBUG] exchange codes seen: {exchanges_seen}", file=sys.stderr)
print(f"[DEBUG] (exchanges: N=NYSE, Q=Nasdaq, P=Arca, D=FINRA, K=EDGX...)", file=sys.stderr)
print(f"[DEBUG] (tapes: A=NYSE-listed, B=regional, C=Nasdaq-listed)", file=sys.stderr)
print(f"Done: {total} trades, {api_calls} API calls.", file=sys.stderr)
if total == 0:
print("No trades found. Try --no-filter or different --tape/symbols/dates.", file=sys.stderr)
elif not args.all and data.get("next_page_token"):
print(f"More available; use --all to paginate.", file=sys.stderr)
if __name__ == "__main__":
main()