commit 8969a3d9ea7264780614ba73c5d0ff73cebd42bd Author: Denis Tereshkin Date: Wed Apr 11 00:15:56 2018 +0700 Initial commit diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..bb5fdc6 --- /dev/null +++ b/.hgignore @@ -0,0 +1,3 @@ +syntax: glob +.* +*/__pycache__/* diff --git a/data/__init__.py b/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/series.py b/data/series.py new file mode 100644 index 0000000..6f1b4f2 --- /dev/null +++ b/data/series.py @@ -0,0 +1,53 @@ +''' +''' +import csv +import datetime + +class Series: + + def __init__(self): + self.data = [] + + def load_from_finam_csv(self, filepath): + with open(filepath) as fp: + self.ticker_ = None + reader = csv.reader(fp, delimiter=',') + next(reader) + for row in reader: + try: + self.ticker_ = row[0] + open_ = float(row[4]) + high = float(row[5]) + low = float(row[6]) + close = float(row[7]) + volume = int(row[8]) + date = row[2] + time = row[3] + dt = datetime.datetime.strptime(date + "_" + time, "%Y%m%d_%H%M%S") + self.append_bar(dt, open_, high, low, close, volume) + except IndexError: + pass + + def append_bar(self, dt, open_, high, low, close, volume): + self.data.append((dt, open_, high, low, close, volume)) + + def get_dt(self, index): + return self.data[index][0] + + def get_open(self, index): + return self.data[index][1] + + def get_high(self, index): + return self.data[index][2] + + def get_low(self, index): + return self.data[index][3] + + def get_close(self, index): + return self.data[index][4] + + def get_volume(self, index): + return self.data[index][5] + + def length(self): + return len(self.data) \ No newline at end of file diff --git a/data/signal.py b/data/signal.py new file mode 100644 index 0000000..1cb7e89 --- /dev/null +++ b/data/signal.py @@ -0,0 +1,186 @@ +''' +''' +import random + +import talib +import numpy + +class Signal: + + def __init__(self): + pass + + def calculate(self, series): + pass + + def get_text(self): + pass + +class PriceComparisonSignalGenerator: + + def __init__(self): + pass + + def generate(self): + lhs = random.randint(PriceComparisonSignal.OPEN, PriceComparisonSignal.CLOSE) + lhs_shift = random.randint(0, 10) + rhs = random.randint(PriceComparisonSignal.OPEN, PriceComparisonSignal.CLOSE) + rhs_shift = random.randint(0, 10) + return PriceComparisonSignal(lhs, lhs_shift, rhs, rhs_shift) + +class PriceComparisonSignal(Signal): + + OPEN = 0 + HIGH = 1 + LOW = 2 + CLOSE = 3 + + def __init__(self, lhs, lhs_shift, rhs, rhs_shift): + self.lhs = lhs + self.lhs_shift = lhs_shift + self.rhs = rhs + self.rhs_shift = rhs_shift + + def calculate(self, series): + result = [] + for i in range(0, series.length()): + result.append(self.calculate_at_index(series, i)) + + return result + + def calculate_at_index(self, series, index): + try: + if self.lhs == PriceComparisonSignal.OPEN: + lhs = series.get_open(index - self.lhs_shift) + elif self.lhs == PriceComparisonSignal.HIGH: + lhs = series.get_high(index - self.lhs_shift) + elif self.lhs == PriceComparisonSignal.LOW: + lhs = series.get_low(index - self.lhs_shift) + elif self.lhs == PriceComparisonSignal.CLOSE: + lhs = series.get_close(index - self.lhs_shift) + else: + raise Exception('Invalid lhs type') + + if self.rhs == PriceComparisonSignal.OPEN: + rhs = series.get_open(index - self.rhs_shift) + elif self.rhs == PriceComparisonSignal.HIGH: + rhs = series.get_high(index - self.rhs_shift) + elif self.rhs == PriceComparisonSignal.LOW: + rhs = series.get_low(index - self.rhs_shift) + elif self.rhs == PriceComparisonSignal.CLOSE: + rhs = series.get_close(index - self.rhs_shift) + else: + raise Exception('Invalid lhs type') + + return lhs < rhs + + except IndexError: + return False + + def get_text(self): + return self.component_to_str(self.lhs) + '[' + str(self.lhs_shift) + '] < ' + self.component_to_str(self.rhs) + '[' + str(self.rhs_shift) + ']' + + def component_to_str(self, component): + if component == PriceComparisonSignal.OPEN: + return "open" + elif component == PriceComparisonSignal.HIGH: + return "high" + elif component == PriceComparisonSignal.LOW: + return "low" + elif component == PriceComparisonSignal.CLOSE: + return "close" + else: + return "??" + + +class RsiSignalGenerator: + + def __init__(self): + pass + + def generate(self): + period = random.randint(2, 30) + threshold = random.randrange(1, 9) * 10 + ineq_type = random.randint(RsiSignal.LT, RsiSignal.GT) + return RsiSignal(period, threshold, ineq_type) + +class RsiSignal(Signal): + + LT = 0 + GT = 1 + + def __init__(self, period, threshold, inequality_type): + self.period = period + self.threshold = threshold + self.inequality_type = inequality_type + if inequality_type == RsiSignal.LT: + self.inequality_sign_str = '<' + else: + self.inequality_sign_str = '>' + + def calculate(self, series): + closes = numpy.array([series.get_close(i) for i in range(0, series.length())]) + + rsi = talib.RSI(closes, self.period) + + result = [self.calc_signal(v) for v in rsi] + return result + + def calc_signal(self, value): + if self.inequality_type == RsiSignal.LT: + return value < self.threshold + else: + return value > self.threshold + + def get_text(self): + return "rsi(c, " + str(self.period) + ') ' + self.inequality_sign_str + ' ' + str(self.threshold) + + + +class AtrSignalGenerator: + + def __init__(self): + pass + + def generate(self): + period = random.randint(2, 30) + threshold = random.randint(1, 30) * 0.001 + ineq_type = random.randint(AtrSignal.LT, AtrSignal.GT) + return AtrSignal(period, threshold, ineq_type) + +class AtrSignal(Signal): + + LT = 0 + GT = 1 + + def __init__(self, period, threshold_factor, inequality_type): + self.period = period + self.threshold_factor = threshold_factor + self.inequality_type = inequality_type + if inequality_type == RsiSignal.LT: + self.inequality_sign_str = '<' + else: + self.inequality_sign_str = '>' + + def calculate(self, series): + closes = numpy.array([series.get_close(i) for i in range(0, series.length())]) + highs = numpy.array([series.get_high(i) for i in range(0, series.length())]) + lows = numpy.array([series.get_low(i) for i in range(0, series.length())]) + + atr = talib.ATR(highs, lows, closes, self.period) + + result = [self.calc_signal(v, c) for (v, c) in zip(atr, closes)] + return result + + def calc_signal(self, value, close): + if self.inequality_type == AtrSignal.LT: + return value < self.threshold_factor * close + else: + return value > self.threshold_factor * close + + def get_text(self): + return "atr(" + str(self.period) + ') ' + self.inequality_sign_str + ' close[0] * ' + "{:.3f}".format(self.threshold_factor) + + + + \ No newline at end of file diff --git a/edgeyopt.py b/edgeyopt.py new file mode 100644 index 0000000..c69b2a6 --- /dev/null +++ b/edgeyopt.py @@ -0,0 +1,17 @@ +''' + +''' + +import sys + +from PyQt5.QtWidgets import QApplication + +from ui.mainwindow import MainWindow + +if __name__ == '__main__': + app = QApplication(sys.argv) + + wnd = MainWindow() + wnd.show() + + app.exec_() \ No newline at end of file diff --git a/execution/__init__.py b/execution/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/execution/executor.py b/execution/executor.py new file mode 100644 index 0000000..9ba9b7a --- /dev/null +++ b/execution/executor.py @@ -0,0 +1,52 @@ +''' +''' + +from .trade import Trade + +class Executor(object): + ''' + ''' + + + def __init__(self, series): + ''' + Constructor + ''' + self.series = series + self.max_hold_bars = 1 + + def execute(self, signals): + self.trades = [] + sig_vectors = [] + vec_length = 0 + for signal in signals: + vec = signal.calculate(self.series) + sig_vectors.append(vec) + if vec_length == 0: + vec_length = len(vec) + else: + assert(vec_length == len(vec)) + + in_trade = False + current_entry_price = None + bar_counter = 0 + + for i in range(0, vec_length): + if not in_trade: + has_signal = True + for vec in sig_vectors: + if vec[i] is None or vec[i] == False: + has_signal = False + break + + if has_signal and i + 1 < vec_length: + in_trade = True + current_entry_price = self.series.get_open(i + 1) + bar_counter = 0 + else: + bar_counter += 1 + if bar_counter >= self.max_hold_bars: + in_trade = False + self.trades.append(Trade(current_entry_price, self.series.get_close(i))) + + return self.trades \ No newline at end of file diff --git a/execution/trade.py b/execution/trade.py new file mode 100644 index 0000000..c24b7da --- /dev/null +++ b/execution/trade.py @@ -0,0 +1,31 @@ +''' +''' + +class Trade: + ''' + ''' + + LONG = 1 + SHORT = 2 + + def __init__(self, entry_price, exit_price, direction = LONG): + ''' + Constructor + ''' + + self.direction = direction + self.entry_price = entry_price + self.exit_price = exit_price + + + def pnl(self): + if self.direction == Trade.LONG: + return self.exit_price - self.entry_price + else: + return self.entry_price - self.exit_price + + def pnl_percentage(self): + if self.direction == Trade.LONG: + return (self.exit_price - self.entry_price) / self.entry_price * 100 + else: + return (self.entry_price - self.exit_price) / self.entry_price * 100 \ No newline at end of file diff --git a/solver/__init__.py b/solver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solver/solver.py b/solver/solver.py new file mode 100644 index 0000000..ecea999 --- /dev/null +++ b/solver/solver.py @@ -0,0 +1,79 @@ +''' +''' +from data import series +from execution.executor import Executor + +import random +from math import inf +import numpy + +class Solver(): + ''' + + ''' + + + def __init__(self, series): + ''' + Constructor + ''' + + self.series = series + self.executor = Executor(series) + self.generators = [] + + def add_generator(self, generator): + self.generators.append(generator) + + def solve(self): + max_signals = 3 + max_strategies = 1000 + self.results = [] + + for x in range(0, max_strategies): + sig_num = random.randint(1, max_signals) + strategy = [] + for i in range(0, sig_num): + strategy.append(random.choice(self.generators).generate()) + + trades = self.executor.execute(strategy) + result = self.evaluate_trades(trades) + result['display_name'] = ' && '.join([signal.get_text() for signal in strategy]) + self.results.append(result) + + return self.results + + def evaluate_trades(self, trades): + result = {} + + profits = [x.pnl() for x in trades] + + result['trades_number'] = len(trades) + result['total_pnl'] = sum(profits) + + if len(trades) > 0: + result['avg_percentage'] = sum([trade.pnl_percentage() for trade in trades]) / len(trades) + else: + result['avg_percentage'] = 0 + + gross_profit = sum([max(0, x.pnl()) for x in trades]) + gross_loss = sum([min(0, x.pnl()) for x in trades]) + + if gross_loss != 0: + result['profit_factor'] = gross_profit / (-gross_loss) + else: + result['profit_factor'] = inf + + if len(profits) > 0: + mean = numpy.mean(profits) + stddev = numpy.std(profits) + if stddev != 0: + result['sharpe'] = mean / stddev + else: + result['sharpe'] = 0 + else: + result['sharpe'] = 0 + + return result + + \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/mainwindow.py b/ui/mainwindow.py new file mode 100644 index 0000000..b7fbec9 --- /dev/null +++ b/ui/mainwindow.py @@ -0,0 +1,43 @@ +''' +''' + +from PyQt5.QtWidgets import QMainWindow, QTreeWidgetItem + +from .ui_mainwindow import Ui_MainWindow +from solver.solver import Solver +from data.series import Series +from data.signal import PriceComparisonSignalGenerator, RsiSignalGenerator,\ + AtrSignalGenerator +from PyQt5.Qt import Qt + +class MainWindow(QMainWindow, Ui_MainWindow): + ''' + ''' + + def __init__(self, parent=None): + ''' + Constructor + ''' + super().__init__(parent) + self.setupUi(self) + + self.series = Series() + self.series.load_from_finam_csv('/home/asakul/tmp/daily/RTSI_20000101_20171231_daily.csv') + self.solver = Solver(self.series) + self.solver.add_generator(PriceComparisonSignalGenerator()) + self.solver.add_generator(RsiSignalGenerator()) + self.solver.add_generator(AtrSignalGenerator()) + results = self.solver.solve() + + for result in results: + item = QTreeWidgetItem(self.tw_strategies) + item.setText(0, result['display_name']) + item.setText(1, str(result['trades_number'])) + item.setText(2, str(result['total_pnl'])) + item.setText(3, "{:.2f}".format(result['profit_factor'])) + item.setText(4, "{:.2f}".format(result['sharpe'])) + item.setText(5, "{:.2f}%".format(result['avg_percentage'])) + item.setData(0, Qt.UserRole + 1, result) + + def strategyClicked(self, item, column): + result = item.getData(0, Qt.UserRole + 1) \ No newline at end of file diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui new file mode 100644 index 0000000..5b7eff9 --- /dev/null +++ b/ui/mainwindow.ui @@ -0,0 +1,103 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + true + + + + Strategy + + + + + Trades # + + + + + Total PnL + + + + + PF + + + + + Sharpe + + + + + Avg. % + + + + + + + + + + 0 + 0 + 800 + 27 + + + + + + + + + tw_strategies + itemClicked(QTreeWidgetItem*,int) + MainWindow + strategyClicked(QTreeWidgetItem*,int) + + + 106 + 103 + + + 800 + 33 + + + + + + strategyClicked(QTreeWidgetItem*,int) + + diff --git a/ui/ui_mainwindow.py b/ui/ui_mainwindow.py new file mode 100644 index 0000000..91bed5a --- /dev/null +++ b/ui/ui_mainwindow.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mainwindow.ui' +# +# Created by: PyQt5 UI code generator 5.5.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 600) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout.setContentsMargins(2, 2, 2, 2) + self.gridLayout.setObjectName("gridLayout") + self.tw_strategies = QtWidgets.QTreeWidget(self.centralwidget) + self.tw_strategies.setObjectName("tw_strategies") + self.gridLayout.addWidget(self.tw_strategies, 0, 0, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 27)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + self.tw_strategies.itemClicked['QTreeWidgetItem*','int'].connect(MainWindow.strategyClicked) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.tw_strategies.setSortingEnabled(True) + self.tw_strategies.headerItem().setText(0, _translate("MainWindow", "Strategy")) + self.tw_strategies.headerItem().setText(1, _translate("MainWindow", "Trades #")) + self.tw_strategies.headerItem().setText(2, _translate("MainWindow", "Total PnL")) + self.tw_strategies.headerItem().setText(3, _translate("MainWindow", "PF")) + self.tw_strategies.headerItem().setText(4, _translate("MainWindow", "Sharpe")) + self.tw_strategies.headerItem().setText(5, _translate("MainWindow", "Avg. %")) +