A new Settings view covering everything asked for: display currency
(applied to Portfolio/Demo Trading totals via a live USD rate, yfinance
reused rather than a new FX dependency), number formatting, default
market/timeframes on load, chart style (added a real line-chart
renderer alongside the existing candlestick one), gridlines toggle,
reduce-motion override, notification quiet hours + digest frequency,
display name/bio/trading-style tag, a real avatar upload (resized +
center-cropped server-side, stored the same way TelegramConvertedSticker
already stores bytes) with a deterministic initials fallback when none
is set, a stats/badges card computed on the fly from existing data, CSV
export, log-out-everywhere, and account deletion.
Everything new preference/profile field lives in one new UserProfile
table rather than columns on User - this app has no migrations
(Base.metadata.create_all only creates missing tables), so a new table
needs none. Two things genuinely couldn't avoid touch…
Fix mobile overflow, commodity symbols, live P&L, and cross-device tab consistency
- Analyze page's market-tabs row never got flex-wrap when the 4th
(Commodities) tab was added — overflowed off-screen on narrow phones.
Now wraps, and gets the same mobile size reduction .sign already had.
- Demo Trading's own market tabs now share .market-tab's exact shape/
active-state styling instead of a separate pill design, so the two
don't read as different design languages within the same app.
- Commodities now show familiar slash-pair symbols (XAU/USD, WTI/USD,
NATGAS/USD, ...) instead of raw yfinance futures notation (GC=F) —
translated to the actual yfinance ticker server-side
(COMMODITY_ANALYZE_TO_YFINANCE), derived from one source of truth so
the ticker tape and /analyze can't drift apart. Raw =F notation still
accepted for commodities without a standard pretty name (wheat, etc).
- Demo Trading now polls /demo/account every 8s and live-updates P&L/
prices in place (stats, positions, ticket price) without touching the
search box or quantity input, so a…
Add commodities as a first-class market (gold, silver, oil, gas, metals, grains)
Registers commodities alongside crypto/forex/stocks throughout the
analysis pipeline, ticker tape, alerts/portfolio, and website UI. Uses
yfinance (free, no plan gating) instead of Twelve Data, whose free tier
only serves gold and paywalls the rest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix futures scan hanging production: pin binanceusdm's fetchMarkets to linear only
Live bug, found right after the futures-coverage deploy: /scan/gamma got
stuck for 10+ minutes on its first-ever futures fetch, holding
_scan_gamma_lock the whole time (every subsequent cron poll correctly
returned "skipped: busy" rather than double-running, but the endpoint
itself never completed).
Root cause: ccxt.binanceusdm's OWN default fetchMarkets option is
['spot', 'linear', 'inverse'], not just linear futures. An unpinned
instance's first load_markets() call (triggered lazily by the very
first fetch_ohlcv call) therefore ALSO fires publicGetExchangeInfo
(spot, urls["api"]["public"] - never overridden on this instance,
defaults to the geo-blocked api.binance.com) and
dapiPublicGetExchangeInfo (coin-margined, urls["api"]["dapiPublic"] -
also never overridden, defaults to the geo-blocked dapi.binance.com).
Both calls hung/failed against the HF Space's real geo-blocked network
position.
Fixed: options.fetchMarkets pinned to ["linear"] - confirmed live this
loads the identical 86…
Root cause of the missing CYS crash: Pulsar's scanner is spot-only by
design (fetchMarkets: ["spot"]) - CYS never listed on Binance spot at
all, only futures, so it was invisible to Gamma regardless of freshness.
Many of the fastest, most violent moves are futures-driven (leverage,
liquidation cascades), so spot-only coverage was missing exactly the
events this strategy cares most about.
- data/fetcher.py: new fetch_futures_ohlcv via a separate ccxt.binanceusdm
instance (USDT-M perpetuals). Unlike spot's data-api.binance.vision,
fapi.binance.com has NO unrestricted mirror - confirmed live (451 from
a US host) - so the BINANCE_PROXY_BASE_URL relay is required in
production for futures, not just an optimization.
- scan/universe.py: fetch_futures_universe/refresh_futures_universe
build a separate top-N perpetuals universe (contractType ==
"PERPETUAL" AND status == "TRADING", same halted-symbol fix as spot),
written to a new ScannerFuturesSymbol table - kept separate from
…
Rename the Binance proxy auth header away from X-Proxy-Secret
Discovered while testing the deployed Worker live: something in my
own environment's network path was silently blocking any request
carrying a header literally named X-Proxy-Secret (isolated via direct
testing — a header containing the word "secret" elsewhere passed
through fine, so it's specifically the "X-Proxy-" prefix pattern,
likely a proxy-header-stripping security measure somewhere upstream).
Renamed to X-Pulsar-Relay-Auth throughout (Worker, ccxt config, the
plain httpx call in scan/universe.py) to avoid that class of
middleware entirely. Same env var names (BINANCE_PROXY_SECRET,
BINANCE_PROXY_BASE_URL) — only the wire header name changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed live: Pulsar's own scanning workload (klines is a flat
weight-2 call per Binance's own docs) adds up to roughly 68
weight/minute against Binance's published 6000/minute limit — nowhere
near enough to explain a real 418 IP ban that happened anyway,
pointing at a shared outbound IP on HF Spaces' free tier absorbing
some other tenant's unrelated Binance traffic against the same
per-IP budget. The weight-based backoff shipped earlier only makes
sure Pulsar itself never contributes to the problem; it can't fix a
shared IP.
Routes Binance calls through a new Cloudflare Worker instead, the
same pattern already used for the Telegram relay and the custom
domain — gets this traffic onto Cloudflare's own, much larger and
differently-shared IP pool. Optional via BINANCE_PROXY_BASE_URL/
BINANCE_PROXY_SECRET env vars — falls back to calling
data-api.binance.vision directly when unset, so local dev/tests don't
need the Worker deployed. The Worker is a transparent passthrough
that must pres…
Fix Binance rate-limit backoff not being shared across threads
Reported live: a real 418 ban happened again despite the earlier
weight-based backoff already being deployed. Root cause: that backoff
was per-thread (threading.local) and only checked AFTER a request
completed — concurrent threads (overlapping cron cycles, live
/analyze requests) each only saw their OWN last response's weight, so
several could each look comfortably under budget individually while
collectively blowing past the real shared-IP limit before any single
one of them noticed.
Replaced with a process-wide, lock-protected backoff window checked
BEFORE every request, not just recorded after one — one thread
noticing high usage now actually stops every other thread from firing
too. Also tightened the safety margin (75% instead of 80% of the
6000/min limit) and lengthened the pause (15s instead of 5s) to leave
more headroom for the blind spot that let this recur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Back off on Binance's real used-weight header to avoid IP bans
Confirmed live in production: a real 418 "Way too much request weight
used" IP ban from Binance, despite this app's own estimated request
volume looking comfortably under Binance's published 6000/minute
REQUEST_WEIGHT limit (confirmed live via GET /api/v3/exchangeInfo)
in isolation. ccxt's enableRateLimit only paces this process's own
request rate — it has no visibility into other traffic that might
share the same IP (common on cloud hosts), which also counts against
the same per-IP budget.
Fixed by watching Binance's own x-mbx-used-weight-1m response header
(ground truth for this IP, including anything else sharing it) after
every fetch and backing off proactively once it's within 80% of the
real limit, rather than only reacting after the fact.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cache the ccxt Binance exchange instance per worker thread
fetch_ohlcv created a brand-new ccxt.binance() on every single call. Two
compounding costs from that: (1) markets are lazily loaded per-instance,
so a fresh instance re-fetches exchangeInfo (full market metadata) every
time instead of once; (2) ccxt's enableRateLimit pacing state lives on
the instance too, so it was being reset every call and never actually
throttling anything across requests.
This just got the production Space's IP rate-limit banned by Binance
("418 I'm a teapot ... Way too much request weight used") once the
scanner's higher call volume (157 symbols vs. the original 50, 3 fits/
tick vs. 1) pushed it over the edge — but the bug predates the scanner
and affects every Binance-backed feature (/analyze, /alerts/check,
ticker), just wasn't visible at lower call volume before.
Fixed by caching one exchange instance per worker thread (fetch_ohlcv
runs inside run_in_executor's thread pool) via threading.local — per-
thread rather than a single shared instance, since ccxt's …
Add forex/stocks live ticker tape and scanning animation
Backend: new /tickers endpoint (data/quotes.py) serves cached quotes
for forex and stock tickers via yfinance. Symbol lists are fixed
server-side (not caller-supplied) so the endpoint can't be used to
probe arbitrary symbols or burn upstream quota, and results are cached
for 5 minutes with a lock against concurrent-request stampedes. Crypto
keeps reading Binance directly from the browser, unchanged.
Frontend: the ticker tape now switches data source per market tab
(Binance direct for crypto, /tickers for forex/stocks, refreshed on
different intervals — 30s vs 5min — matching each source's actual
freshness). Loading state replaced with an animated fake candlestick
scanner instead of plain skeleton cards. Price formatting is now
market-aware (no more hardcoded $ prefix on stock tickers that trade
in INR or forex pairs that are exchange rates, not currency amounts).
Verified end-to-end in a real headless-Chromium session: ticker tape
correctly repopulates on each tab switch, scanner ani…
Fix Indian stocks: switch stocks to yfinance (Twelve Data gates by exchange)
Twelve Data's free tier 404s on Indian exchange symbols (VEDL, HCL,
etc) with "available starting with the Grow/Venture plan" — a paid-
tier restriction, not a bug. Switched the stocks market to yfinance
instead: free, no API key, and not gated by exchange (confirmed
AAPL/MSFT and VEDL.NS/HCLTECH.NS/RELIANCE.NS/TATASTEEL.NS all work).
Twelve Data stays for forex, which isn't affected by this.
- Stock symbol regex now allows exchange suffixes (.NS, .BO, etc),
not just single-letter share classes like BRK.B
- News search strips the exchange suffix ("VEDL.NS" -> "VEDL") so it
doesn't pollute the NewsAPI query
- Frontend: updated stock placeholder to hint at the suffix convention,
added RELIANCE.NS to the stock quick-picks
Also fixed a real bug hit during testing: Yahoo sometimes returns a
placeholder row for the current in-progress period (e.g. "today"
before the daily candle closes) with all-NaN OHLC — this crashed the
whole /analyze response with a 500 (NaN isn't JSON-serializab…
Add forex and stocks support via Twelve Data (backend only)
/analyze gains a market param (crypto | forex | stocks). Crypto keeps
using Binance unchanged; forex/stocks route to a new Twelve Data
fetcher returning the same DataFrame shape, so indicators/scorer/
backtest engine needed zero changes to work with the new asset
classes — confirmed end-to-end (e.g. logistic regression beat the
static baseline 56% vs 32% on AAPL, gradient boosting hit 75.5% on
GBP/JPY in testing).
- Per-market symbol validation (crypto/forex: BASE/QUOTE, stocks:
bare ticker with optional share-class suffix like BRK.B)
- News query wording adapts per market ("AAPL stock" vs "BTC
cryptocurrency") and cache keys are namespaced by market to avoid
cross-asset-class collisions
- Twelve Data's own errors surface through the same safe-error-message
path as ccxt's (fixed a bug where their error body wasn't checked
before raise_for_status(), so it was falling into the generic
"internal error" bucket instead of showing their actual message)
- No frontend changes yet …
fetchMarkets defaults to spot+linear+inverse futures; loading futures
exchangeInfo (fapi.binance.com) hit the same US geo-block as spot with
no unrestricted mirror available. We only ever fetch spot OHLCV, so
skip loading futures/margin market lists entirely.
api.binance.com geo-blocks US-based IPs (including Render's default
region), returning HTTP 451 for every candle fetch. Point public OHLCV
requests at data-api.binance.vision instead — Binance's own read-only
market-data mirror intended for exactly this, no geo-block, no API key.