📈 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;
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))){
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;
else if(isAllGreen && areSoldiers) return 2;
else if(isAllGreen) return 1;
return 0;
}
public static bool isHammer(KlineCandleStickResponse response, float bodyToShadowRatio = 0.3f)
{
float bodyLength = response.getCandleLength();
float totalLength = response.getTotalLength();
if (bodyLength / totalLength > bodyToShadowRatio) return false;
float lowerShadow = (float)Math.Min(response.Open, response.Close) - (float)response.Low;
if (lowerShadow < bodyLength * 2) return false;
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;
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;
if(lastMACross == 0) {
lastMACross = k;
continue;
}
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
Post a Comment