How I Built My First Crypto Trading Bot — A Neo's Journey into the Matrix

How I Built My First Crypto Trading Bot — A Neo’s Journey into the Matrix

我是如何构建第一个加密货币交易机器人的——“尼奥”的矩阵之旅

The Quest Begins (The “Why”)

探索的开始(“为什么”)

Honestly, I was tired of staring at candlestick charts at 2 a.m., coffee gone cold, wishing I had a tiny robot that could buy the dip while I slept. I’d dabbled in manual trades on Binance, felt the rush of a winning trade, and then the gut‑punch of missing a sudden spike because I was busy debugging a completely unrelated script. Sound familiar? That moment — when I realized I’d just watched a 5% move happen while I was refreshing a Reddit thread — felt like Neo taking the red pill. I wanted out of the simulation of endless manual clicking and into a world where my code could watch the markets 24/7, execute a simple strategy, and let me focus on actually building stuff instead of chasing price ticks. So I embarked on a quest: build a crypto trading bot that could run on a cheap VPS, use a popular exchange API, and implement a basic mean‑reversion strategy. If I could get that working, I’d have a solid foundation to experiment with more complex ideas later.

老实说,我厌倦了在凌晨两点盯着 K 线图,咖啡早已凉透,心里盼望着能有一个小机器人,在我睡觉时帮我“抄底”。我曾在币安(Binance)尝试过手动交易,体验过获利时的快感,也尝过因为忙于调试一段毫不相关的脚本而错过行情暴涨的挫败感。听起来很耳熟吧?那一刻——当我意识到自己正盯着 Reddit 帖子刷新页面,却眼睁睁看着行情波动了 5% 时——那种感觉就像尼奥服下了红色药丸。我想要逃离这种无休止手动点击的“模拟世界”,进入一个代码可以 24/7 全天候监控市场、执行简单策略的世界,让我能专注于创造价值,而不是追逐价格跳动。于是,我踏上了征程:构建一个可以在廉价 VPS 上运行、使用主流交易所 API 并实现基础均值回归策略的加密货币交易机器人。如果能成功,我将为后续尝试更复杂的想法打下坚实的基础。

The Revelation (The Insight)

启示(洞察)

The biggest “aha!” wasn’t some exotic algorithm; it was realizing that the hard part isn’t the math — it’s the plumbing. Once you have reliable market data, a clean way to place orders, and solid error handling, the strategy itself becomes just a few lines of logic. I settled on three core pieces: Data fetch – using the ccxt library to pull OHLCV candles from Binance. Signal generation – a simple mean‑reversion rule: if the price drops more than 2% below its 20‑period SMA, we go long; if it rises 2% above the SMA, we exit. Order execution – market orders with a fixed USD amount, plus basic safety checks (min‑order size, balance). The magic was in separating concerns: fetch → decide → act. When each piece worked in isolation, wiring them together felt like casting a spell that actually worked.

最大的“顿悟”并非来自某种奇特的算法,而是意识到最难的部分不是数学,而是“管道工程”。一旦你拥有了可靠的市场数据、简洁的下单方式以及稳健的错误处理机制,策略本身就只是几行逻辑代码而已。我确定了三个核心部分:数据获取——使用 ccxt 库从币安拉取 OHLCV(开高低收量)K 线数据;信号生成——一个简单的均值回归规则:如果价格跌破 20 日移动平均线(SMA)超过 2%,则做多;如果价格涨超 SMA 2%,则平仓;订单执行——使用固定美元金额进行市价交易,并辅以基本的安全检查(最小订单量、余额检查)。其魔力在于关注点分离:获取 → 决策 → 执行。当每个部分都能独立工作时,将它们串联起来就像施展了一个真正有效的魔法。

Wielding the Power (Code & Examples)

掌握力量(代码与示例)

The Struggle (Before)

挣扎(之前)

My first attempt was a monolithic script that fetched data, calculated indicators, placed an order, then slept for a minute — all inside a while True loop with zero error handling. It looked something like this:

我的第一次尝试是一个单体脚本,它在 while True 循环中执行获取数据、计算指标、下单、然后休眠一分钟的操作,且没有任何错误处理。代码大致如下:

import time, ccxt
exchange = ccxt.binance({'enableRateLimit': True})
symbol = 'BTC/USDT'
amount_usd = 20

while True:
    ohlcv = exchange.fetch_ohlcv(symbol, timeframe='1m', limit=21)
    closes = [c[4] for c in ohlcv]
    sma = sum(closes[-20:]) / 20
    price = closes[-1]
    
    if price < sma * 0.98: # 2% below SMA → buy
        exchange.create_market_buy_order(symbol, amount_usd / price)
    elif price > sma * 1.02: # 2% above SMA → sell (close)
        # Oops! No position tracking → we might sell nothing or short!
        exchange.create_market_sell_order(symbol, amount_usd / price)
    time.sleep(60)

