#!/usr/bin/env python3 """BTC/USD live trade ticks from Binance — the fastest, most chaotic source. Every sub-second trade fires the market's pulse; price-to-price velocity is raw volatility you can hear, driving rhythm and jitter in the modulation matrix. """ import argparse import asyncio import json import urllib.request from pythonosc.udp_client import SimpleUDPClient NAME = "world_crypto" WS_URL = "wss://stream.binance.com:9443/ws/btcusdt@trade" REST_URL = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT" POLL_S = 2.0 # REST fallback poll interval USER_AGENT = "godstrument/1.0 (+world_crypto)" def emit(client, last_price, price): """Send price + tick-to-tick velocity. Returns the new last_price.""" client.send_message("/gs/crypto/price", float(price)) vel = 0.0 if last_price is None else abs(price - last_price) client.send_message("/gs/crypto/vel", float(vel)) return price async def run_websocket(client): """Stream trades over the websocket. Returns True if it ever connected (so the caller keeps favoring the socket), False if it never connected (so the caller can fall back to REST).""" import websockets # part of the required stack; import here to keep top clean ever_connected = False last_price = None async with websockets.connect(WS_URL, ping_interval=20, ping_timeout=20) as ws: ever_connected = True async for raw in ws: try: msg = json.loads(raw) price = float(msg["p"]) except (ValueError, KeyError, TypeError): continue # malformed / non-trade frame; skip last_price = emit(client, last_price, price) return ever_connected async def poll_rest(client): """Fallback: poll the REST ticker forever at POLL_S. Never returns.""" last_price = None loop = asyncio.get_running_loop() while True: try: def fetch(): req = urllib.request.Request(REST_URL, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=10) as resp: return json.loads(resp.read().decode()) data = await loop.run_in_executor(None, fetch) price = float(data["price"]) last_price = emit(client, last_price, price) except Exception as e: # transient network / parse error — keep going print(f"[{NAME}] REST poll error: {e}; retrying") await asyncio.sleep(POLL_S) async def main(): parser = argparse.ArgumentParser(description="Binance BTCUSDT trade ticks -> OSC") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=9000) args = parser.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[{NAME}] emitting /gs/crypto/price,/gs/crypto/vel every <1s (ws) / {POLL_S:g}s (rest)") backoff = 1.0 rest_only = False while True: if rest_only: # We could not open the socket at all; stay on REST but keep # occasionally retrying the socket by resetting the flag on error. try: await poll_rest(client) except Exception as e: print(f"[{NAME}] REST loop error: {e}; retrying") await asyncio.sleep(POLL_S) continue try: await run_websocket(client) # Clean disconnect (stream ended): reconnect quickly. print(f"[{NAME}] websocket closed; reconnecting") backoff = 1.0 except Exception as e: print(f"[{NAME}] websocket error: {e}; backoff {backoff:g}s") await asyncio.sleep(backoff) backoff = min(backoff * 2, 30.0) # If we have never managed to connect, drop to REST fallback, # but keep trying the socket periodically via a bounded retry. if backoff >= 8.0 and not rest_only: print(f"[{NAME}] websocket unreachable; falling back to REST polling") rest_only = True if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass