├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── circular_buffer.hpp ├── main.cpp ├── plot.gnu ├── position.cpp ├── position.hpp ├── strategies ├── simple_pips_diff_trigger.cpp └── simple_pips_diff_trigger.hpp ├── strategy.cpp ├── strategy.hpp ├── tests └── circular_buffer_test.cpp ├── tick.cpp └── tick.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *~ 3 | *.csv 4 | backtest 5 | positions.dat -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Adrien Jarthon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX=clang++ 2 | CXXFLAGS=-g -std=c++0x -W -Wall -O3 -pedantic 3 | BIN=backtest 4 | 5 | SRC=$(wildcard *.cpp strategies/*.cpp) 6 | OBJ=$(SRC:%.cpp=%.o) 7 | 8 | all: $(OBJ) 9 | $(CXX) -o $(BIN) $^ 10 | 11 | %.o: %.cpp 12 | $(CXX) $(CXXFLAGS) -c $< -o $@ -I. 13 | 14 | clean: 15 | rm -f *.o strategies/*.o 16 | rm -f $(BIN) 17 | 18 | re: clean all test 19 | 20 | test: 21 | $(CXX) $(CXXFLAGS) -I ./ tests/*.cpp -o unit_tests 22 | @echo -n "\033[31;01m" 23 | @valgrind -q --leak-check=full ./unit_tests 24 | @echo -n "\033[0m" 25 | @rm -f unit_tests 26 | 27 | pull: 28 | rsync --progress --checksum -z deploy@alpha.rootbox.fr:~/bitcoin/XBTEUR.csv data 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backtest 2 | 3 | A simple & fast bitcoin trading strategy backtesting tool. 4 | 5 | ``` 6 | make 7 | ./backtest 8 | gnuplot -p plot.gnu 9 | ``` 10 | 11 | ## Market data 12 | 13 | This program is designed to load market data formatted in CSV like this: 14 | 15 | | timestamp | bid | ask | bid volume | ask volume | 16 | 17 | ``` 18 | 1449528932,363.85,364.28,9.0,9.0 19 | 1449528942,363.84,364.28,9.0,9.0 20 | 1449529022,363.96,364.28,9.0,9.0 21 | 1449529081,363.84,364.99,9.0,9.0 22 | 1449529142,363.73,364.89,9.0,9.0 23 | 1449529202,363.84,364.5,9.0,1.0 24 | 1449529262,363.74,364.4899,9.0,5.0 25 | 1449529322,363.74,364.48,9.0,9.0 26 | 1449529382,363.74,364.48,9.0,9.0 27 | 1449529442,363.75,364.47,9.0,9.0 28 | ``` 29 | 30 | ## Strategies 31 | 32 | The strategies are defined as C++ subclasses of the `Strategy` class. 33 | 34 | ## Dependencies 35 | 36 | `make` and `clang` or `g++` (`make CXX=g++` in this case). -------------------------------------------------------------------------------- /circular_buffer.hpp: -------------------------------------------------------------------------------- 1 | #include "assert.h" 2 | #include 3 | 4 | using namespace std; 5 | 6 | template 7 | class CircularBuffer 8 | { 9 | public: 10 | 11 | CircularBuffer(unsigned size = 100) 12 | { 13 | this->size = size; 14 | this->next_slot = 0; 15 | buffer = new T[size]; 16 | }; 17 | 18 | ~CircularBuffer() 19 | { 20 | delete[] buffer; 21 | }; 22 | 23 | void insert(const T& value) 24 | { 25 | buffer[next_slot] = value; 26 | ++next_slot; 27 | if (next_slot >= size) 28 | next_slot = 0; 29 | }; 30 | 31 | const T& get(unsigned lookback = 0) const 32 | { 33 | assert(lookback < size); 34 | int index = (next_slot - 1 - lookback + size) % size; 35 | return buffer[index]; 36 | }; 37 | 38 | private: 39 | 40 | T* buffer; 41 | unsigned next_slot; 42 | unsigned size; 43 | }; 44 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "tick.hpp" 5 | #include "strategies/simple_pips_diff_trigger.hpp" 6 | 7 | int main(int, char**) 8 | { 9 | FILE* market = fopen("data/XBTEUR.csv", "r"); 10 | FILE* positions = fopen("positions.dat", "w"); 11 | char buffer[200]; 12 | Tick tick; 13 | unsigned line = 0; 14 | vector strategies; 15 | 16 | // strategy 17 | printf(" parameters:"); 18 | for(float t = 5; t <= 5; t += 2) 19 | { 20 | Strategy *strategy = new SimplePipsDiffTrigger(t); 21 | strategies.push_back(strategy); 22 | printf(",%9s", strategy->name().c_str()); 23 | } 24 | printf("\n"); 25 | 26 | while(fgets(buffer, 200, market) != NULL) 27 | { 28 | tick.time = atoi(strtok(buffer, ",")); 29 | tick.bid = atof(strtok(NULL, ",")); 30 | tick.ask = atof(strtok(NULL, ",")); 31 | tick.bid_volume = atof(strtok(NULL, ",")); 32 | tick.ask_volume = atof(strtok(NULL, ",")); 33 | line++; 34 | 35 | for(auto& strategy : strategies) 36 | strategy->new_tick(tick); 37 | 38 | if (line % 10000 == 0) 39 | { 40 | printf(" %d", tick.time); 41 | for(auto& strategy : strategies) 42 | printf(" ,%+7.0f (%3lu)", strategy->gain(), strategy->positions().size()); 43 | printf("\n"); 44 | } 45 | } 46 | 47 | for(auto& strategy : strategies) { 48 | float gain = 0; 49 | for(auto& position : strategy->positions()) { 50 | if (position.type == Position::SHORT) { 51 | fprintf(positions, "%d, %f, %d, %f,,,, %f\n", position.open_time, position.open_price, position.close_time - position.open_time, position.close_price - position.open_price, gain); 52 | } else { 53 | fprintf(positions, "%d,,,, %f, %d, %f, %f\n", position.open_time, position.open_price, position.close_time - position.open_time, position.close_price - position.open_price, gain); 54 | } 55 | gain += position.gain(); 56 | } 57 | } 58 | 59 | fclose(market); 60 | fclose(positions); 61 | } 62 | -------------------------------------------------------------------------------- /plot.gnu: -------------------------------------------------------------------------------- 1 | set terminal wxt size 2500,1380 enhanced font "arial,10" 2 | set datafile separator "," 3 | set xdata time 4 | set timefmt "%s" 5 | set ylabel "EUR" 6 | set grid 7 | # set xrange ["1389174260":"1389184260"] 8 | set pointsize 1 9 | set style line 1 lt 2 lc rgb "#555555" lw 1 10 | set style line 2 lt 2 lc rgb "#55ee22" lw 2 11 | set style arrow 2 head filled lc rgb "#ff3300" lw 2 12 | set style arrow 3 head filled lc rgb "#5588ff" lw 2 13 | 14 | set lmargin 10 15 | 16 | set multiplot 17 | set size 1, 0.6 18 | set origin 0, 0.4 19 | 20 | plot "data/XBTEUR.csv" every 60 using 1:2:3 title 'krakenEUR' with filledcu ls 1, "positions.dat" using 1:2:3:4 title 'Short positions' with vectors arrowstyle 2, "positions.dat" using 1:5:6:7 title 'Long positions' with vectors arrowstyle 3 21 | 22 | 23 | set size 1, 0.4 24 | set origin 0, 0 25 | 26 | plot "positions.dat" using 1:8 title 'Gain' with lines ls 2 27 | unset multiplot -------------------------------------------------------------------------------- /position.cpp: -------------------------------------------------------------------------------- 1 | #include "position.hpp" 2 | 3 | float Position::diff() const 4 | { 5 | return (close_price - open_price) * type; 6 | } 7 | 8 | float Position::gain() const 9 | { 10 | return diff() * lots - (abs(diff()) * lots * fee); 11 | } 12 | -------------------------------------------------------------------------------- /position.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace std; 5 | 6 | class Position 7 | { 8 | public: 9 | 10 | enum Type { SHORT = -1, LONG = 1 }; 11 | 12 | float diff() const; 13 | float gain() const; 14 | 15 | Type type; 16 | int open_time, close_time; 17 | float open_price, close_price; 18 | float lots; 19 | float fee; 20 | }; -------------------------------------------------------------------------------- /strategies/simple_pips_diff_trigger.cpp: -------------------------------------------------------------------------------- 1 | #include "simple_pips_diff_trigger.hpp" 2 | 3 | SimplePipsDiffTrigger::SimplePipsDiffTrigger(float trigger) 4 | { 5 | this->trigger = trigger; 6 | this->min = 1000000; 7 | this->max = 0; 8 | char buff[100]; 9 | sprintf(buff, "t=%.3f", trigger); 10 | this->_name = buff; 11 | } 12 | 13 | void SimplePipsDiffTrigger::tick(const Tick& tick) 14 | { 15 | if (short_positions.empty() && tick.bid < (max - trigger)) // Go short 16 | { 17 | if (!long_positions.empty()) 18 | close_buy(); 19 | sell(); 20 | max = 0; 21 | } 22 | if (long_positions.empty() && tick.ask > (min + trigger)) // Go long 23 | { 24 | if (!short_positions.empty()) 25 | close_sell(); 26 | buy(); 27 | min = 1000000; 28 | } 29 | if (tick.ask < min) 30 | min = tick.ask; 31 | if (tick.bid > max) 32 | max = tick.bid; 33 | } -------------------------------------------------------------------------------- /strategies/simple_pips_diff_trigger.hpp: -------------------------------------------------------------------------------- 1 | #include "strategy.hpp" 2 | 3 | class SimplePipsDiffTrigger: public Strategy 4 | { 5 | public: 6 | 7 | SimplePipsDiffTrigger(float trigger = 1); 8 | void tick(const Tick& tick); 9 | 10 | private: 11 | 12 | float min; 13 | float max; 14 | float trigger; 15 | }; -------------------------------------------------------------------------------- /strategy.cpp: -------------------------------------------------------------------------------- 1 | #include "strategy.hpp" 2 | 3 | Strategy::Strategy() 4 | { 5 | this->_gain = 0; 6 | this->fee = 0.0026; // 0.26 % -> base taker kraken fee 7 | this->_name = "Strategy"; 8 | } 9 | 10 | void Strategy::new_tick(const Tick& tick) 11 | { 12 | last_ticks.insert(tick); 13 | this->tick(tick); 14 | } 15 | 16 | float Strategy::gain() const 17 | { 18 | return _gain; 19 | } 20 | 21 | const vector& Strategy::positions() const 22 | { 23 | return history; 24 | } 25 | 26 | const string& Strategy::name() const 27 | { 28 | return _name; 29 | } 30 | 31 | void Strategy::buy(float lots) 32 | { 33 | Position pos; 34 | 35 | pos.type = Position::Type::LONG; 36 | pos.open_price = last_ticks.get(0).ask; 37 | pos.open_time = last_ticks.get(0).time; 38 | pos.lots = lots; 39 | pos.fee = this->fee; 40 | 41 | long_positions.push_back(pos); 42 | } 43 | 44 | void Strategy::sell(float lots) 45 | { 46 | Position pos; 47 | 48 | pos.type = Position::Type::SHORT; 49 | pos.open_price = last_ticks.get(0).bid; 50 | pos.open_time = last_ticks.get(0).time; 51 | pos.lots = lots; 52 | pos.fee = this->fee; 53 | 54 | short_positions.push_back(pos); 55 | } 56 | 57 | void Strategy::close_buy() 58 | { 59 | Position pos = long_positions.back(); 60 | 61 | long_positions.pop_back(); 62 | pos.close_price = last_ticks.get(0).bid; 63 | pos.close_time = last_ticks.get(0).time; 64 | history.push_back(pos); 65 | 66 | _gain += pos.gain(); 67 | // printf("close LONG %f -> %f (%+5.1f) gain: %+.1f\n", pos.open_price, pos.close_price, pos.diff(), gain()); 68 | } 69 | 70 | void Strategy::close_sell() 71 | { 72 | Position pos = short_positions.back(); 73 | 74 | short_positions.pop_back(); 75 | pos.close_price = last_ticks.get(0).ask; 76 | pos.close_time = last_ticks.get(0).time; 77 | history.push_back(pos); 78 | 79 | _gain += pos.gain(); 80 | // printf("close SHORT %f -> %f (%+5.1f) gain: %+.1f\n", pos.open_price, pos.close_price, pos.diff(), gain()); 81 | } 82 | 83 | -------------------------------------------------------------------------------- /strategy.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "circular_buffer.hpp" 3 | #include "tick.hpp" 4 | #include "position.hpp" 5 | 6 | using namespace std; 7 | 8 | class Strategy 9 | { 10 | public: 11 | 12 | Strategy(); 13 | 14 | // Called when a new tick arrives on the market 15 | void new_tick(const Tick& tick); 16 | float gain() const; 17 | const vector& positions() const; 18 | const string& name() const; 19 | 20 | protected: 21 | 22 | // Methods dedicated to implementations (subclasses) 23 | virtual void tick(const Tick& tick) = 0; 24 | void buy(float lots = 1); 25 | void sell(float lots = 1); 26 | void close_buy(); 27 | void close_sell(); 28 | 29 | CircularBuffer last_ticks; 30 | vector long_positions; 31 | vector short_positions; 32 | string _name; 33 | 34 | private: 35 | 36 | vector history; 37 | float _gain; 38 | float fee; 39 | }; 40 | -------------------------------------------------------------------------------- /tests/circular_buffer_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "circular_buffer.hpp" 3 | 4 | int main() 5 | { 6 | CircularBuffer cb(3); 7 | 8 | // push item 9 | cb.insert(1); 10 | assert(cb.get(0) == 1); 11 | // push again 12 | cb.insert(2); 13 | assert(cb.get(0) == 2); 14 | assert(cb.get(1) == 1); 15 | // push again 16 | cb.insert(3); 17 | assert(cb.get(0) == 3); 18 | assert(cb.get(1) == 2); 19 | assert(cb.get(2) == 1); 20 | // cycles 21 | cb.insert(4); 22 | assert(cb.get(0) == 4); 23 | assert(cb.get(1) == 3); 24 | assert(cb.get(2) == 2); 25 | } -------------------------------------------------------------------------------- /tick.cpp: -------------------------------------------------------------------------------- 1 | #include "tick.hpp" 2 | -------------------------------------------------------------------------------- /tick.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace std; 4 | 5 | #ifndef TICK_HPP 6 | #define TICK_HPP 7 | 8 | class Tick 9 | { 10 | public: 11 | int time; 12 | float bid, ask; 13 | float bid_volume, ask_volume; 14 | }; 15 | 16 | #endif --------------------------------------------------------------------------------