DEV Community

Snapon Equipment
Snapon Equipment

Posted on

I Rebuilt My Broken Forex Bot From Scratch Using GitHub Copilot — Here's What Actually Happened

GitHub “Finish-Up-A-Thon” Challenge Submission

This is a submission for the GitHub Finish-Up-A-Thon Challenge

What I Built

A fully autonomous, institutional-grade Forex trading bot — APEX v12 — running live 24/5 on a private VPS, trading 10 currency pairs using liquidity sweep detection, V20 streaming price feeds, and Kelly Criterion position sizing.

But it didn't start that way.

It started as a broken, half-finished script that had a 1.94% win rate over 103 trades and was hemorrhaging money.


The Before: A Bot That Was Embarrassing to Look At

Around six months ago I started building an automated FX trading system. The idea was simple: use technical indicators (EMA crossovers, RSI, MACD) to identify trades, size them properly, and let it run.

What I actually built was a mess:

  • REST API polling every 30 minutes — meaning I was always trading on stale, outdated prices
  • Hardcoded stop-losses with no dynamic sizing
  • A "quantum" scoring module that wasn't actually connected to anything meaningful
  • 1.94% win rate after 103 live paper trades
  • The bot would silently crash and nobody (including me) would know for hours

The log file was a wall of errors. The strategy was essentially random. I kept telling myself I'd "fix it later" and never did.

The project sat abandoned for weeks.


The Comeback: GitHub Copilot as My Senior Dev

When I picked this back up, I decided to stop patching and just rebuild it right — with GitHub Copilot as my pair programmer throughout.

Here's what the Copilot-assisted rebuild actually looked like:

1. Ripping Out REST Polling → V20 Streaming

The biggest issue was stale prices. I was polling the OANDA REST API every 30 minutes. By the time my bot "saw" a price, it was ancient history.

Copilot helped me architect a full async streaming solution using OANDA's V20 Streaming API. Instead of polling, the bot now maintains a persistent live connection and processes every price tick in real-time.

class StreamingPriceFeed:
    async def connect(self) -> None:
        pairs_str = "%2C".join(PAIRS)
        url = f"{OANDA_STREAM}/v3/accounts/{OANDA_ACC}/pricing/stream?instruments={pairs_str}"

        while self._running:
            async with aiohttp.ClientSession() as session:
                async with session.get(url, headers=HDR, timeout=aiohttp.ClientTimeout(total=None)) as resp:
                    async for line in resp.content:
                        msg = json.loads(line.decode("utf-8").strip())
                        if msg.get("type") == "PRICE":
                            pair = msg.get("instrument")
                            bid = float(msg["bids"][0]["price"])
                            ask = float(msg["asks"][0]["price"])
                            self.prices[pair] = (bid, ask)
                            self.last_update[pair] = time.time()
Enter fullscreen mode Exit fullscreen mode

Copilot not only helped me write this — it flagged that I needed exponential backoff on reconnect, a heartbeat monitor, and a stale price threshold. Things I would have forgotten.

2. Dumping Indicator-Chasing → Liquidity Sweep Logic

The old bot chased EMA crossovers. The new strategy is built on liquidity sweep detection — a concept used by institutional traders.

The idea: large players intentionally push price beyond key highs/lows to trigger retail stop-losses, collect that liquidity, then reverse hard. If you can detect that sweep and enter on the rejection, you get institutional-quality entries.

Copilot helped me implement the full sweep detection engine:

async def detect_sweep(self, pair: str, candles: List[dict]) -> Optional[dict]:
    # Identify key levels from last 20 daily + 8 weekly highs/lows
    levels = await self.get_key_levels(pair, candles)

    current_price = self.stream.get_price(pair)

    for level in levels:
        pip = 0.01 if "JPY" in pair else 0.0001

        # Price swept BELOW a key low — then rejected back above it
        if level.level_type == "DAILY_LOW":
            swept = current_price < (level.price - SWEEP_BUFFER_PIPS * pip)
            rejected = current_price > level.price  # back above

            if swept and rejected:
                return {"type": "BULLISH_SWEEP", "level": level.price, "entry": current_price}
Enter fullscreen mode Exit fullscreen mode

When Copilot reviewed my initial implementation, it caught that I wasn't waiting for confirmation bars before entering — I was jumping in the moment price touched back above the level, which is exactly what gets fake-out'd. It suggested adding a SWEEP_CONFIRM_BARS = 2 parameter to wait for two M5 candles to close back above the level before entering.

That one suggestion would have saved me dozens of bad trades.

3. Fixed Lot Sizes → Kelly Criterion

The old bot used a fixed 0.1 lot size on every trade. No relationship to account size, volatility, or win rate.

Copilot introduced me to the Kelly Criterion implementation for trading:

async def run_audit(self, session) -> PerformanceStats:
    trades = await self.fetch_closed_trades(session)

    wins = [t for t in trades if float(t.get("realizedPL", 0)) > 0]
    losses = [t for t in trades if float(t.get("realizedPL", 0)) < 0]

    win_rate = len(wins) / len(trades)
    avg_win = sum(float(t["realizedPL"]) for t in wins) / len(wins)
    avg_loss = abs(sum(float(t["realizedPL"]) for t in losses) / len(losses))

    R = avg_win / avg_loss  # Reward/Risk ratio
    kelly = win_rate - ((1 - win_rate) / R)
    kelly_fraction = kelly * 0.25  # Fractional Kelly for safety

    return kelly_fraction
Enter fullscreen mode Exit fullscreen mode

The bot now calculates optimal position size from its own historical performance on every startup. As it learns, it self-optimizes.

Hard caps: minimum 0.5% per trade, maximum 6% per trade. Never bet the farm.

4. No Monitoring → Full Async Daemon + Watchdog

The old bot had no crash recovery. If it died, it was dead until I SSH'd in and manually restarted it.

The new architecture has a daemon watchdog that:

  • Monitors the bot process every 15 minutes
  • Auto-restarts if it crashes
  • Logs all restarts with timestamps
  • Sends a Telegram alert when something goes wrong
def watchdog_loop():
    while True:
        time.sleep(900)  # 15 min
        if not is_bot_running():
            log.warning("⚠️ Bot not running — restarting...")
            start_bot()
            notify_telegram("🔄 APEX v12 auto-restarted by watchdog")
Enter fullscreen mode Exit fullscreen mode

Copilot's contribution here was suggesting I use a lock file to prevent multiple instances from spawning — a classic race condition I would have hit immediately.


The Demo: What v12 Looks Like Running

The bot is live right now on a paper trading account ($100k starting balance):

Current Status (June 2026):

  • Balance: $94,902.80
  • Streaming: 10 pairs live (EUR/USD, GBP/USD, USD/JPY, AUD/USD, GBP/JPY, EUR/JPY, USD/CAD, NZD/USD, USD/CHF, EUR/GBP)
  • Scanning: Every 5 minutes
  • Open trades: Active with real-time P&L tracking
  • Uptime: 99.9% (daemon + watchdog)

The Telegram integration sends a daily summary:

📊 APEX v12 Daily Report
Balance: $94,902.80 | NAV: $94,894.80
Open Trades: GBP_USD — 100k units | P&L: -$10.00
Scans Today: 288 | Setups Detected: 3 | Trades Taken: 1
Next Scan: 10:35 PM ET
Enter fullscreen mode Exit fullscreen mode

The Comeback Story

The original bot had a 1.94% win rate. That's not a typo.

The rebuild took about 3 intensive sessions with Copilot — refactoring architecture, rethinking strategy, adding proper risk management. Things that would have taken me weeks of solo research, Copilot accelerated by explaining institutional concepts, suggesting edge cases I'd miss, and catching bugs before they hit production.

The biggest mindset shift: Copilot pushed me to stop treating the bot like a script and start treating it like a system. Scripts break. Systems recover.

Key Copilot contributions that actually mattered:

  1. Flagged the stale price problem before I even asked — noticed the 30-second threshold was too tight for low-volume sessions
  2. Suggested the confirmation bars pattern for sweep entries
  3. Introduced fractional Kelly sizing (I was going to use full Kelly, which is extremely aggressive)
  4. Added the lock file to the watchdog (prevented a multi-instance bug)
  5. Recommended logging to a persistent file path with auto-directory creation

The project isn't finished — no trading bot ever is. But it went from something I was ashamed of to something I'm actually proud to run. That's the finish-up-a-thon in a nutshell.


The Code

The full v12 bot is available on my VPS and I'm happy to share the core modules on request. Key files:

  • fx_bot_runner.py — Main async engine (1,100+ lines)
  • apex_v12_daemon.py — Watchdog daemon
  • apex_edge_strategy.py — Liquidity sweep detection

Built with GitHub Copilot, Python, aiohttp, OANDA V20 API. Running 24/5 on Ubuntu VPS.

Top comments (0)