signalbetorg
crypto-signal
TypeScriptโœจ New

production grade real time trading signal platform on binance, bybit, coinbase and upbit

Last updated Jul 1, 2026
97
Stars
808
Forks
0
Issues
0
Stars/day
Attention Score
97
Language breakdown
No language data available.
โ–ธ Files click to expand
README

Crypto Signal App

Screenshot_2

A production-grade real-time cryptocurrency trading signal platform. Connect to Binance, Bybit, Coinbase, or Upbit and receive live BUY/SELL/WATCH signals generated by a proprietary multi-timeframe algorithm โ€” with confidence scores, target prices, stop-losses, and Web Push delivery to pro subscribers.

Built to demonstrate production-grade full-stack system design โ€” not just "display a price ticker."


Key Features

| Feature | Description | |---|---| | Multi-exchange WebSocket | Live price and candlestick streams from Binance, Bybit, Coinbase, and Upbit via a unified adapter pattern | | Proprietary Signal Engine | Multi-timeframe BUY/SELL/WATCH signals (5m/15m/1h) with two independent strategies: breakout and EMA pullback | | 90-Day Backtest Engine | Full replay with parameter sweep, trigger sweep, and per-confidence-tier win rate breakdown | | Signal History & Outcomes | Per-user WIN/LOSS/EXPIRED tracking with stats panel, outcome prices, and strategy sub-labels | | Web Push Notifications | VAPID-based push delivery gated behind Pro tier; server-side fanout via background Railway scanner | | Stripe Subscriptions | Checkout, Billing Portal, and webhook-driven tier sync with RLS-enforced write protection | | Background Scanner | Separate Railway server continuously polls 15 coins across all 4 exchanges, runs the signal generator on every poll, and deduplicates via a unique DB index โ€” guaranteeing each signal fires exactly once per candle | | Auth | Supabase email/password + GitHub OAuth with SSR-safe session handling and route protection |


Tech Stack

Frontend

  • Next.js 16.1.6 (App Router), React 19, TypeScript 5, Tailwind CSS 4
  • lightweight-charts v5 โ€” candlestick chart with live price overlay
Backend & Infrastructure
  • Supabase (PostgreSQL + RLS + SSR auth)
  • Stripe (Checkout + Billing Portal + webhooks)
  • Web Push API (web-push, VAPID)
  • Background scanner: Node.js on Railway (separate private repo)
  • Deployment: Vercel (Next.js app) + Railway (scanner)

Architecture

Signal Flow

Railway Scanner (background)
  โ””โ”€ continuously polls all 4 exchanges' REST APIs (sequential round-robin per coin)
       โ””โ”€ runs signal generator on every poll (breakout + EMA pullback)
            โ””โ”€ INSERT coin_signals (dedup: symbol ร— exchange ร— timestamp ร— strategy)
                 โ””โ”€ fanout.ts: query pro subscribers by exchange
                      โ”œโ”€ Web Push โ†’ each subscriber's browser
                      โ””โ”€ INSERT signal_history (per-user outcome tracking)

Next.js App (Vercel) โ””โ”€ WebSocket adapters โ†’ useLivePrice + useCandles โ””โ”€ useSignal (client-side signal hook, same generator) โ””โ”€ LivePrice.tsx (chart + signal banner + indicators)

Backtest โ””โ”€ /api/backtest โ†’ fetchHistoricalCandles (Binance REST, 90 days) โ””โ”€ runBacktest โ†’ iterates 15m candles, scores by confidence tier โ””โ”€ /backtest page (parameter sweep + trigger sweep UI)

Project Structure

app/
โ”œโ”€โ”€ components/         LivePrice.tsx, CandleChart.tsx
โ”œโ”€โ”€ api/
โ”‚   โ”œโ”€โ”€ backtest/       90-day replay endpoint (30s Vercel timeout)
โ”‚   โ”œโ”€โ”€ push/           subscribe + send (VAPID)
โ”‚   โ”œโ”€โ”€ stripe/         checkout, webhook, portal, sync, test-downgrade
โ”‚   โ”œโ”€โ”€ history/        CSV export (pro only)
โ”‚   โ””โ”€โ”€ account/        DELETE (profile โ†’ auth user, service role)
โ”œโ”€โ”€ backtest/           Parameter sweep + trigger sweep + EMA pullback UI
โ”œโ”€โ”€ history/            Signal history with WIN/LOSS/EXPIRED badges
โ””โ”€โ”€ settings/           Exchange, trading type, push toggle, Stripe billing

hooks/ โ”œโ”€โ”€ useLivePrice.ts Wraps ExchangeAdapter for ticker stream โ”œโ”€โ”€ useCandles.ts Rolling 100-candle window; WS for Binance, REST poll for others โ”œโ”€โ”€ useSignal.ts Memoized signal + 20-entry dedup history โ”œโ”€โ”€ useProfile.ts Supabase profile, tier, exchange, updateExchange() โ”œโ”€โ”€ usePushNotifications.ts SW registration, subscribe/unsubscribe โ””โ”€โ”€ useSignalHistory.ts Paginated signal history; free = 10 BTC-only rows

