Skip to content

Latest commit

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Crypto Backtest System

A cryptocurrency quantitative backtesting system with a React frontend and FastAPI backend.

Features

  • Custom Python Strategies: Write your own trading strategies in Python
  • Real-time Backtesting: Execute strategies with historical K-line data
  • Interactive Charts: TradingView-style candlestick charts with buy/sell markers
  • Performance Metrics: Comprehensive backtest results including profit rate, max drawdown, win rate
  • Multi-symbol Support: BTCUSDT, ETHUSDT, BNBUSDT, and more
  • Multiple Timeframes: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d, 1w

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Frontend (React) │
├─────────────────────────────────────────────────────────────────┤
│ BacktestPanel │
│ ├── BacktestConfig (Symbol, Interval, Time Range, etc.) │
│ ├── StrategyEditor (Python Code Editor) │
│ ├── TradingViewChart (K-line Chart + Equity Curve) │
│ └── BacktestResult (Performance Metrics) │
├─────────────────────────────────────────────────────────────────┤
│ Pyodide Engine │
│ - Executes Python strategies in browser │
│ - Built-in tools: IStrategy, Kline, BacktestContext │
├─────────────────────────────────────────────────────────────────┤
│ KlineCache │
│ - Paginated caching (1000 K-lines per page) │
│ - Auto-fetch when buffer is low │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ GET /api/v1/kline/{symbol}/{interval}?start_time=&limit= │
│ GET /api/v1/kline/symbols │
│ GET /api/v1/kline/intervals │
└─────────────────────────────────────────────────────────────────┘

Tech Stack

Frontend

  • React 18 + TypeScript
  • lightweight-charts (TradingView)
  • Pyodide (Python in browser)
  • Axios

Backend

  • FastAPI
  • SQLAlchemy (async)
  • PostgreSQL
  • Redis (rate limiting)

Getting Started

Prerequisites

  • Node.js 18+
  • Python 3.11+
  • PostgreSQL
  • Redis (optional)

Backend Setup

cd backend
# Install dependencies
pip install pipenv
pipenv install
# Configure environment
cp .env.example .env
# Edit .env with your database credentials

Create a .env file in the backend directory with the following variables:

# Required: Database connection URLDATABASE_URL=postgresql+asyncpg://user:password@host:port/database# Optional: Redis connection URL (default: redis://localhost:6379/0)REDIS_URL=redis://localhost:6379/0# Optional: Enable debug mode (default: false)DEBUG=false# Optional: Rate limiting settingsRATE_LIMIT_PER_SECOND=30RATE_LIMIT_PER_MINUTE=1000

Then start the server:

pipenv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Frontend Setup

cd frontend
# Install dependencies
npm install
# Start development server
npm run dev

Database Setup

Ensure your PostgreSQL database has K-line data tables with the following structure:

CREATETABLEt_kline_btcusdt_1h (
open_time BIGINTPRIMARY KEY,
open_price NUMERIC(20, 8),
high_price NUMERIC(20, 8),
low_price NUMERIC(20, 8),
close_price NUMERIC(20, 8),
base_volume NUMERIC(20, 8),
close_time BIGINT,
quote_volume NUMERIC(20, 8),
trades_count INTEGER,
taker_buy_base_volume NUMERIC(20, 8),
taker_buy_quote_volume NUMERIC(20, 8)
);

Strategy Development Guide

Strategy Basic Structure

All strategies must inherit from IStrategy and implement the run method:

classStrategy(IStrategy):
defrun(self, context: BacktestContext, kline: Kline, params: dict):
# Your strategy logic herepass

Built-in Enums

OrderType

Order type enumeration.

ValueDescription
OrderType.BUYBuy order
OrderType.SELLSell order

Example:

ifcondition:
order=Order(timestamp, OrderType.BUY, price, amount)

OrderSide

Order direction enumeration.

ValueDescription
OrderSide.LONGLong position
OrderSide.SHORTShort position

PositionSide

Position direction enumeration.

ValueDescription
PositionSide.LONGLong position
PositionSide.SHORTShort position
PositionSide.BOTHBoth directions

Built-in Classes

Kline

K-line data structure, representing a single candlestick.

Properties:

