<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eric Khoo</title>
    <description>The latest articles on DEV Community by Eric Khoo (@kzyxx11).</description>
    <link>https://dev.to/kzyxx11</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4037680%2F21d2a6a4-d0ce-429c-8db4-1ea09a1c0637.jpg</url>
      <title>DEV Community: Eric Khoo</title>
      <link>https://dev.to/kzyxx11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kzyxx11"/>
    <language>en</language>
    <item>
      <title>How I built a serverless ETF backtest bot with GitHub Actions and Telegram</title>
      <dc:creator>Eric Khoo</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:04:48 +0000</pubDate>
      <link>https://dev.to/kzyxx11/how-i-built-a-serverless-etf-backtest-bot-with-github-actions-and-telegram-3g20</link>
      <guid>https://dev.to/kzyxx11/how-i-built-a-serverless-etf-backtest-bot-with-github-actions-and-telegram-3g20</guid>
      <description>&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiog4ui1bne1at9z6yzo6.jpeg" width="532" height="1080"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fti30m26g57ap1qbxmf0j.jpeg" width="668" height="1280"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a small, open-source Telegram bot that tracks major ETFs (CSPX, QQQM), calculates Trend/Momentum scores, and shows historical win rates for current market structures — using only free serverless tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: "Is now a good time?"
&lt;/h2&gt;

&lt;p&gt;If you’ve ever dollar-cost averaged into ETFs, you’ve probably asked yourself this question at some point.&lt;/p&gt;

&lt;p&gt;The market is up — &lt;em&gt;should I wait for a pullback?&lt;/em&gt;&lt;br&gt;
The market is down — &lt;em&gt;is this a buying opportunity or a falling knife?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I couldn’t find a tool that gave me a &lt;strong&gt;data-backed&lt;/strong&gt; answer without requiring a Bloomberg terminal or a PhD in finance. So I built one.&lt;/p&gt;


&lt;h2&gt;
  
  
  The solution: A bot that shows historical probabilities
&lt;/h2&gt;

&lt;p&gt;Instead of telling me what &lt;em&gt;might happen&lt;/em&gt;, the bot tells me what &lt;em&gt;historically happened&lt;/em&gt; when the market looked like this.&lt;/p&gt;

&lt;p&gt;Here’s what a daily update looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📜 Historical Match: 138 occurrences
  • Next 90d: Win 84.1% | Avg +5.9% | MaxDD -18.4%
  • Next 180d: Win 76.1% | Avg +9.1% | MaxDD -18.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s not a prediction. That’s data.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works (the 30-second version)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Every trading day, the bot fetches &lt;strong&gt;15 years of historical data&lt;/strong&gt; for CSPX and QQQM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It calculates &lt;strong&gt;MA50&lt;/strong&gt;, &lt;strong&gt;MA200&lt;/strong&gt;, and &lt;strong&gt;RSI (14)&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It combines those into two composite scores:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Trend Score (0–100)&lt;/strong&gt; – measures structural health&lt;br&gt;
&lt;strong&gt;Momentum Score (0–100)&lt;/strong&gt; – measures short-term strength&lt;/p&gt;

&lt;p&gt;It scans the entire 15-year history for &lt;strong&gt;structurally similar periods&lt;/strong&gt; (within ±8 points on both scores).&lt;/p&gt;

&lt;p&gt;It calculates &lt;strong&gt;forward returns&lt;/strong&gt; for those matched periods — 90 days and 180 days out.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tech stack (all free, all serverless)
&lt;/h2&gt;

&lt;p&gt;Component | Tool&lt;br&gt;
Data source | Yahoo Finance (via yfinance)&lt;br&gt;
Calculation engine | Python (pandas, matplotlib)&lt;br&gt;
Scheduling &amp;amp; execution  | GitHub Actions (cron + on-demand)&lt;br&gt;
API gateway | Cloudflare Workers&lt;br&gt;
Delivery | Telegram Bot API&lt;br&gt;
Source code | GitHub (MIT license)&lt;/p&gt;

&lt;p&gt;The entire pipeline costs &lt;strong&gt;$0/month&lt;/strong&gt; to run. GitHub Actions provides free compute, Cloudflare Workers provides free API routing, and Telegram provides free delivery.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core logic: Trend Score explained
&lt;/h2&gt;

&lt;p&gt;I wanted a scoring system that was &lt;strong&gt;simple enough to explain&lt;/strong&gt; but &lt;strong&gt;robust enough to be useful&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Trend Score is calculated as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Start at 50 (neutral)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If price &amp;gt; MA200 → +20 points, plus bonus for distance above&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If price &amp;gt; MA50 → +10 points&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If MA50 &amp;gt; MA200 (golden cross) → +5 points&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vice versa for bearish conditions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives a score from &lt;strong&gt;0 to 100&lt;/strong&gt; that reflects how “bullish” or “bearish” the current structure is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The historical backtest: how the probability is calculated
&lt;/h2&gt;

&lt;p&gt;This is the part that took the most thought.&lt;/p&gt;

&lt;p&gt;For every trading day in the last 15 years, I calculate the Trend and Momentum scores. I then find all days where both scores are &lt;strong&gt;within ±8 points&lt;/strong&gt; of today’s values.&lt;/p&gt;

&lt;p&gt;Those days become the &lt;strong&gt;historical match set&lt;/strong&gt;. For each matched day, I calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The price 90 days later&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The price 180 days later&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The return percentage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The max drawdown during that period&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The results are aggregated and displayed as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Win rate (% of positive returns)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Average return&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maximum drawdown&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a backtest of a trading strategy — it’s a &lt;strong&gt;structural similarity analysis&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  One design decision I’m proud of
&lt;/h2&gt;

&lt;p&gt;I didn’t optimize the parameters.&lt;/p&gt;

&lt;p&gt;The tolerance (±8 points) and the indicator set (MA50, MA200, RSI) were chosen &lt;strong&gt;before&lt;/strong&gt; running the backtest. I didn’t tweak them to make the numbers look better.&lt;/p&gt;

&lt;p&gt;This means the results are &lt;strong&gt;honest&lt;/strong&gt;. They might not be perfect, but they’re not fabricated either.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More assets&lt;/strong&gt; — I’m planning to add more ETFs, and eventually individual stocks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;More indicators&lt;/strong&gt; — I may add volatility (ATR) and participation (volume) to the scoring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Public dashboard&lt;/strong&gt; — a simple web view of the daily analysis.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The bot is &lt;strong&gt;free and open source&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔗 &lt;strong&gt;GitHub repo:&lt;/strong&gt; &lt;a href="https://dev.tourl"&gt;github.com/kzyxx11/quant_trade_bot&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📱 &lt;strong&gt;Daily updates:&lt;/strong&gt; &lt;a href="https://dev.tourl"&gt;@ETF_Trend_Monitor&lt;/a&gt; on Telegram&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re a developer, you can fork the repo and adapt it to your own assets. If you’re an investor, just join the channel and check the daily update.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;If you have thoughts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The scoring methodology&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What assets to add next&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How to improve the historical backtest&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’d genuinely appreciate hearing from you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by Eric Khoo. Not financial advice — just data.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>fintech</category>
      <category>serverless</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