lib/ โ”œโ”€โ”€ exchanges/ โ”‚ โ”œโ”€โ”€ base-adapter.ts Abstract BaseAdapter โ€” exponential backoff reconnect โ”‚ โ”œโ”€โ”€ binance / bybit / โ”‚ โ”‚ coinbase / upbit / Exchange adapters (ticker + kline) โ”‚ โ”œโ”€โ”€ normalize.ts toExchangeSymbol(), isSupported(), getExchangeLabel() โ”‚ โ””โ”€โ”€ types.ts Candle, ExchangeAdapter, KlineInterval, PriceUpdate โ”œโ”€โ”€ signals/ โ”‚ โ”œโ”€โ”€ types.ts SignalType, Signal, SignalFeatures interfaces โ”‚ โ””โ”€โ”€ generator.ts โ† excluded (see Omitted Files) โ”œโ”€โ”€ indicators/ โ† excluded (see Omitted Files) โ”œโ”€โ”€ backtest/ โ† excluded (see Omitted Files) โ””โ”€โ”€ supabase/ Browser + server Supabase clients, middleware

supabase/migrations/ 11 incremental SQL migrations public/sw.js Service worker โ€” push event โ†’ showNotification


Engineering Deep Dive

Six non-trivial system design problems encountered and solved during development.


Challenge 1: Unified Multi-Exchange Adapter with Binary WebSocket Support

Problem. Each exchange uses a different WebSocket protocol: Binance uses plain JSON streams, Bybit requires a subscription message + 20s ping keepalive, Coinbase requires a JSON subscribe frame on connect, and Upbit sends binary ArrayBuffer data instead of text. Building per-exchange spaghetti would make adding or modifying any exchange a full rewrite risk.

Solution. lib/exchanges/base-adapter.ts defines an abstract BaseAdapter with exponential backoff reconnection (max 5 retries, capped delay) and two hook points โ€” afterConnect() and afterKlineConnect() โ€” that subclasses override to send subscription frames without touching the connection logic. Binary WebSocket support is handled once in BaseAdapter: binaryType = 'arraybuffer' is set on every socket, and the onmessage handler runs incoming data through a TextDecoder before passing it to parseMessage(). UpbitAdapter.parseMessage() receives a plain string and needs no special handling. Adding a new exchange requires only a new file extending BaseAdapter โ€” the reconnection, keepalive, and binary handling are inherited automatically.


Challenge 2: REST + WebSocket Hybrid Candle Feed Without Stale State

Problem. Binance provides a kline WebSocket stream that pushes every tick of the current open candle. Bybit, Coinbase, and Upbit provide no kline WebSocket โ€” only REST endpoints. Naively combining both approaches produces two failure modes: (a) Binance's open candle gets duplicated if the REST snapshot is not trimmed, and (b) Coinbase/Upbit show a frozen last candle between 15-second polls.

Solution. hooks/useCandles.ts diverges by adapter type. For Binance, the REST snapshot drops its last entry (data.slice(0, -1)) and the WebSocket stream takes over the live open candle โ€” same time โ†’ replace, new time โ†’ append. For Bybit, Coinbase, and Upbit, all REST candles are kept (no slice), and a displayCandlesLive memo in LivePrice.tsx overlays the current ticker price onto the last candle's close between polls. This produces smooth price movement on chart for all four exchanges with a single unified CandleChart component.


Challenge 3: Preventing Chart Re-renders on Every Price Tick

Problem. CandleChart.tsx calls series.setData() to render candlestick data. The live ticker fires at ~1s intervals via WebSocket. Without memoization, every price tick triggers a full chart redraw โ€” setData() on 100 candles at 1Hz causes visible jank and unnecessary GPU work.

Solution. CandleChart is wrapped in React.memo. The candle array prop is only updated on actual new candles (15m intervals), not on price ticks โ€” the ticker overlay is computed in displayCandlesLive and passed as a separate stable memo. A subtler issue: last5History (signal markers passed to the chart) was initially computed as an inline .slice().reverse(), which creates a new array reference on every render and silently bypasses React.memo. The fix is useMemo(() => history.slice(-5).reverse(), [history]) โ€” producing a stable reference that only changes when signal history actually changes.


Challenge 4: Stripe Webhook Tier Enforcement with RLS Write Protection

Problem. Stripe webhook events (checkout.session.completed, customer.subscription.deleted) must update the tier and stripecustomerid columns in Supabase profiles. But these same columns must be read-only to the anon client โ€” otherwise any authenticated user could self-upgrade their tier by calling the Supabase client directly.

