diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..878a454 --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,771 @@ +# Flowex API Reference + +Full API reference for the flowex library. For installation, quick start, and usage examples, see [README.md](README.md). + +--- + +## Table of Contents + +- [Snapshot & Manager Interface](#snapshot--manager-interface) +- [Data Models](#data-models) +- [Depth Metrics Reference](#depth-metrics-reference) +- [Depth Store Query Methods](#depth-store-query-methods) +- [Historical Data Seeding](#historical-data-seeding) +- [Candle Deduplication](#candle-deduplication) +- [Worker Monitoring](#worker-monitoring) +- [Worker Error Tracking](#worker-error-tracking) +- [Handler Callbacks](#handler-callbacks) +- [Convenience Worker Accessors](#convenience-worker-accessors) +- [Auto-Reconnect Behavior](#auto-reconnect-behavior) +- [Handler Types Reference](#handler-types-reference) +- [Technical Indicators (Optimized)](#technical-indicators-optimized) +- [Signal & Movement Types](#signal--movement-types) + +--- + +## Snapshot & Manager Interface + +Every exchange manager implements the `ws.Manager` interface: + +```go +type Manager interface { + SubscribeCandle(symbol string, handler CandleHandler) error + SubscribeDepth(symbol string, handler DepthHandler) error + SubscribeTrade(symbol string, handler TradeHandler) error + SubscribeAll(symbol string, ch CandleHandler, dh DepthHandler, th TradeHandler) error + Unsubscribe(symbol string, streamType StreamType) error + UnsubscribeAll(symbol string) error + GetSnapshot(symbol string) *Snapshot + GetStatus() map[string]interface{} + Shutdown() +} +``` + +`GetSnapshot` returns an immutable, point-in-time view: + +```go +type Snapshot struct { + Timestamp time.Time // when the snapshot was taken + Candles []models.CandleHLCV // historical + live candle bars + DepthStore *depth.Store // order book metrics with time-bucketed storage + Trades []models.NormalizedTrade // recent trades, normalized across exchanges +} +``` + +Snapshots are updated atomically at a configurable interval (default 1s). Readers never contend with the writer — safe to call from any goroutine. + +--- + +## Data Models + +### CandleHLCV + +OHLCV bar from any exchange. Used in snapshots and by indicators that need volume. + +```go +type CandleHLCV struct { + Ts int64 // Unix millisecond timestamp + Open float64 + High float64 + Low float64 + Close float64 + Volume float64 +} +``` + +Helper methods: `GetTimestamp()`, `HL2()` (High+Low)/2, `HLC3()` (High+Low+Close)/3. + +### CandleHLC + +Lighter candle without Open/Volume. Used by ATR, Bollinger, and Support/Resistance indicators. + +```go +type CandleHLC struct { + // ts is unexported — access via GetTimestamp() + High float64 + Low float64 + Close float64 +} +``` + +Methods: `GetTimestamp()`, `GetHigh()`, `GetLow()`, `GetClose()`. + +### NormalizedTrade + +Unified trade format across all exchanges. + +```go +type NormalizedTrade struct { + Timestamp int64 // Unix milliseconds + Price float64 + Size float64 // base currency amount + SizeUSD float64 + Side string // "buy" or "sell" + TradeID string + Symbol string // e.g. "BTCUSDT" + Exchange string // "binance", "bybit", "bitget" +} +``` + +### TickerData + +Defined in `models/ticker.go`. Currently reserved for future ticker stream support — not actively used by any manager. + +```go +type TickerData struct { + Symbol string + LastPr float64 + Bid float64 + Ask float64 + BidStr string + AskStr string + Price float64 + PriceStr string +} +``` + +--- + +## Depth Metrics Reference + +`depth.DepthMetrics` contains 75 computed fields from raw order book data. Fields are grouped by category. + +### Spread (7 fields) + +| Field | Type | Description | +|-------|------|-------------| +| `Timestamp` | int64 | Unix milliseconds | +| `Symbol` | string | Trading pair | +| `BestBid` | float64 | Best bid price | +| `BestAsk` | float64 | Best ask price | +| `Spread` | float64 | ask - bid | +| `SpreadBps` | float64 | spread / mid * 10000 (basis points) | +| `MidPrice` | float64 | (bid + ask) / 2 | + +### Liquidity — USD Value (8 fields) + +Dollar value of resting orders at each depth level. + +| Field | Type | Description | +|-------|------|-------------| +| `BidLiquidity5` | float64 | Bid-side liquidity, top 5 levels | +| `AskLiquidity5` | float64 | Ask-side liquidity, top 5 levels | +| `BidLiquidity10` | float64 | Top 10 levels | +| `AskLiquidity10` | float64 | Top 10 levels | +| `BidLiquidity20` | float64 | Top 20 levels | +| `AskLiquidity20` | float64 | Top 20 levels | +| `BidLiquidity50` | float64 | Top 50 levels | +| `AskLiquidity50` | float64 | Top 50 levels | + +### Volume — Coin Size (8 fields) + +Raw coin volume at each depth level (not USD-denominated). + +| Field | Type | Description | +|-------|------|-------------| +| `BidVolume5` | float64 | Bid volume, top 5 levels | +| `AskVolume5` | float64 | Ask volume, top 5 levels | +| `BidVolume10` | float64 | Top 10 levels | +| `AskVolume10` | float64 | Top 10 levels | +| `BidVolume20` | float64 | Top 20 levels | +| `AskVolume20` | float64 | Top 20 levels | +| `BidVolume50` | float64 | Top 50 levels | +| `AskVolume50` | float64 | Top 50 levels | + +### Imbalance (6 fields) + +Measures bid/ask asymmetry. Ratio > 1 = bid-heavy (bullish signal). Delta ranges -100 to +100. + +| Field | Type | Description | +|-------|------|-------------| +| `ImbalanceRatio5` | float64 | bid_liq / ask_liq at 5 levels | +| `ImbalanceRatio10` | float64 | At 10 levels | +| `ImbalanceRatio20` | float64 | At 20 levels | +| `ImbalanceRatio50` | float64 | At 50 levels | +| `ImbalanceDelta10` | float64 | (bid-ask)/(bid+ask)*100 at 10 levels | +| `ImbalanceDelta20` | float64 | (bid-ask)/(bid+ask)*100 at 20 levels | + +### Walls — Largest Single Orders (6 fields) + +Detects large resting orders that may act as support/resistance. + +| Field | Type | Description | +|-------|------|-------------| +| `LargestBidSize` | float64 | Biggest single bid order (coin size) | +| `LargestBidPrice` | float64 | Price level of that bid | +| `LargestBidValue` | float64 | USD value of that bid | +| `LargestAskSize` | float64 | Biggest single ask order (coin size) | +| `LargestAskPrice` | float64 | Price level of that ask | +| `LargestAskValue` | float64 | USD value of that ask | + +### Slippage Estimation (16 fields) + +Estimated price impact (%) for a market order of a given USD size. + +| Field | Type | Description | +|-------|------|-------------| +| `SlippageBuy100` | float64 | Slippage to buy $100 | +| `SlippageSell100` | float64 | Slippage to sell $100 | +| `SlippageBuy1K` | float64 | $1,000 | +| `SlippageSell1K` | float64 | $1,000 | +| `SlippageBuy5K` | float64 | $5,000 | +| `SlippageSell5K` | float64 | $5,000 | +| `SlippageBuy10K` | float64 | $10,000 | +| `SlippageSell10K` | float64 | $10,000 | +| `SlippageBuy50K` | float64 | $50,000 | +| `SlippageSell50K` | float64 | $50,000 | +| `SlippageBuy100K` | float64 | $100,000 | +| `SlippageSell100K` | float64 | $100,000 | +| `SlippageBuy500K` | float64 | $500,000 | +| `SlippageSell500K` | float64 | $500,000 | +| `SlippageBuy1M` | float64 | $1,000,000 | +| `SlippageSell1M` | float64 | $1,000,000 | + +### Velocity — Rate of Change (5 fields) + +How fast metrics are changing. Computed from historical store data. + +| Field | Type | Description | +|-------|------|-------------| +| `LiquidityVelocity10` | float64 | Rate of change of liquidity at 10 levels | +| `LiquidityVelocity50` | float64 | At 50 levels | +| `ImbalanceVelocity` | float64 | Rate of imbalance shift | +| `SpreadVelocity` | float64 | Rate of spread change | +| `WallVelocity` | float64 | Rate of wall size change | + +### Momentum (4 fields) + +Trend direction indicators derived from order flow. + +| Field | Type | Description | +|-------|------|-------------| +| `BuyPressureMomentum` | float64 | Buy-side pressure trend | +| `SellPressureMomentum` | float64 | Sell-side pressure trend | +| `WallBuildingBid` | bool | True if bid wall is growing over time | +| `WallBuildingAsk` | bool | True if ask wall is growing over time | + +### Statistical Z-Scores (3 fields) + +How unusual the current value is compared to recent history. High absolute z-score = unusual. + +| Field | Type | Description | +|-------|------|-------------| +| `LiquidityZScore10` | float64 | How unusual current liquidity is | +| `ImbalanceZScore` | float64 | How unusual imbalance is | +| `SpreadZScore` | float64 | How unusual spread is | + +### Depth Quality & Microstructure (12 fields) + +| Field | Type | Description | +|-------|------|-------------| +| `BidLevelsCount` | int | Number of bid price levels in the book | +| `AskLevelsCount` | int | Number of ask price levels | +| `AvgBidSize10` | float64 | Average bid size in top 10 levels | +| `AvgAskSize10` | float64 | Average ask size in top 10 levels | +| `TopBidConcentration5` | float64 | How concentrated top 5 bids are | +| `TopAskConcentration5` | float64 | How concentrated top 5 asks are | +| `SpreadNormImbalanceDelta10` | float64 | Spread-normalized imbalance at 10 levels | +| `SpreadNormImbalanceDelta20` | float64 | Spread-normalized imbalance at 20 levels | +| `SlippageGradientBuy` | float64 | How slippage scales with order size (buy) | +| `SlippageGradientSell` | float64 | How slippage scales with order size (sell) | +| `SlippageSkew1K` | float64 | Buy vs sell slippage asymmetry at $1K | +| `SlippageSkew10K` | float64 | Buy vs sell slippage asymmetry at $10K | + +All fields have JSON tags (e.g., `json:"spread_bps"`). The full struct is defined in `depth/metrics.go`. + +--- + +## Depth Store Query Methods + +`depth.Store` provides time-bucketed storage with several query methods: + +```go +store := snap.DepthStore + +// Most recent metric, or nil if no data yet +latest := store.GetLatest() + +// Copy of the recent metrics buffer (default last 100 entries) +recent := store.GetRecent() + +// All metrics from the last N seconds +last30s := store.GetLastNSeconds(30) + +// Metrics within a specific time window (Unix milliseconds, inclusive) +ranged := store.GetByTimeRange(startMs, endMs) + +// Total number of stored metrics +count := store.Size() +``` + +All methods are thread-safe (read-locked). `GetLatest()` and `GetRecent()` return copies — safe to hold across calls. + +--- + +## Historical Data Seeding + +Most strategies need candle history on startup. Fetch via REST, then feed into the worker: + +```go +mgr := binance.NewManager() +mgr.SubscribeAll("BTCUSDT", nil, nil, nil) + +// Access the worker +worker := mgr.GetOrCreateWorker("BTCUSDT") + +// Fetch historical candles via REST +hist, err := candles.FetchBinanceCandles("BTCUSDT", "1m", 500) +if err != nil { + log.Fatal(err) +} + +// Seed them into the worker +for _, c := range hist { + worker.EnqueueCandle(ws.CandleMsg{ + Timestamp: c.Ts, + Open: fmt.Sprintf("%f", c.Open), + High: fmt.Sprintf("%f", c.High), + Low: fmt.Sprintf("%f", c.Low), + Close: fmt.Sprintf("%f", c.Close), + Volume: fmt.Sprintf("%f", c.Volume), + }) +} +// Snapshot now has 500 candles immediately — no waiting for live bars +``` + +The `CandleMsg` struct expects string values (matching the raw WebSocket format): + +```go +type CandleMsg struct { + Timestamp int64 + Open, High, Low, Close, Volume string +} +``` + +Similarly available: `EnqueueDepth(DepthMsg)` and `EnqueueTrade(TradeMsg)`. + +All enqueue methods are non-blocking. If the channel is full, the oldest message is dropped and the drop counter increments. + +--- + +## Candle Deduplication + +The worker automatically deduplicates candles using timestamp logic: + +| Incoming candle timestamp | Behavior | +|--------------------------|----------| +| **Same** as last candle | Updates in place: High (if higher), Low (if lower), Close, Volume | +| **Newer** than last candle | Appends as new bar, trims oldest if over `MaxCandles` | +| **Older** than last candle | Silently ignored | + +This means you can safely overlap historical and live data — for example, fetch 500 historical candles then subscribe to live, even if some timestamps overlap. The worker handles dedup automatically. No risk of duplicate bars. + +--- + +## Worker Monitoring + +Track worker health via `GetMetrics()`: + +```go +worker := mgr.GetOrCreateWorker("BTCUSDT") +metrics := worker.GetMetrics() + +metrics["processed"] // total messages processed across all channels +metrics["candle_dropped"] // candle messages dropped (channel was full) +metrics["depth_dropped"] // depth messages dropped +metrics["trade_dropped"] // trade messages dropped +metrics["candle_queue"] // current candle channel fill level +metrics["depth_queue"] // current depth channel fill level +metrics["trade_queue"] // current trade channel fill level +``` + +**When to act:** +- If `*_dropped` counts are climbing, the worker can't keep up. Increase the corresponding `*ChSize` in `WorkerConfig`. +- If `*_queue` values are consistently near capacity, consider reducing subscription load or increasing buffer sizes. +- `processed` count growing steadily = healthy worker. + +--- + +## Worker Error Tracking + +Workers track the last 10 parse/processing errors: + +```go +worker := mgr.GetOrCreateWorker("BTCUSDT") +errors := worker.GetRecentErrors() +// Returns []string, e.g.: +// ["[15:04:05] parse candle: strconv.ParseFloat: ..."] +``` + +Useful for detecting malformed exchange data or API format changes. Errors are timestamped with `[HH:MM:SS]` prefix. + +--- + +## Handler Callbacks + +### Subscribe-time handlers + +The subscribe methods accept handler functions that fire on every raw message from the WebSocket: + +```go +// Called for every candle update from the exchange +mgr.SubscribeCandle("BTCUSDT", func(candle models.CandleHLCV) { + fmt.Printf("candle: O=%.2f C=%.2f V=%.4f\n", candle.Open, candle.Close, candle.Volume) +}) + +// Called for every depth snapshot +mgr.SubscribeDepth("BTCUSDT", func(bids, asks [][]string, ts int64) { + fmt.Printf("depth: %d bids, %d asks\n", len(bids), len(asks)) +}) + +// Called for every trade +mgr.SubscribeTrade("BTCUSDT", func(trade models.NormalizedTrade) { + fmt.Printf("trade: %s $%.0f @ %.2f\n", trade.Side, trade.SizeUSD, trade.Price) +}) + +// Pass nil for any handler you don't need +mgr.SubscribeAll("BTCUSDT", nil, nil, nil) +``` + +### Worker hooks (SetOn*Update) + +Worker hooks fire inside the worker goroutine **after** state has been mutated: + +```go +worker := mgr.GetOrCreateWorker("BTCUSDT") + +// Called after candle state is updated — receives the full candle slice +worker.SetOnCandleUpdate(func(candles []models.CandleHLCV) { + // candles includes all history, not just the latest +}) + +// Called after depth metrics are computed +worker.SetOnDepthUpdate(func(m depth.DepthMetrics) { + // m is the freshly computed metric +}) + +// Called after a trade is normalized and stored +worker.SetOnTradeUpdate(func(t models.NormalizedTrade) { + // t is the single new trade +}) +``` + +**Key difference:** +- **Subscribe handlers** fire on the dispatch path (raw WebSocket messages, before processing) +- **Worker hooks** fire inside the worker loop (after state mutation, with access to full state) + +Use subscribe handlers for logging/forwarding raw data. Use worker hooks for strategy logic that depends on accumulated state. + +--- + +## Convenience Worker Accessors + +Shortcuts that read from the snapshot internally: + +```go +worker := mgr.GetOrCreateWorker("BTCUSDT") + +candles := worker.GetCandles() // []models.CandleHLCV (or nil) +store := worker.GetDepthStore() // *depth.Store (or nil) +trades := worker.GetNormalizedTrades() // []models.NormalizedTrade (or nil) +``` + +These are equivalent to `worker.GetSnapshot().Candles`, etc., with nil-safety built in. + +--- + +## Auto-Reconnect Behavior + +The WebSocket client automatically handles connection drops: + +1. On read error: waits `ReconnectDelay` (default **2 seconds**), then reconnects +2. After reconnect: calls `ResubscribeFunc` to restore all active stream subscriptions +3. No manual intervention needed — the manager handles the full lifecycle + +**Connection defaults:** + +```go +ClientConfig{ + ReadBufferSize: 16 * 1024, // 16 KB + WriteBufferSize: 16 * 1024, // 16 KB + ReconnectDelay: 2 * time.Second, +} +``` + +**Additional details:** +- WebSocket compression is enabled by default (`dialer.EnableCompression = true`) +- Heartbeat pings are exchange-specific and handled automatically by each adapter +- Binance: no application-level ping (uses WebSocket protocol pings) +- Bybit/Bitget: application-level pings configured internally by their `NewClient()` functions + +--- + +## Handler Types Reference + +Defined in `ws/interfaces.go`: + +```go +// CandleHandler is called when a new candle update arrives. +type CandleHandler func(candle models.CandleHLCV) + +// DepthHandler is called when a new order book snapshot arrives. +type DepthHandler func(bids, asks [][]string, timestamp int64) + +// TradeHandler is called when a new trade arrives. +type TradeHandler func(trade models.NormalizedTrade) +``` + +Stream type constants for unsubscribe: + +```go +const ( + StreamCandle StreamType = "candle" + StreamDepth StreamType = "depth" + StreamTrade StreamType = "trade" +) +``` + +--- + +## Technical Indicators (Optimized) + +The `indicators/technical` package provides batch-optimized indicator calculations with pre-computed multipliers, single-pass algorithms, and pooled memory. These complement the standard `indicators/` package. + +### CalculateTechnicalIndicators + +Computes all indicators in one call. Returns a `TechnicalIndicators` struct with RSI, SMA, EMA, MACD, Bollinger Bands, ATR, StochRSI, MMI, and TradingView-style summary signals. + +```go +import "github.com/KhavrTrading/flowex/indicators/technical" + +// Needs at least 20 candles, 200+ for full SMA200/EMA200 +result := technical.CalculateTechnicalIndicators(candles, currentPrice) +if result == nil { + return // not enough data +} + +// Individual indicators +result.RSI14 // RSI (14-period) +result.EMA9 // EMA 9 +result.SMA200 // SMA 200 +result.MACDLine // MACD line +result.SignalLine // MACD signal +result.Histogram // MACD histogram +result.BBUpper // Bollinger upper band +result.BBMiddle // Bollinger middle +result.BBLower // Bollinger lower band +result.ATR // Average True Range (14) +result.StochRSI // Stochastic RSI +result.MMI // Market Manipulation Index (0-100: 0-30=clean, 30-70=normal, 70-100=manipulated) + +// TradingView-style summary signals +result.MASummary // technical.SignalStrongBuy / SignalBuy / SignalNeutral / SignalSell / SignalStrongSell +result.OscillatorSum // same scale +result.OverallSum // combined weighted signal + +// Signal counts +result.MABuy, result.MASell, result.MANeutral // how many MAs agree +result.OscillBuy, result.OscillSell, result.OscillNeutr // how many oscillators agree +``` + +### Standalone optimized functions + +```go +// EMA with pre-computed multiplier (faster than indicators.CalculateEMA) +ema := technical.CalculateEMAFast(prices, 20, 2.0/21.0) + +// ATR directly from CandleHLCV (no conversion to CandleHLC needed) +atr := technical.CalculateATRFast(candles, 14) + +// ADX — trend strength (0-100: <20=weak, 20-40=strong, >40=very strong) +adx := technical.CalculateADXFast(candles, 14) +``` + +### TechnicalIndicators struct + +```go +type TechnicalIndicators struct { + RSI14 float64 // RSI (14-period) + SMA20 float64 // Simple Moving Averages + SMA50 float64 + SMA200 float64 + EMA9 float64 // Exponential Moving Averages + EMA12 float64 + EMA20 float64 + EMA21 float64 + EMA26 float64 + EMA50 float64 + EMA200 float64 + MACDLine float64 // MACD + SignalLine float64 + Histogram float64 + BBUpper float64 // Bollinger Bands + BBMiddle float64 + BBLower float64 + ATR float64 // Average True Range + StochRSI float64 // Stochastic RSI + MMI float64 // Market Manipulation Index (0-100) + + // TradingView-style summaries + MASummary IndicatorSignal // StrongBuy(-2) to StrongSell(2) + OscillatorSum IndicatorSignal + OverallSum IndicatorSignal + + // Signal counts + MABuy, MASell, MANeutral int + OscillBuy, OscillSell, OscillNeutr int +} +``` + +--- + +## Signal & Movement Types + +The `indicators/technical` package also defines types for building real-time signal pipelines and cross-exchange analysis. + +### Signal Classification + +```go +// What kind of signal was generated +type SignalType string + +const ( + SignalFirstTouch SignalType = "first_touch" // Threshold crossed for first time + SignalMomentumShift SignalType = "momentum_shift" // Sharp acceleration detected + SignalPeakDetected SignalType = "peak_detected" // Price hit extreme, started reversing + SignalReversal SignalType = "reversal" // Direction changed with conviction + SignalDeepening SignalType = "deepening" // Movement continuing same direction + SignalExhaustion SignalType = "exhaustion" // Movement slowing, volume declining + SignalContinuation SignalType = "continuation" // Movement resumed after brief pause + SignalConsensus SignalType = "consensus" // All exchanges agree + SignalDivergence SignalType = "divergence" // Exchange deviation detected +) + +// How confident is the signal +type SignalConfidence string + +const ( + ConfidenceHigh SignalConfidence = "high" // All exchanges agree + ConfidenceMedium SignalConfidence = "medium" // 2/3 exchanges agree + ConfidenceLow SignalConfidence = "low" // Single exchange or high divergence +) + +// Overall market state +type MarketCondition string + +const ( + MarketSmooth MarketCondition = "smooth" // Clean directional move + MarketChoppy MarketCondition = "choppy" // Oscillating, >3 direction changes + MarketFlash MarketCondition = "flash" // Flash crash/pump (<2s duration) +) +``` + +### MovementState + +Tracks a symbol's price action as a real-time state machine. Thread-safe via embedded `sync.RWMutex`. + +```go +state := &technical.MovementState{ + Symbol: "BTCUSDT", + Exchange: "binance", +} + +// Query movement +duration := state.GetMovementDuration() // how long active +priceRange := state.GetPriceRange() // PriceRange{Min, Max, SpanPct} +state.IncrementAlertsSent() // track alerts + +// Key fields +state.CurrentPrice // latest price +state.PeakPrice // highest this movement +state.ValleyPrice // lowest this movement +state.Direction // "up" or "down" +state.CurrentVelocity // %/second +state.MarketCondition // smooth/choppy/flash +state.DirectionChanges // reversal count (choppiness) +state.IsActive // movement in progress +``` + +### CrossExchangeMetrics + +Holds analysis across multiple exchanges for the same symbol. + +```go +type CrossExchangeMetrics struct { + AvgPrice float64 // average price across exchanges + AvgChange float64 // average price change + StdDeviation float64 // price spread between exchanges + BestEntryPrice float64 // best price for entry + + ExchangePrices map[string]float64 // exchange -> price + ExchangeChanges map[string]float64 // exchange -> change % + LeadingExchange string // which exchange moved first/most + ExchangesAgree int // count in agreement (2 or 3) + + Confidence SignalConfidence + IsDivergence bool // exchanges disagree significantly + DivergenceSize float64 // max deviation from average (%) + ArbitrageOpportunity bool // price spread > threshold + ArbitrageSpread float64 // size of opportunity +} +``` + +### TradingSignal + +The final enriched signal with full context — price action, cross-exchange data, technical indicators, and movement metadata. + +```go +type TradingSignal struct { + // Identity + Type SignalType // first_touch, reversal, exhaustion, etc. + Exchange string + Symbol string + Timeframe string + + // Price data + PriceChange float64 + Open float64 + Close float64 + PeakPrice float64 + ValleyPrice float64 + + // Movement context + MovementID string + SignalRank int // 1=best, 2=average, 3=initial + PriceRange PriceRange // {Min, Max, SpanPct} + TimeInMotion float64 // seconds + Velocity float64 // %/second + DirectionChanges int + + // Cross-exchange + Confidence SignalConfidence + ExchangesAgree int + CrossExchange *SignalCrossExchangeData + + // Market context + MarketCondition MarketCondition + IsCounterTrend bool + + // Technical indicators + Indicators *TechnicalIndicators + + // Lifecycle + ValidUntil time.Time + CreatedAt time.Time +} +``` + +### SignalBatch + +Groups prioritized signals for one movement. + +```go +type SignalBatch struct { + MovementID string + Symbol string + Signals []TradingSignal + BatchTime time.Time + MovementStart time.Time + MovementEnd time.Time +} +``` diff --git a/README.md b/README.md index bc91a3b..e18174c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,23 @@ Production-tested Go library for real-time cryptocurrency market data via WebSoc go get github.com/KhavrTrading/flowex ``` +Requires **Go 1.22+** + +## Package Map + +``` +flowex/ + binance/ — Binance Futures WebSocket manager + bybit/ — Bybit Linear WebSocket manager + bitget/ — Bitget Futures/Spot WebSocket manager + ws/ — Core engine: client, worker, manager, snapshots + models/ — Candle, Trade, Ticker types + depth/ — Order book metrics (75 fields) + time-bucketed store + candles/ — REST fetchers + timeframe aggregation + indicators/ — EMA, RSI, MACD, ATR, Bollinger, StochRSI, S/R + examples/ — Working examples +``` + ## Quick Start ```go @@ -60,6 +77,8 @@ func main() { } ``` +See [examples/basic/main.go](examples/basic/main.go) for a complete working example with worker hooks, snapshot polling, and metrics monitoring. + --- ## Connecting to Exchanges @@ -340,7 +359,17 @@ mgr := bitget.NewManagerWithConfig(cfg) ## Reading Data: Snapshots -Every symbol produces immutable snapshots every second (configurable). Read them lock-free from any goroutine: +Every symbol produces immutable snapshots every second (configurable). Read them lock-free from any goroutine. + +```go +// Snapshot is an immutable, point-in-time view of a symbol's state. +type Snapshot struct { + Timestamp time.Time // when the snapshot was taken + Candles []models.CandleHLCV // historical + live candle bars + DepthStore *depth.Store // order book metrics with time-bucketed storage + Trades []models.NormalizedTrade // recent trades, normalized across exchanges +} +``` ```go snap := mgr.GetSnapshot("BTCUSDT") @@ -373,6 +402,54 @@ recent := snap.DepthStore.GetLastNSeconds(30) --- +## Data Models + +### CandleHLCV + +```go +type CandleHLCV struct { + Ts int64 // Unix millisecond timestamp + Open float64 + High float64 + Low float64 + Close float64 + Volume float64 +} +``` + +Helper methods: `GetTimestamp()`, `HL2()`, `HLC3()`. + +### CandleHLC + +Lighter candle without Open/Volume — used by ATR, Bollinger, and Support/Resistance indicators. + +```go +type CandleHLC struct { + High float64 + Low float64 + Close float64 +} +``` + +### NormalizedTrade + +Unified trade format across all exchanges. + +```go +type NormalizedTrade struct { + Timestamp int64 // Unix milliseconds + Price float64 + Size float64 // base currency + SizeUSD float64 + Side string // "buy" or "sell" + TradeID string + Symbol string // e.g. "BTCUSDT" + Exchange string // "binance", "bybit", "bitget" +} +``` + +--- + ## Custom Processing Hooks Workers fire callbacks on every state change. Plug your own logic: @@ -547,10 +624,13 @@ supportPct, resistancePct, srScore := indicators.SupportResistance(hlcCandles, 5 | `bybit/` | Bybit V5 Linear adapter (depth 1/50/200/500, all candle intervals) | | `bitget/` | Bitget V2 adapter (books/books5/books15, spot/futures, all candle intervals) | | `models/` | CandleHLC, CandleHLCV, NormalizedTrade, TickerData | -| `depth/` | Order book metrics (100+ fields) + time-bucketed store with enrichment | +| `depth/` | Order book metrics (75 fields) + time-bucketed store with enrichment | | `indicators/` | EMA, RSI, ATR, MACD, StochRSI, Bollinger, Support/Resistance | +| `indicators/technical/` | Batch-optimized calculator, ADX, MMI, signal types, movement tracking | | `candles/` | REST fetchers (Binance/Bybit/Bitget) + timeframe aggregator | +See [DOCUMENTATION.md](DOCUMENTATION.md) for the full API reference — all 75 depth metric fields, store query methods, worker monitoring, historical data seeding, and more. + ## Dependencies Only two: diff --git a/indicators/technical/calculator.go b/indicators/technical/calculator.go new file mode 100644 index 0000000..d3acfa1 --- /dev/null +++ b/indicators/technical/calculator.go @@ -0,0 +1,142 @@ +package technical + +import ( + "sync" + + "github.com/KhavrTrading/flowex/indicators" + "github.com/KhavrTrading/flowex/models" +) + +// closesPool reduces allocations by reusing float64 slices for close prices +var closesPool = sync.Pool{ + New: func() interface{} { + s := make([]float64, 0, 1000) // Pre-allocate for up to 1000 candles + return &s + }, +} + +// CalculateTechnicalIndicators calculates all technical indicators from historical candles +// Uses 12 hours of 1m candle data (720 candles minimum for SMA200) +// Optimized with sync.Pool to reduce memory allocations by ~15-20% +func CalculateTechnicalIndicators(candles []models.CandleHLCV, currentPrice float64) *TechnicalIndicators { + if len(candles) < 20 { + return nil // Need at least 20 candles for basic indicators + } + + // Get reusable buffer from pool + closesPtr := closesPool.Get().(*[]float64) + closes := (*closesPtr)[:0] // Reset length, keep capacity + + // Extract close prices + for i := range candles { + closes = append(closes, candles[i].Close) + } + + // Ensure we return the buffer to pool when done + defer closesPool.Put(closesPtr) + + ind := &TechnicalIndicators{} + + // ============================================================ + // RSI (Relative Strength Index) + // ============================================================ + if len(closes) >= 14 { + ind.RSI14 = indicators.CalculateRSI(closes, 14) + } + + // ============================================================ + // Simple Moving Averages (SMA) - Optimized batch calculation + // ============================================================ + if len(closes) >= 200 { + // Calculate all SMAs in one pass (reuse sum for 20, extend for 50, extend for 200) + ind.SMA20, ind.SMA50, ind.SMA200 = calculateSMABatch(closes) + } else if len(closes) >= 50 { + ind.SMA20, ind.SMA50, _ = calculateSMABatch(closes) + } else if len(closes) >= 20 { + ind.SMA20, _, _ = calculateSMABatch(closes) + } + + // ============================================================ + // Exponential Moving Averages (EMA) - Using cached multipliers + // ============================================================ + if len(closes) >= 9 { + ind.EMA9 = CalculateEMAFast(closes, 9, emaMultipliers[9]) + } + if len(closes) >= 12 { + ind.EMA12 = CalculateEMAFast(closes, 12, emaMultipliers[12]) + } + if len(closes) >= 20 { + ind.EMA20 = CalculateEMAFast(closes, 20, emaMultipliers[20]) + } + if len(closes) >= 21 { + ind.EMA21 = CalculateEMAFast(closes, 21, emaMultipliers[21]) + } + if len(closes) >= 26 { + ind.EMA26 = CalculateEMAFast(closes, 26, emaMultipliers[26]) + } + if len(closes) >= 50 { + ind.EMA50 = CalculateEMAFast(closes, 50, emaMultipliers[50]) + } + if len(closes) >= 200 { + ind.EMA200 = CalculateEMAFast(closes, 200, emaMultipliers[200]) + } + + // ============================================================ + // MACD (Moving Average Convergence Divergence) + // ============================================================ + if len(closes) >= 26 { + macdLine, signalLine, histogram := indicators.CalculateMACD(closes) + if len(macdLine) > 0 { + ind.MACDLine = macdLine[len(macdLine)-1] + } + if len(signalLine) > 0 { + ind.SignalLine = signalLine[len(signalLine)-1] + } + if len(histogram) > 0 { + ind.Histogram = histogram[len(histogram)-1] + } + } + + // ============================================================ + // Bollinger Bands (20-period, 2 StdDev) - Optimized single-pass + // ============================================================ + if len(closes) >= 20 { + // Reuse SMA20 if already calculated + middle := ind.SMA20 + if middle == 0 { + middle = calculateSMAFast(closes, 20) + } + upper, lower := calculateBollingerBandsFast(closes, middle, 20, 2.0) + ind.BBUpper = upper + ind.BBMiddle = middle + ind.BBLower = lower + } + + // ============================================================ + // ATR (Average True Range) - 14 period - Optimized + // ============================================================ + if len(candles) >= 14 { + ind.ATR = CalculateATRFast(candles, 14) + } + + // ============================================================ + // Stochastic RSI - 14 period - Optimized + // ============================================================ + if len(closes) >= 14 { + ind.StochRSI = calculateStochRSIFast(closes, 14) + } + + // ============================================================ + // MMI (Market Manipulation Index) - Optimized for 1m + // ============================================================ + if len(candles) >= 30 { + ind.MMI = calculateMMIFast(candles) + } + + // ============================================================ + // Calculate Summary Signals (TradingView-style) + // ============================================================ + calculateSummarySignals(ind, currentPrice) + + return ind +} diff --git a/indicators/technical/lib.go b/indicators/technical/lib.go new file mode 100644 index 0000000..ae3e504 --- /dev/null +++ b/indicators/technical/lib.go @@ -0,0 +1,681 @@ +package technical + +import ( + "math" + + "github.com/KhavrTrading/flowex/models" +) + +// Pre-computed EMA multipliers (cached for performance) +var emaMultipliers = map[int]float64{ + 9: 2.0 / 10.0, // 2/(9+1) + 12: 2.0 / 13.0, // 2/(12+1) + 20: 2.0 / 21.0, // 2/(20+1) + 21: 2.0 / 22.0, // 2/(21+1) + 26: 2.0 / 27.0, // 2/(26+1) + 50: 2.0 / 51.0, // 2/(50+1) + 200: 2.0 / 201.0, // 2/(200+1) +} + +// Pre-computed period dividers for SMA (avoid repeated float conversions) +var periodDividers = map[int]float64{ + 20: 1.0 / 20.0, + 50: 1.0 / 50.0, + 200: 1.0 / 200.0, +} + +// calculateSMAFast calculates Simple Moving Average with pre-computed divider +func calculateSMAFast(prices []float64, period int) float64 { + if len(prices) < period { + return 0 + } + sum := 0.0 + startIdx := len(prices) - period + // Manual loop unrolling for common case (helps compiler optimize) + for i := startIdx; i < len(prices); i++ { + sum += prices[i] + } + divider, ok := periodDividers[period] + if ok { + return sum * divider // Multiplication is faster than division + } + return sum / float64(period) +} + +// calculateSMABatch calculates multiple SMAs in one pass (20, 50, 200) +// Returns (sma20, sma50, sma200) +func calculateSMABatch(prices []float64) (float64, float64, float64) { + n := len(prices) + if n < 20 { + return 0, 0, 0 + } + + // Calculate sum for SMA20 + sum20 := 0.0 + for i := n - 20; i < n; i++ { + sum20 += prices[i] + } + sma20 := sum20 * periodDividers[20] + + if n < 50 { + return sma20, 0, 0 + } + + // Extend to SMA50 (add 30 more prices) + sum50 := sum20 + for i := n - 50; i < n-20; i++ { + sum50 += prices[i] + } + sma50 := sum50 * periodDividers[50] + + if n < 200 { + return sma20, sma50, 0 + } + + // Extend to SMA200 (add 150 more prices) + sum200 := sum50 + for i := n - 200; i < n-50; i++ { + sum200 += prices[i] + } + sma200 := sum200 * periodDividers[200] + + return sma20, sma50, sma200 +} + +// CalculateEMAFast calculates EMA with pre-computed multiplier +func CalculateEMAFast(prices []float64, period int, multiplier float64) float64 { + if len(prices) < period { + return 0 + } + + // Calculate initial SMA for first EMA value + sum := 0.0 + for i := 0; i < period; i++ { + sum += prices[i] + } + ema := sum / float64(period) + + // Apply EMA formula with cached multiplier + oneMinusK := 1.0 - multiplier + for i := period; i < len(prices); i++ { + ema = prices[i]*multiplier + ema*oneMinusK + } + + return ema +} + +// calculateBollingerBandsFast calculates BB bands using pre-computed middle (SMA) +// Returns (upper, lower) - middle is passed as parameter +// Optimized: single-pass standard deviation calculation +func calculateBollingerBandsFast(prices []float64, middle float64, period int, numStdDev float64) (float64, float64) { + if len(prices) < period { + return 0, 0 + } + + // Calculate standard deviation in single pass + startIdx := len(prices) - period + sumSquaredDiff := 0.0 + for i := startIdx; i < len(prices); i++ { + diff := prices[i] - middle + sumSquaredDiff += diff * diff + } + + // Using Welford's method for numerical stability + variance := sumSquaredDiff * periodDividers[period] + stdDev := math.Sqrt(variance) + + // Calculate bands + band := numStdDev * stdDev + upper := middle + band + lower := middle - band + + return upper, lower +} + +// calculateSummarySignals generates TradingView-style summary using typed constants +func calculateSummarySignals(ind *TechnicalIndicators, currentPrice float64) { + // ============================================================ + // Moving Averages Summary + // ============================================================ + maBuy := 0 + maSell := 0 + maNeutral := 0 + + // Helper function to compare price with MA + comparePriceMA := func(ma float64) { + if ma <= 0 { + return + } + if currentPrice > ma { + maBuy++ + } else if currentPrice < ma { + maSell++ + } else { + maNeutral++ + } + } + + // Compare current price with each MA + comparePriceMA(ind.EMA9) + comparePriceMA(ind.EMA12) + comparePriceMA(ind.EMA21) + comparePriceMA(ind.EMA26) + comparePriceMA(ind.EMA50) + comparePriceMA(ind.EMA200) + comparePriceMA(ind.SMA20) + comparePriceMA(ind.SMA50) + comparePriceMA(ind.SMA200) + + ind.MABuy = maBuy + ind.MASell = maSell + ind.MANeutral = maNeutral + + // ============================================================ + // Oscillators Summary + // ============================================================ + oscillBuy := 0 + oscillSell := 0 + oscillNeutral := 0 + + // RSI (Relative Strength Index) + if ind.RSI14 > 0 { + if ind.RSI14 < 30 { + oscillBuy++ // Oversold + } else if ind.RSI14 > 70 { + oscillSell++ // Overbought + } else { + oscillNeutral++ + } + } + + // MACD + if ind.MACDLine != 0 && ind.SignalLine != 0 { + if ind.MACDLine > ind.SignalLine { + oscillBuy++ // Bullish crossover + } else if ind.MACDLine < ind.SignalLine { + oscillSell++ // Bearish crossover + } else { + oscillNeutral++ + } + } + + ind.OscillBuy = oscillBuy + ind.OscillSell = oscillSell + ind.OscillNeutr = oscillNeutral + + // ============================================================ + // Determine Summary Labels (using typed constants) + // ============================================================ + // Moving Averages Summary + ind.MASummary = calculateSignal(maBuy, maSell, maNeutral) + + // Oscillators Summary + ind.OscillatorSum = calculateSignal(oscillBuy, oscillSell, oscillNeutral) + + // Overall Summary (weighted combination) + overallBuy := maBuy + oscillBuy + overallSell := maSell + oscillSell + overallNeutral := maNeutral + oscillNeutral + ind.OverallSum = calculateSignal(overallBuy, overallSell, overallNeutral) +} + +// calculateSignal determines IndicatorSignal based on buy/sell/neutral counts +func calculateSignal(buy, sell, neutral int) IndicatorSignal { + total := buy + sell + neutral + if total == 0 { + return SignalNeutral + } + + buyPct := float64(buy) / float64(total) + sellPct := float64(sell) / float64(total) + + // Strong signals require 70%+ agreement + if buyPct >= 0.7 { + return SignalStrongBuy + } + if sellPct >= 0.7 { + return SignalStrongSell + } + + // Regular signals require 50%+ agreement + if buyPct >= 0.5 { + return SignalBuy + } + if sellPct >= 0.5 { + return SignalSell + } + + return SignalNeutral +} + +// ============================================================ +// Optimized Indicator Calculations (No Allocations) +// ============================================================ + +// CalculateATRFast calculates ATR directly from CandleHLCV without conversion +// Uses Wilder's smoothing method (exponential moving average) +func CalculateATRFast(candles []models.CandleHLCV, period int) float64 { + if len(candles) < period { + return 0 + } + + // Calculate True Range for each candle + trSum := 0.0 + for i := len(candles) - period; i < len(candles); i++ { + var tr float64 + if i == 0 { + // First candle: just use high-low + tr = candles[i].High - candles[i].Low + } else { + // True Range = max(high-low, |high-prevClose|, |low-prevClose|) + hl := candles[i].High - candles[i].Low + hc := math.Abs(candles[i].High - candles[i-1].Close) + lc := math.Abs(candles[i].Low - candles[i-1].Close) + tr = math.Max(hl, math.Max(hc, lc)) + } + trSum += tr + } + + // Initial ATR = average of first period TRs + atr := trSum / float64(period) + + // Apply Wilder's smoothing to remaining candles + multiplier := 1.0 / float64(period) + for i := len(candles) - period + 1; i < len(candles); i++ { + var tr float64 + hl := candles[i].High - candles[i].Low + hc := math.Abs(candles[i].High - candles[i-1].Close) + lc := math.Abs(candles[i].Low - candles[i-1].Close) + tr = math.Max(hl, math.Max(hc, lc)) + + // Wilder's smoothing: ATR = ((period-1) * prevATR + TR) / period + atr = ((atr * float64(period-1)) + tr) * multiplier + } + + return atr +} + +// calculateStochRSIFast calculates Stochastic RSI optimized for single value +// Returns the last value only (no series allocation) +func calculateStochRSIFast(closes []float64, period int) float64 { + if len(closes) < period*2 { + return 50.0 // Neutral default + } + + // Calculate RSI series for last 'period' values + rsiValues := make([]float64, 0, period) + + // First, calculate enough RSI values to compute StochRSI + startIdx := len(closes) - period*2 + for i := startIdx; i < len(closes); i++ { + // Calculate RSI for this point + rsi := calculateRSIAtIndex(closes, i, period) + if i >= len(closes)-period { + rsiValues = append(rsiValues, rsi) + } + } + + if len(rsiValues) == 0 { + return 50.0 + } + + // Find min and max RSI in the period + minRSI := rsiValues[0] + maxRSI := rsiValues[0] + for _, rsi := range rsiValues { + if rsi < minRSI { + minRSI = rsi + } + if rsi > maxRSI { + maxRSI = rsi + } + } + + // Calculate StochRSI: (currentRSI - minRSI) / (maxRSI - minRSI) * 100 + currentRSI := rsiValues[len(rsiValues)-1] + rsiRange := maxRSI - minRSI + + if rsiRange == 0 { + return 50.0 // Neutral if no range + } + + stochRSI := ((currentRSI - minRSI) / rsiRange) * 100 + return stochRSI +} + +// calculateRSIAtIndex calculates RSI at a specific index +func calculateRSIAtIndex(prices []float64, endIdx, period int) float64 { + if endIdx < period { + return 50.0 + } + + startIdx := endIdx - period + gains := 0.0 + losses := 0.0 + + // Calculate average gains and losses + for i := startIdx + 1; i <= endIdx; i++ { + change := prices[i] - prices[i-1] + if change > 0 { + gains += change + } else { + losses -= change // Make positive + } + } + + avgGain := gains / float64(period) + avgLoss := losses / float64(period) + + if avgLoss == 0 { + return 100.0 + } + + rs := avgGain / avgLoss + rsi := 100.0 - (100.0 / (1.0 + rs)) + return rsi +} + +// calculateMMIFast calculates Market Manipulation Index optimized for 1m timeframe +// MMI combines sine-wave fitting, predictability, and spectral analysis +// Returns 0-100 (0-30=clean, 30-70=normal, 70-100=manipulated) +func calculateMMIFast(candles []models.CandleHLCV) float64 { + n := len(candles) + if n < 30 { + return 50.0 // Neutral default + } + + // Use last 30 candles for 1m timeframe (adaptive window) + window := 30 + if n < window { + window = n + } + + startIdx := n - window + prices := make([]float64, window) + for i := 0; i < window; i++ { + prices[i] = candles[startIdx+i].Close + } + + // Component 1: Sine-based Market Index (40% weight) + sineMI := calculateSineManipulation(prices) + + // Component 2: Predictability Index (40% weight) + predMI := calculatePredictability(prices) + + // Component 3: Spectral Energy (20% weight) + spectralMI := calculateSpectralManipulation(prices) + + // Weighted combination + mmi := (sineMI * 0.4) + (predMI * 0.4) + (spectralMI * 0.2) + + // Clamp to 0-100 + if mmi < 0 { + return 0 + } + if mmi > 100 { + return 100 + } + + return mmi +} + +// calculateSineManipulation fits a sine wave and measures deviation +func calculateSineManipulation(prices []float64) float64 { + n := len(prices) + if n < 10 { + return 50.0 + } + + // Detrend prices + mean := 0.0 + for _, p := range prices { + mean += p + } + mean /= float64(n) + + // Calculate variance from mean + variance := 0.0 + for _, p := range prices { + diff := p - mean + variance += diff * diff + } + variance /= float64(n) + + if variance == 0 { + return 0.0 + } + + // Simple sine fitting: measure how much price deviates from smooth curve + // Higher deviation = more manipulation (choppy, non-trending) + stdDev := math.Sqrt(variance) + + // Normalize to 0-100 scale (0 = smooth trending, 100 = choppy) + // Typical stddev for clean market: <2% of mean + // Manipulated market: >5% of mean + relativeStdDev := (stdDev / mean) * 100 + + if relativeStdDev < 2.0 { + return 0.0 + } else if relativeStdDev > 5.0 { + return 100.0 + } + + // Linear scale between 2% and 5% + return ((relativeStdDev - 2.0) / 3.0) * 100 +} + +// calculatePredictability measures autocorrelation (how predictable price is) +func calculatePredictability(prices []float64) float64 { + n := len(prices) + if n < 5 { + return 50.0 + } + + // Calculate 1-lag autocorrelation + mean := 0.0 + for _, p := range prices { + mean += p + } + mean /= float64(n) + + // Autocovariance at lag 1 + autocovar := 0.0 + variance := 0.0 + + for i := 0; i < n-1; i++ { + diff1 := prices[i] - mean + diff2 := prices[i+1] - mean + autocovar += diff1 * diff2 + variance += diff1 * diff1 + } + + if variance == 0 { + return 50.0 + } + + // Autocorrelation coefficient + autocorr := autocovar / variance + + // High autocorrelation = trending/predictable = LOW manipulation + // Low/negative autocorrelation = random/choppy = HIGH manipulation + // Scale: autocorr 0.5+ → MMI 0, autocorr -0.5- → MMI 100 + mmi := (0.5 - autocorr) * 100 + + if mmi < 0 { + return 0 + } + if mmi > 100 { + return 100 + } + + return mmi +} + +// calculateSpectralManipulation analyzes frequency distribution +func calculateSpectralManipulation(prices []float64) float64 { + n := len(prices) + if n < 10 { + return 50.0 + } + + // Simplified spectral analysis: measure high-frequency energy + // Count direction changes (zigzags) = high-frequency noise + changes := 0 + for i := 2; i < n; i++ { + // Direction change if: (p[i] > p[i-1] AND p[i-1] < p[i-2]) OR vice versa + if (prices[i] > prices[i-1] && prices[i-1] < prices[i-2]) || + (prices[i] < prices[i-1] && prices[i-1] > prices[i-2]) { + changes++ + } + } + + // More zigzags = more manipulation + // Typical: 30% direction changes = normal + // >60% = highly manipulated + changeRate := float64(changes) / float64(n-2) * 100 + + if changeRate < 30.0 { + return 0.0 + } else if changeRate > 60.0 { + return 100.0 + } + + return ((changeRate - 30.0) / 30.0) * 100 +} + +// calculateADXFast calculates Average Directional Index (ADX) from candles +// ADX measures trend strength (0-100): <20=weak/no trend, 20-40=strong trend, >40=very strong trend +// Returns the last ADX value only (optimized for single value calculation) +func CalculateADXFast(candles []models.CandleHLCV, period int) float64 { + n := len(candles) + lookbackTotal := (2 * period) - 1 + + if n <= lookbackTotal { + return 0.0 // Not enough data + } + + const epsilon = 1e-10 // For zero checks + periodF := float64(period) + periodInv := 1.0 / periodF // Pre-compute division + + // Inline helper: calculate True Range + calcTR := func(high, low, prevClose float64) float64 { + tr := high - low + hc := math.Abs(high - prevClose) + lc := math.Abs(low - prevClose) + return math.Max(tr, math.Max(hc, lc)) + } + + // Initialize with first candle + today := 0 + prevHigh := candles[today].High + prevLow := candles[today].Low + prevClose := candles[today].Close + prevMinusDM := 0.0 + prevPlusDM := 0.0 + prevTR := 0.0 + + // Phase 1: Accumulate initial DM and TR (first 'period' bars) + for i := 1; i < period; i++ { + high := candles[i].High + low := candles[i].Low + + diffP := high - prevHigh + diffM := prevLow - low + + // Directional Movement + if diffM > 0 && diffP < diffM { + prevMinusDM += diffM + } else if diffP > 0 && diffP > diffM { + prevPlusDM += diffP + } + + prevTR += calcTR(high, low, prevClose) + prevHigh = high + prevLow = low + prevClose = candles[i].Close + } + + // Phase 2: Calculate smoothed DI and accumulate DX (next 'period' bars) + sumDX := 0.0 + endPhase2 := period * 2 + for i := period; i < endPhase2 && i < n; i++ { + high := candles[i].High + low := candles[i].Low + + diffP := high - prevHigh + diffM := prevLow - low + + // Smooth DM (Wilder's smoothing) + prevMinusDM -= prevMinusDM * periodInv + prevPlusDM -= prevPlusDM * periodInv + + if diffM > 0 && diffP < diffM { + prevMinusDM += diffM + } else if diffP > 0 && diffP > diffM { + prevPlusDM += diffP + } + + // Smooth TR + tr := calcTR(high, low, prevClose) + prevTR = prevTR - (prevTR * periodInv) + tr + + // Calculate DX + if prevTR > epsilon { + minusDI := prevMinusDM / prevTR + plusDI := prevPlusDM / prevTR + sumDI := minusDI + plusDI + + if sumDI > epsilon { + dx := math.Abs(minusDI-plusDI) / sumDI + sumDX += dx + } + } + + prevHigh = high + prevLow = low + prevClose = candles[i].Close + } + + // Initial ADX = average of DX values + prevADX := (sumDX * periodInv) * 100.0 + + // Phase 3: Smooth ADX to the end (remaining bars) + for i := endPhase2; i < n; i++ { + high := candles[i].High + low := candles[i].Low + + diffP := high - prevHigh + diffM := prevLow - low + + prevMinusDM -= prevMinusDM * periodInv + prevPlusDM -= prevPlusDM * periodInv + + if diffM > 0 && diffP < diffM { + prevMinusDM += diffM + } else if diffP > 0 && diffP > diffM { + prevPlusDM += diffP + } + + tr := calcTR(high, low, prevClose) + prevTR = prevTR - (prevTR * periodInv) + tr + + if prevTR > epsilon { + minusDI := prevMinusDM / prevTR + plusDI := prevPlusDM / prevTR + sumDI := minusDI + plusDI + + if sumDI > epsilon { + dx := (math.Abs(minusDI-plusDI) / sumDI) * 100.0 + // Smooth ADX: ADX = ((period-1) * prevADX + currentDX) / period + prevADX = ((prevADX * (periodF - 1.0)) + dx) * periodInv + } + } + + prevHigh = high + prevLow = low + prevClose = candles[i].Close + } + + return prevADX +} diff --git a/indicators/technical/models.go b/indicators/technical/models.go new file mode 100644 index 0000000..6454c7b --- /dev/null +++ b/indicators/technical/models.go @@ -0,0 +1,312 @@ +package technical + +import ( + "sync" + "time" +) + +type SignalType string + +const ( + SignalFirstTouch SignalType = "first_touch" // Threshold crossed for first time + SignalMomentumShift SignalType = "momentum_shift" // Sharp acceleration detected + SignalPeakDetected SignalType = "peak_detected" // Price hit extreme and started reversing + SignalReversal SignalType = "reversal" // Direction changed with conviction + SignalDeepening SignalType = "deepening" // Movement continuing same direction + SignalExhaustion SignalType = "exhaustion" // Movement slowing, volume declining + SignalContinuation SignalType = "continuation" // Movement resumed after brief pause + SignalConsensus SignalType = "consensus" // All exchanges agree + SignalDivergence SignalType = "divergence" // Exchange deviation detected +) + +// SignalConfidence levels +type SignalConfidence string + +const ( + ConfidenceHigh SignalConfidence = "high" // All exchanges agree + ConfidenceMedium SignalConfidence = "medium" // 2/3 exchanges agree + ConfidenceLow SignalConfidence = "low" // Single exchange or high divergence +) + +// MarketCondition describes overall market state +type MarketCondition string + +const ( + MarketSmooth MarketCondition = "smooth" // Clean directional move + MarketChoppy MarketCondition = "choppy" // Oscillating, >3 direction changes + MarketFlash MarketCondition = "flash" // Flash crash/pump (<2s duration) +) + +// ============================================================ +// Movement Tracking (Per-Symbol State Machine) +// ============================================================ + +// MovementState tracks a symbol's price action in real-time +type MovementState struct { + Mu sync.RWMutex + + // Identity + Symbol string + Exchange string + + // Price tracking + FirstPrice float64 // Price when movement started + PeakPrice float64 // Highest price this candle + ValleyPrice float64 // Lowest price this candle + CurrentPrice float64 // Latest price + LastAlertPrice float64 // Last price we sent alert for + LastAlertTime time.Time // When we sent last alert + + // Movement metadata + Direction string // "up" or "down" + MovementStartTime time.Time // When threshold first crossed + DirectionChanges int // Count of reversals (choppiness) + LastDirectionTime time.Time // When direction last changed + PeakTime time.Time // When peak was hit + ValleyTime time.Time // When valley was hit + + // Velocity tracking + PriceHistory []PricePoint // Last N price points for velocity calculation + CurrentVelocity float64 // %/second + + // State flags + IsActive bool // Movement in progress + LastUpdateTime time.Time // Last data received + MarketCondition MarketCondition + AlertsSent int // Count of alerts for this movement + MovementID string // Unique ID for this movement +} + +// GetMovementDuration returns how long movement has been active +func (state *MovementState) GetMovementDuration() time.Duration { + state.Mu.RLock() + defer state.Mu.RUnlock() + + if !state.IsActive { + return 0 + } + + return time.Since(state.MovementStartTime) +} + +// GetPriceRange returns price range for this movement +func (state *MovementState) GetPriceRange() PriceRange { + state.Mu.RLock() + defer state.Mu.RUnlock() + + min := state.ValleyPrice + max := state.PeakPrice + + if min == 0 { + return PriceRange{} + } + + span := (max - min) / min + + return PriceRange{ + Min: min, + Max: max, + SpanPct: span * 100, + } +} + +// IncrementAlertsSent increments the alerts sent counter +func (state *MovementState) IncrementAlertsSent() { + state.Mu.Lock() + defer state.Mu.Unlock() + state.AlertsSent++ +} + +// PricePoint for velocity calculation +type PricePoint struct { + Price float64 + Timestamp time.Time +} + +// ============================================================ +// Cross-Exchange Analysis +// ============================================================ + +// CrossExchangeMetrics holds analysis across all exchanges +type CrossExchangeMetrics struct { + // Basic stats + AvgPrice float64 // Average price across exchanges + AvgChange float64 // Average price change across exchanges + StdDeviation float64 // Price spread between exchanges + BestEntryPrice float64 // Lowest for drops, highest for pumps + + // Exchange data + ExchangePrices map[string]float64 // exchange -> price + ExchangeChanges map[string]float64 // exchange -> change % + LeadingExchange string // Which exchange moved first/most + ExchangesAgree int // Count of exchanges in agreement (2 or 3) + + // Signal classification + Confidence SignalConfidence + IsDivergence bool // True if exchanges disagree significantly + DivergenceSize float64 // Max deviation from average (%) + + // Opportunity flags + ArbitrageOpportunity bool // Price spread > threshold + ArbitrageSpread float64 // Size of arbitrage opportunity +} + +// ============================================================ +// Signal Generation +// ============================================================ + +// IndicatorSignal represents buy/sell/neutral signals from indicators +type IndicatorSignal int + +const ( + SignalStrongSell IndicatorSignal = iota - 2 + SignalSell + SignalNeutral + SignalBuy + SignalStrongBuy +) + +// String converts IndicatorSignal to string for JSON +func (s IndicatorSignal) String() string { + switch s { + case SignalStrongBuy: + return "strong_buy" + case SignalBuy: + return "buy" + case SignalNeutral: + return "neutral" + case SignalSell: + return "sell" + case SignalStrongSell: + return "strong_sell" + default: + return "neutral" + } +} + +// MarshalJSON implements json.Marshaler for IndicatorSignal +func (s IndicatorSignal) MarshalJSON() ([]byte, error) { + return []byte(`"` + s.String() + `"`), nil +} + +// TechnicalIndicators holds calculated indicators from historical candle data +type TechnicalIndicators struct { + // RSI (Relative Strength Index) - 14 period + RSI14 float64 `json:"rsi_14"` + + // Moving Averages + SMA20 float64 `json:"sma_20"` // Simple Moving Average 20 + SMA50 float64 `json:"sma_50"` // Simple Moving Average 50 + SMA200 float64 `json:"sma_200"` // Simple Moving Average 200 + EMA9 float64 `json:"ema_9"` // Exponential Moving Average 9 + EMA12 float64 `json:"ema_12"` // Exponential Moving Average 12 + EMA20 float64 `json:"ema_20"` // Exponential Moving Average 20 + EMA21 float64 `json:"ema_21"` // Exponential Moving Average 21 + EMA26 float64 `json:"ema_26"` // Exponential Moving Average 26 + EMA50 float64 `json:"ema_50"` // Exponential Moving Average 50 + EMA200 float64 `json:"ema_200"` // Exponential Moving Average 200 + + // MACD + MACDLine float64 `json:"macd_line"` + SignalLine float64 `json:"signal_line"` + Histogram float64 `json:"histogram"` + + // Bollinger Bands (20-period, 2 StdDev) + BBUpper float64 `json:"bb_upper"` + BBMiddle float64 `json:"bb_middle"` + BBLower float64 `json:"bb_lower"` + + // Volatility & Advanced Indicators + ATR float64 `json:"atr"` // Average True Range (14-period) + StochRSI float64 `json:"stoch_rsi"` // Stochastic RSI + MMI float64 `json:"mmi"` // Market Manipulation Index (0-100) + + // Summary Signals (using typed constants) + MASummary IndicatorSignal `json:"ma_summary"` + OscillatorSum IndicatorSignal `json:"oscillator_sum"` + OverallSum IndicatorSignal `json:"overall_sum"` + + // Counts for TradingView-style summary + MABuy int `json:"ma_buy"` + MASell int `json:"ma_sell"` + MANeutral int `json:"ma_neutral"` + OscillBuy int `json:"oscill_buy"` + OscillSell int `json:"oscill_sell"` + OscillNeutr int `json:"oscill_neutral"` +} + +// SignalCrossExchangeData holds cross-exchange metrics for signals +type SignalCrossExchangeData struct { + Binance float64 `json:"binance"` + Bitget float64 `json:"bitget"` + Bybit float64 `json:"bybit"` + Avg float64 `json:"avg"` + StdDev float64 `json:"std_dev"` + BestPrice float64 `json:"best_price"` + LeadingExchange string `json:"leading_exchange"` + ArbitrageSpread float64 `json:"arbitrage_spread,omitempty"` +} + +// ============================================================ +// Signal Batching +// ============================================================ + +// SignalBatch represents a group of prioritized signals for one movement +type SignalBatch struct { + MovementID string + Symbol string + Signals []TradingSignal + BatchTime time.Time + MovementStart time.Time + MovementEnd time.Time +} + +// PriceRange describes the price movement range +type PriceRange struct { + Min float64 `json:"min"` + Max float64 `json:"max"` + SpanPct float64 `json:"span_pct"` +} + +// TradingSignal is the final enriched signal sent to strategies +type TradingSignal struct { + // Basic info + Type SignalType `json:"type"` + Exchange string `json:"exchange"` + Symbol string `json:"symbol"` + Timeframe string `json:"timeframe"` + PriceChange float64 `json:"price_change"` + Open float64 `json:"open"` + Close float64 `json:"close"` + Timestamp string `json:"timestamp"` + + // Movement context + MovementID string `json:"movement_id"` + SignalRank int `json:"signal_rank"` // 1=best, 2=average, 3=initial + PriceRange PriceRange `json:"price_range"` + PeakPrice float64 `json:"peak_price"` // Highest price in movement + PeakTime time.Time `json:"peak_time"` // When peak occurred + ValleyPrice float64 `json:"valley_price"` // Lowest price in movement + ValleyTime time.Time `json:"valley_time"` // When valley occurred + TimeInMotion float64 `json:"time_in_motion"` // seconds + Velocity float64 `json:"movement_velocity"` // %/second + DirectionChanges int `json:"direction_changes"` + + // Cross-exchange data + Confidence SignalConfidence `json:"confidence"` + ExchangesAgree int `json:"exchanges_agreeing"` + CrossExchange *SignalCrossExchangeData `json:"cross_exchange,omitempty"` + + // Market context + MarketCondition MarketCondition `json:"market_condition,omitempty"` + IsCounterTrend bool `json:"counter_trend,omitempty"` + + // Technical indicators (calculated from 12h historical data) + Indicators *TechnicalIndicators `json:"indicators,omitempty"` + + // Expiry + ValidUntil time.Time `json:"valid_until"` + + // Metadata + CreatedAt time.Time `json:"created_at"` +}