Coin Alert - DevLog

Devlog: Building a Real-Time Crypto Watchdog CLI in C#

📈 Devlog: Building a Real-Time Crypto Watchdog CLI in C#

Date: [Your Date Here]
Author: Sewmina Dilshan

🔧 Project Overview

This project started as a C# CLI program designed to help me (and a couple of my colleagues) stay updated on crypto coin price actions and patterns — without having to stare at the charts all day.

Think of it like having a vigilant assistant watching the market for you: if something notable happens, it sends you a message. Specifically, the program detects predefined strategies in real-time, for any chosen timeframes, and then sends Telegram notifications via a bot I set up.

🖼 Chart Rendering with ImageSharp

For rendering charts, I used the SixLabors.ImageSharp library. It's incredibly fast and customizable, which made it a great fit. However, it didn't offer built-in support for charting or graphs, so I rolled up my sleeves and built my own chart-drawing methods from scratch using basic shapes and lines.

Here's how I implemented the core chart rendering:

public static void DrawChart(List<TAReport> reports, string filename="test", KlineInterval interval = KlineInterval.FifteenMinutes){ decimal max = 0; decimal min = 10000000000000; decimal volMax = 0; // Calculate price and volume ranges foreach(TAReport report in reports){ if(max < report.High) max = report.High; if(min > report.Low) min = report.Low; if(volMax < report.candle.Volume) volMax = report.candle.Volume; } decimal heightRange = max-min; float height = 1080; float width = 1920; float widthMultiplier = width / (float)reports.Count; float heightMultiplier = (height/ (float)heightRange) * 0.6f; using (Image img = new Image<Rgba32>((int)width + 100, (int)height, Color.FromRgb(13,18,25))){ // Draw candlesticks for(int i=0; i < reports.Count; i++){ float openVal = height - ((float)(reports[i].candle.Open - min) * heightMultiplier + candlesOffset); float closeVal = height - ((float)(reports[i].candle.Close - min) * heightMultiplier + candlesOffset); Color candleColor = reports[i].candle.Close > reports[i].candle.Open ? Color.Green : Color.Red; img.Mutate(ctx=> ctx. DrawLine(candleColor, 3, rangePoints). DrawLine(candleColor, 10, points). DrawLine(rsiColor, 8, points) ); } } }

While it was extra work, this gave me complete control over how things looked — and helped me understand the visual structure of market data better.

🧠 Strategy + Indicator Implementation

Although I noticed there are plenty of Python libraries that handle technical indicators and strategies, I made a conscious decision not to use them. I wanted to deepen my understanding of how each indicator works mathematically, so I implemented all indicators and pattern detection from scratch, based solely on their formulas.

This became a great learning journey in itself.

🔍 Indicators I Built

Trend & Moving Averages: SMA, EMA, MACD, VWAP (multi-timeframe)

public static decimal getSMA(List<decimal> responses, int curIndex, int interval, int startIndex = 0) { decimal total = 0; for (int x = (curIndex > 1500) ? 1500 : 0; x < interval; x++) { total += (curIndex - x >= 0) ? responses[curIndex - x] : (decimal)0; } return total / (decimal)interval; } public static decimal getEMA(List<decimal> responses, int curIndex, int startIndex, int periods) { if (curIndex < 1) { return 0; } decimal multiplier = (decimal)2 / ((decimal)(periods + 1)); decimal firstHalf = responses[curIndex] * multiplier; decimal secondHalf = getEMA(responses, curIndex - 1, startIndex, periods) * (1 - multiplier); return firstHalf + secondHalf; } public static decimal getMACD(List<KlineCandleStickResponse> responses, int shortPeriod, int longPeriod, int curIndex) { return getEMA(responses, curIndex, curIndex, shortPeriod) - getEMA(responses, curIndex, curIndex, longPeriod); }

Volatility & Momentum: ATR, RSI, Stochastic

public static decimal getRSI(List<KlineCandleStickResponse> responses, int index, int period = 14) { decimal avgGain = 0; decimal avgLoss = 0; if (index < period) return 0; for (int i = index; i > index - period; i--) { decimal change = responses[i].Close - responses[i - 1].Close; if (change > 0) avgGain += change; else avgLoss += Math.Abs(change); } avgGain /= period; avgLoss /= period; if (avgLoss == 0) return 100; decimal rs = avgGain / avgLoss; decimal rsi = 100 - (100 / (1 + rs)); return rsi; }

Pattern Detection: Three White Soldiers, Three Black Crows, Hammer

public static int GetThreeWhiteSoldiers(List<KlineCandleStickResponse> responses, int curIndex) { if(curIndex < 10) return 0; KlineCandleStickResponse candle1 = responses[curIndex]; KlineCandleStickResponse candle2 = responses[curIndex-1]; KlineCandleStickResponse candle3 = responses[curIndex-2]; bool isAllGreen = candle1.isGreen() && candle2.isGreen() && candle3.isGreen(); bool areSoldiers = AreAllSolid([candle1,candle2, candle3], 0.6f); bool areSameSize = AreSameCandleSizes([candle1,candle2,candle3]); if(isAllGreen && areSoldiers && areSameSize) return 3; // Best else if(isAllGreen && areSoldiers) return 2; // Not strong else if(isAllGreen) return 1; // Meh return 0; } public static bool isHammer(KlineCandleStickResponse response, float bodyToShadowRatio = 0.3f) { float bodyLength = response.getCandleLength(); float totalLength = response.getTotalLength(); // Body should be small compared to total length if (bodyLength / totalLength > bodyToShadowRatio) return false; // Lower shadow should be at least twice the body length float lowerShadow = (float)Math.Min(response.Open, response.Close) - (float)response.Low; if (lowerShadow < bodyLength * 2) return false; // Upper shadow should be minimal float upperShadow = (float)(response.High - Math.Max(response.Open, response.Close)); if (upperShadow > totalLength * 0.1) return false; return true; }

Support/Resistance Detection

public static List<decimal> GetSupportiveLevels(List<TAReport> reports, float tolerance = 0.1f, int steps = 5, string SMA="SMA20") { List<decimal> allSups = new List<decimal>(); int lastMACross = 0; for(int k = 0; k < reports.Count; k++) { if(k < steps * 3) continue; // Not enough history bool MATopNow = decimal.Parse(Utils.GetPropByName(reports[k], SMA).ToString() ?? "0") > reports[k].candle.High; bool MATopBefore = decimal.Parse(Utils.GetPropByName(reports[k-1], SMA).ToString() ?? "0") > reports[k-1].candle.High; bool MACrossed = MATopBefore != MATopNow; if(!MACrossed) continue; // No cross if(lastMACross == 0) { lastMACross = k; continue; // First Cross } // Get lowest point between this and last cross decimal lowestInPeriod = 1000000; for(int i = lastMACross; i < k; i++) { if(reports[i].candle.Low < lowestInPeriod) { decimal candleBot = reports[i].candle.Open < reports[i].candle.Close ? reports[i].candle.Open : reports[i].candle.Close; lowestInPeriod = candleBot; } } allSups.Add(lowestInPeriod); } return allSups; }
Key Insights: Out of all the confirmations I used, Stochastic K% (K3 line) became my favorite — it turned out to be one of the most effective momentum indicators in my tests. Pattern-wise, TWS (Three White Soldiers) combined with Hammer gave surprisingly good signals. VWAP was the latest addition, based on a colleague's recommendation. It looks promising, but I haven't had enough live testing with it yet.

📤 Telegram Bot Notifications

The notification system is powered by a custom Telegram bot that interacts with the Telegram Bot API. The program:

  • Scans a specified list of coins and timeframes.
  • At the end of each candle, evaluates all active strategies.
  • If any strategies are triggered, sends a message to Telegram with strategy names, current price, TradingView link, and a custom-rendered chart image.
public class Messenger { TelegramBotClient botClient; public List<MessageQueueItem> messagesSchedule = new List<MessageQueueItem>(); public void ScheduleMessage(string text, string symbol="", string interval=""){ messagesSchedule.Add(new MessageQueueItem(){ symbol = symbol, interval = interval, text = text }); } public async void InitializeScheduler(){ while(true){ await Task.Delay(1500); if(messagesSchedule.Count <= 0) continue; MessageQueueItem queueItem = messagesSchedule[0]; try{ if(symbol.Length < 2){ await botClient.SendTextMessageAsync(_chatId, message); } else { FileStream fsSource = new FileStream(filename, FileMode.Open, FileAccess.Read); InputFile photo = InputFile.FromStream(fsSource); string url = $"https://www.tradingview.com/chart/?symbol=BINANCE%3A{symbol}"; string formattedMessage = message.Replace( $"`{symbol}`", $"<a href=\"{url}\">{symbol}</a>" ) + $"\n#{symbol}"; await botClient.SendPhotoAsync(_chatId, photo, caption: formattedMessage, parseMode: ParseMode.Html); } messagesSchedule.Remove(queueItem); } catch(Exception e){ Console.WriteLine($"Error occurred while sending {message} to {filename}"); } } } }

Each coin can have different timeframes set (e.g., 5m, 1h, 4h), depending on its volatility and importance — making this very flexible for real-world use.

🎯 Why I Built This

Initially, this was just for personal use. Me and a few colleagues were busy with work, and we couldn't afford to monitor the charts all day. I wanted a tool that could keep an eye on the market and alert us only when something meaningful happens.

Currently, the bot sends messages to a shared Telegram group. But the next step will be to make the system user-personalized, so each person gets alerts tailored to their own strategies and coins of interest.

🚀 What's Next

  • User-specific configuration (DM alerts instead of group)
  • GUI Dashboard to monitor activity and manage coin lists
  • Backtesting engine for strategies
  • Advanced pattern combinations
  • Bull Flag detection improvements

🧠 Final Thoughts

There's no better way to learn how indicators and strategies work than by implementing them from scratch. This project helped me bridge theory and practice — and has already improved the way I approach my own trades.

Thanks for reading — and happy coding/trading!

Comments

Popular Posts