Godstrument/workers/world_crypto.py
type-two aa81535379 📈 the trading desk — see the data, pin the ticker, hear the market think
Right-click any source: its actual number, live, with a two-minute
sparkline right in the menu — and 📌 pin it to the ticker, where your
watchlist scrolls ahead of the news with live values. The Binance tap
widens to ETH, SOL and the euro itself (EUR/USD on a 1s miniTicker,
since the pair trades thin), the ECB's daily fixes arrive as glacial
drones (GBP/USD, USD/JPY, AUD/USD), and Bitcoin speaks its own
technical analysis on 5s closes: the MACD line, Wilder's RSI(14), and
cross bells that ring once at every golden and death cross. All of it
routed through the Spirits like any money — to the solar plexus, to
Hod, to Nidavellir, to fehu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:29:08 +10:00

219 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""Live trade ticks from Binance — the fastest, most chaotic sources.
One websocket tap, four markets: BTC (the lead, legacy keys), ETH, SOL, and
EUR/USD via the EURUSDT pair — a real FX rate riding the same firehose. Every
trade fires price + tick-to-tick velocity.
And a trend kit on BTC, computed on 5-second closes so it is stable no matter
how fast the trades arrive: the MACD line (fast EMA minus slow), Wilder's
RSI(14), and the cross bells — /gs/crypto/cross_up rings 1.0 the moment the
fast average climbs over the slow (a golden cross, in miniature), cross_down
when it falls through. The market's mood, as playable signals.
"""
import argparse
import asyncio
import json
import urllib.parse
import urllib.request
from pythonosc.udp_client import SimpleUDPClient
NAME = "world_crypto"
SYMBOLS = { # binance symbol -> (price key, velocity key)
"BTCUSDT": ("crypto/price", "crypto/vel"), # legacy keys: BTC is the lead
"ETHUSDT": ("crypto/eth", "crypto/eth_vel"),
"SOLUSDT": ("crypto/sol", "crypto/sol_vel"),
"EURUSDT": ("fx/eurusd", "fx/eurusd_vel"), # the euro, live, off the same tap
}
# The cryptos ride raw trades; EURUSDT is thinly traded, so it rides the 1s
# miniTicker instead — a steady pulse rather than sparse, minutes-apart ticks.
STREAM_OF = {"EURUSDT": "@miniTicker"}
WS_URL = ("wss://stream.binance.com:9443/stream?streams="
+ "/".join(s.lower() + STREAM_OF.get(s, "@trade") for s in SYMBOLS))
REST_URL = ("https://api.binance.com/api/v3/ticker/price?symbols="
+ urllib.parse.quote(json.dumps(sorted(SYMBOLS), separators=(",", ":"))))
POLL_S = 2.0 # REST fallback poll interval
USER_AGENT = "godstrument/1.0 (+world_crypto)"
TREND_SYMBOL = "BTCUSDT" # the trend kit listens to the lead
class TrendKit:
"""MACD line, Wilder RSI and cross bells on sampled closes.
Tickless: feed it (t, price) whenever one arrives; it takes a close every
`sample_s` seconds and returns {osc_key: value} to emit (empty between
samples). EMAs seed on the first close; RSI stays quiet until it has
rsi_n changes to chew on.
"""
def __init__(self, fast=12, slow=60, rsi_n=14, sample_s=5.0):
self.kf, self.ks = 2.0 / (fast + 1), 2.0 / (slow + 1)
self.rsi_n, self.sample_s = rsi_n, sample_s
self.ema_f = self.ema_s = None
self.avg_gain = self.avg_loss = None
self.changes = [] # pre-seed buffer for the RSI
self.last_close = None
self.next_t = None
self.macd_sign = 0
def on_price(self, t, price):
if self.next_t is None:
self.next_t = t + self.sample_s
if t < self.next_t:
return {}
self.next_t = t + self.sample_s
return self._close(float(price))
def _close(self, c):
out = {}
# EMAs & the MACD line
if self.ema_f is None:
self.ema_f = self.ema_s = c
else:
self.ema_f += self.kf * (c - self.ema_f)
self.ema_s += self.ks * (c - self.ema_s)
macd = self.ema_f - self.ema_s
out["crypto/macd"] = macd
# the cross bells — ring on the sign flip, silent otherwise
sign = 1 if macd > 0 else (-1 if macd < 0 else 0)
out["crypto/cross_up"] = 1.0 if (sign > 0 and self.macd_sign < 0) else 0.0
out["crypto/cross_down"] = 1.0 if (sign < 0 and self.macd_sign > 0) else 0.0
if sign:
self.macd_sign = sign
# Wilder RSI
if self.last_close is not None:
ch = c - self.last_close
gain, loss = max(ch, 0.0), max(-ch, 0.0)
if self.avg_gain is None:
self.changes.append((gain, loss))
if len(self.changes) >= self.rsi_n:
self.avg_gain = sum(g for g, _ in self.changes) / self.rsi_n
self.avg_loss = sum(l for _, l in self.changes) / self.rsi_n
else:
a = 1.0 / self.rsi_n
self.avg_gain += a * (gain - self.avg_gain)
self.avg_loss += a * (loss - self.avg_loss)
if self.avg_gain is not None:
rs = self.avg_gain / self.avg_loss if self.avg_loss > 1e-12 else float("inf")
out["crypto/rsi"] = 100.0 - 100.0 / (1.0 + rs)
self.last_close = c
return out
def make_emitter(client):
"""Per-symbol price+velocity emit, plus the trend kit on the lead."""
last = {}
kit = TrendKit()
def emit(t, symbol, price):
keys = SYMBOLS.get(symbol)
if not keys:
return
client.send_message("/gs/" + keys[0], float(price))
prev = last.get(symbol)
client.send_message("/gs/" + keys[1], 0.0 if prev is None else abs(price - prev))
last[symbol] = price
if symbol == TREND_SYMBOL:
for k, v in kit.on_price(t, price).items():
client.send_message("/gs/" + k, float(v))
return emit
async def run_websocket(emit):
"""Stream trades over the combined websocket."""
import websockets
loop = asyncio.get_running_loop()
async with websockets.connect(WS_URL, ping_interval=20, ping_timeout=20) as ws:
async for raw in ws:
try:
msg = json.loads(raw)["data"]
symbol = msg["s"]
price = float(msg["p"] if "p" in msg else msg["c"]) # trade / miniTicker close
except (ValueError, KeyError, TypeError):
continue # malformed / unknown frame; skip
emit(loop.time(), symbol, price)
async def poll_rest(emit):
"""Fallback: poll the combined REST ticker forever at POLL_S."""
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())
for row in await loop.run_in_executor(None, fetch):
emit(loop.time(), row["symbol"], float(row["price"]))
except Exception as e: # transient network / parse error — keep going
print(f"[{NAME}] REST poll error: {e}; retrying")
await asyncio.sleep(POLL_S)
def selftest():
"""The one runnable check: the trend kit's math, on synthetic prices."""
kit = TrendKit(fast=3, slow=8, rsi_n=3, sample_s=1.0)
t, out = 0.0, {}
seen = {"up": 0, "down": 0}
rsis = []
for i in range(60): # rise then fall then rise
price = 100 + (i if i < 20 else (40 - i if i < 40 else i - 40))
t += 1.0
out = kit.on_price(t + 0.5, price) # t+0.5 always past the 1s boundary
seen["up"] += out.get("crypto/cross_up", 0) > 0
seen["down"] += out.get("crypto/cross_down", 0) > 0
if "crypto/rsi" in out:
rsis.append(out["crypto/rsi"])
assert "crypto/macd" in out, "macd should emit every close"
assert seen["down"] >= 1 and seen["up"] >= 1, f"expected both crosses, saw {seen}"
assert all(0.0 <= r <= 100.0 for r in rsis) and rsis, "rsi must stay in [0,100]"
assert min(rsis) < 30 < 70 < max(rsis), "rsi should swing wide on a full reversal"
assert kit.on_price(t + 1.2, 100.0) == {} , "between samples the kit is silent"
print(f"[{NAME}] selftest ok — {len(rsis)} rsi samples, crosses {seen}")
async def main():
parser = argparse.ArgumentParser(description="Binance trade ticks (+BTC trend kit) -> OSC")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=9000)
parser.add_argument("--selftest", action="store_true", help="check the trend math and exit")
args = parser.parse_args()
if args.selftest:
selftest()
return
emit = make_emitter(SimpleUDPClient(args.host, args.port))
print(f"[{NAME}] emitting {', '.join('/gs/' + k for k, _ in SYMBOLS.values())} "
f"+ btc trend kit (macd/rsi/cross) every <1s (ws) / {POLL_S:g}s (rest)")
backoff = 1.0
while True:
try:
await run_websocket(emit)
# Clean disconnect (stream ended): reconnect quickly.
print(f"[{NAME}] websocket closed; reconnecting")
backoff = 1.0
await asyncio.sleep(1.0) # avoid a hot loop on an instant re-close
except Exception as e:
# Bridge the gap on REST for the backoff window, then always retry
# the socket — never latch onto REST for the rest of the session.
print(f"[{NAME}] websocket error: {e}; REST bridge {backoff:g}s, then retry")
try:
await asyncio.wait_for(poll_rest(emit), timeout=backoff)
except asyncio.TimeoutError:
pass
except Exception as e2:
print(f"[{NAME}] REST bridge error: {e2}")
await asyncio.sleep(min(backoff, POLL_S))
backoff = min(backoff * 2, 30.0)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass