[{"content":"What You\u0026rsquo;ll Build By the end of this guide, you\u0026rsquo;ll have a working crypto trading bot that:\nConnects to Binance Futures Fetches real-time price data Detects entry signals based on technical indicators Places orders automatically Manages stop losses and take profits Prerequisites: Basic Python knowledge. That\u0026rsquo;s it.\nStep 1: Set Up Your Environment 1 pip install ccxt pandas ta ccxt — Connects to 100+ crypto exchanges with one API pandas — Data manipulation ta — Technical indicators (RSI, MACD, etc.) Step 2: Connect to Binance First, create a Binance account and generate API keys (under API Management).\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 import ccxt exchange = ccxt.binance({ \u0026#39;apiKey\u0026#39;: \u0026#39;YOUR_API_KEY\u0026#39;, \u0026#39;secret\u0026#39;: \u0026#39;YOUR_SECRET_KEY\u0026#39;, \u0026#39;options\u0026#39;: { \u0026#39;defaultType\u0026#39;: \u0026#39;future\u0026#39;, # Futures trading \u0026#39;adjustForTimeDifference\u0026#39;: True, # Sync clocks }, \u0026#39;enableRateLimit\u0026#39;: True, }) # IMPORTANT: Load time difference to avoid timestamp errors exchange.load_time_difference() Security warning: Never hardcode API keys in your script. Use environment variables:\n1 2 3 4 5 6 7 import os exchange = ccxt.binance({ \u0026#39;apiKey\u0026#39;: os.environ[\u0026#39;BINANCE_API_KEY\u0026#39;], \u0026#39;secret\u0026#39;: os.environ[\u0026#39;BINANCE_SECRET\u0026#39;], # ... }) Step 3: Fetch Price Data 1 2 3 4 5 6 7 8 9 10 11 12 import pandas as pd def get_candles(symbol, timeframe=\u0026#39;5m\u0026#39;, limit=100): \u0026#34;\u0026#34;\u0026#34;Fetch OHLCV candles from Binance.\u0026#34;\u0026#34;\u0026#34; ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit) df = pd.DataFrame(ohlcv, columns=[\u0026#39;timestamp\u0026#39;, \u0026#39;open\u0026#39;, \u0026#39;high\u0026#39;, \u0026#39;low\u0026#39;, \u0026#39;close\u0026#39;, \u0026#39;volume\u0026#39;]) df[\u0026#39;timestamp\u0026#39;] = pd.to_datetime(df[\u0026#39;timestamp\u0026#39;], unit=\u0026#39;ms\u0026#39;) return df # Example df = get_candles(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;5m\u0026#39;, 100) print(df.tail()) Output:\ntimestamp open high low close volume 95 2026-04-08 12:35:00 84521.0 84589.0 84498.0 84562.0 1523.456 96 2026-04-08 12:40:00 84562.0 84601.0 84531.0 84577.0 1102.789 97 2026-04-08 12:45:00 84577.0 84612.0 84555.0 84598.0 987.654 98 2026-04-08 12:50:00 84598.0 84634.0 84570.0 84615.0 1345.012 99 2026-04-08 12:55:00 84615.0 84678.0 84601.0 84667.0 1876.543 Step 4: Calculate Indicators 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import ta def add_indicators(df): \u0026#34;\u0026#34;\u0026#34;Add technical indicators to the dataframe.\u0026#34;\u0026#34;\u0026#34; # RSI - Relative Strength Index df[\u0026#39;rsi\u0026#39;] = ta.momentum.RSIIndicator(df[\u0026#39;close\u0026#39;], window=14).rsi() # Volume Ratio - current volume vs 20-period average df[\u0026#39;vol_avg\u0026#39;] = df[\u0026#39;volume\u0026#39;].rolling(20).mean() df[\u0026#39;vol_ratio\u0026#39;] = df[\u0026#39;volume\u0026#39;] / df[\u0026#39;vol_avg\u0026#39;] # Candle body size as percentage df[\u0026#39;body_pct\u0026#39;] = abs(df[\u0026#39;close\u0026#39;] - df[\u0026#39;open\u0026#39;]) / df[\u0026#39;open\u0026#39;] * 100 return df df = add_indicators(df) Step 5: Define Entry Signals Here\u0026rsquo;s a simple trend-following signal:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def check_signal(df): \u0026#34;\u0026#34;\u0026#34;Check if we should enter a trade.\u0026#34;\u0026#34;\u0026#34; last = df.iloc[-1] # Current candle prev = df.iloc[-2] # Previous candle # Long signal conditions long_signal = ( last[\u0026#39;body_pct\u0026#39;] \u0026gt;= 0.5 # Strong candle and last[\u0026#39;close\u0026#39;] \u0026gt; last[\u0026#39;open\u0026#39;] # Bullish and last[\u0026#39;vol_ratio\u0026#39;] \u0026gt;= 1.5 # Above-average volume and last[\u0026#39;rsi\u0026#39;] \u0026gt; 50 # Upward momentum and last[\u0026#39;rsi\u0026#39;] \u0026lt; 70 # Not overbought ) # Short signal conditions short_signal = ( last[\u0026#39;body_pct\u0026#39;] \u0026gt;= 0.5 # Strong candle and last[\u0026#39;close\u0026#39;] \u0026lt; last[\u0026#39;open\u0026#39;] # Bearish and last[\u0026#39;vol_ratio\u0026#39;] \u0026gt;= 1.5 # Above-average volume and last[\u0026#39;rsi\u0026#39;] \u0026lt; 50 # Downward momentum and last[\u0026#39;rsi\u0026#39;] \u0026gt; 30 # Not oversold ) if long_signal: return \u0026#39;long\u0026#39; elif short_signal: return \u0026#39;short\u0026#39; return None Step 6: Place Orders 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def open_position(symbol, side, usdt_amount, leverage=3): \u0026#34;\u0026#34;\u0026#34;Open a futures position.\u0026#34;\u0026#34;\u0026#34; # Set leverage exchange.set_leverage(leverage, symbol) # Get current price ticker = exchange.fetch_ticker(symbol) price = ticker[\u0026#39;last\u0026#39;] # Calculate position size amount = (usdt_amount * leverage) / price # Place market order order_side = \u0026#39;buy\u0026#39; if side == \u0026#39;long\u0026#39; else \u0026#39;sell\u0026#39; order = exchange.create_order( symbol=symbol, type=\u0026#39;market\u0026#39;, side=order_side, amount=amount, ) print(f\u0026#34;Opened {side} {symbol} | Size: {amount:.4f} | Price: {price}\u0026#34;) return order def close_position(symbol, side, amount): \u0026#34;\u0026#34;\u0026#34;Close a futures position.\u0026#34;\u0026#34;\u0026#34; order_side = \u0026#39;sell\u0026#39; if side == \u0026#39;long\u0026#39; else \u0026#39;buy\u0026#39; order = exchange.create_order( symbol=symbol, type=\u0026#39;market\u0026#39;, side=order_side, amount=amount, params={\u0026#39;reduceOnly\u0026#39;: True} ) print(f\u0026#34;Closed {side} {symbol}\u0026#34;) return order Step 7: Add Stop Loss Critical: Always use exchange-side stop losses, not client-side monitoring.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def place_stop_loss(symbol, side, entry_price, sl_pct=0.02): \u0026#34;\u0026#34;\u0026#34;Place a stop loss order on the exchange.\u0026#34;\u0026#34;\u0026#34; if side == \u0026#39;long\u0026#39;: stop_price = entry_price * (1 - sl_pct) order_side = \u0026#39;sell\u0026#39; else: stop_price = entry_price * (1 + sl_pct) order_side = \u0026#39;buy\u0026#39; # Round to valid price precision stop_price = float(exchange.price_to_precision(symbol, stop_price)) order = exchange.create_order( symbol=symbol, type=\u0026#39;STOP_MARKET\u0026#39;, side=order_side, amount=position_size, params={ \u0026#39;stopPrice\u0026#39;: stop_price, \u0026#39;reduceOnly\u0026#39;: True, } ) print(f\u0026#34;SL placed at {stop_price} ({sl_pct*100}% from entry)\u0026#34;) return order Step 8: The Main Loop Putting it all together:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import time SYMBOL = \u0026#39;BTC/USDT\u0026#39; TRADE_USDT = 100 # $100 per trade LEVERAGE = 3 SL_PCT = 0.02 # 2% stop loss SCAN_INTERVAL = 300 # 5 minutes position = None while True: try: # Fetch fresh data df = get_candles(SYMBOL, \u0026#39;5m\u0026#39;, limit=50) df = add_indicators(df) if position is None: # Check for entry signal signal = check_signal(df) if signal: order = open_position(SYMBOL, signal, TRADE_USDT, LEVERAGE) entry_price = float(order[\u0026#39;average\u0026#39;]) sl_order = place_stop_loss( SYMBOL, signal, entry_price, SL_PCT ) position = { \u0026#39;side\u0026#39;: signal, \u0026#39;entry_price\u0026#39;: entry_price, \u0026#39;sl_order_id\u0026#39;: sl_order[\u0026#39;id\u0026#39;], } print(f\u0026#34;Position opened: {position}\u0026#34;) else: # Monitor existing position current_price = df.iloc[-1][\u0026#39;close\u0026#39;] if position[\u0026#39;side\u0026#39;] == \u0026#39;long\u0026#39;: pnl_pct = (current_price - position[\u0026#39;entry_price\u0026#39;]) / position[\u0026#39;entry_price\u0026#39;] else: pnl_pct = (position[\u0026#39;entry_price\u0026#39;] - current_price) / position[\u0026#39;entry_price\u0026#39;] print(f\u0026#34;Position PnL: {pnl_pct*100:.2f}%\u0026#34;) # Take profit at 3% if pnl_pct \u0026gt;= 0.03: close_position(SYMBOL, position[\u0026#39;side\u0026#39;], order[\u0026#39;amount\u0026#39;]) position = None print(\u0026#34;Take profit hit!\u0026#34;) time.sleep(SCAN_INTERVAL) except ccxt.NetworkError as e: print(f\u0026#34;Network error: {e}\u0026#34;) time.sleep(10) except Exception as e: print(f\u0026#34;Error: {e}\u0026#34;) time.sleep(10) Step 9: What This Bot Is Missing This tutorial gives you a working foundation. But a production bot needs more:\nFeature Why It Matters Trailing stop Lock in profits as price moves in your favor Position sizing Risk management based on account balance Multiple coins Diversification and more opportunities State persistence Survive restarts without losing track of positions Logging Debug issues when you\u0026rsquo;re not watching PID lockfile Prevent duplicate bot instances Backtest Verify your strategy works before risking real money I\u0026rsquo;ve built all of these. Check the rest of this blog for deep dives on each topic.\nStep 10: Test Safely DO NOT run this with real money immediately.\nBacktest first — Test on historical data Testnet — Binance has a futures testnet at testnet.binancefuture.com Dry run — Run the bot with logging but no real orders Small size — Start with the minimum trade amount ($5-10) The biggest risk isn\u0026rsquo;t a bad strategy — it\u0026rsquo;s a bug in your code placing an order you didn\u0026rsquo;t intend.\nCommon Pitfalls 1. Timestamp Errors Timestamp for this request was 1000ms ahead of the server\u0026#39;s time Fix: Use adjustForTimeDifference: True and call load_time_difference().\n2. Insufficient Balance Make sure you have USDT in your Futures wallet, not your Spot wallet. Transfer first.\n3. Leverage Not Set If you don\u0026rsquo;t call set_leverage(), Binance uses whatever leverage was set last (possibly 20x from when you were clicking around the UI).\n4. Rounding Errors Every trading pair has different precision requirements. Always use:\n1 2 amount = exchange.amount_to_precision(symbol, amount) price = exchange.price_to_precision(symbol, price) Next Steps This is just the beginning. To build a bot that actually makes money consistently, you\u0026rsquo;ll need to:\nBuild a proper backtest — Don\u0026rsquo;t trust your strategy without testing it Understand risk-reward — Win rate doesn\u0026rsquo;t matter as much as you think Handle stop losses properly — The most important feature in your bot Avoid overfitting — The trap every new bot builder falls into Happy building. And remember — backtest before you trade real money.\nThis guide gets you from zero to a working bot. The other 50 posts on this blog are about going from a working bot to a profitable one.\n","permalink":"https://kimchibot.com/posts/how-to-build-a-crypto-trading-bot-with-python-step-by-step/","summary":"A complete beginner\u0026rsquo;s guide to building a crypto trading bot with Python and ccxt. From zero to a working bot on Binance Futures.","title":"How to Build a Crypto Trading Bot with Python: Step-by-Step Guide"},{"content":"Why ccxt? ccxt is a Python library that provides a unified API for 100+ crypto exchanges. Write your bot once, run it on any exchange.\n1 pip install ccxt This guide covers Binance USDT-M Futures specifically — the most popular futures market for bot trading.\nConnection Setup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import ccxt exchange = ccxt.binance({ \u0026#39;apiKey\u0026#39;: \u0026#39;your_api_key\u0026#39;, \u0026#39;secret\u0026#39;: \u0026#39;your_secret\u0026#39;, \u0026#39;options\u0026#39;: { \u0026#39;defaultType\u0026#39;: \u0026#39;future\u0026#39;, \u0026#39;adjustForTimeDifference\u0026#39;: True, }, \u0026#39;enableRateLimit\u0026#39;: True, }) # MUST call this — prevents timestamp errors exchange.load_time_difference() Key Options Explained Option What It Does Required? defaultType: future Routes all calls to Futures API Yes adjustForTimeDifference Syncs your clock with Binance Yes enableRateLimit Auto-throttles API calls Recommended Fetching Market Data OHLCV Candles 1 2 3 4 5 6 7 8 # Fetch 100 five-minute candles for BTC ohlcv = exchange.fetch_ohlcv(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;5m\u0026#39;, limit=100) # Each candle: [timestamp, open, high, low, close, volume] # Convert to DataFrame import pandas as pd df = pd.DataFrame(ohlcv, columns=[\u0026#39;ts\u0026#39;, \u0026#39;open\u0026#39;, \u0026#39;high\u0026#39;, \u0026#39;low\u0026#39;, \u0026#39;close\u0026#39;, \u0026#39;volume\u0026#39;]) df[\u0026#39;ts\u0026#39;] = pd.to_datetime(df[\u0026#39;ts\u0026#39;], unit=\u0026#39;ms\u0026#39;) Available timeframes: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d, 1w\nTicker (Current Price) 1 2 3 4 5 ticker = exchange.fetch_ticker(\u0026#39;BTC/USDT\u0026#39;) print(ticker[\u0026#39;last\u0026#39;]) # Last price print(ticker[\u0026#39;bid\u0026#39;]) # Best bid print(ticker[\u0026#39;ask\u0026#39;]) # Best ask print(ticker[\u0026#39;volume\u0026#39;]) # 24h volume All Markets 1 2 3 exchange.load_markets() futures_pairs = [s for s in exchange.symbols if s.endswith(\u0026#39;/USDT\u0026#39;) and \u0026#39;:USDT\u0026#39; in s] print(f\u0026#34;Available futures pairs: {len(futures_pairs)}\u0026#34;) Account \u0026amp; Balance 1 2 3 4 5 6 7 8 9 # Fetch futures balance balance = exchange.fetch_balance() # Your USDT balance free_usdt = balance[\u0026#39;USDT\u0026#39;][\u0026#39;free\u0026#39;] # Available used_usdt = balance[\u0026#39;USDT\u0026#39;][\u0026#39;used\u0026#39;] # In positions total_usdt = balance[\u0026#39;USDT\u0026#39;][\u0026#39;total\u0026#39;] # Total print(f\u0026#34;Balance: {total_usdt} USDT (Free: {free_usdt}, Used: {used_usdt})\u0026#34;) Important: Don\u0026rsquo;t pass {'type': 'future'} to fetch_balance(). On some ccxt versions, this returns wrong data. Just call it plain.\nSetting Leverage 1 2 3 4 5 # Set leverage BEFORE opening a position exchange.set_leverage(3, \u0026#39;BTC/USDT\u0026#39;) # Set margin mode (isolated is safer for bots) exchange.set_margin_mode(\u0026#39;isolated\u0026#39;, \u0026#39;BTC/USDT\u0026#39;) Warning: If you don\u0026rsquo;t set leverage explicitly, Binance uses whatever was set last — possibly from manual trading in the web UI.\nPlacing Orders Market Order (Immediate Fill) 1 2 3 4 5 # Long (buy) order = exchange.create_order(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;market\u0026#39;, \u0026#39;buy\u0026#39;, 0.001) # Short (sell) order = exchange.create_order(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;market\u0026#39;, \u0026#39;sell\u0026#39;, 0.001) Limit Order (Fill at Specific Price) 1 2 # Buy limit at $84,000 order = exchange.create_order(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;limit\u0026#39;, \u0026#39;buy\u0026#39;, 0.001, 84000) Stop Loss (STOP_MARKET) 1 2 3 4 5 6 7 8 # Stop loss for a long position (sell when price drops to $82,000) order = exchange.create_order( \u0026#39;BTC/USDT\u0026#39;, \u0026#39;STOP_MARKET\u0026#39;, \u0026#39;sell\u0026#39;, 0.001, params={ \u0026#39;stopPrice\u0026#39;: 82000, \u0026#39;reduceOnly\u0026#39;: True, } ) Stop Loss (STOP — Limit Order) 1 2 3 4 5 6 7 8 # Limit stop loss — triggers at stopPrice, fills at price order = exchange.create_order( \u0026#39;BTC/USDT\u0026#39;, \u0026#39;STOP\u0026#39;, \u0026#39;sell\u0026#39;, 0.001, 81800, # limit price params={ \u0026#39;stopPrice\u0026#39;: 82000, # trigger price \u0026#39;reduceOnly\u0026#39;: True, } ) Take Profit 1 2 3 4 5 6 7 8 # Take profit for a long position at $90,000 order = exchange.create_order( \u0026#39;BTC/USDT\u0026#39;, \u0026#39;TAKE_PROFIT_MARKET\u0026#39;, \u0026#39;sell\u0026#39;, 0.001, params={ \u0026#39;stopPrice\u0026#39;: 90000, \u0026#39;reduceOnly\u0026#39;: True, } ) Position Management Check Open Positions 1 2 3 4 5 6 7 8 positions = exchange.fetch_positions() for pos in positions: if float(pos[\u0026#39;contracts\u0026#39;]) \u0026gt; 0: print(f\u0026#34;{pos[\u0026#39;symbol\u0026#39;]}: {pos[\u0026#39;side\u0026#39;]} | \u0026#34; f\u0026#34;Size: {pos[\u0026#39;contracts\u0026#39;]} | \u0026#34; f\u0026#34;Entry: {pos[\u0026#39;entryPrice\u0026#39;]} | \u0026#34; f\u0026#34;PnL: {pos[\u0026#39;unrealizedPnl\u0026#39;]}\u0026#34;) Close a Position 1 2 3 4 5 6 7 def close_position(symbol, side, amount): \u0026#34;\u0026#34;\u0026#34;Close position with a market order.\u0026#34;\u0026#34;\u0026#34; close_side = \u0026#39;sell\u0026#39; if side == \u0026#39;long\u0026#39; else \u0026#39;buy\u0026#39; return exchange.create_order( symbol, \u0026#39;market\u0026#39;, close_side, amount, params={\u0026#39;reduceOnly\u0026#39;: True} ) Order Management Check Order Status 1 2 3 4 order = exchange.fetch_order(order_id, \u0026#39;BTC/USDT\u0026#39;) print(order[\u0026#39;status\u0026#39;]) # \u0026#39;open\u0026#39;, \u0026#39;closed\u0026#39;, \u0026#39;canceled\u0026#39; print(order[\u0026#39;filled\u0026#39;]) # Amount filled print(order[\u0026#39;average\u0026#39;]) # Average fill price Cancel an Order 1 exchange.cancel_order(order_id, \u0026#39;BTC/USDT\u0026#39;) List Open Orders 1 2 3 open_orders = exchange.fetch_open_orders(\u0026#39;BTC/USDT\u0026#39;) for o in open_orders: print(f\u0026#34;{o[\u0026#39;type\u0026#39;]} {o[\u0026#39;side\u0026#39;]} {o[\u0026#39;amount\u0026#39;]} @ {o[\u0026#39;price\u0026#39;]}\u0026#34;) The Algo Order Problem This is the #1 gotcha on Binance Futures.\nAny order with stopPrice (STOP, STOP_MARKET, TAKE_PROFIT, TAKE_PROFIT_MARKET) is treated as an algorithmic/conditional order. Standard order endpoints can\u0026rsquo;t see them.\n1 2 3 4 5 6 7 8 9 10 11 12 # This WON\u0026#39;T find your stop loss order: order = exchange.fetch_order(sl_order_id, \u0026#39;BTC/USDT\u0026#39;) # ❌ Not found # You need the algo order endpoints: # Check status result = exchange.fapiPrivateGetAlgoOrder({\u0026#39;algoId\u0026#39;: sl_order_id}) # Cancel result = exchange.fapiPrivateDeleteAlgoOrder({\u0026#39;algoId\u0026#39;: sl_order_id}) # List all open algo orders result = exchange.fapiPrivateGetOpenAlgoOrders() Price \u0026amp; Amount Precision Every trading pair has different precision requirements. Sending too many decimals causes errors.\n1 2 3 4 5 6 7 8 9 10 # Get market info market = exchange.market(\u0026#39;BTC/USDT\u0026#39;) # Round to valid precision amount = exchange.amount_to_precision(\u0026#39;BTC/USDT\u0026#39;, 0.00123456789) price = exchange.price_to_precision(\u0026#39;BTC/USDT\u0026#39;, 84123.456789) # Get minimum order size min_amount = market[\u0026#39;limits\u0026#39;][\u0026#39;amount\u0026#39;][\u0026#39;min\u0026#39;] min_cost = market[\u0026#39;limits\u0026#39;][\u0026#39;cost\u0026#39;][\u0026#39;min\u0026#39;] Error Handling 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 try: order = exchange.create_order(...) except ccxt.InsufficientFunds: print(\u0026#34;Not enough balance\u0026#34;) except ccxt.InvalidOrder as e: if \u0026#39;-2022\u0026#39; in str(e): print(\u0026#34;ReduceOnly rejected — position already closed\u0026#34;) elif \u0026#39;-4411\u0026#39; in str(e): print(\u0026#34;TradFi token — needs agreement signing\u0026#34;) else: print(f\u0026#34;Invalid order: {e}\u0026#34;) except ccxt.NetworkError: print(\u0026#34;Network timeout — retry\u0026#34;) except ccxt.ExchangeError as e: print(f\u0026#34;Exchange error: {e}\u0026#34;) Common Error Codes Code Meaning Solution -1021 Timestamp out of range Call load_time_difference() -2019 Margin insufficient Reduce position size or add margin -2022 ReduceOnly rejected Position already closed, ignore -4411 TradFi agreement needed Exclude equity tokens -1111 Precision over maximum Use amount_to_precision() Rate Limits Binance allows ~1200 requests per minute. With enableRateLimit: True, ccxt auto-throttles. But with multiple bots on the same IP, you can still hit limits.\nTips:\nDon\u0026rsquo;t fetch data you don\u0026rsquo;t need Cache market info (load_markets() once at startup) Stagger API calls if running multiple bots Use 5-10 second intervals between scan cycles Complete Example: Fetch, Analyze, Trade 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import ccxt import pandas as pd import ta import time import os # Setup exchange = ccxt.binance({ \u0026#39;apiKey\u0026#39;: os.environ[\u0026#39;BINANCE_API_KEY\u0026#39;], \u0026#39;secret\u0026#39;: os.environ[\u0026#39;BINANCE_SECRET\u0026#39;], \u0026#39;options\u0026#39;: {\u0026#39;defaultType\u0026#39;: \u0026#39;future\u0026#39;, \u0026#39;adjustForTimeDifference\u0026#39;: True}, \u0026#39;enableRateLimit\u0026#39;: True, }) exchange.load_time_difference() exchange.set_leverage(3, \u0026#39;ETH/USDT\u0026#39;) # Fetch and analyze df = pd.DataFrame( exchange.fetch_ohlcv(\u0026#39;ETH/USDT\u0026#39;, \u0026#39;5m\u0026#39;, limit=50), columns=[\u0026#39;ts\u0026#39;, \u0026#39;open\u0026#39;, \u0026#39;high\u0026#39;, \u0026#39;low\u0026#39;, \u0026#39;close\u0026#39;, \u0026#39;volume\u0026#39;] ) df[\u0026#39;rsi\u0026#39;] = ta.momentum.RSIIndicator(df[\u0026#39;close\u0026#39;], window=14).rsi() last = df.iloc[-1] print(f\u0026#34;ETH Price: {last[\u0026#39;close\u0026#39;]}, RSI: {last[\u0026#39;rsi\u0026#39;]:.1f}\u0026#34;) # Check balance balance = exchange.fetch_balance() print(f\u0026#34;Available: {balance[\u0026#39;USDT\u0026#39;][\u0026#39;free\u0026#39;]} USDT\u0026#34;) # Place a test order (UNCOMMENT WHEN READY) # order = exchange.create_order(\u0026#39;ETH/USDT\u0026#39;, \u0026#39;market\u0026#39;, \u0026#39;buy\u0026#39;, 0.01) # print(f\u0026#34;Order filled at {order[\u0026#39;average\u0026#39;]}\u0026#34;) Further Reading Building a complete trading bot — Full strategy implementation Stop loss implementation — Exchange-side STOP_LIMIT Binance API gotchas — Edge cases and quirks ccxt makes Binance easy to connect to. Making money with it is the hard part.\n","permalink":"https://kimchibot.com/posts/ccxt-binance-futures-complete-guide/","summary":"Everything you need to know about using ccxt with Binance Futures in Python. Connection, orders, positions, stop losses, and all the gotchas.","title":"ccxt + Binance Futures: The Complete Python Guide"},{"content":"Who Am I? I\u0026rsquo;m a developer from South Korea who wanted to make money while sleeping.\nI can code. But building a profitable trading bot is a completely different beast — it\u0026rsquo;s not about clean code, it\u0026rsquo;s about markets, statistics, and not fooling yourself with pretty backtests.\nMy weapon of choice: Claude Code. An AI that doesn\u0026rsquo;t complain when you ask it to rewrite the same function 47 times.\nThe goal of this blog: Document everything so honestly that even a complete beginner could follow along and build their own bot. Every strategy, every failure, every line of reasoning — explained.\nThe Idea Was Simple Build a crypto trading bot. Let it trade on Binance Futures. Sit back. Profit.\nSpoiler: the coding was the easy part. Everything else was hard.\n6 Bots Built. 4 Killed. Over the past few months, I built 6 different trading bots with Claude Code. Here\u0026rsquo;s the scoreboard:\n1. Grid Bot - DEAD The classic \u0026ldquo;buy low, sell high at preset levels\u0026rdquo; strategy. Sounds great in theory.\nWhat killed it: Slippage ate the profits alive, and in a trending market? It just kept buying into the abyss. This wasn\u0026rsquo;t a bug — it was a structural flaw. Grid bots are a lie in trending markets.\n2. RSI Scalping Bot - SHELVED A quick-trade bot based on RSI signals. It worked\u0026hellip; okay.\nWhy I shelved it: The trend-following bot just performed better. Why keep the bronze medalist?\n3. Trend Following Bot v4.0 - ALIVE AND RUNNING This is the survivor. The one that made it through backtesting, dry runs, and is now trading real money on Binance Futures.\n5-minute candle body + Volume Ratio signals CHOP filter to avoid sideways markets Trailing stop on 5m candle close (not tick-by-tick — learned that the hard way) 3x leverage, compounding 80% of balance More on this beast in future posts.\n4. Market Maker Bot - DEAD (NOT ENOUGH CAPITAL) You need $100k+ to market-make effectively. I do not have $100k+. Next.\n5. Lead-Lag Bot - DEAD Tried to exploit price differences between correlated pairs. By the time I built it, the opportunity had evaporated. Crypto moves fast.\n6. Momentum Bot - DEAD Looked amazing in backtests. Suspiciously amazing.\nWhat killed it: Overfitting. The backtest was basically memorizing the past, not predicting the future. This taught me one of the most important lessons in quant trading.\nWhat I Learned (The Hard Way) A few principles that are now tattooed on my brain:\nWin rate doesn\u0026rsquo;t matter as much as risk-reward ratio. A 35% win rate with 1:1.5 risk-reward beats a 60% win rate with 1:0.5.\nIf your backtest looks too good, it\u0026rsquo;s lying to you. Always check for overfitting. Out-of-sample testing is not optional.\nThe gap between backtest and live trading is where dreams go to die. Slippage, latency, exchange quirks — they all add up.\nClaude Code is incredibly powerful, but it doesn\u0026rsquo;t know your strategy is stupid. It\u0026rsquo;ll build exactly what you ask for, beautifully. Even if what you asked for is garbage.\nWhat\u0026rsquo;s Coming Next This blog is the real, unfiltered journal of building trading bots with AI. I\u0026rsquo;ll cover:\nStrategy breakdowns — how each bot works, with code Backtest vs reality — the numbers, honestly The debugging nightmares — PID lockfiles, timezone bugs, exchange API quirks Live performance reports — wins, losses, everything No \u0026ldquo;3-minute bot\u0026rdquo; clickbait. No fake screenshots. Just the messy, frustrating, occasionally profitable truth.\nIf you\u0026rsquo;ve ever wondered whether AI can actually build you a working trading bot — stick around. The answer is yes, but with a lot of asterisks.\n","permalink":"https://kimchibot.com/posts/i-tortured-claude-code-into-building-me-a-trading-bot/","summary":"A developer from Korea built 6 crypto trading bots using Claude Code. 4 failed. Here\u0026rsquo;s the honest story — and a guide so you can build one too.","title":"I Tortured Claude Code Into Building Me a Trading Bot — Here's What Happened"},{"content":"What Is Overfitting? Your trading bot\u0026rsquo;s backtest shows +500% returns. You deploy it. It loses money immediately.\nOverfitting means your strategy learned the noise in historical data instead of the signal. It memorized the past instead of discovering patterns that repeat in the future.\nThis is the #1 reason trading bots fail in production.\nThe Overfitting Checklist Use this checklist every time you build or modify a strategy. If you answer \u0026ldquo;yes\u0026rdquo; to any of these, you might be overfitting.\nRed Flag 1: Too Many Parameters Question: Does your strategy have more than 5-7 tunable parameters?\nEvery parameter is a degree of freedom. More freedom = more room to fit noise.\nParameters Risk Level 1-3 Low — probably capturing real signal 4-7 Medium — be careful 8-12 High — likely overfitting 13+ Almost certainly overfitting My approach: My trend-following bot has 6 main parameters (SL%, trailing activation, trailing stop, body threshold, volume ratio, CHOP threshold). I optimize 2-3 at a time, never all at once.\nRed Flag 2: Optimization on Short Data Question: Did you optimize on less than 3 months of data?\nShort periods have specific market conditions. Parameters optimized on a 2-week rally will fail in the next sideways period.\nData Period Reliability 1-2 weeks Useless 1 month Dangerous 3 months Minimum 6-12 months Good Multiple years Best Red Flag 3: No Out-of-Sample Test Question: Have you tested on data the strategy has never seen?\nThis is the single most important test. Split your data:\n|-------- Training (70%) --------|----- Testing (30%) -----| Optimize here Validate here If performance drops significantly on the test set, you\u0026rsquo;re overfitting.\nMy experience:\nStrategy Training PnL Out-of-Sample PnL Verdict Momentum Bot +800U -200U Overfitting — killed it Trend Following +343U +245U Real edge — deployed FVG Bot +444U +548U (1yr) Real edge — deployed Red Flag 4: Spike vs Plateau Question: Does a 10% change in any parameter destroy performance?\nGood parameters sit on plateaus — nearby values work almost as well. Overfit parameters sit on spikes — any small change collapses returns.\nGOOD (Plateau): SL 1.5%: +200U | SL 2.0%: +250U | SL 2.5%: +230U BAD (Spike): SL 1.5%: -100U | SL 2.0%: +500U | SL 2.5%: -80U If your strategy only works with SL at exactly 2.0%, you don\u0026rsquo;t have an edge. You have a coincidence.\nRed Flag 5: Win Rate Too High Question: Is your backtest win rate above 65%?\nIn crypto, with typical trend-following or mean-reversion strategies, realistic win rates are:\nTrend following: 40-60% Mean reversion: 25-40% Scalping: 50-65% If your backtest shows 80%+ win rate, something is wrong. You\u0026rsquo;re probably:\nLooking at too short a period Not accounting for slippage and fees Using future data accidentally (look-ahead bias) Red Flag 6: No Losing Months Question: Does your backtest have zero losing months?\nReal strategies have drawdowns. Every strategy has market conditions where it underperforms. If your equity curve is a smooth line going up, it\u0026rsquo;s fiction.\nMy FVG bot\u0026rsquo;s quarterly results:\nQ2 2025: +271U ✓ Q3 2025: +38U ✓ (barely) Q4 2025: +273U ✓ Q1 2026: -36U ✗ (sideways market) One losing quarter out of four. That\u0026rsquo;s realistic. If all four were positive with smooth returns, I\u0026rsquo;d be suspicious.\nRed Flag 7: Complexity Without Justification Question: Can you explain WHY each rule exists?\nEvery condition in your strategy should have a logical reason:\nRule Why It Exists Body ≥ 0.7% Filter out noise/sideways candles Volume Ratio ≥ 1.5 Confirm strong move, not just random fluctuation CHOP \u0026lt; 50 Avoid choppy markets where trend-following fails SL at 2% Based on typical crypto noise level on 5m timeframe If you added a rule just because it improved the backtest but you can\u0026rsquo;t explain the logic, it\u0026rsquo;s probably overfitting.\nThe Prevention Protocol Step 1: Walk-Forward Analysis Instead of one backtest, do this:\nPeriod 1: Train on Jan-Mar → Test on Apr Period 2: Train on Feb-Apr → Test on May Period 3: Train on Mar-May → Test on Jun ... If performance is consistent across all test periods, the edge is real.\nStep 2: Multiple Market Conditions Test in:\nBull market (strong uptrend) Bear market (strong downtrend) Sideways (ranging/choppy) High volatility events (crashes, pumps) A real strategy works in at least 2-3 of these. An overfit strategy only works in the specific condition it was trained on.\nStep 3: Trimmed Returns Calculate PnL with the top 10% and bottom 10% of trades removed.\nIf your total PnL is +500U but your trimmed PnL is -50U, your returns depend on a few outlier trades. That\u0026rsquo;s not a robust strategy — it\u0026rsquo;s luck.\nStep 4: Trade Count Minimum Under 30 trades: Statistically meaningless 30-100 trades: Weak but indicative 100-500 trades: Reasonable confidence 500+ trades: Good statistical significance My trend-following bot validation: 1,800 trades. FVG bot OOS test: 1,506 trades. Large enough samples that the results mean something.\nStep 5: Keep It Simple \u0026ldquo;Everything should be made as simple as possible, but no simpler.\u0026rdquo; — Einstein\nThe best trading strategies are surprisingly simple. When I added complexity to my bots (rolling RSI, regime detection, multiple indicator crossovers), performance almost always got worse on out-of-sample data.\nThe strategies that survived:\nTrend following: body + volume + CHOP filter → 3 conditions FVG: gap detection + size filter + trend filter → 3 conditions Simple strategies are harder to overfit.\nThe Ultimate Test Ask yourself:\n\u0026ldquo;If I showed this strategy to someone with no knowledge of my training data, would they agree the logic makes sense?\u0026rdquo;\nIf the answer is yes, you probably have a real edge. If the answer is \u0026ldquo;well, the backtest shows\u0026hellip;\u0026rdquo; — you\u0026rsquo;re probably overfitting.\nThe market doesn\u0026rsquo;t care about your backtest. It only cares about whether your edge is real.\n","permalink":"https://kimchibot.com/posts/how-to-avoid-overfitting-in-trading-bots/","summary":"Overfitting is the #1 killer of trading bots. Here\u0026rsquo;s a practical checklist to detect and prevent it, based on real experience.","title":"How to Avoid Overfitting in Trading Bots: A Practical Checklist"},{"content":"The Promise Every crypto trading channel sells you the same dream: grid bots.\nThe pitch is seductive. You set price levels. The bot buys at each level going down, sells at each level going up. It\u0026rsquo;s like a money-printing machine that works 24/7.\nI built one with Claude Code. It took about 2 hours. Clean code, nice logging, proper error handling. A beautiful piece of engineering.\nIt was also completely useless.\nHow a Grid Bot Works Sell -------- $105 Sell -------- $104 Sell -------- $103 ^ current price $102 Buy -------- $101 Buy -------- $100 Buy -------- $99 Simple, right? Price bounces between levels, you collect the spread. In a ranging market, this prints money.\nThe keyword is \u0026ldquo;ranging.\u0026rdquo;\nWhat Actually Happens Scenario 1: The Market Goes Up Price blasts through all your sell levels. Now what? You sold everything at $103-105 while the price hits $120. You\u0026rsquo;re sitting on USDT watching the chart go vertical.\nGrid bots are structurally short in a bull market.\nScenario 2: The Market Goes Down Price crashes through all your buy levels. Now you\u0026rsquo;re holding bags at $99-101 while the price dumps to $80. Every grid level you bought is underwater.\nGrid bots are structurally long in a bear market.\nScenario 3: The Invisible Killer — Slippage Even in a \u0026ldquo;perfect\u0026rdquo; ranging market, slippage ate my profits alive. Each trade lost 0.05-0.1% to slippage. When your grid spread is 1%, losing 0.1% on both sides means 20% of your profit is gone before you even count fees.\nAnd in crypto, slippage gets worse exactly when you need it least — during volatile moves when everyone is trading.\nThe Math That Killed It I ran my grid bot for a week. Here\u0026rsquo;s the reality:\nRanging periods: Made small profits. Felt great. Trending periods: Lost everything the ranging periods made, plus more. Net result: Negative, after fees and slippage. The fundamental problem is that crypto trends more than it ranges. Grid bots need sideways action, but crypto gives you 20% moves in a day.\nThe Expert Principle I Wish I Knew Earlier \u0026ldquo;Grid bots fail in trending markets.\u0026rdquo;\nThis isn\u0026rsquo;t a bug. It\u0026rsquo;s a structural flaw. No amount of parameter tuning fixes it. You can adjust grid spacing, number of levels, range bounds — none of it matters when the market decides to pick a direction.\nWhat I Learned Strategies that work \u0026ldquo;everywhere\u0026rdquo; usually work nowhere. If it sounds too simple, it probably doesn\u0026rsquo;t account for the thing that will kill it.\nSlippage is not a footnote. In backtests, slippage is a parameter you set to 0.05% and forget. In live trading, it\u0026rsquo;s the difference between profit and loss.\nDon\u0026rsquo;t fight the market structure. Crypto trends. Build strategies that profit from trends, not strategies that pray for sideways.\nThis is why I moved to trend-following. More on that in the next post.\nKill count: 1 bot down, 5 more to test.\n","permalink":"https://kimchibot.com/posts/why-grid-bots-are-a-beautiful-lie/","summary":"Grid bots look perfect on paper. Buy low, sell high, automatically. Here\u0026rsquo;s why I killed mine after a week.","title":"Why Grid Bots Are a Beautiful Lie"},{"content":"The Problem With Tutorials Every \u0026ldquo;build a trading bot\u0026rdquo; tutorial gives you a single Python file with everything crammed in: connection, data fetching, signals, orders, and a while loop.\nThis works for learning. It doesn\u0026rsquo;t work for production.\nWhen your bot runs 24/7 with real money, you need:\nClean separation of concerns State persistence across restarts Error handling at every level Logging you can actually debug with The Architecture Here\u0026rsquo;s how I structure my trading bots:\nbot/ ├── bot_main.py # Main loop and orchestration ├── config.py # All parameters in one place ├── exchange_client.py # Exchange connection and API calls ├── signals.py # Entry/exit signal detection ├── position_manager.py # Position tracking and state ├── order_manager.py # Order placement and management ├── state.json # Persistent state file └── logs/ └── bot_2026-04-05.log Let me walk through each piece.\n1. Config: One Source of Truth 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # config.py # Exchange API_KEY = os.environ[\u0026#39;BINANCE_API_KEY\u0026#39;] SECRET = os.environ[\u0026#39;BINANCE_SECRET\u0026#39;] # Strategy parameters LEVERAGE = 3 TRADE_PCT = 0.80 # Use 80% of balance TOP_N = 8 # Number of coins to trade # Entry MIN_BODY_PCT = 0.007 # 0.7% candle body LONG_VR = 1.8 # Volume ratio for longs SHORT_VR = 1.5 # Volume ratio for shorts CHOP_THRESHOLD = 50 # Choppiness index # Exit SL_PCT = 0.02 # 2% stop loss TRAIL_ACTIVATE = 0.02 # 2% trailing activation TRAIL_STOP = 0.005 # 0.5% trailing stop SURGE_THRESHOLD = 0.04 # 4% surge detection # Timing SCAN_INTERVAL = 10 # seconds between checks COIN_ROTATE_HOURS = 3 # coin rotation interval Why this matters: When you want to change a parameter, you change it in one place. No hunting through 500 lines of code to find where 0.02 appears.\n2. Exchange Client: Wrap the API 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # exchange_client.py class ExchangeClient: def __init__(self): self.exchange = ccxt.binance({...}) self.exchange.load_time_difference() def get_candles(self, symbol, timeframe=\u0026#39;5m\u0026#39;, limit=100): \u0026#34;\u0026#34;\u0026#34;Fetch OHLCV with retry logic.\u0026#34;\u0026#34;\u0026#34; for attempt in range(3): try: return self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit) except ccxt.NetworkError: time.sleep(2 ** attempt) return None def get_balance(self): \u0026#34;\u0026#34;\u0026#34;Get available USDT balance.\u0026#34;\u0026#34;\u0026#34; try: bal = self.exchange.fetch_balance() return float(bal[\u0026#39;USDT\u0026#39;][\u0026#39;free\u0026#39;]) except Exception as e: log.error(f\u0026#34;Balance fetch failed: {e}\u0026#34;) return None def place_market_order(self, symbol, side, amount): \u0026#34;\u0026#34;\u0026#34;Place market order with error handling.\u0026#34;\u0026#34;\u0026#34; try: order = self.exchange.create_order( symbol, \u0026#39;market\u0026#39;, side, amount ) log.info(f\u0026#34;Order filled: {side} {amount} {symbol} @ {order[\u0026#39;average\u0026#39;]}\u0026#34;) return order except ccxt.InsufficientFunds: log.error(f\u0026#34;Insufficient funds for {symbol}\u0026#34;) return None except Exception as e: log.error(f\u0026#34;Order failed: {e}\u0026#34;) return None Why wrap it? Every API call gets retry logic and error handling automatically. Your strategy code stays clean.\n3. Signals: Pure Logic, No Side Effects 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # signals.py def check_entry(df, side=\u0026#39;long\u0026#39;): \u0026#34;\u0026#34;\u0026#34;Check entry conditions. Returns True/False. This function has NO side effects — no API calls, no state changes, no logging. Just math. \u0026#34;\u0026#34;\u0026#34; last = df.iloc[-1] body_pct = abs(last[\u0026#39;close\u0026#39;] - last[\u0026#39;open\u0026#39;]) / last[\u0026#39;open\u0026#39;] if side == \u0026#39;long\u0026#39;: return ( body_pct \u0026gt;= config.MIN_BODY_PCT and last[\u0026#39;close\u0026#39;] \u0026gt; last[\u0026#39;open\u0026#39;] and last[\u0026#39;vol_ratio\u0026#39;] \u0026gt;= config.LONG_VR and last[\u0026#39;chop\u0026#39;] \u0026lt; config.CHOP_THRESHOLD ) else: return ( body_pct \u0026gt;= config.MIN_BODY_PCT and last[\u0026#39;close\u0026#39;] \u0026lt; last[\u0026#39;open\u0026#39;] and last[\u0026#39;vol_ratio\u0026#39;] \u0026gt;= config.SHORT_VR and last[\u0026#39;chop\u0026#39;] \u0026lt; config.CHOP_THRESHOLD ) def check_trailing_exit(position, current_price): \u0026#34;\u0026#34;\u0026#34;Check if trailing stop should trigger.\u0026#34;\u0026#34;\u0026#34; if position.best_price is None: return False if position.side == \u0026#39;long\u0026#39;: trail_pct = (position.best_price - current_price) / position.best_price else: trail_pct = (current_price - position.best_price) / position.best_price return trail_pct \u0026gt;= config.TRAIL_STOP Why pure functions? They\u0026rsquo;re easy to test. You can unit test signals without connecting to an exchange. You can backtest by feeding them historical data.\n4. Position Manager: State That Survives Restarts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 # position_manager.py import json class PositionManager: def __init__(self, state_file=\u0026#39;state.json\u0026#39;): self.state_file = state_file self.positions = {} self.load() def load(self): \u0026#34;\u0026#34;\u0026#34;Load state from disk.\u0026#34;\u0026#34;\u0026#34; try: with open(self.state_file, \u0026#39;r\u0026#39;) as f: data = json.load(f) self.positions = data.get(\u0026#39;positions\u0026#39;, {}) except FileNotFoundError: self.positions = {} def save(self): \u0026#34;\u0026#34;\u0026#34;Save state to disk. Called after EVERY change.\u0026#34;\u0026#34;\u0026#34; with open(self.state_file, \u0026#39;w\u0026#39;) as f: json.dump({\u0026#39;positions\u0026#39;: self.positions}, f, indent=2) def add_position(self, symbol, side, entry_price, size, sl_order_id): \u0026#34;\u0026#34;\u0026#34;Record a new position.\u0026#34;\u0026#34;\u0026#34; self.positions[symbol] = { \u0026#39;side\u0026#39;: side, \u0026#39;entry_price\u0026#39;: entry_price, \u0026#39;size\u0026#39;: size, \u0026#39;sl_order_id\u0026#39;: sl_order_id, \u0026#39;best_price\u0026#39;: entry_price, \u0026#39;entry_time\u0026#39;: datetime.now().isoformat(), } self.save() # Immediately persist def remove_position(self, symbol): \u0026#34;\u0026#34;\u0026#34;Remove a closed position.\u0026#34;\u0026#34;\u0026#34; if symbol in self.positions: del self.positions[symbol] self.save() def update_best_price(self, symbol, price): \u0026#34;\u0026#34;\u0026#34;Update best price for trailing stop.\u0026#34;\u0026#34;\u0026#34; pos = self.positions.get(symbol) if pos: if pos[\u0026#39;side\u0026#39;] == \u0026#39;long\u0026#39; and price \u0026gt; pos[\u0026#39;best_price\u0026#39;]: pos[\u0026#39;best_price\u0026#39;] = price self.save() elif pos[\u0026#39;side\u0026#39;] == \u0026#39;short\u0026#39; and price \u0026lt; pos[\u0026#39;best_price\u0026#39;]: pos[\u0026#39;best_price\u0026#39;] = price self.save() Why save after every change? Because your bot will crash at the worst possible moment. If it crashes between opening a position and saving state, it forgets the position exists on restart.\n5. Main Loop: The Orchestrator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # bot_main.py def main(): # Initialize client = ExchangeClient() positions = PositionManager() acquire_pid_lock() log.info(\u0026#34;Bot started\u0026#34;) # Sync with exchange on startup sync_positions_with_exchange(client, positions) while True: try: for symbol in active_coins: # Check existing positions if symbol in positions.positions: handle_exit(client, positions, symbol) # Check for new entries elif can_open_new_position(positions): handle_entry(client, positions, symbol) # Rotate coins periodically if should_rotate_coins(): active_coins = select_best_coins(client) time.sleep(config.SCAN_INTERVAL) except KeyboardInterrupt: log.info(\u0026#34;Shutdown requested\u0026#34;) break except Exception as e: log.error(f\u0026#34;Main loop error: {e}\u0026#34;) time.sleep(30) if __name__ == \u0026#39;__main__\u0026#39;: main() 6. Logging: Your Black Box Recorder 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import logging log = logging.getLogger(\u0026#39;bot\u0026#39;) log.setLevel(logging.INFO) # File handler — one file per day handler = logging.FileHandler( f\u0026#39;logs/bot_{datetime.now().strftime(\u0026#34;%Y-%m-%d\u0026#34;)}.log\u0026#39; ) handler.setFormatter(logging.Formatter( \u0026#39;%(asctime)s [%(levelname)s] %(message)s\u0026#39; )) log.addHandler(handler) # Console handler console = logging.StreamHandler() console.setFormatter(logging.Formatter(\u0026#39;%(asctime)s %(message)s\u0026#39;)) log.addHandler(console) Log everything:\n2026-04-05 14:32:15 [INFO] Signal: LONG SIREN/USDT body=0.82% VR=2.1 CHOP=43 2026-04-05 14:32:16 [INFO] Order filled: buy 1000 SIREN/USDT @ 0.0523 2026-04-05 14:32:16 [INFO] SL placed at 0.0512 (2.0%) 2026-04-05 14:47:15 [INFO] Trail activated: SIREN/USDT best=0.0545 (+4.2%) 2026-04-05 14:52:15 [INFO] Trail exit: SIREN/USDT @ 0.0541 (+3.4%) When something goes wrong at 3 AM, these logs are your only evidence.\nThe Anti-Patterns Things I did wrong before arriving at this structure:\n1. Global State Everywhere 1 2 3 4 # BAD — who modified this? when? why? current_position = None best_price = 0 sl_order_id = None Use a PositionManager class instead. State changes are explicit and logged.\n2. API Calls Inside Signal Logic 1 2 3 4 5 # BAD — signals should be pure math def check_signal(): price = exchange.fetch_ticker(symbol)[\u0026#39;last\u0026#39;] # API call here! if price \u0026gt; threshold: exchange.create_order(...) # And an order here?! Fetch data → Calculate signals → Execute orders. Three separate steps.\n3. No Error Recovery 1 2 3 4 # BAD — one error kills the whole bot while True: df = exchange.fetch_ohlcv(symbol) # Network error = crash process(df) Wrap every external call in try/except. Log the error. Continue.\nStart Simple, Add Complexity You don\u0026rsquo;t need all of this on day one. Start with:\nA single file with the main loop Add config separation Add state persistence Add proper logging Split into modules Each step makes the bot more maintainable and debuggable. But don\u0026rsquo;t over-architect before you have a working strategy.\nGood architecture doesn\u0026rsquo;t make a bad strategy profitable. But bad architecture will definitely make a good strategy unprofitable.\n","permalink":"https://kimchibot.com/posts/python-trading-bot-architecture-for-beginners/","summary":"Most trading bot tutorials show you the strategy but not the structure. Here\u0026rsquo;s how to organize a production-ready trading bot in Python.","title":"Python Trading Bot Architecture: How to Structure Your Code"},{"content":"The Dopamine Hit I\u0026rsquo;ll never forget the moment. My momentum bot\u0026rsquo;s backtest came back with insane returns. Win rate through the roof. Drawdowns barely visible. The equity curve looked like a staircase going up.\nI was ready to quit my job.\nThen I tested it on data it hadn\u0026rsquo;t seen before.\nWhat Is Overfitting? Imagine you\u0026rsquo;re studying for an exam. Instead of understanding the material, you memorize every answer from past exams. You score 100% on practice tests.\nThen the real exam comes, and the questions are slightly different. You fail.\nThat\u0026rsquo;s overfitting. Your strategy isn\u0026rsquo;t learning patterns — it\u0026rsquo;s memorizing history.\nHow My Momentum Bot Fooled Me The bot used multiple indicators with very specific parameters:\nRSI with a custom period Moving average crossovers with precise windows Volume filters with exact thresholds Entry and exit conditions tuned to perfection Every parameter was optimized to maximize returns on my test data. The result looked incredible.\nThe problem? Those parameters were perfectly shaped to match the past. They had no predictive power for the future.\nThe Out-of-Sample Test Here\u0026rsquo;s what I should have done from the start:\nSplit your data. Train on 70%, test on 30% the strategy has never seen. Walk-forward analysis. Optimize on month 1-3, test on month 4. Optimize on month 2-4, test on month 5. Repeat. Multiple market conditions. Test in bull markets, bear markets, and sideways. If it only works in one, it\u0026rsquo;s not a strategy — it\u0026rsquo;s a coincidence. When I finally did this properly with my FVG bot later, I tested across 4 quarters over a full year:\nQuarter PnL Result 2025 Q2 +271U Profitable 2025 Q3 +38U Barely profitable 2025 Q4 +273U Profitable 2026 Q1 -36U Loss 3 out of 4 quarters profitable, with one weak quarter during a sideways market. That\u0026rsquo;s what a real strategy looks like — not perfect, but consistently positive.\nRed Flags Your Backtest Is Lying Win rate above 70% — In crypto, even great strategies win 30-40% of the time. A high win rate usually means you\u0026rsquo;re curve-fitting.\nNo losing months — Real strategies have drawdowns. If yours doesn\u0026rsquo;t, you\u0026rsquo;re overfitting.\nToo many parameters — Each parameter you add is another degree of freedom to fit noise. The best strategies are simple.\nSmooth equity curve — Real trading is messy. If your equity curve looks like a smooth line going up, be suspicious.\nHuge returns on short data — +500% in 2 weeks? That\u0026rsquo;s noise, not signal.\nThe Expert Principle \u0026ldquo;Total returns are far more influenced by risk-reward ratio than by win rate.\u0026rdquo;\nA strategy with 35% win rate and 1:1.5 risk-reward beats a strategy with 60% win rate and 1:0.5 risk-reward. Every time.\nStop chasing high win rates. Start thinking about how much you make when you win versus how much you lose when you lose.\nWhat I Do Now For every strategy I build:\nBacktest on historical data — Does it work at all? Out-of-sample test — Does it work on data it hasn\u0026rsquo;t seen? Dry run — Does it match the backtest in real-time (without real money)? Live with small size — Does it survive the real market? Each step kills about 80% of strategies. The ones that survive all four are worth running.\nMy momentum bot didn\u0026rsquo;t survive step 2. My trend-following bot survived all four. That\u0026rsquo;s the difference between a dream and a strategy.\nThe most expensive lesson in trading isn\u0026rsquo;t a bad trade — it\u0026rsquo;s trusting a good backtest.\n","permalink":"https://kimchibot.com/posts/the-backtest-looked-amazing-it-was-lying/","summary":"My momentum bot showed incredible returns in backtests. Then I learned about overfitting — the hard way.","title":"The Backtest Looked Amazing. It Was Lying."},{"content":"Mistake #1: Skipping the Backtest \u0026ldquo;My strategy makes sense logically, so it must work.\u0026rdquo;\nNo. Logic and profitability are completely different things. I\u0026rsquo;ve had strategies that made perfect sense — buy when RSI is oversold, sell when overbought — that lost money consistently.\nThe market doesn\u0026rsquo;t care about your logic. It cares about statistical edge.\nFix: Backtest every strategy on at least 3 months of data before risking a single dollar.\nMistake #2: Trusting the Backtest Too Much The opposite mistake. Your backtest shows +500%. You\u0026rsquo;re rich!\nNo. Your backtest probably has:\nZero slippage assumption Perfect fills at exact prices No exchange downtime No network latency Parameters optimized on the test data (overfitting) Fix: Assume your live performance will be 30-50% worse than your backtest. If it\u0026rsquo;s still profitable at 50% worse, you might have something.\nMistake #3: Using High Leverage \u0026ldquo;20x leverage means 20x profits!\u0026rdquo;\nIt also means 20x losses. A 5% move against you = 100% loss = liquidation.\nI\u0026rsquo;ve seen traders blow up $10,000 accounts in a single trade with high leverage. The account doesn\u0026rsquo;t slowly bleed — it evaporates.\nFix: Use 2-5x leverage maximum. My bots use 3x. Boring? Yes. Still solvent? Also yes.\nMistake #4: No Stop Loss \u0026ldquo;I\u0026rsquo;ll just watch it and exit manually if it goes bad.\u0026rdquo;\nYou won\u0026rsquo;t. You\u0026rsquo;ll freeze. You\u0026rsquo;ll hope. You\u0026rsquo;ll \u0026ldquo;give it a little more room.\u0026rdquo; Then you\u0026rsquo;ll watch your account drain while telling yourself \u0026ldquo;it\u0026rsquo;ll come back.\u0026rdquo;\nAnd even if you\u0026rsquo;re disciplined — you sleep. You shower. You have a life. The market doesn\u0026rsquo;t pause for your bathroom break.\nFix: Exchange-side stop losses. Placed automatically. Non-negotiable. Every single trade.\nMistake #5: Optimizing Win Rate \u0026ldquo;My bot wins 80% of the time!\u0026rdquo;\nHow much does it win? $2. How much does it lose? $10.\n80 wins × $2 = $160. 20 losses × $10 = $200. Net: -$40.\nYour 80% win rate bot is a money loser.\nFix: Focus on risk-reward ratio. A 35% win rate with 1:3 risk-reward beats a 70% win rate with 1:0.5 risk-reward. Every time.\nMistake #6: Running the Bot Once and Forgetting It \u0026ldquo;Set and forget!\u0026rdquo;\nMarkets change. A strategy that worked in a trending market will lose money in a sideways market. A strategy optimized for high volatility will underperform in calm periods.\nI\u0026rsquo;ve had periods where my bot printed money for 3 weeks, then gave it all back in week 4 because the market regime changed.\nFix: Monitor weekly at minimum. Compare live results against backtest expectations. If they diverge significantly, investigate.\nMistake #7: Not Handling Crashes Your bot will crash. When it does:\nOpen positions have no stop loss monitoring New signals are missed State is potentially corrupted I once had my bot crash at 3 AM with 4 open positions and no exchange-side stop losses. Woke up to a mess.\nFix:\nUse exchange-side stop losses (survive bot crashes) Save state to disk after every change Add crash recovery on startup (sync with exchange) Use PID lockfiles to prevent duplicate instances Mistake #8: Mixing Timezones My backtest was in UTC. My live bot was in KST (Korean time, UTC+9). I compared them directly.\nResult: the first 9 hours of every backtest day had zero entries. Weeks of analysis were invalid.\nFix: Pick one timezone. Standardize everything. Label every timestamp with the timezone. When comparing live vs backtest, verify they\u0026rsquo;re in the same timezone first.\nMistake #9: Trading Too Many (or Too Few) Coins Too few: Your bot sits idle because SOL doesn\u0026rsquo;t move enough on the 5-minute timeframe.\nToo many: Your capital is spread so thin that winning trades make $0.50.\nI started with 5 large-cap coins (zero trades). Then tried 20 coins (diluted returns). Ended at 8 coins with automatic rotation based on volatility.\nFix: Use automatic coin selection based on recent performance. Let data pick the coins, not your gut.\nMistake #10: Going Live Too Soon The path should be:\nStrategy idea → Backtest → Out-of-sample test → Dry run → Small live → Full live Most people do:\nStrategy idea → Small live → Full live → Cry I deployed my first bot (grid bot) live after a single backtest on 1 week of data. It lost money within hours.\nFix: Every stage exists for a reason. The dry run alone caught 5 bugs in my code that would have cost real money. The extra week of testing costs nothing. The bugs it catches could cost everything.\nThe Expensive Summary Mistake What It Cost Me What Fixed It No backtest ~$50 in bad trades Always backtest first Trusting backtest $200 in overfitted strategy Out-of-sample validation High leverage N/A (I was cautious) 3x max No stop loss Nearly $100 in one night Exchange-side STOP_LIMIT Win rate obsession Weeks of wasted optimization Focus on risk-reward Set and forget ~$150 in regime change Weekly monitoring No crash handling $80 in unmanaged positions State persistence + recovery Timezone mixing Weeks of invalid analysis Standardize to KST Wrong coins Weeks of zero trades Auto coin rotation Going live too soon ~$50 on grid bot Full testing pipeline Total cost of my mistakes: ~$630 and months of wasted time.\nThis blog exists so your number is lower than mine.\nThe tuition for trading bot school is expensive. This post is the discount version.\n","permalink":"https://kimchibot.com/posts/trading-bot-mistakes-beginners-make/","summary":"I\u0026rsquo;ve built 6 trading bots and made every mistake possible. Here are the 10 most expensive ones so you don\u0026rsquo;t have to repeat them.","title":"10 Trading Bot Mistakes Every Beginner Makes (I Made All of Them)"},{"content":"The Gap You build a bot. You backtest it. It shows beautiful numbers. You go live.\nThen reality hits.\nYour backtest says +500U. Your live bot shows +200U. Where did the other 300 go?\nI spent weeks hunting down every source of discrepancy between my backtest and live bot. Here\u0026rsquo;s the complete list of everything that was wrong.\n1. Slippage: The Silent Killer Backtest assumption: Orders fill at the exact price you want.\nReality: They don\u0026rsquo;t.\nWhen you place a market order, the price moves between your decision and execution. In crypto, this is typically 0.02-0.1%, but during volatile moments it can be much worse.\nHow I fixed it: I switched from market orders to limit orders for entries. The bot places a limit order, waits 5 seconds, and if it\u0026rsquo;s not filled, falls back to a market order. This alone saved ~0.05% per trade.\n2. Trailing Stop: Tick vs Candle Close This one cost me weeks to figure out.\nThe bug: My live bot checked trailing stops every 1 second using WebSocket ticks. My backtest checked them on 5-minute candle closes.\nThe effect: The live bot would trigger trailing stops on momentary price dips that recovered within the same candle. The backtest never saw these dips because it only looked at the close price.\nExample: Price drops 0.8% for 3 seconds, then recovers. Live bot: stopped out. Backtest: still in the trade, goes on to make +15U profit.\nHow I fixed it: Changed the live bot to only check trailing stops at 5-minute candle closes, matching the backtest exactly. This was the single biggest improvement in live-backtest consistency.\n3. Timezone Bugs The bug: My backtest used UTC. My live bot used local time (KST, UTC+9). I was comparing apples to oranges for the first two weeks.\nThe effect: The first 9 hours of every backtest period had zero entries because the time window was offset.\nHow I fixed it: Standardized everything to KST. Added KST_OFFSET = +9h to all timestamp labels. Now both systems speak the same language.\n4. Candle Resampling vs Native Data The bug: My backtest built 30-minute and 1-hour candles by resampling 5-minute data. My live bot fetched native 30m/1h candles from Binance.\nThe effect: Resampled RSI values were slightly different from native candle RSI values. This caused different entry/exit signals in edge cases.\nHow I fixed it: Switched the backtest to fetch native 30m/1h/4h candles directly from the exchange. RSI values now match exactly.\n5. The Best Price Problem The bug: My live bot tracked \u0026ldquo;best price\u0026rdquo; (highest for longs, lowest for shorts) using WebSocket extreme values. My backtest used 1-minute candle high/low.\nThe effect: WebSocket catches price spikes that don\u0026rsquo;t show up in 1-minute candles. This meant different trailing stop activation points.\nOne trade example: Live bot best_price = $0.1597 (WS spike). Backtest best_price = $0.1593 (1m high). Trail stop triggered at different times. PnL difference: 23U.\nHow I fixed it: Removed WebSocket entirely. Both live bot and backtest now use 1-minute candle extremes for best_price tracking.\nThe Result After fixing all these issues, I ran a comparison:\nBefore fixes: ~50% of trades matched between backtest and live.\nAfter fixes: 15 out of 18 trades matched (83%). The remaining 3 had minor SL/trailing timing differences of 0.06-0.4U.\nThe Verification Process Now I run this comparison regularly:\n1 python compare_live_bt.py \u0026#34;2026-03-10\u0026#34; \u0026#34;2026-03-12\u0026#34; This script:\nPulls live trade logs Runs the same period through the backtest Compares entry prices, exit reasons, and PnL Flags any discrepancies If your backtest doesn\u0026rsquo;t match your live bot, your backtest is fiction. Fix the gaps before you trust any optimization results.\nKey Takeaways Build a comparison tool early. Don\u0026rsquo;t wait until you\u0026rsquo;ve lost money to discover discrepancies.\nMatch everything exactly. Same data source, same timeframe, same price type (close vs tick), same timezone.\nThe live bot is always right. When there\u0026rsquo;s a discrepancy, the backtest is wrong, not the live bot. Reality doesn\u0026rsquo;t have bugs — your simulation does.\n0.1% matters. In trading, small systematic errors compound into large losses over thousands of trades.\nAn honest backtest that shows +200U is worth more than a fantasy backtest showing +2000U.\n","permalink":"https://kimchibot.com/posts/backtest-vs-reality-where-dreams-die/","summary":"My bot showed +500U in backtests. Live trading showed +200U. Here\u0026rsquo;s every gap I found and how I closed them.","title":"Backtest vs Reality: Where Dreams Go to Die"},{"content":"The Context I built 6 crypto trading bots using AI assistance. Most of that work was done with Claude Code, but I\u0026rsquo;ve also used ChatGPT (with and without Code Interpreter) for comparison.\nThis isn\u0026rsquo;t a general \u0026ldquo;which AI is better\u0026rdquo; post. This is specifically about building trading bots — writing Python, debugging exchange APIs, and iterating on complex financial logic.\nClaude Code: What It Does Well 1. Working With Your Actual Codebase Claude Code runs in your terminal. It reads your files, understands your project structure, and edits code in place. When I say \u0026ldquo;fix the trailing stop bug in bot_trendfollow.py line 342,\u0026rdquo; it opens the file, finds the line, understands the context, and makes a surgical edit.\nThis is a massive advantage for iterative development. You\u0026rsquo;re not copying and pasting code back and forth.\n2. Complex Multi-File Changes \u0026ldquo;Add separate long/short parameters across the bot and the backtest\u0026rdquo; — this touches 5+ files and 50+ locations. Claude Code navigates the project, finds all the relevant code, and makes consistent changes.\n3. Debugging With Context When my bot threw an error at 3 AM, I could paste the error and Claude Code would:\nRead the relevant source file Understand the full function context Trace the call chain Identify the root cause Not just pattern-matching on the error message — actually understanding the code.\n4. Iterative Refinement \u0026ldquo;Now add error handling to that function. Actually, also make it retry on network errors. And log each attempt.\u0026rdquo; — Claude Code applies each change to the existing code without losing context.\nClaude Code: What It Does Poorly 1. Strategy Design Claude Code will implement whatever strategy you describe. It will not tell you the strategy is bad. I asked it to build a grid bot — it built a perfect grid bot that was structurally doomed in trending markets.\nAI doesn\u0026rsquo;t have market intuition. You need to know what to build.\n2. Statistical Judgment \u0026ldquo;Is this backtest overfitting?\u0026rdquo; — Claude can list signs of overfitting, but it can\u0026rsquo;t look at your specific results and give you a confident assessment. It doesn\u0026rsquo;t have the experience of seeing hundreds of backtest results.\n3. Very Long Sessions After many iterations in a single conversation, context can drift. Variables from earlier discussions get mixed up. Starting a fresh conversation for new features often produces better results.\nChatGPT: What It Does Well 1. Explaining Concepts \u0026ldquo;Explain the Choppiness Index in simple terms\u0026rdquo; — ChatGPT is excellent at educational explanations. When I needed to understand a new indicator before implementing it, ChatGPT was my first stop.\n2. Quick Prototypes For throwaway scripts and quick calculations, ChatGPT with Code Interpreter is fast. \u0026ldquo;Calculate the risk-reward ratio for these parameters\u0026rdquo; — done in seconds with a visual chart.\n3. Research \u0026ldquo;What are common crypto trading strategies for 5-minute timeframes?\u0026rdquo; — ChatGPT provides a broader survey of ideas. Good for the brainstorming phase.\nChatGPT: What It Does Poorly 1. Working With Your Files ChatGPT doesn\u0026rsquo;t see your codebase. You have to paste code, which loses context. When your bot spans multiple files with shared state and config, this is painful.\n2. Consistency Across Changes \u0026ldquo;Add this feature to the bot\u0026rdquo; — ChatGPT gives you a code snippet. You paste it in. It doesn\u0026rsquo;t know about the other 500 lines in your file. Merge conflicts, inconsistent variable names, and subtle integration bugs are common.\n3. Exchange-Specific Details ChatGPT\u0026rsquo;s knowledge of Binance Futures API quirks is hit-or-miss. The algo order issue (STOP orders not being findable via normal endpoints) — ChatGPT didn\u0026rsquo;t know about this. Claude Code found it by reading the ccxt source.\nWhat I Actually Use Task Tool Why Writing bot code Claude Code Direct file access, context awareness Debugging Claude Code Can read actual error + source code Strategy research ChatGPT Broader knowledge base Concept explanation ChatGPT Better at teaching Backtest implementation Claude Code Multi-file consistency Quick calculations ChatGPT + Code Interpreter Visual output Refactoring Claude Code Understands full project 80% Claude Code, 20% ChatGPT — that\u0026rsquo;s my actual split.\nThe Shared Limitation Neither tool will:\nTell you if your strategy has real edge Warn you about overfitting Know the current market conditions Replace your judgment on risk AI is the builder. You are the architect.\nThe strategies that work in my portfolio weren\u0026rsquo;t designed by AI. They were designed by me, through reading, experimentation, and failure. AI just writes the code faster than I can.\nAdvice for Beginners Start with ChatGPT to learn concepts and build your first prototype Move to Claude Code when you have a project with multiple files that need to work together Never deploy AI-generated code without reading every line Test relentlessly — AI writes confident-looking code that sometimes has subtle bugs The AI doesn\u0026rsquo;t know your risk tolerance, your account size, or your sleep schedule. It builds what you ask for. Make sure you\u0026rsquo;re asking for the right thing.\nThe best AI for trading bots is whichever one you verify most carefully.\n","permalink":"https://kimchibot.com/posts/claude-code-vs-chatgpt-for-coding-trading-bots/","summary":"I\u0026rsquo;ve used both AI tools to build trading bots. Here\u0026rsquo;s what each is good at, what each fails at, and which one I actually use.","title":"Claude Code vs ChatGPT for Building Trading Bots: An Honest Comparison"},{"content":"The Survivor Out of 6 bots I built, this is the one that lived. The Trend Following Bot v4.0.\nIt\u0026rsquo;s not sexy. It doesn\u0026rsquo;t have a fancy name. It doesn\u0026rsquo;t promise 1000% returns. But it makes money — consistently, across different market conditions, with real capital.\nHere\u0026rsquo;s how it works.\nThe Core Idea Follow the trend. Don\u0026rsquo;t predict it.\nWhen a coin starts moving with strong momentum and volume, jump on. When the momentum fades, get out. Don\u0026rsquo;t try to catch tops or bottoms. Don\u0026rsquo;t try to be clever.\nMost beginner traders try to predict reversals. \u0026ldquo;It\u0026rsquo;s gone up too much, it must come down.\u0026rdquo; This is how you blow up. Markets can stay irrational longer than you can stay solvent.\nEntry Conditions The bot scans every 5 minutes. To enter a trade, ALL of these must be true:\n1. Candle Body Size ≥ 0.7% The current 5-minute candle must have a body (open-to-close) of at least 0.7%. This filters out noise and sideways chop.\n2. Volume Ratio (VR) Long: VR ≥ 1.8 Short: VR ≥ 1.5 Volume must be significantly above average. Big moves on low volume are traps.\n3. CHOP Index \u0026lt; 50 The Choppiness Index measures how \u0026ldquo;choppy\u0026rdquo; (sideways) the market is. Below 50 means trending. Above 50 means ranging.\nThis is the most important filter. It keeps the bot out of sideways markets where trend-following strategies get chopped up (pun intended).\n4. BTC RSI ≥ 35 (Longs Only) Don\u0026rsquo;t go long on altcoins when Bitcoin is in freefall. Simple but effective.\nExit Strategy Getting in is easy. Getting out is where the real money is made or lost.\nStop Loss: 2.0% (STOP_LIMIT Order) Placed as a limit order on the exchange, not monitored client-side No slippage from cascade liquidations If the limit order doesn\u0026rsquo;t fill, auto-converts to market order Trailing Stop: TA 2.0% / TS 0.5% Activates when profit reaches 2.0% Trails at 0.5% behind the best price Critical: Checked only at 5-minute candle closes, not tick-by-tick This prevents getting stopped out by momentary price dips Surge Detection When a trade moves +4% before the first 5-minute candle closes:\nPhase 1: Tight trailing stop (0.5%) on 1-minute candle closes Phase 2: Reverts to normal trailing This captures explosive moves without giving back too much profit.\nTime-Based Take Profit After 30 minutes, if profit ≥ 0.5%, switch to a tight 0.25% trailing stop. This prevents winners from becoming losers due to momentum exhaustion.\nCoin Selection The bot doesn\u0026rsquo;t trade the same coins forever. Every 3 hours, it automatically:\nFilters all Binance Futures pairs by volatility (avg candle body ≥ 0.4%, 24h range ≥ 12%) Backtests each passing coin over the last 6 hours Selects the top 8 by PnL This way, the bot always trades the coins that are actually moving right now.\nMoney Management Uses 80% of balance, divided equally among 8 coins 3x leverage (conservative for crypto) Compound growth — profits increase position sizes The Numbers This isn\u0026rsquo;t a get-rich-quick setup. Here\u0026rsquo;s what realistic performance looks like:\nWin rate: ~57% (not spectacular, and that\u0026rsquo;s fine) Risk-reward: Better than 1:1 on average Stop losses: ~2.6% of trades Average hold time: ~2.6 hours The key insight from an expert I respect:\n\u0026ldquo;Total returns are far more influenced by risk-reward ratio than by win rate. A 1:1.5 risk-reward with 30-40% win rate is enough.\u0026rdquo;\nWhat Makes It Different 1. Separate Long/Short Parameters Longs and shorts behave differently in crypto. Crashes are faster than rallies. The bot uses different VR thresholds, SL levels, and trailing parameters for each direction.\n2. No Prediction, Just Reaction The bot doesn\u0026rsquo;t try to predict where the market is going. It reacts to what\u0026rsquo;s already happening. This is boring but profitable.\n3. Ruthless Filtering Most signals are rejected. CHOP filter alone eliminates ~60% of potential entries. Better to miss a trade than take a bad one.\n4. Backtest-Live Parity Every parameter in the live bot has an exact match in the backtest. I verify regularly that live trades match backtest predictions within 0.5U. This took weeks of debugging but it\u0026rsquo;s the foundation of trust in the system.\nThe Ugly Parts It\u0026rsquo;s not all profits:\nSideways markets hurt. When crypto goes flat, the bot either doesn\u0026rsquo;t trade (good) or gets chopped (bad). SL cascades happen. Sometimes 3-4 stop losses in a row. Psychologically brutal, mathematically expected. Coin selection can fail. Hot coins in the last 6 hours aren\u0026rsquo;t always hot in the next 6. The key is accepting that ~40% of trades will lose, and the winners will more than cover the losses. If you can\u0026rsquo;t stomach that, automated trading isn\u0026rsquo;t for you.\nNext up: the FVG (Fair Value Gap) bot — a completely different approach that also works.\n","permalink":"https://kimchibot.com/posts/building-a-trend-following-bot-that-actually-works/","summary":"After killing 4 bots, I built one that survived backtesting, dry runs, and live trading. Here\u0026rsquo;s the full breakdown.","title":"Building a Trend Following Bot That Actually Works"},{"content":"Why Stop Loss Is Everything Your entry strategy doesn\u0026rsquo;t matter if your exit is broken.\nI spent weeks perfecting entry signals — candle patterns, volume ratios, RSI crossovers. Beautiful logic. Meanwhile, my stop loss was a simple market order that was silently destroying my returns through slippage.\nVersion 1: Client-Side Market Order (Bad) My first implementation:\nEvery 1 second: Check current price via WebSocket If loss \u0026gt; 2%: Place market sell order Problems:\nLatency: 200-500ms between price check and order execution. In a crash, price moves 0.5% in that window. Slippage: Market order during high volatility = terrible fill price. Cascade effect: Everyone\u0026rsquo;s bots trigger at the same level → massive sell pressure → worse slippage for everyone. Real example: SL set at -2.0%. Actual exit at -2.8%. That 0.8% difference on a $200 position with 3x leverage = $4.8 extra loss. Multiply by hundreds of trades.\nVersion 2: Exchange-Side STOP_LIMIT (Good) The fix was moving the stop loss to the exchange itself:\n1 2 3 4 5 6 7 8 exchange.create_order( symbol=symbol, type=\u0026#39;STOP\u0026#39;, side=\u0026#39;sell\u0026#39;, # for longs amount=position_size, price=limit_price, # limit price (where to fill) params={\u0026#39;stopPrice\u0026#39;: trigger_price} # trigger price ) Why this is better:\nThe exchange monitors the price, not your bot No network latency for the trigger Limit order means you control the fill price Your bot can crash and the SL still works The Gotchas Nobody Tells You 1. Binance Treats STOP Orders as \u0026ldquo;Algo Orders\u0026rdquo; This took hours to figure out. On Binance Futures, any order with stopPrice is a conditional/algo order. Normal fetch_order() and cancel_order() don\u0026rsquo;t work.\nYou need special API endpoints:\n1 2 3 4 5 6 7 8 # Check order status exchange.fapiPrivateGetAlgoOrder(...) # Cancel order exchange.fapiPrivateDeleteAlgoOrder(...) # List open orders exchange.fapiPrivateGetOpenAlgoOrders(...) If you\u0026rsquo;re using ccxt and wondering why your stop order \u0026ldquo;disappears\u0026rdquo; — this is why.\n2. STOP_LIMIT Can Fail to Fill The trigger fires, but the limit order sits unfilled because price blew right through it.\nMy solution: After the stop triggers, check if the limit order filled within a few seconds. If not, convert to a market order immediately.\n1 2 3 4 5 6 def check_sl_filled(order_id): order = exchange.fapiPrivateGetAlgoOrder(...) if order[\u0026#39;status\u0026#39;] == \u0026#39;TRIGGERED\u0026#39;: # Limit order is live but might not fill if not filled_within_timeout: close_at_market() # Emergency exit 3. Cancel Failures Are Real Sometimes cancel_order fails. Network timeout, exchange hiccup, whatever. If you just ignore it:\nYou exit your position via trailing stop The orphaned SL triggers later It opens a new position in the opposite direction You now have an unintended trade My fix: Keep the SL order ID. Before any exit, try to cancel the SL. If cancel fails, retry. If still fails, keep the ID and try again after the exit.\nThe Order of Operations Bug I had the exit sequence wrong for months:\nWrong order:\nCancel SL order (takes 0.5-1 second) Place market close order → Price moves against you during step 1 Correct order:\nPlace market close order (exit first!) Cancel SL order (it\u0026rsquo;s fine if it triggers on a closed position) Handle any ReduceOnly rejection gracefully The SL is a conditional order — it only triggers at a specific price. Your market close is immediate. Always close first, clean up later.\nbest_price vs Current Price Another subtle bug: I was using the same price variable for two different things.\nbest_price: The most favorable price reached (for trailing stop calculation) current_price: The actual current price (for exit decisions) When a 1-minute candle has a long wick, the extreme value and the close are very different. Mixing them up meant my trailing stops weren\u0026rsquo;t triggering when they should have.\nFix: Separate cp_best (extreme value, updates best_price) and cp (current close price, used for exit checks).\nWhat I\u0026rsquo;d Tell My Past Self Put the SL on the exchange from day 1. Client-side monitoring is unreliable. Test the failure modes. What happens when cancel fails? When the limit doesn\u0026rsquo;t fill? When your bot crashes? The exit order matters. Close first, then clean up. Separate your price variables. One price for tracking, another for decisions. The stop loss is the most boring part of a trading bot. It\u0026rsquo;s also the part that will cost you the most money if you get it wrong.\nA good stop loss won\u0026rsquo;t make you money. A bad stop loss will definitely lose you money.\n","permalink":"https://kimchibot.com/posts/stop-loss-the-most-important-feature-you-will-get-wrong/","summary":"I went from market order SL to exchange-side STOP_LIMIT and it changed everything. Here\u0026rsquo;s every mistake along the way.","title":"Stop Loss: The Most Important Feature You'll Get Wrong"},{"content":"The Problem With Fixed Coin Lists When I started, I hardcoded 5 coins: SOL, XRP, DOGE, AVAX, SUI.\nBig names. High volume. Safe choices.\nZero trades.\nWhy? These large-cap coins barely move on the 5-minute timeframe. My entry condition requires a candle body ≥ 0.7%. Blue chips like SOL average 0.3% per candle. They\u0026rsquo;re too stable for a momentum strategy.\nMeanwhile, smaller altcoins like SIREN, LYN, and PIPPIN were printing money — 0.5-1.3% average candle bodies with 100%+ weekly ranges.\nI was watching the wrong market.\nThe Solution: Automatic Coin Rotation Every 3 hours, my bot runs this process:\nStep 1: Volatility Filter Fetch 7 days of 5-minute OHLCV data for all Binance Futures pairs. Keep only coins that meet:\nAverage candle body ≥ 0.4% — Enough movement to trigger entries 24h price range ≥ 12% — Coin is actually moving, not just twitching This immediately eliminates ~80% of pairs. BTC, ETH, SOL — all gone. They\u0026rsquo;re great for holding, terrible for scalping.\nStep 2: Exclude Problematic Pairs Some pairs look good but will break your bot:\nTradFi tokens (TSLA, NVDA, GOOGL) — Require special agreement signing Commodity tokens (XAU, XAG) — Different trading rules Index tokens (BTCDOM, DEFI) — Low liquidity Pre-market tokens — Can halt unexpectedly I auto-exclude anything where underlyingType != 'COIN'. This filters out ~24 problematic pairs.\nStep 3: Backtest Ranking For each surviving coin, run a 6-hour backtest using the exact same strategy the live bot uses. Rank by PnL.\nTake the top 8.\nWhy 8 Coins? I tested different numbers:\nTOP_N Total PnL Avg Return Problem 3 +109U 73% Too concentrated, scan failure risk 5 +155U 62% Slightly better but still risky 8 +209U 52% Sweet spot 10 +238U 48% Most days can\u0026rsquo;t find 10 qualifying coins 14 +201U 32% Diluted with mediocre performers 8 is the sweet spot — enough diversification to survive bad picks, few enough to concentrate on the best opportunities.\nThe Results Before (fixed 5 large-cap coins):\nPeriod PnL Trades 7 days +476U 150 After (auto-rotation with volatility filter):\nPeriod PnL Trades 7 days +2,265U 194 4.7x improvement just by trading the right coins.\nThe Gotchas 1. Hot Coins Cool Off A coin that\u0026rsquo;s pumping for the last 6 hours might be exhausted for the next 6. The 6-hour backtest window is a compromise — long enough to identify real movers, short enough to stay current.\n2. New Listings Are Dangerous Freshly listed coins have extreme volatility but also extreme spreads and erratic behavior. The volatility filter catches them, but the backtest period is too short to be reliable.\n3. Don\u0026rsquo;t Fight the Filter I\u0026rsquo;ve been tempted to manually add coins I \u0026ldquo;feel good about.\u0026rdquo; Every time I did, they underperformed the auto-selected ones.\nTrust the data over your gut.\nKey Takeaway Your entry strategy is only as good as the coins you apply it to. A perfect trend-following setup on a coin that doesn\u0026rsquo;t trend is worthless.\nLet the bot pick its own coins. It\u0026rsquo;s better at it than you are.\nThe best trade is sometimes the one you didn\u0026rsquo;t take — on a coin you shouldn\u0026rsquo;t have been watching.\n","permalink":"https://kimchibot.com/posts/how-i-pick-coins-for-my-bot-to-trade/","summary":"My bot doesn\u0026rsquo;t trade the same coins forever. Every 3 hours, it picks the best ones automatically. Here\u0026rsquo;s the algorithm.","title":"How I Pick Coins for My Bot to Trade (Automatically)"},{"content":"The Obvious Choice Every trading bot tutorial tells you the same thing: use WebSocket for real-time prices.\nAnd it makes sense. WebSocket gives you price updates every second. REST API requires you to poll. Real-time is always better than polling, right?\nI built a full WebSocketManager class. ~150 lines of code. Async connections, automatic reconnection, price buffering, extreme value tracking.\nThen I deleted all of it.\nWhat WebSocket Was Doing My WebSocket connection served three purposes:\nExtreme value tracking — Catching the highest/lowest price every second for trailing stop calculations Real-time price — Current price for exit decisions Candle close detection — Knowing exactly when a 5-minute candle closed All three seemed critical. None of them were.\nWhy I Didn\u0026rsquo;t Need It 1. Extreme Values: 1-Minute Candles Are Better My backtest uses 1-minute candle high/low for extreme values. My live bot was using WebSocket tick-by-tick extremes.\nThey gave different numbers.\nWebSocket catches micro-spikes that don\u0026rsquo;t even show up on 1-minute candles. This meant my live bot\u0026rsquo;s trailing stop activated at different points than the backtest predicted.\nThe fix wasn\u0026rsquo;t to make the backtest use ticks — it was to make the live bot use 1-minute candles too. One REST call every minute: fetch_ohlcv(symbol, '1m', limit=6).\nSimpler. More reliable. Matches the backtest exactly.\n2. Real-Time Price: STOP_LIMIT Makes It Unnecessary My original bot checked the price every second to detect stop loss hits. But after switching to exchange-side STOP_LIMIT orders, the exchange monitors the price for me.\nThe SL triggers server-side. My bot doesn\u0026rsquo;t need to watch the price anymore.\n3. Candle Close Detection: The Clock Works Fine I had a clever system that detected when a new candle arrived via WebSocket.\nTurns out, datetime.now().minute % 5 == 0 works just as well. 5-minute candles close every 5 minutes. The clock is right there.\nWhat I Replaced It With 1 2 3 4 5 6 7 8 9 10 SCAN_INTERVAL = 10 # seconds while True: for symbol in active_coins: check_and_exit(symbol) if is_new_5m_candle(): scan_for_entries() time.sleep(SCAN_INTERVAL) 10-second REST polling. That\u0026rsquo;s it.\nThe Benefits of Deletion Fewer Failure Modes WebSocket connections drop. They need reconnection logic. They have authentication timeouts. They consume memory with buffered data.\nREST polling either works or raises an exception. Simple.\nEasier Debugging With WebSocket, I was debugging async race conditions, stale data in buffers, and connection state management.\nWith REST, the data is always fresh — you asked for it just now.\nLower Complexity 150 lines deleted. Three imports removed (websockets, asyncio, deque). One fewer class to maintain.\nThe best code is code that doesn\u0026rsquo;t exist.\nWhen You DO Need WebSocket WebSocket is genuinely necessary when:\nYou\u0026rsquo;re market-making and need sub-100ms price updates You\u0026rsquo;re doing high-frequency trading You need order book depth in real-time Your strategy depends on tick-by-tick price action My bot trades on 5-minute candles. Checking the price every 10 seconds is more than enough. I was over-engineering a solution for a problem I didn\u0026rsquo;t have.\nThe Lesson Don\u0026rsquo;t add complexity because it seems professional. Add it because you need it.\nI built WebSocket because \u0026ldquo;real trading bots use WebSocket.\u0026rdquo; I deleted it because my actual strategy didn\u0026rsquo;t benefit from it. The bot got simpler, more reliable, and easier to match with backtests.\nSometimes the best engineering decision is to delete the thing you spent a week building.\nLines of code deleted: 150. Bugs fixed by deletion: at least 3. Regrets: 0.\n","permalink":"https://kimchibot.com/posts/websocket-vs-rest-api-i-deleted-150-lines/","summary":"I spent weeks building a WebSocket price feed. Then I realized I didn\u0026rsquo;t need it at all.","title":"WebSocket vs REST API: I Deleted 150 Lines and Nothing Broke"},{"content":"What Is a Fair Value Gap? A Fair Value Gap (FVG) is a price imbalance visible on the chart. It happens when a candle moves so aggressively that it leaves a \u0026ldquo;gap\u0026rdquo; between the candle before and after it.\nThink of it like a rubber band being stretched. The price moved too fast, and it wants to come back to fill that gap.\nThe Three-Candle Pattern Bullish FVG: C1: Normal candle C2: BIG bullish candle (the gap creator) C3: Normal candle — its LOW is above C1\u0026#39;s HIGH The gap = space between C1\u0026#39;s high and C3\u0026#39;s low Bearish FVG: C1: Normal candle C2: BIG bearish candle (the gap creator) C3: Normal candle — its HIGH is below C1\u0026#39;s LOW The gap = space between C1\u0026#39;s low and C3\u0026#39;s high When price returns to this gap zone, it often bounces. That\u0026rsquo;s the trade.\nThe Bot Logic Detect FVG on 5-minute candles Place limit order at the FVG edge (entry price) Stop loss at the opposite edge of the FVG Take profit at 3x the risk (RR 3.0) Simple. Clean. No indicators needed — just price action.\nThe Filters That Matter Not every FVG is worth trading. After extensive testing, these filters survived:\nGap Size: 1.5% - 4.0% Too small (\u0026lt; 1.5%): Noise. Fees eat the profit. Too large (\u0026gt; 4.0%): Usually panic moves that don\u0026rsquo;t revert. Candle Body ≥ 2.0% The C2 candle (gap creator) needs a substantial body. Small-bodied candles with long wicks create fake FVGs.\nC2 Body Overlap The gap must overlap with C2\u0026rsquo;s body (open-to-close range). Gaps that only exist in the wick zone are unreliable.\nMA20 Trend Filter Price above 1h MA20 → Only take longs Price below 1h MA20 → Only take shorts Don\u0026rsquo;t fight the trend, even with mean-reversion setups.\nSL Ratio \u0026lt; 65% If more than 65% of historical trades on a coin hit stop loss, skip that coin. Some coins just don\u0026rsquo;t respect FVGs.\nWhat I Tested and Rejected This is equally important — what looked promising but failed:\nFilter Result Verdict Bollinger Band width \u0026lt; 2% No edge over 2 weeks Rejected Entry delay (wait 1-3 candles) PnL decreased with each candle of delay Rejected Large C2 body cap (\u0026gt; 3%) Filtered out profitable trades too Rejected CE (50%) entry Worse than edge entry at all RR levels Rejected Every one of these \u0026ldquo;made sense\u0026rdquo; logically. Bollinger Bands for choppy markets, delayed entry for confirmation — they all sound reasonable.\nData said otherwise. This is why you test everything.\nOut-of-Sample Validation Here\u0026rsquo;s where most strategies die. I tested across 4 quarters, 10 coins:\nQuarter Trades Win Rate PnL 2025 Q2 ~380 38% +271U 2025 Q3 ~350 35% +38U 2025 Q4 ~400 40% +273U 2026 Q1a ~300 32% -36U 3 out of 4 quarters profitable. The losing quarter was a sideways market — a known weakness.\nTotal: +548U over 1 year on data the strategy never trained on.\nThis isn\u0026rsquo;t overfitting. This is edge.\nLive Performance After the out-of-sample validation, I deployed it live:\n24h PnL-based coin scanning every 6 hours 10 coins running simultaneously $200 per trade, 3x leverage Real slippage, real fees, real market conditions The live-backtest match rate after fixing all the bugs: 100% entry price match, minor SL timing differences only.\nFVG vs Trend Following I now run both bots simultaneously. They complement each other:\nAspect Trend Following FVG Market type Trending Any (with trend filter) Entry style Breakout Mean reversion Hold time ~2.6 hours ~1-4 hours Win rate ~57% ~33% Risk-reward ~1:1.2 1:3 Different strategies, different edges. When one struggles, the other often thrives.\nThe best portfolio isn\u0026rsquo;t one strategy optimized to death — it\u0026rsquo;s multiple strategies that disagree with each other.\n","permalink":"https://kimchibot.com/posts/fair-value-gaps-the-strategy-that-changed-everything/","summary":"After trend-following, I built a completely different bot based on Fair Value Gaps. It passed a full year of out-of-sample testing.","title":"Fair Value Gaps: The Strategy That Changed Everything"},{"content":"The Symptom I noticed something weird. My backtest showed entries throughout the day, but the first batch of trades always started around 9 AM KST.\nFor the first 9 hours? Nothing. Zero entries. Every single day.\nMy live bot was trading fine during those hours. But my backtest was blind to them.\nThe Hunt I checked everything:\nEntry conditions? Working. Data feed? Complete. Candle timestamps? Looked correct. RSI warmup? Sufficient. Everything looked fine individually. The bug was in the interaction between two correct-looking pieces of code.\nThe Bug 1 2 3 4 5 6 7 8 # What I wrote start_dt = datetime(2026, 3, 10, 0, 0) # \u0026#34;March 10, midnight\u0026#34; # What I meant # March 10, 00:00 KST # What the exchange interpreted # March 10, 00:00 UTC = March 10, 09:00 KST I was passing a \u0026ldquo;naive\u0026rdquo; datetime (no timezone info) to an API that expected UTC. My local time is KST (UTC+9).\nSo when I said \u0026ldquo;start at midnight,\u0026rdquo; the exchange heard \u0026ldquo;start at midnight UTC,\u0026rdquo; which is 9 AM in Korea.\nNine hours of trades, invisible.\nWhy It Was Hard to Find The timestamps in my logs looked correct because I was formatting them in local time. The data was correct — just starting 9 hours late. And since I usually analyzed data from \u0026ldquo;yesterday\u0026rdquo; (a full day), the missing morning hours weren\u0026rsquo;t obvious.\nIt only became visible when I compared live trades against the backtest hour by hour.\nThe Fix 1 2 3 # Convert local time to UTC before API calls KST_OFFSET = 9 # hours start_dt_utc = start_dt - timedelta(hours=KST_OFFSET) And for display, always convert back to KST:\n1 ts_label = utc_time + timedelta(hours=KST_OFFSET) One rule: all internal times in UTC, all display times in KST. No exceptions.\nThe Deeper Problem This bug existed because I was sloppy about timezones from the start. I mixed:\ndatetime.now() (local KST) in the live bot UTC timestamps from the exchange API Naive datetimes in the backtest Every combination of two worked fine. All three together created a 9-hour ghost.\nTimezone Rules for Trading Bots After this disaster, I follow these rules:\nPick one internal timezone and stick with it. UTC is standard, but whatever you pick, be consistent.\nNever use naive datetimes. Always attach timezone info, or at minimum, document which timezone every variable uses.\nLabel your outputs. Every timestamp in logs should say (KST) or (UTC). Future you will thank present you.\nTest across timezone boundaries. If your backtest starts at midnight, does it start at YOUR midnight or the exchange\u0026rsquo;s midnight?\nCompare live vs backtest hourly. Daily totals can mask timezone offsets that cancel out.\nThe Cost This wasn\u0026rsquo;t just a code quality issue. Those 9 missing hours in the backtest meant:\nOptimization results were wrong (trained on partial data) Strategy comparisons were skewed I couldn\u0026rsquo;t verify live trades against backtest for morning sessions A one-line bug that invalidated weeks of analysis.\nThe scariest bugs aren\u0026rsquo;t the ones that crash your program. They\u0026rsquo;re the ones that silently give you wrong answers.\n","permalink":"https://kimchibot.com/posts/the-timezone-bug-that-cost-me-9-hours-of-trades/","summary":"My bot was missing every trade for the first 9 hours of each day. The cause? Mixing UTC and KST in one line of code.","title":"The Timezone Bug That Cost Me 9 Hours of Trades"},{"content":"What Claude Code Actually Is Claude Code is an AI coding assistant that runs in your terminal. You describe what you want, and it writes the code. It can read your files, edit them, run commands, and iterate based on errors.\nIt\u0026rsquo;s genuinely powerful. I built 6 trading bots with it, and I\u0026rsquo;m not the kind of developer who builds trading bots.\nBut it has limits. Understanding those limits is the difference between building something that works and building something that looks like it works.\nWhat Claude Code Is Great At Writing Boilerplate Exchange API connections, order placement, data fetching — Claude handles these perfectly. The ccxt library setup, Binance Futures authentication, OHLCV data fetching: all standard patterns that Claude knows well.\n1 2 3 4 5 6 7 8 9 10 11 12 13 # Tell Claude: \u0026#34;Connect to Binance Futures and fetch 5m candles\u0026#34; # You\u0026#39;ll get something like: import ccxt exchange = ccxt.binance({ \u0026#39;apiKey\u0026#39;: API_KEY, \u0026#39;secret\u0026#39;: API_SECRET, \u0026#39;options\u0026#39;: {\u0026#39;defaultType\u0026#39;: \u0026#39;future\u0026#39;}, \u0026#39;enableRateLimit\u0026#39;: True, }) ohlcv = exchange.fetch_ohlcv(\u0026#39;BTC/USDT\u0026#39;, \u0026#39;5m\u0026#39;, limit=100) This code works. First try. Every time.\nImplementing Known Algorithms RSI calculation, moving averages, Bollinger Bands, CHOP index — Claude knows these. If you ask for an RSI calculation, you\u0026rsquo;ll get a correct implementation.\nDebugging This is where Claude really shines. Paste an error, describe the context, and it\u0026rsquo;ll find the bug faster than you can read the stack trace. My timezone bug? Claude found it in seconds once I described the symptom.\nRefactoring \u0026ldquo;Make this function handle both long and short positions\u0026rdquo; — Claude excels at structural changes. It understands code patterns and can transform them cleanly.\nWhat Claude Code Is Terrible At Trading Strategy Design Claude will build whatever strategy you describe. It will not tell you the strategy is bad.\nI asked Claude to build a grid bot. It built a beautiful grid bot. It worked flawlessly — the code was perfect. The strategy was structurally broken in trending markets.\nClaude didn\u0026rsquo;t warn me. It\u0026rsquo;s not a quant researcher. It\u0026rsquo;s a code generator.\nYou need to know what to build. Claude knows how to build it.\nBacktest Interpretation Claude can tell you the numbers. It cannot tell you if those numbers mean anything.\n\u0026ldquo;Your win rate is 85%!\u0026rdquo; — Claude will report this proudly. It won\u0026rsquo;t mention that an 85% win rate with a 1:0.3 risk-reward ratio is a losing strategy.\nYou need to understand trading statistics. Claude just runs the calculations.\nMarket Intuition \u0026ldquo;Should I use a 14-period or 21-period RSI?\u0026rdquo; Claude will give you a reasonable-sounding answer. It has no idea which one works better in crypto specifically, in the current market regime, for your timeframe.\nTest everything. Trust data, not AI opinions.\nHow to Get the Best Results 1. Be Specific About Requirements Bad prompt:\n\u0026ldquo;Build me a trading bot\u0026rdquo;\nGood prompt:\n\u0026ldquo;Build a trend-following bot for Binance USDT-M Futures. Entry: 5m candle body ≥ 0.7% AND volume ratio ≥ 1.8 AND CHOP index \u0026lt; 50. Exit: 2% trailing stop activated at 2% profit, checked on 5m candle close only. Use ccxt library.\u0026rdquo;\nThe more specific you are, the less Claude needs to guess. Guesses are where bugs hide.\n2. Build Incrementally Don\u0026rsquo;t ask Claude to build the entire bot at once. Build in layers:\nExchange connection + data fetching Signal detection (entries) Order placement Exit logic (SL, trailing, TP) Position management Logging and monitoring Test each layer before adding the next. This way, when something breaks, you know which layer caused it.\n3. Always Read the Code Claude writes correct-looking code that sometimes has subtle bugs. The stop loss might check \u0026gt; instead of \u0026gt;=. The timestamp might be off by one candle. The order type might be wrong for your exchange.\nRead every line. Understand every line. If you can\u0026rsquo;t explain what a line does, you shouldn\u0026rsquo;t deploy it with real money.\n4. Build the Backtest First Before building the live bot, build the backtest. This gives you:\nA reference implementation to compare against Confidence that the strategy works (or doesn\u0026rsquo;t) A testing framework for parameter changes Then build the live bot to match the backtest exactly. This is hard — I spent weeks aligning them — but it\u0026rsquo;s essential.\n5. Ask Claude to Explain, Not Just Build \u0026ldquo;Explain how this trailing stop logic works step by step\u0026rdquo;\n\u0026ldquo;What edge cases could break this position sizing code?\u0026rdquo;\n\u0026ldquo;Walk me through what happens when the exchange returns an error here\u0026rdquo;\nClaude\u0026rsquo;s explanations are often better than its code. Use them to build your understanding.\nThe Process That Works 1. Research strategy (YOU, not Claude) 2. Define exact rules on paper (YOU) 3. Build backtest (Claude + YOU reviewing) 4. Validate with out-of-sample data (YOU analyzing) 5. Build live bot matching backtest (Claude) 6. Dry run comparison (YOU monitoring) 7. Live deploy with small capital (YOU deciding) Steps 3 and 5 are where Claude does the heavy lifting. Steps 1, 2, 4, 6, and 7 are where you do the heavy lifting.\nAI is the tool. You are the engineer.\nClaude Code can build you a trading bot in a day. Whether that bot makes money depends entirely on what you told it to build.\n","permalink":"https://kimchibot.com/posts/how-to-use-claude-code-to-build-a-trading-bot/","summary":"A practical guide to building trading bots with AI. What Claude Code is great at, what it\u0026rsquo;s terrible at, and how to get the best results.","title":"How to Use Claude Code to Build a Trading Bot (Honestly)"},{"content":"The Win Rate Trap New traders — and new bot builders — obsess over win rate.\n\u0026ldquo;My bot wins 70% of the time!\u0026rdquo;\nCool. How much does it win? How much does it lose?\nIf you win $1 on 70 trades and lose $3 on 30 trades:\nWins: 70 × $1 = $70 Losses: 30 × $3 = $90 Net: -$20 Your 70% win rate bot is a money incinerator.\nThe Math That Actually Matters Expected value = (Win Rate × Avg Win) - (Loss Rate × Avg Loss)\nLet\u0026rsquo;s compare two bots:\nBot A: \u0026ldquo;High Win Rate\u0026rdquo; Win rate: 70% Average win: $5 Average loss: $15 RR ratio: 1:0.33 Expected per trade: (0.7 × $5) - (0.3 × $15) = $3.50 - $4.50 = -$1.00\nBot B: \u0026ldquo;Low Win Rate\u0026rdquo; Win rate: 35% Average win: $15 Average loss: $5 RR ratio: 1:3 Expected per trade: (0.35 × $15) - (0.65 × $5) = $5.25 - $3.25 = +$2.00\nBot B wins 35% of the time and makes money. Bot A wins 70% of the time and loses money.\nHow This Changed My Bots Trend Following Bot Win rate: ~57% RR ratio: ~1:1.2 Edge: moderate win rate + slightly positive RR = consistent profits FVG Bot Win rate: ~33% RR ratio: 1:3 Edge: low win rate + high RR = large profits when it hits The FVG bot loses 2 out of every 3 trades. It\u0026rsquo;s still profitable because winners are 3x the size of losers.\nCan you handle losing 67% of the time? Most people can\u0026rsquo;t. That\u0026rsquo;s why most people don\u0026rsquo;t make money trading.\nThe Psychological Problem Here\u0026rsquo;s why this is harder than it sounds:\nImagine 10 trades:\nLoss, Loss, Loss, Win (+$15), Loss, Loss, Win (+$15), Loss, Loss, Loss You just experienced 8 losses and 2 wins. You\u0026rsquo;re up $30 - $40 = -$10 after 10 trades.\nYour brain screams: \u0026ldquo;THE BOT IS BROKEN. TURN IT OFF.\u0026rdquo;\nBut over 100 trades:\n35 wins × $15 = $525 65 losses × $5 = $325 Net: +$200 Small samples lie. This is why you need enough trades for the edge to manifest, and enough discipline to not pull the plug during drawdowns.\nPractical Guidelines Win Rate Minimum RR to Break Even Comfortable RR 30% 1:2.33 1:3+ 40% 1:1.50 1:2+ 50% 1:1.00 1:1.5+ 60% 1:0.67 1:1+ 70% 1:0.43 1:0.75+ My rule: never deploy a strategy with RR below 1:1.\nIf you can\u0026rsquo;t get positive RR, your entry isn\u0026rsquo;t good enough or your stop loss is too tight. Fix the strategy, don\u0026rsquo;t lower the bar.\nHow I Measure It I don\u0026rsquo;t just look at total PnL. I look at two things:\nTotal PnL — The raw number Trimmed PnL — Remove top 10% and bottom 10% of trades Why trimmed? Because a few lucky big winners can mask a broken strategy. If your trimmed PnL is negative but your total PnL is positive, you\u0026rsquo;re relying on outliers. That\u0026rsquo;s not a strategy — that\u0026rsquo;s gambling.\nThe Expert Principle \u0026ldquo;Total returns are far more influenced by risk-reward ratio than by win rate. A 1:1.5 risk-reward with 30-40% win rate is enough.\u0026rdquo;\nThis single principle saved me from chasing high win rates and instead focusing on what actually drives returns.\nWin rate is vanity. Risk-reward is sanity. PnL is reality.\nThe best traders don\u0026rsquo;t win more often. They win bigger.\n","permalink":"https://kimchibot.com/posts/risk-reward-ratio-the-only-number-that-matters/","summary":"Forget win rate. A 35% win rate can make you rich. A 70% win rate can bankrupt you. Here\u0026rsquo;s the math.","title":"Risk-Reward Ratio: The Only Number That Matters"},{"content":"1. Time Synchronization Will Break Everything Your first API call will probably fail with:\nTimestamp for this request was 1000ms ahead of the server\u0026#39;s time Binance requires your request timestamp to be within 1 second of their server time. Your computer\u0026rsquo;s clock drifts. Their server\u0026rsquo;s clock drifts.\nFix:\n1 2 3 4 5 6 exchange = ccxt.binance({ \u0026#39;options\u0026#39;: { \u0026#39;adjustForTimeDifference\u0026#39;: True } }) exchange.load_time_difference() And resync every hour. I learned this after random failures at 3 AM when nobody was around to restart the bot.\n2. fetch_balance() — Don\u0026rsquo;t Pass Parameters On Binance Futures, the \u0026ldquo;correct\u0026rdquo; way to fetch your USDT balance:\n1 2 3 4 5 # WRONG — returns empty or wrong data balance = exchange.fetch_balance({\u0026#39;type\u0026#39;: \u0026#39;future\u0026#39;}) # RIGHT — just call it plain balance = exchange.fetch_balance() The type parameter behaves differently across exchange versions. Without parameters, ccxt handles the routing correctly for futures.\nI spent half a day debugging \u0026ldquo;why is my balance always zero\u0026rdquo; because of this.\n3. STOP Orders Are \u0026ldquo;Algo Orders\u0026rdquo; This is the biggest gotcha. On Binance Futures, any order with a stopPrice is treated as a conditional/algorithmic order. This means:\nexchange.fetch_order(id) → Can\u0026rsquo;t find it exchange.cancel_order(id) → Fails silently You need special API endpoints:\n1 2 3 4 5 6 7 8 # Check status exchange.fapiPrivateGetAlgoOrder({\u0026#39;algoId\u0026#39;: order_id}) # Cancel exchange.fapiPrivateDeleteAlgoOrder({\u0026#39;algoId\u0026#39;: order_id}) # List open orders exchange.fapiPrivateGetOpenAlgoOrders() This is barely documented. I only found it by reading ccxt source code and Binance API changelogs.\n4. ReduceOnly Rejection After Position Close Race condition: your trailing stop closes a position via market order, then your exchange-side SL triggers on the same position. The SL order gets rejected with:\n-2022: ReduceOnly order is getting rejected This is actually fine — it means the position is already closed. But if your bot doesn\u0026rsquo;t handle this error gracefully, it\u0026rsquo;ll spam retry loops for 30 seconds.\nFix: Catch -2022 errors and treat them as \u0026ldquo;position already closed, move on.\u0026rdquo;\n5. TradFi Tokens Need Agreement Signing If your bot tries to trade TSLA/USDT, NVDA/USDT, or other equity-backed tokens:\n-4411: Please sign TradFi-Perps agreement contract fapi You need to sign a legal agreement on the Binance website first. But better yet, just auto-exclude them:\n1 2 3 for symbol, market in exchange.markets.items(): if market.get(\u0026#39;info\u0026#39;, {}).get(\u0026#39;underlyingType\u0026#39;) != \u0026#39;COIN\u0026#39;: exclude_list.append(symbol) This catches equities, commodities, indices, and pre-market tokens — about 24 pairs that will cause problems.\n6. Candle Data Timing When you call fetch_ohlcv() right after a 5-minute candle closes, the API might return stale data. The new candle takes 1-3 seconds to appear.\nWrong approach:\n1 2 time.sleep(2) # Hope for the best data = exchange.fetch_ohlcv(symbol, \u0026#39;5m\u0026#39;, limit=1) Right approach:\n1 2 3 4 5 6 7 for attempt in range(5): data = exchange.fetch_ohlcv(symbol, \u0026#39;5m\u0026#39;, limit=2) latest_ts = data[-1][0] expected_ts = current_5m_boundary() if latest_ts \u0026gt;= expected_ts: break time.sleep(1) Verify the timestamp. Don\u0026rsquo;t trust sleep timers.\n7. Order Dust After closing a position, you sometimes have tiny leftover amounts (0.001 of a coin) that are too small to trade. These show up as phantom positions in your balance.\nThe API says you have a position. You can\u0026rsquo;t close it because it\u0026rsquo;s below minimum order size. Your bot thinks it\u0026rsquo;s still in a trade.\nFix: Check position size against the market\u0026rsquo;s minimum order quantity before treating it as an active position.\n8. Rate Limits Are Per-IP, Not Per-Key If you\u0026rsquo;re running multiple bots from the same server, they share rate limits. I found this out when my trend-following bot and FVG bot started getting 429 Too Many Requests errors simultaneously.\nFix: Stagger your API calls. Use enableRateLimit: True in ccxt, and add small delays between bot instances.\n9. Resampled vs Native Candles This is subtle. If you build 1-hour candles by combining 5-minute candles:\n1 2 3 4 5 # Resampled — NOT the same as exchange native df_1h = df_5m.resample(\u0026#39;1h\u0026#39;).agg({ \u0026#39;open\u0026#39;: \u0026#39;first\u0026#39;, \u0026#39;high\u0026#39;: \u0026#39;max\u0026#39;, \u0026#39;low\u0026#39;: \u0026#39;min\u0026#39;, \u0026#39;close\u0026#39;: \u0026#39;last\u0026#39; }) The RSI calculated from these resampled candles will be slightly different from RSI calculated on native 1-hour candles from the exchange.\nThe difference is tiny — maybe 0.5 RSI points. But when your entry condition is \u0026ldquo;RSI \u0026gt; 50,\u0026rdquo; that 0.5 can flip the signal.\nFix: Always fetch native candles for the timeframe you need. Don\u0026rsquo;t resample.\nThe Meta-Lesson Binance\u0026rsquo;s API is powerful but full of edge cases. The documentation covers the happy path. The unhappy paths — the ones that hit at 3 AM on a Sunday — aren\u0026rsquo;t documented anywhere.\nBuild defensively. Log everything. Handle every error code. And test with real (small) money before scaling up.\nThe API documentation tells you how it should work. Production tells you how it actually works.\n","permalink":"https://kimchibot.com/posts/binance-api-gotchas-that-will-waste-your-weekend/","summary":"A collection of Binance Futures API quirks that aren\u0026rsquo;t in the documentation. Each one cost me hours.","title":"Binance API Gotchas That Will Waste Your Weekend"},{"content":"The Problem With One Strategy Every trading strategy has a weakness:\nTrend following dies in sideways markets. No trend = no signal = no trades. Or worse, false breakouts that trigger entries and immediately reverse.\nMean reversion (FVG) struggles when trends are strong. Price creates a gap and just keeps going — never coming back to fill it.\nNo single strategy works in all market conditions. Period.\nThe Solution: Strategy Diversification I run two bots simultaneously:\nBot 1: Trend Following v4.0 When it thrives: Strong directional moves, high volatility When it struggles: Choppy, sideways markets Win rate: ~57% Risk-reward: ~1:1.2 Bot 2: FVG (Fair Value Gap) When it thrives: Any market with clear price imbalances When it struggles: Strong trends that don\u0026rsquo;t retrace Win rate: ~33% Risk-reward: 1:3 How They Complement Each Other Think of it like a seesaw:\nStrong Trend: TF Bot ████████ | FVG Bot ██ Mild Trend: TF Bot █████ | FVG Bot █████ Sideways: TF Bot ██ | FVG Bot ████████ When one bot is having a bad day, the other is often having a good day.\nReal Example March 2026, Week 3:\nMonday-Tuesday: Strong uptrend. TF bot caught multiple entries. FVG bot got stopped out on gap trades that never filled. Wednesday-Thursday: Market consolidated. TF bot sat idle (CHOP filter blocked entries). FVG bot caught mean-reversion bounces in the range. Friday: Sharp selloff. TF bot caught the short. FVG bot caught the bounce at the FVG level. Combined PnL was smoother than either bot alone.\nWhy Not Three Bots? Or Five? I killed 4 other bots for good reasons:\nBot Why It Died Grid Bot Structural failure in trending markets RSI Scalping Outperformed by trend following Market Maker Requires $100k+ capital Lead-Lag Opportunity window already closed Momentum Overfitting — beautiful backtest, terrible live More bots ≠ better diversification. Each bot needs:\nA genuine edge (proven out-of-sample) Different market conditions where it works Enough capital to size positions properly Two bots with real edges beat five bots where three are mediocre.\nCapital Allocation My current split:\nBot Capital Leverage Coins Per-Trade Size Trend Following 80% of balance 3x 8 coins Balance × 0.8 ÷ 8 FVG Fixed per trade 3x 10 coins $200 The trend-following bot uses percentage-based sizing (compound growth). The FVG bot uses fixed sizing (newer, still building confidence).\nThe Key Insight Diversification in trading isn\u0026rsquo;t about trading more coins or more timeframes. It\u0026rsquo;s about having strategies that disagree with each other.\nIf both your strategies go long in the same conditions and short in the same conditions, you don\u0026rsquo;t have diversification. You have two copies of the same bet.\nA trend-follower and a mean-reverter naturally disagree. When one says \u0026ldquo;price is going up, get in,\u0026rdquo; the other says \u0026ldquo;price went up too fast, it\u0026rsquo;s coming back.\u0026rdquo; This tension is the whole point.\nWhen To Add a Third Bot I\u0026rsquo;ll add another strategy when I find one that:\nHas a different market regime preference Passes out-of-sample testing Has backtest-live parity Doesn\u0026rsquo;t correlate with my existing bots Until then, two is enough. Quality over quantity.\nThe goal isn\u0026rsquo;t to make money every day. It\u0026rsquo;s to make money every month. Two uncorrelated bots make that much more likely.\n","permalink":"https://kimchibot.com/posts/why-i-run-two-bots-not-one/","summary":"One bot follows trends. The other trades mean reversion. Together, they cover each other\u0026rsquo;s weaknesses.","title":"Why I Run Two Bots, Not One"},{"content":"The Nightmare Scenario Picture this: you wake up at 3 AM to check your bot. Everything looks normal — except your position sizes are doubled. Every trade has two identical entries.\nYour bot is running twice.\nHow? You restarted the bot but the old process didn\u0026rsquo;t die. Or your system service spawned a second instance. Or you ran the script in a second terminal and forgot.\nTwo bots, same API keys, same strategy, same coins. Every signal triggers two orders. Your risk is now 2x what you planned.\nThe Problem Python scripts don\u0026rsquo;t have built-in protection against duplicate instances. If you run python bot_trendfollow.py twice, you get two bots. Both connect to Binance. Both place orders. Chaos.\nThis is especially dangerous because:\nBoth instances see the same signals Both try to open positions → double exposure Both try to close positions → one succeeds, one gets ReduceOnly errors Log files get interleaved → impossible to debug The Solution: PID Lockfile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import os import sys LOCKFILE = \u0026#39;/tmp/bot_trendfollow.pid\u0026#39; def acquire_lock(): if os.path.exists(LOCKFILE): with open(LOCKFILE, \u0026#39;r\u0026#39;) as f: old_pid = int(f.read().strip()) # Check if the old process is still running try: os.kill(old_pid, 0) # Signal 0 = just check print(f\u0026#34;Bot already running (PID {old_pid}). Exiting.\u0026#34;) sys.exit(1) except OSError: # Old process is dead, stale lockfile print(f\u0026#34;Removing stale lockfile (PID {old_pid})\u0026#34;) # Write our PID with open(LOCKFILE, \u0026#39;w\u0026#39;) as f: f.write(str(os.getpid())) def release_lock(): if os.path.exists(LOCKFILE): os.remove(LOCKFILE) At startup: check if another instance is running. If yes, refuse to start. If the old process crashed (stale lockfile), clean up and proceed.\nWhy Not Just Check the Process Name? You might think: just check if python bot_trendfollow.py is in the process list.\nProblems:\nMultiple Python scripts might be running Process names can be ambiguous What if the script was renamed? What if it\u0026rsquo;s running inside a virtual environment with a different Python path? PID lockfiles are explicit: \u0026ldquo;process X claimed this lock.\u0026rdquo; If process X is dead, the lock is stale. Simple.\nThe Stale Lockfile Problem What if your bot crashes without cleaning up the lockfile? The next time you start it, it finds the lockfile, checks the PID, and\u0026hellip; the PID belongs to a completely different process that reused the number.\nThis is rare on modern systems (PID numbers go up to 32768+ and cycle slowly), but it happens.\nSolution: Add a process name check as a secondary validation:\n1 2 3 4 5 6 7 8 import psutil def is_our_process(pid): try: proc = psutil.Process(pid) return \u0026#39;bot_trendfollow\u0026#39; in \u0026#39; \u0026#39;.join(proc.cmdline()) except (psutil.NoSuchProcess, psutil.AccessDenied): return False What I Actually Use My bot\u0026rsquo;s startup sequence:\nCheck lockfile — Is another instance running? Sync with exchange — What positions are already open? Resync time — Match clock with Binance Start main loop — Begin scanning And on shutdown (including crashes):\n1 2 3 4 5 6 import atexit import signal atexit.register(release_lock) signal.signal(signal.SIGTERM, lambda *_: (release_lock(), sys.exit(0))) signal.signal(signal.SIGINT, lambda *_: (release_lock(), sys.exit(0))) The atexit handler covers normal exits. Signal handlers cover CTRL+C and system kills.\nOther Safeguards Beyond PID lockfiles, I added:\nPosition size validation — If a position is larger than expected (2x normal), refuse to add more Duplicate order detection — Check if an identical order was placed in the last 60 seconds Balance sanity check — If available balance is too low (positions already open), skip new entries Defense in depth. The lockfile prevents the obvious case. These catch the edge cases.\nThe Lesson Every trading bot tutorial focuses on strategy and signals. Nobody talks about operational safety:\nWhat if it runs twice? What if it crashes mid-order? What if the exchange connection drops during a close? What if the balance fetch fails? Your bot will run 24/7. Everything that can go wrong will go wrong at 3 AM when you\u0026rsquo;re asleep.\nBuild your bot like it\u0026rsquo;s going to crash. Because it will.\nThe best feature in a trading bot isn\u0026rsquo;t the entry signal — it\u0026rsquo;s the thing that stops it from doing something stupid at 3 AM.\n","permalink":"https://kimchibot.com/posts/the-3am-bug-pid-lockfiles-and-duplicate-processes/","summary":"My bot was running twice. Two instances, same account, doubling every order. Here\u0026rsquo;s how PID lockfiles saved me.","title":"The 3 AM Bug: PID Lockfiles and Duplicate Processes"},{"content":"The Problem You enter a trade. Within minutes, it\u0026rsquo;s up 4%. Your normal trailing stop (2% activation, 0.5% trail) hasn\u0026rsquo;t even activated yet.\nThen the price reverses. Your 4% winner becomes a 1% winner. Or worse, a loser.\nExplosive moves need explosive risk management.\nWhat Is a Surge? A surge is when price moves aggressively in your favor immediately after entry — before the first 5-minute candle even closes.\nIn crypto, this happens more often than you\u0026rsquo;d think. A coin gaps up 5% in 2 minutes on a whale buy, then retraces half of it.\nThe question is: how do you keep most of that profit?\nMy Two-Phase System Phase 1: The First 5 Minutes (Pre-Candle Close) If profit reaches +4% before the first 5-minute candle closes:\nActivate tight trailing stop: 0.5% behind best price Check on 1-minute candle closes (not tick-by-tick) Why 1-minute instead of 5-minute? Because in a surge, 5 minutes is an eternity. The move could completely reverse. 1-minute gives you a balance between capturing the move and not getting stopped by noise.\nPhase 2: After First 5-Minute Close Once the first 5-minute candle closes, switch to normal trailing:\nStandard trailing activation (2.0%) Standard trail stop (0.5%) Back to 5-minute candle close checks The logic: if the surge survived the first 5-minute candle, it might have legs. Give it room to run with normal parameters.\nWhy Not Just Use a Tight Trail From the Start? I tested this. Results:\nApproach 7-Day PnL Surge Trades No surge detection +800U n/a Tight trail from entry +750U 71 Phase 1 + Phase 2 +968U 71 Tight trailing from entry catches surges but also prematurely exits normal trades that dip slightly before continuing. The two-phase approach only activates tight trailing when there\u0026rsquo;s evidence of a genuine surge.\nThe Parameter Decisions Surge Threshold: 4% Why not 3%? Too many false surges — normal volatility hits 3% regularly.\nWhy not 5%? Too few triggers — you miss most real surges.\n4% was the sweet spot in backtesting across multiple coin sets and time periods.\nTrail Stop: 0.5% This is wider than you might expect for a \u0026ldquo;tight\u0026rdquo; trail. The reason: I check on candle closes, not ticks.\nA 1-minute candle can have a 1% wick that recovers. If my trail is 0.3%, that wick stops me out even though the candle closes green.\n0.5% on candle close is equivalent to roughly 0.2% on tick-by-tick. It gives the same protection without the noise.\nWhy Candle Close, Not Ticks? This was one of my most important discoveries. My live bot originally checked trailing stops every second via WebSocket. The backtest checked on candle closes.\nThe live bot was getting stopped out on momentary dips. The backtest was riding through them.\nCandle close = \u0026ldquo;where did the price settle?\u0026rdquo; not \u0026ldquo;where did it spike for 1 second?\u0026rdquo;\nFor surge detection specifically, 1-minute candle closes give you the right resolution without the noise of tick data.\nA Real Trade Example Entry: SIREN/USDT long at $0.0523 Time: 14:32:15 14:33 (1m close): +2.1% — Normal, no surge yet 14:34 (1m close): +3.8% — Getting there... 14:35 (1m close): +5.2% — SURGE DETECTED! Trail activated at 4.7% 14:36 (1m close): +4.9% — Trail holds (best was 5.2%, trail at 4.7%) 14:37 (1m close): +5.8% — New best! Trail moves to 5.3% 14:37 (5m close): Phase 2 starts. Switch to 5m checking. 14:42 (5m close): +4.2% — Trail holds at 5.3% — STOPPED OUT Profit: +4.8% (instead of +2.1% without surge detection) Without surge detection, the normal trailing stop (2% activation) would have activated late and caught less of the move.\nEdge Cases Double Surge Sometimes a coin surges, consolidates, then surges again. Phase 2 handles this naturally — the normal trailing stop gives enough room for a second leg.\nSurge Then Crash The worst case: coin spikes 5%, you activate the tight trail, then it crashes right through your trail level. You exit at +4.5% instead of the theoretical +5%.\nThis is fine. You kept most of the move. The alternative (no surge detection) would have you riding it all the way back down.\nSurge on Entry Candle If the surge happens on the same candle as entry, the 1-minute SL check still applies. You won\u0026rsquo;t get trapped holding through a flash crash.\nSurges are gifts. The question isn\u0026rsquo;t whether to take profit — it\u0026rsquo;s how much profit to let slip away before you do.\n","permalink":"https://kimchibot.com/posts/surge-detection-catching-explosive-moves/","summary":"When a trade moves +4% in the first 5 minutes, you need special handling. Here\u0026rsquo;s my surge detection system.","title":"Surge Detection: Catching Explosive Moves Without Giving It All Back"},{"content":"It Will Happen Your bot will crash. Not if — when.\nNetwork timeout at 3 AM Exchange maintenance window you didn\u0026rsquo;t know about Unhandled exception in an edge case Your VPS runs out of memory Python segfault (yes, really) When it crashes, you might have open positions with no stop loss monitoring. This is where accounts blow up.\nThe Recovery Problem When your bot restarts, it needs to answer:\nDo I have open positions? What were the entry prices? Are there stop loss orders on the exchange? What state was the trailing stop in? If it can\u0026rsquo;t answer these questions, it\u0026rsquo;s blind. It might open duplicate positions, or worse, leave existing positions unmanaged.\nMy Recovery System Step 1: State File Every position change is saved to state.json:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \u0026#34;positions\u0026#34;: { \u0026#34;SIREN/USDT\u0026#34;: { \u0026#34;side\u0026#34;: \u0026#34;long\u0026#34;, \u0026#34;entry_price\u0026#34;: 0.0523, \u0026#34;size\u0026#34;: 1000, \u0026#34;sl_price\u0026#34;: 0.0512, \u0026#34;sl_order_id\u0026#34;: \u0026#34;algo_123456\u0026#34;, \u0026#34;best_price\u0026#34;: 0.0545, \u0026#34;trade_usdt\u0026#34;: 200, \u0026#34;entry_time\u0026#34;: \u0026#34;2026-03-22T14:32:15\u0026#34; } } } This file is the bot\u0026rsquo;s memory. Without it, a restart is a cold start.\nStep 2: Exchange Sync State files can be wrong. Maybe the bot crashed between placing an order and updating the file. So on startup:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def sync_with_exchange(): # What does the exchange say we have? exchange_positions = exchange.fetch_positions() # What does our state file say? state_positions = load_state() # Reconcile for pos in exchange_positions: if pos not in state_positions: # Exchange has it, we don\u0026#39;t know about it # → Recover from exchange data recover_position(pos) for pos in state_positions: if pos not in exchange_positions: # We think we have it, exchange doesn\u0026#39;t # → Position was closed while we were down remove_from_state(pos) Step 3: SL Order Verification For each recovered position, check if the stop loss order is still on the exchange:\nSL exists and active: Great, do nothing SL exists but triggered: Position might be closed, verify SL missing: Place a new one immediately The scariest case is a position with no SL. This is an unprotected position — unlimited downside. The bot\u0026rsquo;s first priority on restart is making sure every position has a stop loss.\nStep 4: Deep Recovery Edge Cases What if the bot crashed right after opening a position but before placing the SL?\n1 2 3 4 5 # In pending_orders, we have the intended SL price if position.sl_price == 0 and symbol in pending_orders: # Recover SL from pending order data position.sl_price = pending_orders[symbol].sl_price place_sl_order(position) What if the position is already at -20% loss?\n1 2 3 4 5 if current_loss_pct \u0026gt; 20: # Don\u0026#39;t place SL — it would trigger immediately # and cascade with slippage log(\u0026#34;WARNING: Position deeply underwater, manual review needed\u0026#34;) skip_sl_placement = True The Balance Check Before any trading logic runs, verify you can actually fetch your balance:\n1 2 3 4 5 6 7 try: balance = exchange.fetch_balance() except Exception as e: log(f\u0026#34;Balance fetch failed: {e}\u0026#34;) # DO NOT scan for new signals # Only monitor existing positions skip_new_entries = True If you can\u0026rsquo;t check your balance, you don\u0026rsquo;t know how much capital is available. Don\u0026rsquo;t open new positions blind.\nDefensive Coding Patterns Every API Call Gets a Try/Except 1 2 3 4 5 6 7 8 9 10 def safe_fetch(func, *args, retries=3, **kwargs): for attempt in range(retries): try: return func(*args, **kwargs) except ccxt.NetworkError: time.sleep(2 ** attempt) except ccxt.ExchangeError as e: log(f\u0026#34;Exchange error: {e}\u0026#34;) return None return None State Saves After Every Change Not at the end of the loop. Not every minute. After every state change.\n1 2 3 4 def open_position(symbol, side, entry_price, size): # ... place orders ... state.positions[symbol] = position_data state.save() # Immediately If the bot crashes 1 second after opening a position, the state file has it.\nGraceful Shutdown 1 2 3 4 5 6 7 8 9 10 import signal def shutdown_handler(signum, frame): log(\u0026#34;Shutdown signal received\u0026#34;) state.save() release_lock() sys.exit(0) signal.signal(signal.SIGTERM, shutdown_handler) signal.signal(signal.SIGINT, shutdown_handler) CTRL+C and system kill signals trigger a clean save before exit.\nThe Lesson The difference between a toy bot and a production bot is crash recovery.\nA toy bot works great when everything is normal. A production bot works great when everything is on fire.\nAssume your bot will crash with open positions. Build the recovery before you build the strategy.\nYour bot\u0026rsquo;s job isn\u0026rsquo;t just to make money. It\u0026rsquo;s to not lose money when things go wrong.\n","permalink":"https://kimchibot.com/posts/what-happens-when-your-bot-crashes-at-3am/","summary":"Your bot will crash. At night. On a weekend. With open positions. Here\u0026rsquo;s how to make sure it recovers gracefully.","title":"What Happens When Your Bot Crashes at 3 AM"},{"content":"The Temptation You have a strategy with 10 parameters. Each parameter has 5 possible values. That\u0026rsquo;s 5^10 = ~10 million combinations.\nSomewhere in those 10 million combinations is one that shows +5000% returns on your backtest data. It\u0026rsquo;s also completely meaningless.\nThe more parameters you optimize, the easier it is to overfit.\nMy Grid Search Process Step 1: Limit the Parameters Don\u0026rsquo;t optimize everything at once. Pick 2-3 parameters that matter most. For my trend-following bot:\nStop Loss %: [1.0, 1.5, 2.0, 2.5, 3.0] Trail Activation %: [1.0, 1.5, 2.0, 2.5] Trail Stop %: [0.3, 0.5, 0.7] That\u0026rsquo;s 60 combinations. Manageable and interpretable.\nStep 2: Use Enough Data Minimum: 3 months of data Better: 6-12 months Best: Multiple years across different market conditions Short backtest periods produce parameters that are tuned to that specific market phase. They\u0026rsquo;ll break when the market changes.\nStep 3: Look for Plateaus, Not Peaks The best parameter isn\u0026rsquo;t the one with the highest PnL. It\u0026rsquo;s the one surrounded by similarly good results.\nSL 1.0% → +100U SL 1.5% → +250U SL 2.0% → +280U ← Peak SL 2.5% → +260U SL 3.0% → +200U SL 2.0% is on a plateau. Nearby values also work well. This parameter is robust.\nSL 1.0% → -50U SL 1.5% → +500U ← Peak SL 2.0% → -30U SL 2.5% → -80U SL 3.0% → -100U SL 1.5% is a spike. Tiny changes destroy performance. This is overfitting.\nChoose parameters on plateaus, not spikes.\nStep 4: Validate Out-of-Sample Take your optimized parameters and test them on data you didn\u0026rsquo;t optimize on. If they still work, you might have something real.\nMy v2.9 optimization result:\nDataset PnL Trades Win Rate Training (3 months) +343U 1,800 72.3% Validation (1 month) +245U 552 76.8% Validation performance was proportionally similar. Good sign.\nWhat I Actually Optimized Round 1: SL Level (Most Impact) Old: SL 1.2%. Result: frequent stop outs, death by a thousand cuts.\nGrid search showed SL 3.0% minimized SL triggers to just 2.6% of trades (250 out of 9,747).\nI chose 2.0% — not the mathematical optimum, but a good balance between protection and breathing room.\nRound 2: Trailing Stop Old: TA 1.5%, TS 0.5%.\nAfter grid search: TA 2.0%, TS 0.5%. The activation threshold needed to match the wider SL — otherwise trades get stopped before having a chance to activate trailing.\nRound 3: Volume Ratio Filter Old: Long VR 0.9, Short VR 0.8. Too permissive — letting in low-volume noise.\nAfter search: Long VR 1.8, Short VR 1.5. This cut total trades by ~60% but improved win rate from ~50% to ~57%.\nFewer trades, better trades.\nThe Separation Principle One crucial decision: separate long and short parameters.\nCrypto markets are asymmetric:\nRallies are gradual (stairs up) Crashes are sudden (elevator down) Using the same SL for longs and shorts means one side is always wrong. Different parameters for each direction improved overall performance by ~15%.\nMistakes I Made 1. Optimizing Too Many Parameters Together My first grid search had 8 parameters × 5 values = 390,625 combinations. The \u0026ldquo;best\u0026rdquo; result was a fluke. When I tested it out-of-sample, it was negative.\nRule: maximum 3 parameters at a time.\n2. Optimizing on Too Short a Period I once optimized on 2 weeks of data. Found amazing parameters. They worked perfectly — for those 2 weeks. The next 2 weeks wiped out all gains.\nRule: minimum 3 months, preferably 6+.\n3. Chasing the Highest PnL The temptation is to always pick the parameter set with the highest backtest PnL. But if it\u0026rsquo;s +500U with 90% of that coming from 3 trades, it\u0026rsquo;s not robust.\nRule: also check trimmed PnL (remove top/bottom 10% of trades).\nThe Meta-Rule If changing a parameter by 10% destroys your strategy, the strategy doesn\u0026rsquo;t have edge — the parameter does.\nGood strategies work across a range of reasonable parameters. Great strategies are almost insensitive to parameters. If your bot only works with SL exactly at 1.73%, you don\u0026rsquo;t have a strategy. You have a coincidence.\nThe goal of optimization isn\u0026rsquo;t to find the perfect parameters. It\u0026rsquo;s to confirm that your strategy works across imperfect ones.\n","permalink":"https://kimchibot.com/posts/parameter-optimization-without-fooling-yourself/","summary":"Grid search found parameters that turned -82U into +343U. Here\u0026rsquo;s how I made sure it wasn\u0026rsquo;t just overfitting.","title":"Parameter Optimization Without Fooling Yourself"},{"content":"The Gap Nobody Talks About The typical trading bot journey:\nBuild strategy ✓ Backtest it ✓ Deploy with real money ← HERE BE DRAGONS What\u0026rsquo;s missing? The dry run.\nA dry run is your bot running in real-time, with real market data, making real decisions — but not placing real orders. Paper trading, but automated.\nWhy Backtests Aren\u0026rsquo;t Enough Your backtest runs on historical data. It has perfect information:\nEvery candle is complete Every price is final There\u0026rsquo;s no latency There\u0026rsquo;s no partial fill The exchange never returns an error Real markets are messier. The dry run catches the mess.\nWhat I Found in Dry Runs 5-minute candle data lag — My bot fetched candles right at the close, but the exchange API hadn\u0026rsquo;t updated yet. The bot was making decisions on stale data.\nRate limiting — With 8 coins, scanning every 5 minutes, plus position monitoring — the API calls added up faster than expected.\nOrder of operations — My backtest checked SL then trailing. My live code checked trailing then SL. Different results on the same data.\nRSI warmup — I was fetching 20 candles for RSI calculation. RSI needs ~40 candles to stabilize. My first few signals were based on garbage RSI values.\nHow to Run a Dry Run The simplest approach:\n1 2 3 4 5 6 7 8 DRY_RUN = True def place_order(symbol, side, amount, price): if DRY_RUN: log(f\u0026#34;[DRY] Would {side} {amount} {symbol} at {price}\u0026#34;) return {\u0026#39;id\u0026#39;: \u0026#39;dry_\u0026#39; + str(time.time()), \u0026#39;status\u0026#39;: \u0026#39;filled\u0026#39;} else: return exchange.create_order(...) Everything else runs exactly the same:\nData fetching: real Signal calculation: real Position tracking: simulated Order placement: logged but not executed The Comparison Tool After running the dry bot for a few days, compare its decisions against the backtest for the same period:\n1 python compare_live_bt.py \u0026#34;2026-03-15\u0026#34; \u0026#34;2026-03-18\u0026#34; This shows:\nEntry matches: Did the dry bot and backtest enter the same trades? Entry price: Would the entry price have been the same? Exit reason: SL, trailing, time-based — do they agree? PnL: How close are the numbers? My First Comparison Result Metric Match Rate Entry signals 78% Entry prices 92% Exit reasons 71% PnL (±10%) 65% 65% PnL match. Terrible. But this was before fixing the timezone bug, the candle resampling issue, and the trailing stop tick-vs-close problem.\nAfter Fixing Everything Metric Match Rate Entry signals 98% Entry prices 100% Exit reasons 94% PnL (±10%) 83% 83% PnL match. The remaining 17% is genuine market slippage and SL timing — things that can\u0026rsquo;t be simulated.\nHow Long to Dry Run Minimum: 1 week. This catches most timing and data bugs.\nBetter: 2-4 weeks. This covers different market conditions and edge cases like exchange maintenance windows.\nWhen to stop: When your live-backtest match rate stabilizes above 80% and you understand every discrepancy.\nThe Psychological Benefit Dry running also prepares you psychologically:\nYou see real drawdowns happening in real-time You experience 5 stop losses in a row You watch a trade go +3% and then reverse to -1% You feel the urge to manually intervene If you can\u0026rsquo;t handle watching the dry bot lose, you definitely can\u0026rsquo;t handle it with real money.\nThe dry run isn\u0026rsquo;t just a technical test. It\u0026rsquo;s a stress test for you.\nThe Final Checklist Before going from dry run to live:\nMatch rate above 80% for 1+ week All known discrepancies explained and documented Edge cases tested (restart recovery, network failure) Psychological readiness (survived watching drawdowns) Start with minimum position size for the first live week The extra week of dry running has never cost me money. Skipping it has.\nA dry run is boring. Losing money because you skipped it is exciting in all the wrong ways.\n","permalink":"https://kimchibot.com/posts/dry-run-the-step-everyone-skips/","summary":"Between backtest and live trading, there\u0026rsquo;s a step most people skip. It\u0026rsquo;s the step that catches the bugs that matter most.","title":"Dry Run: The Step Everyone Skips"},{"content":"The Temptation Binance Futures lets you trade with up to 125x leverage on some pairs.\n$100 with 125x leverage = $12,500 exposure. If the price moves 1% in your favor, you make $125 — a 125% return on your capital.\nSounds amazing. Here\u0026rsquo;s the catch: a 0.8% move against you liquidates your entire position.\nThe Math of Ruin With leverage, your liquidation distance is roughly:\nLiquidation distance ≈ 1 / leverage 125x → 0.8% move liquidates you 50x → 2% move liquidates you 20x → 5% move liquidates you 10x → 10% move liquidates you 3x → 33% move liquidates you In crypto, 2% moves happen every hour. A 5% move happens several times a day. A 10% move happens weekly.\nWith 50x leverage, a normal Tuesday can wipe you out.\nWhy 3x Specifically My stop loss is 2.0%. With 3x leverage:\nActual loss on a stop: 2.0% × 3 = 6% of position capital Liquidation distance: ~33% — my stop loss fires long before this Margin for error: Even if my SL fails completely, I have 30%+ buffer The stop loss is the first line of defense. Liquidation distance is the last. With 3x, there\u0026rsquo;s a massive gap between them.\nThe Position Sizing Formula position_capital = balance × 0.80 / TOP_N notional_value = position_capital × leverage Example: Balance: $1,000 Capital per coin: $1,000 × 0.80 / 8 = $100 Notional: $100 × 3 = $300 If SL hits (2% loss on $300): -$6 That\u0026#39;s 0.6% of total balance per stop loss. A single stop loss costs 0.6% of my account. I need ~160 consecutive stop losses to blow up. That\u0026rsquo;s not going to happen.\nWhy Not 1x (No Leverage)? With 1x leverage and a $1,000 account:\nCapital per coin: $100 Notional: $100 Profit on a 2% move: $2 After fees ($0.14 round trip on Binance), that\u0026rsquo;s $1.86 per trade. It takes forever to compound meaningful gains.\n3x is the sweet spot: enough to make the trades worthwhile, not enough to blow up on a bad day.\nThe Compound Effect I use 80% of balance and let it compound. As the balance grows, position sizes grow:\nBalance Per Coin Notional (3x) 2% Win $1,000 $100 $300 $6 $2,000 $200 $600 $12 $5,000 $500 $1,500 $30 $10,000 $1,000 $3,000 $60 At $10k, each winning trade makes $60. At $1k, it makes $6. Same strategy, same edge, 10x different results.\nThis is why starting capital matters. And why you should never risk blowing up your account with high leverage — you need the account to grow.\nHigh Leverage Horror Stories Things I\u0026rsquo;ve seen in crypto trading communities:\n\u0026ldquo;50x on a memecoin\u0026rdquo; — Liquidated in 4 minutes \u0026ldquo;100x scalping\u0026rdquo; — Worked for a week, lost everything in one trade \u0026ldquo;25x with mental stop loss\u0026rdquo; — Froze and watched the liquidation happen The pattern: high leverage works until it doesn\u0026rsquo;t. And when it doesn\u0026rsquo;t, it doesn\u0026rsquo;t gradually. You don\u0026rsquo;t lose 50%. You lose everything.\nThe Boring Truth Professional quant funds typically use 2-5x leverage. Not because they can\u0026rsquo;t access more. Because they\u0026rsquo;ve done the math.\nThe math says: maximize your expected growth rate, not your expected return.\nWith Kelly Criterion math, over-leveraging doesn\u0026rsquo;t just increase risk — it actually decreases your long-term growth rate. You win bigger but you blow up more often, and blowing up resets you to zero.\n3x leverage with a 2% stop loss and 80% capital utilization is boring. It\u0026rsquo;s also how accounts survive long enough to compound into something meaningful.\nThe traders who got rich quick are the ones you hear about. The traders who got rich slow are the ones who stayed rich.\n","permalink":"https://kimchibot.com/posts/leverage-the-double-edged-sword/","summary":"Binance offers up to 125x leverage. I use 3x. Here\u0026rsquo;s the math behind that boring decision.","title":"Leverage: The Double-Edged Sword (Why I Use 3x)"},{"content":"About KimchiBot I\u0026rsquo;m a developer from South Korea building crypto trading bots with Claude Code.\nThis blog is the honest, unfiltered journal of that process. No fake screenshots, no \u0026ldquo;3-minute bot\u0026rdquo; clickbait. Just the real story — failures included.\nWhat I\u0026rsquo;ve Built 6 trading bots using Python + Claude Code 4 killed (Grid, Momentum, Market Maker, Lead-Lag) 2 running live on Binance Futures with real money What You\u0026rsquo;ll Find Here Strategy breakdowns — how each bot works, explained for beginners Backtest vs reality — the numbers, honestly Debugging nightmares — timezone bugs, API quirks, exchange gotchas Live performance — wins and losses, nothing hidden Overfitting warnings — how to tell if your strategy is lying to you Why \u0026ldquo;KimchiBot\u0026rdquo;? \u0026ldquo;Kimchi premium\u0026rdquo; is a well-known term in crypto — the price difference between Korean and international exchanges. I\u0026rsquo;m Korean, I build bots, and I thought it was funny.\nPhilosophy \u0026ldquo;I tortured Claude Code so you don\u0026rsquo;t have to.\u0026rdquo;\nI believe AI can genuinely help people build trading systems, but the internet is full of dangerous oversimplifications. This blog exists to show what it actually takes — the boring debugging, the failed strategies, the weeks of testing — so you don\u0026rsquo;t blow up your account following a YouTube tutorial.\nContact Got questions? Found a bug in my logic? Want to share your own bot stories?\nReach me on GitHub.\nDisclaimer: Nothing on this site is financial advice. Trading crypto with leverage is risky. I share my experience for educational purposes only. Don\u0026rsquo;t trade money you can\u0026rsquo;t afford to lose.\n","permalink":"https://kimchibot.com/about/","summary":"\u003ch2 id=\"about-kimchibot\"\u003eAbout KimchiBot\u003c/h2\u003e\n\u003cp\u003eI\u0026rsquo;m a developer from South Korea building crypto trading bots with \u003ca href=\"https://claude.ai/claude-code\"\u003eClaude Code\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis blog is the honest, unfiltered journal of that process. No fake screenshots, no \u0026ldquo;3-minute bot\u0026rdquo; clickbait. Just the real story — failures included.\u003c/p\u003e\n\u003ch3 id=\"what-ive-built\"\u003eWhat I\u0026rsquo;ve Built\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e6 trading bots\u003c/strong\u003e using Python + Claude Code\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e4 killed\u003c/strong\u003e (Grid, Momentum, Market Maker, Lead-Lag)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e2 running live\u003c/strong\u003e on Binance Futures with real money\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"what-youll-find-here\"\u003eWhat You\u0026rsquo;ll Find Here\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eStrategy breakdowns\u003c/strong\u003e — how each bot works, explained for beginners\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBacktest vs reality\u003c/strong\u003e — the numbers, honestly\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDebugging nightmares\u003c/strong\u003e — timezone bugs, API quirks, exchange gotchas\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLive performance\u003c/strong\u003e — wins and losses, nothing hidden\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOverfitting warnings\u003c/strong\u003e — how to tell if your strategy is lying to you\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"why-kimchibot\"\u003eWhy \u0026ldquo;KimchiBot\u0026rdquo;?\u003c/h3\u003e\n\u003cp\u003e\u0026ldquo;Kimchi premium\u0026rdquo; is a well-known term in crypto — the price difference between Korean and international exchanges. I\u0026rsquo;m Korean, I build bots, and I thought it was funny.\u003c/p\u003e","title":"About"},{"content":"Last updated: April 9, 2026\nWho We Are KimchiBot (https://kimchibot.com) is a personal blog about algorithmic trading bot development.\nWhat Data We Collect Analytics We may use privacy-friendly analytics to understand how visitors use this site. No personally identifiable information is collected.\nAdvertising This site may display advertisements through Google AdSense or similar services. These services may use cookies to serve ads based on your prior visits to this or other websites. You can opt out of personalized advertising at Google Ads Settings.\nCookies Third-party advertising networks may place cookies on your browser. You can manage cookie preferences through your browser settings.\nWhat We Don\u0026rsquo;t Collect We don\u0026rsquo;t collect personal information (name, email, phone) We don\u0026rsquo;t require account registration We don\u0026rsquo;t sell or share data with third parties beyond ad networks Third-Party Services This site may use the following services:\nGoogle AdSense — for advertising Cloudflare — for hosting and CDN GitHub — for code hosting Each service has its own privacy policy.\nChildren\u0026rsquo;s Privacy This site is not intended for children under 13. We do not knowingly collect information from children.\nChanges We may update this policy from time to time. Changes will be posted on this page.\nContact If you have questions about this privacy policy, contact us through GitHub.\n","permalink":"https://kimchibot.com/privacy/","summary":"\u003cp\u003e\u003cem\u003eLast updated: April 9, 2026\u003c/em\u003e\u003c/p\u003e\n\u003ch2 id=\"who-we-are\"\u003eWho We Are\u003c/h2\u003e\n\u003cp\u003eKimchiBot (\u003ca href=\"https://kimchibot.com\"\u003ehttps://kimchibot.com\u003c/a\u003e) is a personal blog about algorithmic trading bot development.\u003c/p\u003e\n\u003ch2 id=\"what-data-we-collect\"\u003eWhat Data We Collect\u003c/h2\u003e\n\u003ch3 id=\"analytics\"\u003eAnalytics\u003c/h3\u003e\n\u003cp\u003eWe may use privacy-friendly analytics to understand how visitors use this site. No personally identifiable information is collected.\u003c/p\u003e\n\u003ch3 id=\"advertising\"\u003eAdvertising\u003c/h3\u003e\n\u003cp\u003eThis site may display advertisements through Google AdSense or similar services. These services may use cookies to serve ads based on your prior visits to this or other websites. You can opt out of personalized advertising at \u003ca href=\"https://www.google.com/settings/ads\"\u003eGoogle Ads Settings\u003c/a\u003e.\u003c/p\u003e","title":"Privacy Policy"}]