initial commit
This commit is contained in:
commit
a22cd6d9e0
2 changed files with 192 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.csv
|
||||
188
ticker_tape.py
Normal file
188
ticker_tape.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Fetch NYSE trade data from Alpaca and display like an old stock ticker tape."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
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)
|
||||
|
||||
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(trade):
|
||||
"""Format a trade like an old ticker tape: SYM shares price"""
|
||||
sym = trade["sym"]
|
||||
size = trade["s"]
|
||||
price = trade["p"]
|
||||
|
||||
# Old tickers showed round lots (100s). "2s" meant 200 shares.
|
||||
# A bare price meant 100 shares (1 round lot).
|
||||
if size < 100:
|
||||
vol = f"{size}sh"
|
||||
elif size % 100 == 0:
|
||||
lots = size // 100
|
||||
vol = f"{lots}s" if lots > 1 else ""
|
||||
else:
|
||||
vol = f"{size}sh"
|
||||
|
||||
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("--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=1000,
|
||||
help="Max trades to fetch per API call (default: 1000, 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()
|
||||
|
||||
if args.debug:
|
||||
print(f"[DEBUG] start={args.start} end={args.end}", file=sys.stderr)
|
||||
print(f"[DEBUG] symbols={args.symbols}", file=sys.stderr)
|
||||
print(f"[DEBUG] tape_filter={tape_filter} (A=NYSE-listed, B=regional, C=Nasdaq)", file=sys.stderr)
|
||||
|
||||
page_token = None
|
||||
total = 0
|
||||
exchanges_seen = {}
|
||||
while True:
|
||||
data = fetch_trades(api_key, api_secret, args.symbols,
|
||||
args.start, args.end, args.limit, page_token, debug=args.debug)
|
||||
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)
|
||||
print(f"{ts} {line}")
|
||||
total += 1
|
||||
|
||||
page_token = data.get("next_page_token")
|
||||
if not page_token or not args.all:
|
||||
break
|
||||
|
||||
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)
|
||||
|
||||
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"\n--- {total} trades shown. More available; use --all to paginate. ---", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue