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 sizing –
amount_to_traderespects 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函数会遵守交易所的步长和最小名义价值限制,防止那些恼人的“订单过小”拒绝。 - 错误处理 —— 增加了特定的异常捕获,即使网络出现波动,机器人也能持续运行。