PropertyTypeDescription
open_timestrOpening time
openfloatOpening price
highfloatHighest price
lowfloatLowest price
closefloatClosing price
volumefloatTrading volume
close_timestrClosing time

Example:

defrun(self, context: BacktestContext, kline: Kline, params: dict):
current_price=kline.closecurrent_high=kline.highcurrent_low=kline.lowcurrent_volume=kline.volume

KlineCache

K-line history data cache for storing and accessing historical K-lines.

Initialization:

cache=KlineCache(kline_wnd_size=50) # Default window size is 50

Methods:

MethodReturn TypeDescription
append(kline: Kline)NoneAdd a K-line to cache
get_klines()list[Kline]Get all K-lines
get_closes()list[float]Get all closing prices
get_highs()list[float]Get all highest prices
get_lows()list[float]Get all lowest prices
get_volumes()list[float]Get all volumes
get_opens()list[float]Get all opening prices
is_full()boolCheck if cache is full
__len__()intGet cache size

Example:

defrun(self, context: BacktestContext, kline: Kline, params: dict):
closes=self.kline_cache.get_closes()
highs=self.kline_cache.get_highs()
lows=self.kline_cache.get_lows()
iflen(closes) <20:
return# Calculate indicators using historical datama20=calculate_sma(closes, 20)
highest_20=max(highs[-20:])
lowest_20=min(lows[-20:])

Order

Order data structure.

Constructor:

Order(timestamp: str, order_type: OrderType, price: float, amount: float, fee: float=0, order_side: OrderSide=None)

Parameters:

ParameterTypeDescription
timestampstrOrder timestamp
order_typeOrderTypeOrder type (BUY/SELL)
pricefloatOrder price
amountfloatOrder amount
feefloatTransaction fee (default: 0)
order_sideOrderSideOrder direction (optional)

Properties:

PropertyTypeDescription
timestampstrOrder timestamp
typeOrderTypeOrder type
pricefloatOrder price
amountfloatOrder amount
feefloatTransaction fee
order_sideOrderSideOrder direction

BacktestContext

Backtest context class, manages account balance, positions, orders, and fee calculations.

Properties:

PropertyTypeDescription
balanceDecimalAvailable balance (quote asset)
positionDecimalCurrent position (base asset)
initial_balanceDecimalInitial balance
fee_rateDecimalFee rate
tradeslistTrade history
orderslist[Order]Order history
equity_curvelistEquity curve data
max_drawdownDecimalMaximum drawdown

Methods:

buy(price, amount, timestamp)

Execute a buy order.

defbuy(self, price: float, amount: float, timestamp: str) ->bool
ParameterTypeDescription
pricefloatBuy price
amountfloatBuy amount (base asset quantity)
timestampstrOrder timestamp

Returns:bool - Whether the order was successful

Example:

# Buy 0.1 BTC at current pricecontext.buy(kline.close, 0.1, kline.open_time)

sell(price, amount, timestamp)

Execute a sell order.

defsell(self, price: float, amount: float, timestamp: str) ->bool
ParameterTypeDescription
pricefloatSell price
amountfloatSell amount (base asset quantity)
timestampstrOrder timestamp

Returns:bool - Whether the order was successful

Example:

# Sell 0.05 BTC at current pricecontext.sell(kline.close, 0.05, kline.open_time)

sell_all(price, timestamp)

Sell all positions.

defsell_all(self, price: float, timestamp: str) ->bool
ParameterTypeDescription
pricefloatSell price
timestampstrOrder timestamp

Returns:bool - Whether the order was successful

Example:

# Sell all positionscontext.sell_all(kline.close, kline.open_time)

get_position_value(price)

Get current position value.

defget_position_value(self, price: float) ->Decimal

Returns: Position value in quote asset

get_equity(price)

Get total account equity (balance + position value).

defget_equity(self, price: float) ->Decimal

Returns: Total equity

Example:

total_equity=context.get_equity(kline.close)

get_avg_position_price()

Get average position price.

defget_avg_position_price(self) ->float

Returns: Average buy price of current position

Example:

avg_price=context.get_avg_position_price()
ifkline.close>avg_price*1.05: # 5% profitcontext.sell_all(kline.close, kline.open_time)