Solution. Two independent layers enforce this. First, supabase/migrations/007restricttierupdate.sql adds an RLS policy that blocks any UPDATE touching tier or stripecustomerid from the anon key โ€” regardless of which row. Second, app/api/stripe/webhook/route.ts instantiates a Supabase client inline using SUPABASESERVICEROLEKEY (bypasses RLS entirely), never using lib/supabase/server.ts (which uses the anon key with cookies). The webhook also reads the raw body via req.text() rather than req.json() โ€” Stripe's HMAC signature verification requires the exact raw byte sequence, which req.json() would destroy by re-serializing.


Challenge 5: Multi-Strategy Signal Deduplication on a Background Scanner

Problem. The Railway scanner polls continuously in a sequential round-robin loop and must fire a signal exactly once per candle per strategy โ€” never duplicating on subsequent polls within the same candle window, and never conflating signals from two different strategies (breakout vs. EMA pullback) on the same candle.

Solution. coinsignals has a unique index on (symbol, exchange, timestamp, strategy). The scanner calls both generateSignal() and generateSignalEMAPullback() on every poll and routes each through a processSignal() helper that attempts an INSERT โ€” PostgreSQL silently rejects duplicates on the unique index. The first poll that meets signal conditions fires fanout and trade execution; all subsequent polls within the same 15m candle are no-ops at the DB layer. The strategy column in the index means both strategies can fire independently on the same candle timestamp without collision. signalhistory carries the same strategy column so the UI can render per-row strategy sub-labels (EMA pullback vs. breakout).


Challenge 6: Per-User Signal Outcome Tracking with Expiry Pricing

Problem. Standard signal tracking logs whether a signal fired โ€” but not whether it was correct. For a trading signal app, the key metric is whether the price actually moved to the target (WIN), hit the stop-loss (LOSS), or expired with the position neither won nor lost (EXPIRED). EXPIRED signals require special handling because the outcome price has directional meaning โ€” slightly above entry is a different result for a BUY than for a SELL.

Solution. signal_history rows are inserted by fanout.ts at signal fire time with outcome = 'PENDING'. The expiry monitor checks open positions every 60 seconds and resolves outcomes: WIN if price moved โ‰ฅ target direction, LOSS if stop-loss was hit, EXPIRED otherwise. The history UI (app/history/page.tsx) applies directional coloring to EXPIRED outcome prices โ€” emerald if the price moved favorably (within ยฑ0.01% zone relative to entry), red if it moved against โ€” giving users a quick read on near-miss signals without misrepresenting them as wins or losses.


Omitted Files

The following files are excluded from this repository to protect proprietary trading logic. The repository will not compile as-is โ€” these are required dependencies of hooks/useSignal.ts, app/api/backtest/route.ts, and app/backtest/page.tsx.

| Path | What it contains | |---|---| | lib/signals/generator.ts | Core signal algorithm โ€” multi-timeframe entry conditions, two independent strategies, all filter thresholds and parameters | | lib/indicators/ | All technical indicator implementations used by the signal generator and backtest engine | | lib/backtest/engine.ts | Backtest simulation engine โ€” trade result scoring, win/loss evaluation, confidence tier breakdown | | lib/backtest/sweep.ts | Parameter sweep and trigger sweep factory functions | | lib/backtest/fetchHistorical.ts | Paginated Binance REST historical candle fetcher | | scripts/ | Internal backtesting scripts and ML training data export pipeline |


Installation & Setup

Prerequisites

  • Node.js 20+
  • Supabase project (PostgreSQL + auth)
  • Stripe account with a subscription product
  • VAPID key pair (generate via web-push generate-vapid-keys)
Note: This repo is shared for architecture and code review. The omitted algorithm files are required to run the signal and backtest features. The exchange adapters, auth, push, and Stripe flows are fully present.

1. Clone and install

git clone https://github.com/Taehoonryu04/CryptoSignalApp.git
cd CryptoSignalApp
npm install

2. Configure environment variables

Create .env.local at the project root:

# Supabase
NEXTPUBLICSUPABASE_URL=
NEXTPUBLICSUPABASEANONKEY=
SUPABASESERVICEROLE_KEY=

Web Push (VAPID)

NEXTPUBLICVAPIDPUBLICKEY= VAPIDPRIVATEKEY= VAPID_SUBJECT=mailto:you@example.com

Stripe

STRIPESECRETKEY= STRIPEWEBHOOKSECRET= NEXTPUBLICSTRIPEPUBLISHABLEKEY= STRIPEPROPRICE_ID= NEXTPUBLICSITE_URL=http://localhost:3000

3. Run Supabase migrations

Run all 11 SQL files in supabase/migrations/ in order via the Supabase SQL Editor.

4. Run

npm run dev     # http://localhost:3000
npm run build   # Production build
npm start       # Production server

License

ยฉ 2026 Taehoonryu04. All rights reserved.

The source code is publicly visible for portfolio and recruiting review. No permission is granted to use, copy, deploy, or redistribute this project without explicit written consent.

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท signalbetorg/crypto-signal ยท Updated daily from GitHub