Traps I fell into:

  • No position awareness – the bot would keep buying on every dip, even if it already held BTC, quickly exceeding my intended exposure.
  • No error handling – a network hiccup or rate‑limit burst would crash the loop, leaving the bot silent until I noticed.
  • Hard‑coded amount – using a fixed USD amount ignored the fact that the minimum order size on Binance changes with price; sometimes the bot tried to send 0.00001 BTC and got rejected.

我掉进的陷阱:

  • 没有持仓意识 —— 机器人会在每次下跌时不断买入,即使它已经持有 BTC,这很快就会超过我预期的风险敞口。
  • 没有错误处理 —— 网络波动或触发频率限制会导致循环崩溃,机器人会陷入沉默,直到我发现为止。
  • 硬编码金额 —— 使用固定的美元金额忽略了币安的最小订单量会随价格变化的事实;有时机器人尝试发送 0.00001 BTC,结果被拒绝。

The Victory (After)

胜利(之后)

I refactored the script into three clear functions, added a simple position tracker, and wrapped exchange calls in retry logic. Here’s the cleaned‑up version:

我将脚本重构为三个清晰的函数,添加了一个简单的持仓追踪器,并将交易所调用封装在重试逻辑中。以下是优化后的版本:

import time, ccxt, logging
from decimal import Decimal, ROUND_DOWN

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
exchange = ccxt.binance({
    'enableRateLimit': True,
    'options': {'defaultType': 'future'} # adjust if you trade spot or futures
})

symbol = 'BTC/USDT'
timeframe = '1m'
lookback = 20 # SMA period
threshold = Decimal('0.02') # 2%
usd_per_trade = Decimal('20')
position = 0 # positive = long, negative = short, 0 = flat

def fetch_sma():
    ohlcv = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=lookback + 1)
    closes = [Decimal(str(c[4])) for c in ohlcv]
    sma = sum(closes[:-1]) / lookback # exclude the forming candle
    return sma, closes[-1]

def amount_to_trade(price):
    # Ensure we respect the exchange's step size and min notional
    market = exchange.market(symbol)
    step = Decimal(str(market['precision']['amount']))
    min_notional = Decimal(str(market['limits']['cost']['min'])) if market['limits']['cost']['min'] else Decimal('0')
    raw = (usd_per_trade / price).quantize(step, rounding=ROUND_DOWN)
    if raw * price < min_notional:
        raise ValueError(f'Order too small: {raw} {symbol.split("/")[0]} < min_notional')
    return raw

def place_order(side, qty):
    try:
        if side == 'buy':
            order = exchange.create_market_buy_order(symbol, float(qty))
        else:
            order = exchange.create_market_sell_order(symbol, float(qty))
        logging.info(f'{side.upper()} order placed: {qty} {symbol.split("/")[0]} @ market')
        return order
    except Exception as e:
        logging.error(f'Order failed: {e}')
        return None

def main():
    global position
    while True:
        try:
            sma, price = fetch_sma()
            signal = Decimal('0')
            if price < sma * (Decimal('1') - threshold):
                signal = Decimal('1') # go long
            elif price > sma * (Decimal('1') + threshold):
                signal = Decimal('-1') # go flat (exit long)

            if signal == 1 and position <= 0: # enter long
                qty = amount_to_trade(price)
                if place_order('buy', qty):
                    position = float(qty)
            elif signal == -1 and position > 0: # exit long
                qty = amount_to_trade(price)
                if place_order('sell', qty):
                    position = 0
            else:
                logging.debug(f'No action. SMA={sma:.2f}, price={price:.2f}, position={position}')
        except ccxt.NetworkError as e:
            logging.warning(f'Network issue: {e}')
        except ccxt.ExchangeError as e:
            logging.error(f'Exchange error: {e}')
        except Exception as e:
            logging.exception(f'Unexpected error: {e}')
        time.sleep(60) # respect rate limits; adjust as needed

if __name__ == '__main__':
    main()

What changed?

  • Position tracking – we only open a trade when we’re flat and only close when we’re long. No accidental pyramiding.
  • Robust order sizingamount_to_trade respects the exchange’s step size and minimum notional, preventing those pesky “order too small” rejections.
  • Error handling – Added specific exception catching to keep the bot running even if the network blips.

有哪些改变?

  • 持仓追踪 —— 我们只在空仓时开仓,只在持仓时平仓。避免了意外的加仓。
  • 稳健的订单规模 —— amount_to_trade 函数会遵守交易所的步长和最小名义价值限制,防止那些恼人的“订单过小”拒绝。
  • 错误处理 —— 增加了特定的异常捕获,即使网络出现波动,机器人也能持续运行。