get_drawdown(current_price)

Calculate current drawdown.

defget_drawdown(self, current_price: float) ->Decimal

Returns: Current drawdown ratio (0-1)

IStrategy

Strategy base class. All user strategies must inherit from this class.

Properties:

PropertyTypeDescription
kline_cacheKlineCacheK-line cache instance
namestrStrategy name (class name)

Methods:

initialize(kline_wnd_size)

Initialize K-line cache. Called automatically by the backtest engine.

definitialize(self, kline_wnd_size: int=50) ->None

run(context, kline, params) [Must Implement]

Strategy execution method. Called for each K-line.

defrun(self, context: BacktestContext, kline: Kline, params: dict) ->None
ParameterTypeDescription
contextBacktestContextBacktest context
klineKlineCurrent K-line data
paramsdictStrategy parameters

on_trade(trade) [Optional]

Trade callback. Called after each trade.

defon_trade(self, trade: dict) ->None

on_day_end(date, context) [Optional]

Day end callback. Called at the end of each trading day.

defon_day_end(self, date: str, context: BacktestContext) ->None

Built-in Functions

calculate_sma(data, period)

Calculate Simple Moving Average.

defcalculate_sma(data: list, period: int) ->float
ParameterTypeDescription
datalist[float]Price data list
periodintMoving average period

Returns:float - SMA value, or None if insufficient data

Example:

closes=self.kline_cache.get_closes()
iflen(closes) >=20:
sma20=calculate_sma(closes, 20)
sma50=calculate_sma(closes, 50)

calculate_ema(data, period)

Calculate Exponential Moving Average.

defcalculate_ema(data: list, period: int) ->float
ParameterTypeDescription
datalist[float]Price data list
periodintMoving average period

Returns:float - EMA value, or None if insufficient data

Example:

closes=self.kline_cache.get_closes()
ema12=calculate_ema(closes, 12)
ema26=calculate_ema(closes, 26)

calculate_rsi(data, period)

Calculate Relative Strength Index.

defcalculate_rsi(data: list, period: int=14) ->float
ParameterTypeDescription
datalist[float]Price data list
periodintRSI period (default: 14)

Returns:float - RSI value (0-100), or None if insufficient data

Example:

closes=self.kline_cache.get_closes()
rsi=calculate_rsi(closes, 14)
ifrsiisnotNone:
ifrsi<30:
# Oversold - potential buy signalcontext.buy(kline.close, 0.1, kline.open_time)
elifrsi>70:
# Overbought - potential sell signalcontext.sell_all(kline.close, kline.open_time)

calculate_macd(data, fast, slow, signal)

Calculate MACD indicator.

defcalculate_macd(data: list, fast: int=12, slow: int=26, signal: int=9) ->tuple
ParameterTypeDescription
datalist[float]Price data list
fastintFast EMA period (default: 12)
slowintSlow EMA period (default: 26)
signalintSignal line period (default: 9)

Returns:tuple[float, float, float] - (MACD line, Signal line, Histogram), or (None, None, None) if insufficient data

Example:

closes=self.kline_cache.get_closes()
macd_line, signal_line, histogram=calculate_macd(closes)
ifmacd_lineisnotNone:
# MACD golden crossifmacd_line>signal_lineandprev_macd<=prev_signal:
context.buy(kline.close, 0.1, kline.open_time)
# MACD death crossifmacd_line<signal_lineandprev_macd>=prev_signal:
context.sell_all(kline.close, kline.open_time)

Available Built-in Functions

The following Python built-in functions are available in strategy code:

CategoryFunctions
Mathabs, max, min, pow, round, sum
Type Conversionbool, float, int, str, list, dict, set, tuple, frozenset
Sequencelen, range, enumerate, zip, map, filter, sorted, reversed, slice
Logicall, any, isinstance
ConstantsTrue, False, None
ExceptionsException, ValueError, TypeError, KeyError, IndexError, RuntimeError, StopIteration, NotImplementedError

Available Imports

The following modules can be used in strategy code:

fromcollectionsimportdequefromdecimalimportDecimalfromdatetimeimportdatetime

Example:

fromdatetimeimportdatetimeclassStrategy(IStrategy):
defrun(self, context: BacktestContext, kline: Kline, params: dict):
# Parse timestampdt=datetime.fromisoformat(kline.open_time.replace('Z', '+00:00'))
# Trading only during specific hoursif9<=dt.hour<16:
# Your strategy logicpass

Complete Strategy Example

Example 1: Simple Moving Average Crossover Strategy

classStrategy(IStrategy):
defrun(self, context: BacktestContext, kline: Kline, params: dict):
closes=self.kline_cache.get_closes()
fast_period=params.get('fast_period', 10)
slow_period=params.get('slow_period', 30)
iflen(closes) <slow_period:
returnfast_ma=calculate_sma(closes, fast_period)
slow_ma=calculate_sma(closes, slow_period)
prev_fast=calculate_sma(closes[:-1], fast_period)
prev_slow=calculate_sma(closes[:-1], slow_period)
# Golden cross - buy signaliffast_ma>slow_maandprev_fast<=prev_slow:
ifcontext.position==0:
buy_amount=float(context.balance/kline.close) *0.95context.buy(kline.close, buy_amount, kline.open_time)
# Death cross - sell signaleliffast_ma<slow_maandprev_fast>=prev_slow:
ifcontext.position>0:
context.sell_all(kline.close, kline.open_time)

Example 2: RSI + MACD Combined Strategy

classStrategy(IStrategy):
def__init__(self):
super().__init__()
self.prev_macd=Noneself.prev_signal=Nonedefrun(self, context: BacktestContext, kline: Kline, params: dict):
closes=self.kline_cache.get_closes()
iflen(closes) <35:
returnrsi=calculate_rsi(closes, 14)
macd_line, signal_line, histogram=calculate_macd(closes)
ifrsiisNoneormacd_lineisNone:
return# Buy condition: RSI oversold + MACD golden crossifcontext.position==0:
ifrsi<35andhistogram>0:
ifself.prev_macdisnotNoneandself.prev_macd<=self.prev_signal:
buy_amount=float(context.balance/kline.close) *0.95context.buy(kline.close, buy_amount, kline.open_time)
# Sell condition: RSI overbought + MACD death crosselifcontext.position>0:
ifrsi>65andhistogram<0:
ifself.prev_macdisnotNoneandself.prev_macd>=self.prev_signal:
context.sell_all(kline.close, kline.open_time)
self.prev_macd=macd_lineself.prev_signal=signal_line

Example 3: Breakout Strategy with Stop Loss

classStrategy(IStrategy):
defrun(self, context: BacktestContext, kline: Kline, params: dict):
closes=self.kline_cache.get_closes()
highs=self.kline_cache.get_highs()
lows=self.kline_cache.get_lows()
lookback=params.get('lookback', 20)
stop_loss_pct=params.get('stop_loss', 0.03)
take_profit_pct=params.get('take_profit', 0.06)
iflen(closes) <lookback:
returnhighest=max(highs[-lookback:])
lowest=min(lows[-lookback:])
# Entry: Break above resistanceifcontext.position==0:
ifkline.close>highest:
buy_amount=float(context.balance/kline.close) *0.95context.buy(kline.close, buy_amount, kline.open_time)
# Exit: Stop loss or take profitelifcontext.position>0:
avg_price=context.get_avg_position_price()
# Stop lossifkline.close<avg_price* (1-stop_loss_pct):
context.sell_all(kline.close, kline.open_time)
# Take profitelifkline.close>avg_price* (1+take_profit_pct):
context.sell_all(kline.close, kline.open_time)
# Break below supportelifkline.close<lowest:
context.sell_all(kline.close, kline.open_time)

Backtest Configuration Parameters

When running a backtest, the system passes the following configuration parameters:

Basic Backtest Parameters

ParameterTypeDefaultDescription
initialBalancefloat10000Initial capital (USDT)
feeRatefloat0.001Fee rate (default 0.1%)

Strategy Parameters (strategyParams)

ParameterTypeDefaultDescription
klineWndSizeint50K-line cache window size

Example:

# Backtest configuration parameters (configured in UI)backtest_params= {
'initialBalance': 10000, # Initial capital 10000 USDT'feeRate': 0.001, # Fee rate 0.1%'strategyParams': {
'klineWndSize': 100, # K-line cache window size'fast_period': 10, # Custom strategy parameters'slow_period': 30,
'stop_loss': 0.03
}
}

Accessing Parameters in Strategy

classStrategy(IStrategy):
defrun(self, context: BacktestContext, kline: Kline, params: dict):
# Access strategy parameters (params is strategyParams)fast_period=params.get('fast_period', 10)
slow_period=params.get('slow_period', 30)
stop_loss=params.get('stop_loss', 0.03)
# K-line window size is set during initialization# Access via self.kline_cache

Backtest Results

After backtesting completes, the system returns the following results:

FieldTypeDescription
initialBalancefloatInitial capital
finalBalancefloatFinal equity (balance + position value)
profitfloatTotal profit/loss (USDT)
profitRatefloatReturn rate (0-1)
maxDrawdownfloatMaximum drawdown (0-1)
totalTradesintTotal number of trades
winRatefloatWin rate (0-1)
baseAssetfloatRemaining position (base asset)
quoteAssetfloatRemaining balance (quote asset)
baseFeefloatTotal base asset fees
quoteFeefloatTotal quote asset fees
tradeslistTrade history list
equityCurvelistEquity curve data

trades Structure

Each trade record contains:

FieldTypeDescription
timestampstrTrade timestamp
typestrTrade type ('buy' / 'sell')
pricefloatTrade price
amountfloatTrade amount
balancefloatBalance after trade
feefloatFee

equityCurve Structure

Each equity record contains:

FieldTypeDescription
timestampstrTimestamp
equityfloatTotal equity
balancefloatBalance
positionfloatPosition amount

Custom Strategy Parameters

In addition to system parameters, you can configure custom strategy parameters in the UI:

# Strategy parameters (configured in UI)params= {
'fast_period': 10,
'slow_period': 30,
'rsi_period': 14,
'stop_loss': 0.03,
'take_profit': 0.06
}
# Access in strategyfast_period=params.get('fast_period', 10) # Default value: 10

Security Restrictions

For security reasons, the following operations are not allowed in strategy code:

  • File operations (open, file read/write)
  • Code execution (eval, exec, compile)
  • System access (os, sys, subprocess)
  • Network requests (requests, urllib, socket)
  • Module imports (except allowed modules)

Best Practices

  1. Always check data length before calculating indicators
  2. Use params.get() with default values for configurable parameters
  3. Check context.position before executing trades
  4. Implement proper risk management with stop loss and take profit
  5. Avoid over-trading by adding proper entry/exit conditions

API Reference

Get K-lines

GET /api/v1/kline/{symbol}/{interval}

Parameters:

  • symbol: Trading pair (e.g., BTCUSDT)
  • interval: Timeframe (e.g., 1h)
  • start_time: Start time (ISO format)
  • end_time: End time (ISO format)
  • limit: Number of K-lines (max 1000)

Get Available Symbols

GET /api/v1/kline/symbols

Get Available Intervals

GET /api/v1/kline/intervals

Project Structure

CryptoBackTest/
├── backend/
│ ├── app/
│ │ ├── api/v1/endpoints/ # API endpoints
│ │ ├── core/ # Config, rate limiter
│ │ ├── db/ # Database session
│ │ ├── models/ # SQLAlchemy models
│ │ ├── schemas/ # Pydantic schemas
│ │ └── main.py # FastAPI app
│ └── Pipfile
├── frontend/
│ ├── src/
│ │ ├── backtest/ # Backtest components
│ │ │ ├── BacktestPanel.tsx
│ │ │ ├── BacktestConfig.tsx
│ │ │ ├── StrategyEditor.tsx
│ │ │ ├── TradingViewChart.tsx
│ │ │ ├── BacktestResult.tsx
│ │ │ ├── KlineCache.ts
│ │ │ ├── KlineService.ts
│ │ │ ├── PyodideEngine.ts
│ │ │ └── types.ts
│ │ └── App.tsx
│ └── package.json
└── plan.md

License

MIT License

About

A cryptocurrency quantitative backtesting system with a React frontend and FastAPI backend.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages