├── tb_callbacks ├── __init__.py └── SaveOnStepCallback.py ├── logs └── .gitignore ├── models └── .gitignore ├── tensorboard └── .gitignore ├── user_data ├── data │ └── .gitignore ├── logs │ └── .gitignore ├── plot │ └── .gitignore ├── backtest_results │ └── .gitignore ├── hyperopt_results │ └── .gitignore ├── hyperopts │ └── sample_hyperopt_loss.py ├── config.json ├── strategies │ ├── FreqGym.py │ └── FreqGym_normalized.py └── notebooks │ └── strategy_analysis_example.ipynb ├── trading_environments ├── __init__.py ├── SimpleROIEnv.py ├── FreqtradeEnv.py └── GymAnytrading.py ├── README.md ├── environment_cpuonly.yml ├── environment_cuda.yml ├── .gitignore ├── train_freqtrade.py └── LICENSE /tb_callbacks/__init__.py: -------------------------------------------------------------------------------- 1 | from .SaveOnStepCallback import SaveOnStepCallback 2 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /models/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /tensorboard/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /user_data/data/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /user_data/logs/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /user_data/plot/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /user_data/backtest_results/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /user_data/hyperopt_results/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /trading_environments/__init__.py: -------------------------------------------------------------------------------- 1 | from .SimpleROIEnv import SimpleROIEnv 2 | from .FreqtradeEnv import FreqtradeEnv 3 | from .GymAnytrading import GymAnytrading 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freqgym 2 | Combining freqtrade with OpenAI Reinforcement Learning environments 3 | 4 | ## Installation with Conda 5 | 6 | freqgym can be installed with Miniconda or Anaconda. 7 | 8 | ### What is Conda? 9 | 10 | Conda is a package, dependency and environment manager for multiple programming languages: [conda docs](https://docs.conda.io/projects/conda/en/latest/index.html) 11 | 12 | ### Installation with conda 13 | 14 | Prepare freqgym environment, using file `environment_cpuonly.yml` or `environment_cuda.yml`, which exist in main freqgym directory 15 | 16 | ```bash 17 | conda env create -n freqgym -f environment_cpuonly.yml 18 | ``` 19 | 20 | #### Enter/exit freqtrade-conda environment 21 | 22 | To check available environments, type 23 | 24 | ```bash 25 | conda env list 26 | ``` 27 | 28 | Enter installed environment 29 | 30 | ```bash 31 | # enter conda environment 32 | conda activate freqgym 33 | 34 | # exit conda environment 35 | conda deactivate 36 | ``` 37 | 38 | Download data with: 39 | ` 40 | freqtrade download-data --timerange=20201201-20211015 --timeframe 5m --exchange=binance --erase --pairs BTC/USDT 41 | ` 42 | 43 | TODO 44 | 45 | - Multiple pair training 46 | - Vectorized envs 47 | - Normalization 48 | -------------------------------------------------------------------------------- /environment_cpuonly.yml: -------------------------------------------------------------------------------- 1 | name: freqgym 2 | channels: 3 | - pytorch 4 | - conda-forge 5 | # - defaults 6 | dependencies: 7 | # 1/5 req main 8 | - python>=3.7,<3.9 9 | - numpy 10 | - pandas 11 | - pip 12 | 13 | - aiohttp 14 | - SQLAlchemy 15 | - python-telegram-bot 16 | - arrow 17 | - cachetools 18 | - requests 19 | - urllib3 20 | - jsonschema 21 | - TA-Lib 22 | - tabulate 23 | - jinja2 24 | - blosc 25 | - sdnotify 26 | - fastapi 27 | - uvicorn 28 | - pyjwt 29 | - colorama 30 | - questionary 31 | - prompt-toolkit 32 | 33 | 34 | # ============================ 35 | # 2/5 req dev 36 | - black 37 | - coveralls 38 | - flake8 39 | - mypy 40 | - pylint 41 | - pytest 42 | - pytest-asyncio 43 | - pytest-cov 44 | - pytest-mock 45 | - isort 46 | - nbconvert 47 | 48 | # ============================ 49 | # 3/5 req hyperopt 50 | - scipy 51 | - scikit-learn 52 | - filelock 53 | - scikit-optimize 54 | - joblib 55 | - progressbar2 56 | # ============================ 57 | # 4/5 req plot 58 | - plotly 59 | - jupyter 60 | 61 | - pip: 62 | - pycoingecko 63 | - py_find_1st 64 | - tables 65 | - pytest-random-order 66 | - ccxt 67 | - flake8-tidy-imports 68 | # ============================ 69 | # 4/5 req baselines3 70 | - pytorch 71 | - cpuonly 72 | - tensorboard 73 | 74 | - pip: 75 | - mpu 76 | - ta 77 | - freqtrade 78 | - stable-baselines3 79 | - sb3-contrib 80 | - tensortrade -------------------------------------------------------------------------------- /environment_cuda.yml: -------------------------------------------------------------------------------- 1 | name: freqgym 2 | channels: 3 | - pytorch 4 | - conda-forge 5 | # - defaults 6 | dependencies: 7 | # 1/5 req main 8 | - python>=3.7,<3.9 9 | - numpy 10 | - pandas 11 | - pip 12 | 13 | - aiohttp 14 | - SQLAlchemy 15 | - python-telegram-bot 16 | - arrow 17 | - cachetools 18 | - requests 19 | - urllib3 20 | - jsonschema 21 | - TA-Lib 22 | - tabulate 23 | - jinja2 24 | - blosc 25 | - sdnotify 26 | - fastapi 27 | - uvicorn 28 | - pyjwt 29 | - colorama 30 | - questionary 31 | - prompt-toolkit 32 | 33 | 34 | # ============================ 35 | # 2/5 req dev 36 | 37 | - black 38 | - coveralls 39 | - flake8 40 | - mypy 41 | - pylint 42 | - pytest 43 | - pytest-asyncio 44 | - pytest-cov 45 | - pytest-mock 46 | - isort 47 | - nbconvert 48 | 49 | # ============================ 50 | # 3/5 req hyperopt 51 | 52 | - scipy 53 | - scikit-learn 54 | - filelock 55 | - scikit-optimize 56 | - joblib 57 | - progressbar2 58 | # ============================ 59 | # 4/5 req plot 60 | 61 | - plotly 62 | - jupyter 63 | 64 | - pip: 65 | - pycoingecko 66 | - py_find_1st 67 | - tables 68 | - pytest-random-order 69 | - ccxt 70 | - flake8-tidy-imports 71 | # ============================ 72 | # 4/5 req baselines3 73 | - pytorch 74 | - cudatoolkit=11.3 75 | - tensorboard 76 | 77 | - pip: 78 | - mpu 79 | - ta 80 | - freqtrade 81 | - stable-baselines3 82 | - sb3-contrib 83 | - tensortrade -------------------------------------------------------------------------------- /user_data/hyperopts/sample_hyperopt_loss.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from math import exp 3 | from typing import Dict 4 | 5 | from pandas import DataFrame 6 | 7 | from freqtrade.optimize.hyperopt import IHyperOptLoss 8 | 9 | 10 | # Define some constants: 11 | 12 | # set TARGET_TRADES to suit your number concurrent trades so its realistic 13 | # to the number of days 14 | TARGET_TRADES = 600 15 | # This is assumed to be expected avg profit * expected trade count. 16 | # For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades, 17 | # self.expected_max_profit = 3.85 18 | # Check that the reported Σ% values do not exceed this! 19 | # Note, this is ratio. 3.85 stated above means 385Σ%. 20 | EXPECTED_MAX_PROFIT = 3.0 21 | 22 | # max average trade duration in minutes 23 | # if eval ends with higher value, we consider it a failed eval 24 | MAX_ACCEPTED_TRADE_DURATION = 300 25 | 26 | 27 | class SampleHyperOptLoss(IHyperOptLoss): 28 | """ 29 | Defines the default loss function for hyperopt 30 | This is intended to give you some inspiration for your own loss function. 31 | 32 | The Function needs to return a number (float) - which becomes smaller for better backtest 33 | results. 34 | """ 35 | 36 | @staticmethod 37 | def hyperopt_loss_function(results: DataFrame, trade_count: int, 38 | min_date: datetime, max_date: datetime, 39 | config: Dict, processed: Dict[str, DataFrame], 40 | *args, **kwargs) -> float: 41 | """ 42 | Objective function, returns smaller number for better results 43 | """ 44 | total_profit = results['profit_ratio'].sum() 45 | trade_duration = results['trade_duration'].mean() 46 | 47 | trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) 48 | profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) 49 | duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) 50 | result = trade_loss + profit_loss + duration_loss 51 | return result 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # VSCode 132 | .vscode/ -------------------------------------------------------------------------------- /tb_callbacks/SaveOnStepCallback.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | from stable_baselines3.common.callbacks import BaseCallback 5 | from stable_baselines3.common.monitor import load_results 6 | from stable_baselines3.common.results_plotter import ts2xy 7 | 8 | 9 | class SaveOnStepCallback(BaseCallback): 10 | 11 | def __init__(self, check_freq: int, save_name: str, save_dir: str, log_dir: str, verbose=0): 12 | super(SaveOnStepCallback, self).__init__(verbose) 13 | 14 | self.check_freq = check_freq 15 | self.log_dir = Path(log_dir) 16 | self.save_path = Path(save_dir) / save_name 17 | self.best_mean_reward = -np.inf 18 | self.best_net_worth = -np.inf 19 | 20 | assert self.log_dir.exists() 21 | assert Path(save_dir).exists() 22 | 23 | def _on_step(self) -> bool: 24 | # Log scalar value (here a random variable) 25 | # total_profit = self.training_env.buf_infos[0]['total_profit'] 26 | # self.logger.record('total_profit', total_profit) 27 | 28 | # total_reward = self.training_env.buf_infos[0]['total_reward'] 29 | # self.logger.record('total_reward', total_reward) 30 | 31 | net_worth = self.training_env.buf_infos[0]['net_worth'] 32 | self.logger.record('net_worth', net_worth) 33 | 34 | if self.n_calls % self.check_freq == 0: 35 | # Retrieve training reward 36 | x, y = ts2xy(load_results(str(self.log_dir)), 'timesteps') 37 | if len(x) > 0: 38 | # Mean training reward over the last 100 episodes 39 | mean_reward = np.mean(y[-100:]) 40 | if self.verbose: 41 | print(f"Num timesteps: {self.num_timesteps}") 42 | print("Best mean reward: {:.2f} - Last mean reward per episode: {:.2f}".format(self.best_mean_reward, mean_reward)) 43 | 44 | # New best model, lets save 45 | if mean_reward > self.best_mean_reward: 46 | self.best_mean_reward = mean_reward 47 | if self.verbose: 48 | print(f"Saving new best model to {self.save_path}") 49 | self.model.save(self.save_path) # type: ignore 50 | 51 | return True 52 | 53 | def on_rollout_end(self): 54 | net_worth = self.training_env.buf_infos[0]['net_worth'] 55 | if net_worth > self.best_net_worth: 56 | self.best_net_worth = net_worth 57 | save_name = f"{self.save_path}_net_worth" 58 | if self.verbose: 59 | print("Best net worth: {:.2f} - Last net worth: {:.2f}".format(self.best_net_worth, net_worth)) 60 | print(f"Saving new best net worth to {save_name}") 61 | self.model.save(save_name) # type: ignore 62 | -------------------------------------------------------------------------------- /user_data/config.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "strategy": "FreqGym_normalized", 4 | "max_open_trades": 5, 5 | "stake_currency": "USDT", 6 | "stake_amount": 200, 7 | "tradable_balance_ratio": 0.99, 8 | "fiat_display_currency": "USD", 9 | "timeframe": "15m", 10 | "dry_run": true, 11 | "dry_run_wallet": 1000, 12 | "cancel_open_orders_on_exit": false, 13 | "unfilledtimeout": { 14 | "buy": 10, 15 | "sell": 30, 16 | "unit": "minutes" 17 | }, 18 | "bid_strategy": { 19 | "price_side": "bid", 20 | "ask_last_balance": 0.0, 21 | "use_order_book": true, 22 | "order_book_top": 1, 23 | "check_depth_of_market": { 24 | "enabled": false, 25 | "bids_to_ask_delta": 1 26 | } 27 | }, 28 | "ask_strategy": { 29 | "price_side": "ask", 30 | "use_order_book": true, 31 | "order_book_top": 1 32 | }, 33 | "exchange": { 34 | "name": "binance", 35 | "key": "", 36 | "secret": "", 37 | "ccxt_config": {"enableRateLimit": true}, 38 | "ccxt_async_config": { 39 | "enableRateLimit": true, 40 | "rateLimit": 200 41 | }, 42 | "pair_whitelist": [ 43 | "ETH/USDT", 44 | "ADA/USDT", 45 | "EOS/USDT", 46 | "XLM/USDT", 47 | "LTC/USDT", 48 | "TRX/USDT" 49 | ], 50 | "pair_blacklist": [ 51 | "BNB/.*" 52 | ] 53 | }, 54 | "pairlists": [ 55 | { 56 | "method": "StaticPairList" 57 | } 58 | ], 59 | "edge": { 60 | "enabled": false, 61 | "process_throttle_secs": 3600, 62 | "calculate_since_number_of_days": 7, 63 | "allowed_risk": 0.01, 64 | "stoploss_range_min": -0.01, 65 | "stoploss_range_max": -0.1, 66 | "stoploss_range_step": -0.01, 67 | "minimum_winrate": 0.60, 68 | "minimum_expectancy": 0.20, 69 | "min_trade_number": 10, 70 | "max_trade_duration_minute": 1440, 71 | "remove_pumps": false 72 | }, 73 | "telegram": { 74 | "enabled": false, 75 | "token": "", 76 | "chat_id": "" 77 | }, 78 | "api_server": { 79 | "enabled": true, 80 | "listen_ip_address": "127.0.0.1", 81 | "listen_port": 20080, 82 | "verbosity": "error", 83 | "enable_openapi": false, 84 | "jwt_secret_key": "595e9cb58c29748270a739db7aa127fc30a9ecfbf0d11fc9d977de4d8a86037f", 85 | "CORS_origins": [], 86 | "username": "freqtrader", 87 | "password": "freqtrader" 88 | }, 89 | "bot_name": "freqtrade", 90 | "initial_state": "running", 91 | "forcebuy_enable": false, 92 | "internals": { 93 | "process_throttle_secs": 5 94 | } 95 | } -------------------------------------------------------------------------------- /trading_environments/SimpleROIEnv.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import gym 4 | import numpy as np 5 | from gym import spaces 6 | from gym.utils import seeding 7 | 8 | 9 | class Actions(Enum): 10 | Hold = 0 11 | Buy = 1 12 | 13 | class SimpleROIEnv(gym.Env): 14 | 15 | def __init__( 16 | self, 17 | data, 18 | prices, 19 | window_size, 20 | required_startup, 21 | minimum_roi=0.02, 22 | roi_candles=24, 23 | punish_holding_amount=0, 24 | punish_missed_buy=True): 25 | 26 | self.data = data 27 | self.window_size = window_size 28 | self.prices = prices 29 | 30 | self.required_startup = required_startup 31 | self.minimum_roi = minimum_roi 32 | self.roi_candles = roi_candles 33 | self.punish_holding_amount = punish_holding_amount 34 | assert self.punish_holding_amount <= 0, "`punish_holding_amount` should be less or equal to 0" 35 | self.punish_missed_buy = punish_missed_buy 36 | 37 | _, number_of_features = self.data.shape 38 | self.shape = (self.window_size, number_of_features) 39 | 40 | # spaces 41 | self.action_space = spaces.Discrete(len(Actions)) 42 | self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=self.shape, dtype=np.float32) 43 | 44 | self.seed() 45 | 46 | def seed(self, seed=None): 47 | self.np_random, seed = seeding.np_random(seed) 48 | return [seed] 49 | 50 | def reset(self): 51 | self._total_reward = 0. 52 | self._current_tick = self.required_startup + self.window_size + 1 53 | self._end_tick = len(self.data) - self.roi_candles - 1 54 | 55 | return self._get_observation() 56 | 57 | def step(self, action): 58 | done = False 59 | 60 | reward = 0 61 | 62 | if self._current_tick + self.roi_candles >= self._end_tick: 63 | done = True 64 | 65 | current_price = self.prices['close'][self._current_tick] 66 | future_close_price = self.prices['close'][self._current_tick + self.roi_candles] 67 | future_highest_high = self.prices['high'][self._current_tick:self._current_tick + self.roi_candles].max() 68 | 69 | future_highest_high_diff = _pct_change(current_price, future_highest_high) 70 | future_close_price_diff = _pct_change(current_price, future_close_price) 71 | 72 | if action == Actions.Buy.value: 73 | self._current_tick += self.roi_candles 74 | reward = -abs(future_close_price_diff) 75 | if future_highest_high_diff >= self.minimum_roi: 76 | reward = future_highest_high_diff 77 | 78 | if action == Actions.Hold.value: 79 | reward = self.punish_holding_amount 80 | # Missed buying opportunity 81 | if self.punish_missed_buy: 82 | if future_highest_high_diff >= self.minimum_roi: 83 | reward = -future_highest_high_diff 84 | 85 | self._current_tick += 1 86 | 87 | observation = self._get_observation() 88 | 89 | return observation, reward, done, {} 90 | 91 | def _get_observation(self): 92 | return self.data[(self._current_tick-self.window_size):self._current_tick].to_numpy() 93 | 94 | def _pct_change(a, b): 95 | return (b - a) / a 96 | -------------------------------------------------------------------------------- /trading_environments/FreqtradeEnv.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | 4 | import gym 5 | import numpy as np 6 | from freqtrade.persistence import Trade 7 | from gym import spaces 8 | from gym.utils import seeding 9 | 10 | # Based on https://github.com/hugocen/freqtrade-gym/blob/master/freqtradegym.py 11 | 12 | class Actions(Enum): 13 | Hold = 0 14 | Buy = 1 15 | Sell = 2 16 | 17 | class FreqtradeEnv(gym.Env): 18 | """A freqtrade trading environment for OpenAI gym""" 19 | metadata = {'render.modes': ['human', 'system', 'none']} 20 | 21 | def __init__( 22 | self, 23 | data, 24 | prices, 25 | window_size, 26 | pair, 27 | stake_amount, 28 | stop_loss=-0.15, 29 | punish_holding_amount=0, 30 | fee=0.005 31 | ): 32 | 33 | self.data = data 34 | self.window_size = window_size 35 | self.prices = prices 36 | self.pair = pair 37 | self.stake_amount = stake_amount 38 | self.stop_loss = stop_loss 39 | assert self.stop_loss <= 0, "`stoploss` should be less or equal to 0" 40 | self.punish_holding_amount = punish_holding_amount 41 | assert self.punish_holding_amount <= 0, "`punish_holding_amount` should be less or equal to 0" 42 | self.fee = fee 43 | 44 | self.opened_trade = None 45 | self.trades = [] 46 | 47 | self._reward = 0 48 | self.total_reward = 0 49 | 50 | _, number_of_features = self.data.shape 51 | self.shape = (self.window_size, number_of_features) 52 | 53 | self.action_space = spaces.Discrete(len(Actions)) 54 | self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=self.shape, dtype=np.float32) 55 | 56 | self.seed() 57 | 58 | def _get_observation(self): 59 | return self.data[(self._current_tick-self.window_size):self._current_tick].to_numpy() 60 | 61 | def _take_action(self, action): 62 | if action == Actions.Hold.value: 63 | self._reward = self.punish_holding_amount 64 | if (self.opened_trade != None): 65 | profit_percent = self.opened_trade.calc_profit_ratio(rate=self.prices.loc[self._current_tick].open) 66 | if (profit_percent <= self.stop_loss): 67 | self._reward = profit_percent 68 | self.opened_trade = None 69 | return 70 | 71 | if action == Actions.Buy.value: 72 | if self.opened_trade == None: 73 | self.opened_trade = Trade( 74 | pair=self.pair, 75 | open_rate=self.prices.loc[self._current_tick].open, 76 | open_date=self.prices.loc[self._current_tick].date, 77 | stake_amount=self.stake_amount, 78 | amount=self.stake_amount / self.prices.loc[self._current_tick].open, 79 | fee_open=self.fee, 80 | fee_close=self.fee, 81 | is_open=True, 82 | ) 83 | self.trades.append({ 84 | "step": self._current_tick, 85 | "type": 'buy', 86 | "total": self.prices.loc[self._current_tick].open 87 | }) 88 | return 89 | 90 | if action == Actions.Sell.value: 91 | if self.opened_trade != None: 92 | profit_percent = self.opened_trade.calc_profit_ratio(rate=self.prices.loc[self._current_tick].open) 93 | self.opened_trade = None 94 | self._reward = profit_percent 95 | 96 | self.trades.append({ 97 | "step": self._current_tick, 98 | "type": 'sell', 99 | "total": self.prices.loc[self._current_tick].open 100 | }) 101 | return 102 | 103 | def step(self, action): 104 | # Execute one time step within the environment 105 | done = False 106 | 107 | self._reward = 0 108 | 109 | if self._current_tick >= self._end_tick: 110 | done = True 111 | 112 | self._take_action(action) 113 | 114 | self._current_tick += 1 115 | 116 | self.total_reward += self._reward 117 | 118 | observation = self._get_observation() 119 | 120 | return observation, self._reward, done, {} 121 | 122 | def reset(self): 123 | # Reset the state of the environment to an initial state 124 | self.opened_trade = None 125 | self.trades = [] 126 | 127 | self._reward = 0 128 | self.total_reward = 0 129 | 130 | self._current_tick = self.window_size + 1 131 | self._end_tick = len(self.data) - 1 132 | 133 | return self._get_observation() 134 | 135 | def seed(self, seed=None): 136 | self.np_random, seed = seeding.np_random(seed) 137 | return [seed] 138 | -------------------------------------------------------------------------------- /trading_environments/GymAnytrading.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import gym 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from gym import spaces 7 | from gym.utils import seeding 8 | 9 | 10 | class Actions(Enum): 11 | Hold = 0 12 | Buy = 1 13 | Sell = 2 14 | 15 | 16 | class Positions(Enum): 17 | Short = 0 18 | Long = 1 19 | 20 | def opposite(self): 21 | return Positions.Short if self == Positions.Long else Positions.Long 22 | 23 | 24 | class GymAnytrading(gym.Env): 25 | """ 26 | Based on https://github.com/AminHP/gym-anytrading 27 | 28 | """ 29 | 30 | metadata = {'render.modes': ['human']} 31 | 32 | def __init__(self, signal_features, prices, window_size, fee=0.0): 33 | assert signal_features.ndim == 2 34 | 35 | self.seed() 36 | self.signal_features = signal_features 37 | self.prices = prices 38 | self.window_size = window_size 39 | self.fee = fee 40 | self.shape = (window_size, self.signal_features.shape[1]) 41 | 42 | # spaces 43 | self.action_space = spaces.Discrete(len(Actions)) 44 | self.observation_space = spaces.Box(low=-np.inf, high=np.inf, shape=self.shape, dtype=np.float32) 45 | 46 | # episode 47 | self._start_tick = self.window_size 48 | self._end_tick = len(self.prices) - 1 49 | self._done = None 50 | self._current_tick = None 51 | self._last_trade_tick = None 52 | self._position = None 53 | self._position_history = None 54 | self._total_reward = None 55 | self._total_profit = None 56 | self._first_rendering = None 57 | self.history = None 58 | 59 | 60 | def seed(self, seed=None): 61 | self.np_random, seed = seeding.np_random(seed) 62 | return [seed] 63 | 64 | 65 | def reset(self): 66 | self._done = False 67 | self._current_tick = self._start_tick 68 | self._last_trade_tick = self._current_tick - 1 69 | self._position = Positions.Short 70 | self._position_history = (self.window_size * [None]) + [self._position] 71 | self._total_reward = 0. 72 | self._total_profit = 1. # unit 73 | self._first_rendering = True 74 | self.history = {} 75 | return self._get_observation() 76 | 77 | 78 | def step(self, action): 79 | self._done = False 80 | self._current_tick += 1 81 | 82 | if self._current_tick == self._end_tick: 83 | self._done = True 84 | 85 | step_reward = self._calculate_reward(action) 86 | self._total_reward += step_reward 87 | 88 | self._update_profit(action) 89 | 90 | trade = False 91 | if ((action == Actions.Buy.value and self._position == Positions.Short) or 92 | (action == Actions.Sell.value and self._position == Positions.Long)): 93 | trade = True 94 | 95 | if trade: 96 | self._position = self._position.opposite() 97 | self._last_trade_tick = self._current_tick 98 | 99 | self._position_history.append(self._position) 100 | observation = self._get_observation() 101 | info = dict( 102 | total_reward = self._total_reward, 103 | total_profit = self._total_profit, 104 | position = self._position.value 105 | ) 106 | self._update_history(info) 107 | 108 | return observation, step_reward, self._done, info 109 | 110 | 111 | def _get_observation(self): 112 | return self.signal_features[(self._current_tick-self.window_size):self._current_tick] 113 | 114 | 115 | def _update_history(self, info): 116 | if not self.history: 117 | self.history = {key: [] for key in info.keys()} 118 | 119 | for key, value in info.items(): 120 | self.history[key].append(value) 121 | 122 | 123 | def render(self, mode='human'): 124 | def _plot_position(position, tick): 125 | color = None 126 | if position == Positions.Short: 127 | color = 'red' 128 | elif position == Positions.Long: 129 | color = 'green' 130 | if color: 131 | plt.scatter(tick, self.prices[tick], color=color) 132 | 133 | if self._first_rendering: 134 | self._first_rendering = False 135 | plt.cla() 136 | plt.plot(self.prices) 137 | start_position = self._position_history[self._start_tick] 138 | _plot_position(start_position, self._start_tick) 139 | 140 | _plot_position(self._position, self._current_tick) 141 | 142 | plt.suptitle( 143 | "Total Reward: %.6f" % self._total_reward + ' ~ ' + 144 | "Total Profit: %.6f" % self._total_profit 145 | ) 146 | 147 | plt.pause(0.01) 148 | 149 | 150 | def render_all(self, mode='human'): 151 | window_ticks = np.arange(len(self._position_history)) 152 | plt.plot(self.prices) 153 | 154 | short_ticks = [] 155 | long_ticks = [] 156 | for i, tick in enumerate(window_ticks): 157 | if self._position_history[i] == Positions.Short: 158 | short_ticks.append(tick) 159 | elif self._position_history[i] == Positions.Long: 160 | long_ticks.append(tick) 161 | 162 | plt.plot(short_ticks, self.prices[short_ticks], 'ro') 163 | plt.plot(long_ticks, self.prices[long_ticks], 'go') 164 | 165 | plt.suptitle( 166 | "Total Reward: %.6f" % self._total_reward + ' ~ ' + 167 | "Total Profit: %.6f" % self._total_profit 168 | ) 169 | 170 | 171 | def close(self): 172 | plt.close() 173 | 174 | def save_rendering(self, filepath): 175 | plt.savefig(filepath) 176 | 177 | def pause_rendering(self): 178 | plt.show() 179 | 180 | def _calculate_reward(self, action): 181 | step_reward = 0 182 | 183 | trade = False 184 | if ((action == Actions.Buy.value and self._position == Positions.Short) or 185 | (action == Actions.Sell.value and self._position == Positions.Long)): 186 | trade = True 187 | 188 | if trade: 189 | current_price = self.prices[self._current_tick] 190 | last_trade_price = self.prices[self._last_trade_tick] 191 | price_diff = current_price - last_trade_price 192 | 193 | if self._position == Positions.Long: 194 | step_reward += price_diff 195 | 196 | return step_reward 197 | 198 | def _update_profit(self, action): 199 | trade = False 200 | if ((action == Actions.Buy.value and self._position == Positions.Short) or 201 | (action == Actions.Sell.value and self._position == Positions.Long)): 202 | trade = True 203 | 204 | if trade or self._done: 205 | current_price = self.prices[self._current_tick] 206 | last_trade_price = self.prices[self._last_trade_tick] 207 | 208 | if self._position == Positions.Long: 209 | shares = (self._total_profit * (1 - self.fee)) / last_trade_price 210 | self._total_profit = (shares * (1 - self.fee)) * current_price 211 | 212 | def max_possible_profit(self): 213 | current_tick = self._start_tick 214 | last_trade_tick = current_tick - 1 215 | profit = 1. 216 | 217 | while current_tick <= self._end_tick: 218 | position = None 219 | if self.prices[current_tick] < self.prices[current_tick - 1]: 220 | while (current_tick <= self._end_tick and 221 | self.prices[current_tick] < self.prices[current_tick - 1]): 222 | current_tick += 1 223 | position = Positions.Short 224 | else: 225 | while (current_tick <= self._end_tick and 226 | self.prices[current_tick] >= self.prices[current_tick - 1]): 227 | current_tick += 1 228 | position = Positions.Long 229 | 230 | if position == Positions.Long: 231 | current_price = self.prices[current_tick - 1] 232 | last_trade_price = self.prices[last_trade_tick] 233 | shares = profit / last_trade_price 234 | profit = shares * current_price 235 | last_trade_tick = current_tick - 1 236 | 237 | return profit 238 | -------------------------------------------------------------------------------- /train_freqtrade.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from os import access 3 | from pathlib import Path 4 | 5 | import mpu 6 | import tensortrade.env.default as default 7 | import torch as th 8 | from freqtrade.configuration import Configuration, TimeRange 9 | from freqtrade.data import history 10 | from freqtrade.data.dataprovider import DataProvider 11 | from freqtrade.exchange import Exchange as FreqtradeExchange 12 | from freqtrade.resolvers import StrategyResolver 13 | from gym.spaces import Discrete, Space 14 | from stable_baselines3.a2c.a2c import A2C 15 | from stable_baselines3.common.monitor import Monitor 16 | from stable_baselines3.ppo.ppo import PPO 17 | from tensortrade.env.default.actions import BSH, TensorTradeActionScheme 18 | from tensortrade.env.default.rewards import PBR, RiskAdjustedReturns, SimpleProfit, TensorTradeRewardScheme 19 | from tensortrade.env.generic import ActionScheme, TradingEnv 20 | from tensortrade.feed.core import DataFeed, NameSpace, Stream 21 | from tensortrade.oms.exchanges import Exchange, ExchangeOptions 22 | from tensortrade.oms.instruments import BTC, ETH, LTC, USD, Instrument 23 | from tensortrade.oms.orders import proportion_order 24 | from tensortrade.oms.services.execution.simulated import execute_order 25 | from tensortrade.oms.wallets import Portfolio, Wallet 26 | 27 | from tb_callbacks import SaveOnStepCallback 28 | from trading_environments import FreqtradeEnv, GymAnytrading, SimpleROIEnv 29 | 30 | """Settings""" 31 | PAIR = "ADA/USDT" 32 | TRAINING_RANGE = "20210901-20211231" 33 | WINDOW_SIZE = 10 34 | LOAD_PREPROCESSED_DATA = False # useful if you have to calculate a lot of features 35 | SAVE_PREPROCESSED_DATA = True 36 | LEARNING_TIME_STEPS = int(1e9) 37 | LOG_DIR = "./logs/" 38 | TENSORBOARD_LOG = "./tensorboard/" 39 | MODEL_DIR = "./models/" 40 | USER_DATA = Path(__file__).parent / "user_data" 41 | """End of settings""" 42 | 43 | freqtrade_config = Configuration.from_files([str(USER_DATA / "config.json")]) 44 | _preprocessed_data_file = "preprocessed_data.pickle" 45 | from gym.spaces import Discrete, Space 46 | 47 | 48 | class BuySellHold(TensorTradeActionScheme): 49 | """A simple discrete action scheme where the only options are to buy, sell, 50 | or hold. 51 | 52 | Parameters 53 | ---------- 54 | cash : `Wallet` 55 | The wallet to hold funds in the base instrument. 56 | asset : `Wallet` 57 | The wallet to hold funds in the quote instrument. 58 | """ 59 | 60 | registered_name = "bsh" 61 | 62 | def __init__(self, cash: "Wallet", asset: "Wallet"): 63 | super().__init__() 64 | self.cash = cash 65 | self.asset = asset 66 | 67 | self.listeners = [] 68 | 69 | @property 70 | def action_space(self): 71 | return Discrete(3) 72 | 73 | def attach(self, listener): 74 | self.listeners += [listener] 75 | return self 76 | 77 | def get_orders(self, action: int, portfolio: "Portfolio"): 78 | order = None 79 | 80 | if action == 2: # Hold 81 | return [] 82 | 83 | if action == 0: # Buy 84 | if self.cash.balance == 0: 85 | return [] 86 | order = proportion_order(portfolio, self.cash, self.asset, 1.0) 87 | 88 | if action == 1: # Sell 89 | if self.asset.balance == 0: 90 | return [] 91 | order = proportion_order(portfolio, self.asset, self.cash, 1.0) 92 | 93 | for listener in self.listeners: 94 | listener.on_action(action) 95 | 96 | return [order] 97 | 98 | def reset(self): 99 | super().reset() 100 | 101 | 102 | def main(): 103 | 104 | strategy = StrategyResolver.load_strategy(freqtrade_config) 105 | strategy.dp = DataProvider(freqtrade_config, FreqtradeExchange(freqtrade_config), None) 106 | required_startup = strategy.startup_candle_count 107 | timeframe = freqtrade_config.get("timeframe") 108 | data = dict() 109 | 110 | if LOAD_PREPROCESSED_DATA: 111 | assert Path(_preprocessed_data_file).exists(), "Unable to load preprocessed data!" 112 | data = mpu.io.read(_preprocessed_data_file) 113 | assert PAIR in data, f"Loaded preprocessed data does not contain pair {PAIR}!" 114 | else: 115 | data = _load_data(freqtrade_config, timeframe, TRAINING_RANGE) 116 | data = strategy.advise_all_indicators({PAIR: data[PAIR]}) 117 | if SAVE_PREPROCESSED_DATA: 118 | mpu.io.write(_preprocessed_data_file, data) 119 | 120 | pair_data = data[PAIR][required_startup:].copy() 121 | pair_data.reset_index(drop=True, inplace=True) 122 | 123 | del data 124 | 125 | price_data = pair_data[["date", "open", "close", "high", "low", "volume"]].copy() 126 | 127 | pair_data.drop(columns=["date", "open", "close", "high", "low", "volume"], inplace=True) 128 | pair_data.fillna(0, inplace=True) 129 | 130 | ADA = Instrument("ADA", 3, "Cardano") 131 | 132 | price = Stream.source(list(price_data["close"]), dtype="float").rename("USD-ADA") 133 | 134 | exchange_options = ExchangeOptions(commission=0.0035) 135 | binance = Exchange("binance", service=execute_order, options=exchange_options)(price) 136 | 137 | cash = Wallet(binance, 1000 * USD) 138 | asset = Wallet(binance, 0 * ADA) 139 | 140 | portfolio = Portfolio(USD, [cash, asset]) 141 | 142 | features = [Stream.source(list(pair_data[c]), dtype="float").rename(c) for c in pair_data.columns] 143 | 144 | feed = DataFeed(features) 145 | feed.compile() 146 | 147 | renderer_feed = DataFeed( 148 | [ 149 | Stream.source(list(price_data["date"])).rename("date"), 150 | Stream.source(list(price_data["open"]), dtype="float").rename("open"), 151 | Stream.source(list(price_data["high"]), dtype="float").rename("high"), 152 | Stream.source(list(price_data["low"]), dtype="float").rename("low"), 153 | Stream.source(list(price_data["close"]), dtype="float").rename("close"), 154 | Stream.source(list(price_data["volume"]), dtype="float").rename("volume"), 155 | ] 156 | ) 157 | 158 | action_scheme = BuySellHold(cash=cash, asset=asset) 159 | 160 | # reward_scheme = PBR(price) 161 | reward_scheme = SimpleProfit(window_size=8) 162 | 163 | trading_env = default.create( 164 | portfolio=portfolio, 165 | action_scheme=action_scheme, 166 | reward_scheme=reward_scheme, 167 | feed=feed, 168 | renderer_feed=renderer_feed, 169 | window_size=WINDOW_SIZE, 170 | max_allowed_loss=0.50, 171 | ) 172 | 173 | trading_env = Monitor(trading_env, LOG_DIR, info_keywords=("net_worth",)) 174 | 175 | # Optional policy_kwargs 176 | # see https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html?highlight=policy_kwargs#custom-network-architecture 177 | # policy_kwargs = dict(activation_fn=th.nn.ReLU, 178 | # net_arch=[dict(pi=[32, 32], vf=[32, 32])]) 179 | # policy_kwargs = dict(activation_fn=th.nn.Tanh, net_arch=[32, dict(pi=[64, 64], vf=[64, 64])]) 180 | policy_kwargs = dict(net_arch=[128, dict(pi=[128, 128], vf=[128, 128])]) 181 | 182 | start_date = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 183 | 184 | model = PPO( # See https://stable-baselines3.readthedocs.io/en/master/guide/algos.html for other algos with discrete action space 185 | "MlpPolicy", # MlpPolicy MultiInputPolicy 186 | trading_env, 187 | verbose=0, 188 | device="cuda", 189 | tensorboard_log=TENSORBOARD_LOG, 190 | # n_steps = len(pair_data), 191 | # batch_size = 1000, 192 | # n_epochs = 20, 193 | policy_kwargs=policy_kwargs, 194 | ) 195 | 196 | base_name = f"{strategy.get_strategy_name()}_TensorTrade_{model.__class__.__name__}_{start_date}" 197 | 198 | tb_callback = SaveOnStepCallback( 199 | check_freq=10000, save_name=f"best_model_{base_name}", save_dir=MODEL_DIR, log_dir=LOG_DIR, verbose=1 200 | ) 201 | 202 | print(f"You can run tensorboard with: 'tensorboard --logdir {Path(TENSORBOARD_LOG).absolute()}'") 203 | print("Learning started.") 204 | 205 | model.learn(total_timesteps=LEARNING_TIME_STEPS, callback=tb_callback) 206 | model.save(f"{MODEL_DIR}final_model_{base_name}") 207 | 208 | 209 | def _load_data(config, timeframe, timerange): 210 | timerange = TimeRange.parse_timerange(timerange) 211 | 212 | return history.load_data( 213 | datadir=config["datadir"], 214 | pairs=config["pairs"], 215 | timeframe=timeframe, 216 | timerange=timerange, 217 | startup_candles=config["startup_candle_count"], 218 | fail_without_data=True, 219 | data_format=config.get("dataformat_ohlcv", "json"), 220 | ) 221 | 222 | 223 | if __name__ == "__main__": 224 | main() 225 | -------------------------------------------------------------------------------- /user_data/strategies/FreqGym.py: -------------------------------------------------------------------------------- 1 | # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement 2 | 3 | import freqtrade.vendor.qtpylib.indicators as qtpylib 4 | import numpy as np # noqa 5 | import pandas as pd # noqa 6 | import talib.abstract as ta 7 | from freqtrade.strategy.interface import IStrategy 8 | from pandas import DataFrame, Series 9 | from stable_baselines3.ppo.ppo import PPO 10 | from stable_baselines3.a2c.a2c import A2C 11 | from ta import add_all_ta_features 12 | 13 | 14 | class FreqGym(IStrategy): 15 | 16 | # If you've used SimpleROIEnv then use this minimal_roi 17 | minimal_roi = { 18 | "720": -10, 19 | "600": 0.00001, 20 | "60": 0.01, 21 | "30": 0.02, 22 | "0": 0.03 23 | } 24 | 25 | # minimal_roi = { 26 | # "0": 100 27 | # } 28 | 29 | stoploss = -0.99 30 | 31 | # Trailing stop: 32 | trailing_stop = False 33 | trailing_stop_positive = 0.001 34 | trailing_stop_positive_offset = 0.017 35 | trailing_only_offset_is_reached = True 36 | 37 | 38 | ticker_interval = '5m' 39 | 40 | use_sell_signal = True 41 | 42 | # Run "populate_indicators()" only for new candle. 43 | process_only_new_candles = False 44 | 45 | startup_candle_count: int = 200 46 | 47 | model = None 48 | window_size = None 49 | 50 | try: 51 | model = PPO.load('models/best_model_SimpleROIEnv_PPO_20211028_104631') # Note: Make sure you use the same policy as the one used to train 52 | window_size = model.observation_space.shape[0] 53 | except Exception: 54 | pass 55 | 56 | timeperiods = [7, 14, 21] 57 | 58 | def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: 59 | """ 60 | Adds several different TA indicators to the given DataFrame 61 | 62 | Performance Note: For the best performance be frugal on the number of indicators 63 | you are using. Let uncomment only the indicator you are using in your strategies 64 | or your hyperopt configuration, otherwise you will waste your memory and CPU usage. 65 | :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() 66 | :param metadata: Additional information, like the currently traded pair 67 | :return: a Dataframe with all mandatory indicators for the strategies 68 | """ 69 | 70 | # Plus Directional Indicator / Movement 71 | dataframe['plus_dm'] = ta.PLUS_DM(dataframe) 72 | dataframe['plus_di'] = ta.PLUS_DI(dataframe) 73 | 74 | # # Minus Directional Indicator / Movement 75 | dataframe['minus_dm'] = ta.MINUS_DM(dataframe) 76 | dataframe['minus_di'] = ta.MINUS_DI(dataframe) 77 | 78 | # Awesome Oscillator 79 | dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) 80 | 81 | # Ultimate Oscillator 82 | dataframe['uo'] = ta.ULTOSC(dataframe) 83 | 84 | # EWO 85 | dataframe['ewo'] = EWO(dataframe, 50, 200) 86 | 87 | # # Hilbert Transform Indicator - SineWave 88 | # hilbert = ta.HT_SINE(dataframe) 89 | # dataframe['htsine'] = hilbert['sine'] 90 | # dataframe['htleadsine'] = hilbert['leadsine'] 91 | 92 | # # # Heikin Ashi Strategy 93 | # heikinashi = qtpylib.heikinashi(dataframe) 94 | # dataframe['ha_open'] = heikinashi['open'] 95 | # dataframe['ha_close'] = heikinashi['close'] 96 | # dataframe['ha_high'] = heikinashi['high'] 97 | # dataframe['ha_low'] = heikinashi['low'] 98 | 99 | for period in self.timeperiods: 100 | # ADX 101 | dataframe[f'adx_{period}'] = ta.ADX(dataframe, timeperiod=period) 102 | 103 | # Williams %R 104 | dataframe[f'wr_{period}'] = williams_r(dataframe, timeperiod=period) 105 | 106 | # CCI 107 | dataframe[f'cci_{period}'] = ta.CCI(dataframe, timeperiod=period) 108 | 109 | # RSI 110 | dataframe[f'rsi_{period}'] = ta.RSI(dataframe, timeperiod=period) 111 | 112 | # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) 113 | rsi = 0.1 * (dataframe[f'rsi_{period}'] - 50) 114 | dataframe[f'fisher_rsi_{period}'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) 115 | 116 | # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) 117 | dataframe[f'fisher_rsi_norma_{period}'] = 50 * (dataframe[f'fisher_rsi_{period}'] + 1) 118 | 119 | # Aroon, Aroon Oscillator 120 | aroon = ta.AROON(dataframe, timeperiod=period) 121 | dataframe[f'aroonup_{period}'] = aroon['aroonup'] 122 | dataframe[f'aroondown_{period}'] = aroon['aroondown'] 123 | dataframe[f'aroonosc_{period}'] = ta.AROONOSC(dataframe, timeperiod=period) 124 | 125 | # Chande Momentum Oscillator 126 | dataframe[f'cmo_{period}'] = ta.CMO(dataframe, timeperiod=period) 127 | 128 | # Money Flow Index 129 | dataframe[f'mfi_{period}'] = ta.MFI(dataframe, timeperiod=period) 130 | 131 | # # EMA - Exponential Moving Average 132 | # dataframe[f'ema_{period}'] = ta.EMA(dataframe, timeperiod=period) 133 | # # SMA - Simple Moving Average 134 | # dataframe[f'sma_{period}'] = ta.SMA(dataframe, timeperiod=period) 135 | # # TEMA - Triple Exponential Moving Average 136 | # dataframe[f'tema_{period}'] = ta.TEMA(dataframe, timeperiod=period) 137 | 138 | # All other 139 | # dataframe = add_all_ta_features(dataframe, 'open', 'high', 'low', 'close', 'volume', fillna=True) 140 | 141 | return dataframe 142 | 143 | def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: 144 | """ 145 | Based on TA indicators, populates the buy signal for the given dataframe 146 | :param dataframe: DataFrame populated with indicators 147 | :param metadata: Additional information, like the currently traded pair 148 | :return: DataFrame with buy column 149 | """ 150 | # dataframe['buy'] = self.rl_model_predict(dataframe) 151 | action = self.rl_model_predict(dataframe) 152 | dataframe['buy'] = (action == 1).astype('int') 153 | 154 | return dataframe 155 | 156 | def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: 157 | """ 158 | Based on TA indicators, populates the sell signal for the given dataframe 159 | :param dataframe: DataFrame populated with indicators 160 | :param metadata: Additional information, like the currently traded pair 161 | :return: DataFrame with buy column 162 | """ 163 | 164 | action = self.rl_model_predict(dataframe) 165 | dataframe['sell'] = (action == 2).astype('int') 166 | 167 | return dataframe 168 | 169 | def rl_model_predict(self, dataframe): 170 | output = pd.DataFrame(np.zeros((len(dataframe), 1))) 171 | indicators = dataframe[dataframe.columns[~dataframe.columns.isin(['date', 'open', 'close', 'high', 'low', 'volume', 'buy', 'sell', 'buy_tag'])]].fillna(0).to_numpy() 172 | 173 | # TODO: This is slow and ugly, must use .rolling 174 | for window in range(self.window_size, len(dataframe)): 175 | start = window - self.window_size 176 | end = window 177 | observation = indicators[start:end] 178 | res, _ = self.model.predict(observation, deterministic=True) 179 | output.loc[end] = res 180 | 181 | return output 182 | 183 | 184 | def EWO(dataframe, sma1_length=5, sma2_length=35): 185 | df = dataframe.copy() 186 | sma1 = ta.EMA(df, timeperiod=sma1_length) 187 | sma2 = ta.EMA(df, timeperiod=sma2_length) 188 | smadif = (sma1 - sma2) / df['close'] * 100 189 | return smadif 190 | 191 | def williams_r(dataframe: DataFrame, timeperiod: int = 14) -> Series: 192 | """Williams %R, or just %R, is a technical analysis oscillator showing the current closing price in relation to the high and low 193 | of the past N days (for a given N). It was developed by a publisher and promoter of trading materials, Larry Williams. 194 | Its purpose is to tell whether a stock or commodity market is trading near the high or the low, or somewhere in between, 195 | of its recent trading range. 196 | The oscillator is on a negative scale, from −100 (lowest) up to 0 (highest). 197 | """ 198 | 199 | highest_high = dataframe["high"].rolling(center=False, window=timeperiod).max() 200 | lowest_low = dataframe["low"].rolling(center=False, window=timeperiod).min() 201 | 202 | WR = Series( 203 | (highest_high - dataframe["close"]) / (highest_high - lowest_low), 204 | name=f"{timeperiod} Williams %R", 205 | ) 206 | 207 | return WR * -100 208 | -------------------------------------------------------------------------------- /user_data/strategies/FreqGym_normalized.py: -------------------------------------------------------------------------------- 1 | # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement 2 | 3 | import freqtrade.vendor.qtpylib.indicators as qtpylib 4 | import numpy as np # noqa 5 | import pandas as pd # noqa 6 | import talib.abstract as ta 7 | from freqtrade.strategy.interface import IStrategy 8 | from pandas import DataFrame 9 | from stable_baselines3.ppo.ppo import PPO 10 | from stable_baselines3.a2c.a2c import A2C 11 | from freqtrade.strategy import merge_informative_pair 12 | import time 13 | 14 | class FreqGym_normalized(IStrategy): 15 | # # If you've used SimpleROIEnv then use this minimal_roi 16 | # minimal_roi = {"770": -10, "600": 0.00001, "60": 0.01, "30": 0.02, "0": 0.03} 17 | 18 | minimal_roi = { 19 | # "1440": -10 20 | "0": 10, 21 | } 22 | 23 | stoploss = -0.10 24 | 25 | # Trailing stop: 26 | trailing_stop = False 27 | trailing_stop_positive = 0.001 28 | trailing_stop_positive_offset = 0.017 29 | trailing_only_offset_is_reached = True 30 | 31 | timeframe = "15m" 32 | informative_timeframe = "1h" 33 | 34 | use_sell_signal = True 35 | 36 | # Run "populate_indicators()" only for new candle. 37 | process_only_new_candles = False 38 | 39 | startup_candle_count: int = 200 40 | 41 | model = None 42 | window_size = None 43 | 44 | freqtrade_columns = ["date", "open", "close", "high", "low", "volume", "buy", "sell", "buy_tag", "exit_tag"] 45 | informative_freqtrade_columns = [f"{c}_1h" for c in freqtrade_columns] 46 | btc_freqtrade_columns = [f"{c}_btc_1h" for c in freqtrade_columns] 47 | indicators = None 48 | 49 | try: 50 | model = PPO.load("models/best_model") # Note: Make sure you use the same policy as the one used to train 51 | window_size = model.observation_space.shape[0] 52 | except Exception: 53 | pass 54 | 55 | timeperiods = [8, 16, 32] 56 | 57 | def informative_pairs(self): 58 | pairs = self.dp.current_whitelist() 59 | informative_pairs = [(pair, self.informative_timeframe) for pair in pairs] 60 | informative_pairs += [("BTC/USDT", self.informative_timeframe)] 61 | return informative_pairs 62 | 63 | def generate_features(self, df): 64 | 65 | dataframe = df.copy() 66 | # Plus Directional Indicator / Movement 67 | dataframe["plus_di"] = normalize(ta.PLUS_DI(dataframe), 0, 100) 68 | 69 | # # Minus Directional Indicator / Movement 70 | dataframe["minus_di"] = normalize(ta.MINUS_DI(dataframe), 0, 100) 71 | 72 | # Ultimate Oscillator 73 | dataframe["uo"] = normalize(ta.ULTOSC(dataframe), 0, 100) 74 | 75 | # Hilbert Transform Indicator - SineWave 76 | hilbert = ta.HT_SINE(dataframe) 77 | dataframe["htsine"] = normalize(hilbert["sine"], -1, 1) 78 | dataframe["htleadsine"] = normalize(hilbert["leadsine"], -1, 1) 79 | 80 | # BOP Balance Of Power 81 | dataframe["bop"] = normalize(ta.BOP(dataframe), -1, 1) 82 | 83 | # STOCH - Stochastic 84 | stoch = ta.STOCH(dataframe) 85 | dataframe["slowk"] = normalize(stoch["slowk"], 0, 100) 86 | dataframe["slowd"] = normalize(stoch["slowd"], 0, 100) 87 | 88 | # STOCHF - Stochastic Fast 89 | stochf = ta.STOCHF(dataframe) 90 | dataframe["fastk"] = normalize(stochf["fastk"], 0, 100) 91 | dataframe["fastk"] = normalize(stochf["fastk"], 0, 100) 92 | 93 | # Bollinger Bands 94 | bollinger2 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) 95 | bollinger3 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=3) 96 | 97 | dataframe["bb2_lower_gt_close"] = bollinger2["lower"].gt(dataframe["close"]).astype("int") 98 | dataframe["bb3_lower_gt_close"] = bollinger3["lower"].gt(dataframe["close"]).astype("int") 99 | 100 | for period in self.timeperiods: 101 | # ADX Average Directional Movement Index 102 | dataframe[f"adx_{period}"] = normalize(ta.ADX(dataframe, timeperiod=period), 0, 100) 103 | 104 | # Aroon, Aroon Oscillator 105 | aroon = ta.AROON(dataframe, timeperiod=period) 106 | dataframe[f"aroonup_{period}"] = normalize(aroon["aroonup"], 0, 100) 107 | dataframe[f"aroondown_{period}"] = normalize(aroon["aroondown"], 0, 100) 108 | dataframe[f"aroonosc_{period}"] = normalize(ta.AROONOSC(dataframe, timeperiod=period), -100, 100) 109 | 110 | # CMO Chande Momentum Oscillator 111 | dataframe[f"cmo_{period}"] = normalize(ta.CMO(dataframe, timeperiod=period), -100, 100) 112 | 113 | # DX Directional Movement Index 114 | dataframe[f"dx_{period}"] = normalize(ta.DX(dataframe, timeperiod=period), 0, 100) 115 | 116 | # MFI Money Flow Index 117 | dataframe[f"mfi_{period}"] = normalize(ta.MFI(dataframe, timeperiod=period), 0, 100) 118 | 119 | # MINUS_DI Minus Directional Indicator 120 | dataframe[f"minus_di_{period}"] = normalize(ta.MINUS_DI(dataframe, timeperiod=period), 0, 100) 121 | 122 | # PLUS_DI Plus Directional Indicator 123 | dataframe[f"plus_di_{period}"] = normalize(ta.PLUS_DI(dataframe, timeperiod=period), 0, 100) 124 | 125 | # Williams %R 126 | dataframe[f"willr_{period}"] = normalize(ta.WILLR(dataframe, timeperiod=period), -100, 0) 127 | 128 | # RSI 129 | dataframe[f"rsi_{period}"] = normalize(ta.RSI(dataframe, timeperiod=period), 0, 100) 130 | 131 | # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) 132 | rsi = 0.1 * (dataframe[f"rsi_{period}"] - 50) 133 | dataframe[f"fisher_rsi_{period}"] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) 134 | dataframe[f"fisher_rsi_{period}"] = normalize(dataframe[f"fisher_rsi_{period}"], -1, 1) 135 | 136 | # STOCHRSI - Stochastic Relative Strength Index 137 | stoch_rsi = ta.STOCHRSI(dataframe, timeperiod=period) 138 | dataframe[f"stochrsi_k_{period}"] = normalize(stoch_rsi["fastk"], 0, 100) 139 | dataframe[f"stochrsi_d_{period}"] = normalize(stoch_rsi["fastd"], 0, 100) 140 | 141 | # # CORREL - Pearson's Correlation Coefficient (r) 142 | # dataframe[f'correl_{period}'] = normalize(ta.CORREL(dataframe, timeperiod=period), -1, 1) # this is buggy 143 | 144 | # LINEARREG_ANGLE - Linear Regression Angle 145 | dataframe[f"linangle_{period}"] = normalize(ta.LINEARREG_ANGLE(dataframe, timeperiod=period), -90, 90) 146 | 147 | return dataframe 148 | 149 | def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: 150 | """ 151 | Adds several different TA indicators to the given DataFrame 152 | 153 | Performance Note: For the best performance be frugal on the number of indicators 154 | you are using. Let uncomment only the indicator you are using in your strategies 155 | or your hyperopt configuration, otherwise you will waste your memory and CPU usage. 156 | :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() 157 | :param metadata: Additional information, like the currently traded pair 158 | :return: a Dataframe with all mandatory indicators for the strategies 159 | """ 160 | dataframe = self.generate_features(dataframe) 161 | 162 | informative_df = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=self.informative_timeframe) 163 | informative_df = self.generate_features(informative_df) 164 | dataframe = merge_informative_pair( 165 | dataframe, informative_df, self.timeframe, self.informative_timeframe, ffill=True 166 | ) 167 | dataframe = dataframe[dataframe.columns[~dataframe.columns.isin(self.informative_freqtrade_columns)]] 168 | 169 | informative_btc = self.dp.get_pair_dataframe("BTC/USDT", timeframe=self.informative_timeframe) 170 | informative_btc = self.generate_features(informative_btc) 171 | informative_btc.rename(columns=lambda s: s if s == "date" else f"{s}_btc", inplace=True) 172 | dataframe = merge_informative_pair( 173 | dataframe, informative_btc, self.timeframe, self.informative_timeframe, ffill=True 174 | ) 175 | dataframe = dataframe[ 176 | dataframe.columns[~dataframe.columns.isin(self.btc_freqtrade_columns + self.informative_freqtrade_columns)] 177 | ] 178 | 179 | dataframe.fillna(0, inplace=True) 180 | 181 | self.indicators = dataframe[dataframe.columns[~dataframe.columns.isin(self.freqtrade_columns)]].to_numpy() 182 | 183 | assert ( 184 | self.indicators.max() < 1.00001 and self.indicators.min() > -0.00001 185 | ), "Error, values are not normalized!" 186 | 187 | return dataframe 188 | 189 | def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: 190 | """ 191 | Based on TA indicators, populates the buy signal for the given dataframe 192 | :param dataframe: DataFrame populated with indicators 193 | :param metadata: Additional information, like the currently traded pair 194 | :return: DataFrame with buy column 195 | """ 196 | # dataframe['buy'] = self.rl_model_predict(dataframe) 197 | action = self.rl_model_predict(dataframe) 198 | dataframe["buy"] = (action == 0).astype("int") 199 | 200 | return dataframe 201 | 202 | def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: 203 | """ 204 | Based on TA indicators, populates the sell signal for the given dataframe 205 | :param dataframe: DataFrame populated with indicators 206 | :param metadata: Additional information, like the currently traded pair 207 | :return: DataFrame with buy column 208 | """ 209 | 210 | action = self.rl_model_predict(dataframe) 211 | dataframe["sell"] = (action == 1).astype("int") 212 | 213 | return dataframe 214 | 215 | def rl_model_predict(self, dataframe): 216 | 217 | output = pd.DataFrame(np.full((len(dataframe), 1), 2)) 218 | 219 | # start_time = time.time() 220 | def _predict(window): 221 | observation = self.indicators[window.index] 222 | res, _ = self.model.predict(observation, deterministic=True) 223 | return res 224 | 225 | output = output.rolling(window=self.window_size).apply(_predict) 226 | # print("--- rolling %s seconds ---" % (time.time() - start_time)) 227 | 228 | # start_time = time.time() 229 | # for window in range(self.startup_candle_count, len(dataframe)): 230 | # start = window - self.window_size + 1 231 | # end = window 232 | # observation = self.indicators[start:end+1] 233 | # res, _ = self.model.predict(observation, deterministic=True) 234 | # output.iloc[end] = res 235 | # print("--- forloop %s seconds ---" % (time.time() - start_time)) 236 | 237 | return output 238 | 239 | 240 | class FreqGym_normalizedDCA(FreqGym_normalized): 241 | # DCA 242 | position_adjustment_enable = True 243 | max_entry_position_adjustment = 2 244 | max_dca_multiplier = 1.25 245 | dca_stake_multiplier = 1.25 246 | 247 | # This is called when placing the initial order (opening trade) 248 | def custom_stake_amount( 249 | self, 250 | pair: str, 251 | current_time, 252 | current_rate: float, 253 | proposed_stake: float, 254 | min_stake: float, 255 | max_stake: float, 256 | **kwargs, 257 | ) -> float: 258 | 259 | if (self.config["position_adjustment_enable"] == True) and (self.config["stake_amount"] == "unlimited"): 260 | return self.wallets.get_total_stake_amount() / self.config["max_open_trades"] / self.max_dca_multiplier 261 | else: 262 | return proposed_stake 263 | 264 | def adjust_trade_position( 265 | self, 266 | trade, 267 | current_time, 268 | current_rate: float, 269 | current_profit: float, 270 | min_stake: float, 271 | max_stake: float, 272 | **kwargs, 273 | ): 274 | if current_profit > -0.05: 275 | return None 276 | 277 | # Obtain pair dataframe (just to show how to access it) 278 | dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) 279 | # Only buy when not actively falling price. 280 | last_candle = dataframe.iloc[-1].squeeze() 281 | previous_candle = dataframe.iloc[-2].squeeze() 282 | # if previous_candle['close'] < last_candle['close']: 283 | # return None 284 | 285 | filled_buys = trade.select_filled_orders("buy") 286 | count_of_buys = len(filled_buys) 287 | 288 | if 0 < count_of_buys <= self.max_entry_position_adjustment: 289 | try: 290 | # This returns first order stake size 291 | stake_amount = filled_buys[0].cost 292 | # This then calculates current safety order size 293 | stake_amount = stake_amount * self.dca_stake_multiplier 294 | return stake_amount 295 | except Exception as exception: 296 | return None 297 | 298 | return None 299 | 300 | 301 | def normalize(data, min_value, max_value): 302 | return (data - min_value) / (max_value - min_value) 303 | -------------------------------------------------------------------------------- /user_data/notebooks/strategy_analysis_example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Strategy analysis example\n", 8 | "\n", 9 | "Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n", 10 | "The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location." 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "## Setup" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "from pathlib import Path\n", 27 | "from freqtrade.configuration import Configuration\n", 28 | "\n", 29 | "# Customize these according to your needs.\n", 30 | "\n", 31 | "# Initialize empty configuration object\n", 32 | "config = Configuration.from_files([])\n", 33 | "# Optionally, use existing configuration file\n", 34 | "# config = Configuration.from_files([\"config.json\"])\n", 35 | "\n", 36 | "# Define some constants\n", 37 | "config[\"timeframe\"] = \"5m\"\n", 38 | "# Name of the strategy class\n", 39 | "config[\"strategy\"] = \"SampleStrategy\"\n", 40 | "# Location of the data\n", 41 | "data_location = Path(config['user_data_dir'], 'data', 'binance')\n", 42 | "# Pair to analyze - Only use one pair here\n", 43 | "pair = \"BTC/USDT\"" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "# Load data using values set above\n", 53 | "from freqtrade.data.history import load_pair_history\n", 54 | "\n", 55 | "candles = load_pair_history(datadir=data_location,\n", 56 | " timeframe=config[\"timeframe\"],\n", 57 | " pair=pair,\n", 58 | " data_format = \"hdf5\",\n", 59 | " )\n", 60 | "\n", 61 | "# Confirm success\n", 62 | "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", 63 | "candles.head()" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "## Load and run strategy\n", 71 | "* Rerun each time the strategy file is changed" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "# Load strategy using values set above\n", 81 | "from freqtrade.resolvers import StrategyResolver\n", 82 | "strategy = StrategyResolver.load_strategy(config)\n", 83 | "\n", 84 | "# Generate buy/sell signals using strategy\n", 85 | "df = strategy.analyze_ticker(candles, {'pair': pair})\n", 86 | "df.tail()" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "### Display the trade details\n", 94 | "\n", 95 | "* Note that using `data.head()` would also work, however most indicators have some \"startup\" data at the top of the dataframe.\n", 96 | "* Some possible problems\n", 97 | " * Columns with NaN values at the end of the dataframe\n", 98 | " * Columns used in `crossed*()` functions with completely different units\n", 99 | "* Comparison with full backtest\n", 100 | " * having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.\n", 101 | " * Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple \"buy\" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot (\"max_open_trades\") is still available), or on one of the middle signals, as soon as a \"slot\" becomes available. \n" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "# Report results\n", 111 | "print(f\"Generated {df['buy'].sum()} buy signals\")\n", 112 | "data = df.set_index('date', drop=False)\n", 113 | "data.tail()" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "## Load existing objects into a Jupyter notebook\n", 121 | "\n", 122 | "The following cells assume that you have already generated data using the cli. \n", 123 | "They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload." 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "### Load backtest results to pandas dataframe\n", 131 | "\n", 132 | "Analyze a trades dataframe (also used below for plotting)" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": null, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n", 142 | "\n", 143 | "# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n", 144 | "backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", 145 | "# backtest_dir can also point to a specific file \n", 146 | "# backtest_dir = config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "# You can get the full backtest statistics by using the following command.\n", 156 | "# This contains all information used to generate the backtest result.\n", 157 | "stats = load_backtest_stats(backtest_dir)\n", 158 | "\n", 159 | "strategy = 'SampleStrategy'\n", 160 | "# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well.\n", 161 | "# Example usages:\n", 162 | "print(stats['strategy'][strategy]['results_per_pair'])\n", 163 | "# Get pairlist used for this backtest\n", 164 | "print(stats['strategy'][strategy]['pairlist'])\n", 165 | "# Get market change (average change of all pairs from start to end of the backtest period)\n", 166 | "print(stats['strategy'][strategy]['market_change'])\n", 167 | "# Maximum drawdown ()\n", 168 | "print(stats['strategy'][strategy]['max_drawdown'])\n", 169 | "# Maximum drawdown start and end\n", 170 | "print(stats['strategy'][strategy]['drawdown_start'])\n", 171 | "print(stats['strategy'][strategy]['drawdown_end'])\n", 172 | "\n", 173 | "\n", 174 | "# Get strategy comparison (only relevant if multiple strategies were compared)\n", 175 | "print(stats['strategy_comparison'])\n" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "metadata": {}, 182 | "outputs": [], 183 | "source": [ 184 | "# Load backtested trades as dataframe\n", 185 | "trades = load_backtest_data(backtest_dir)\n", 186 | "\n", 187 | "# Show value-counts per pair\n", 188 | "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "## Plotting daily profit / equity line" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "# Plotting equity line (starting with 0 on day 1 and adding daily profit for each backtested day)\n", 205 | "\n", 206 | "from freqtrade.configuration import Configuration\n", 207 | "from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n", 208 | "import plotly.express as px\n", 209 | "import pandas as pd\n", 210 | "\n", 211 | "# strategy = 'SampleStrategy'\n", 212 | "# config = Configuration.from_files([\"user_data/config.json\"])\n", 213 | "# backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", 214 | "\n", 215 | "stats = load_backtest_stats(backtest_dir)\n", 216 | "strategy_stats = stats['strategy'][strategy]\n", 217 | "\n", 218 | "dates = []\n", 219 | "profits = []\n", 220 | "for date_profit in strategy_stats['daily_profit']:\n", 221 | " dates.append(date_profit[0])\n", 222 | " profits.append(date_profit[1])\n", 223 | "\n", 224 | "equity = 0\n", 225 | "equity_daily = []\n", 226 | "for daily_profit in profits:\n", 227 | " equity_daily.append(equity)\n", 228 | " equity += float(daily_profit)\n", 229 | "\n", 230 | "\n", 231 | "df = pd.DataFrame({'dates': dates,'equity_daily': equity_daily})\n", 232 | "\n", 233 | "fig = px.line(df, x=\"dates\", y=\"equity_daily\")\n", 234 | "fig.show()\n" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": [ 241 | "### Load live trading results into a pandas dataframe\n", 242 | "\n", 243 | "In case you did already some trading and want to analyze your performance" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "from freqtrade.data.btanalysis import load_trades_from_db\n", 253 | "\n", 254 | "# Fetch trades from database\n", 255 | "trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", 256 | "\n", 257 | "# Display results\n", 258 | "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "## Analyze the loaded trades for trade parallelism\n", 266 | "This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n", 267 | "\n", 268 | "`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle." 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "from freqtrade.data.btanalysis import analyze_trade_parallelism\n", 278 | "\n", 279 | "# Analyze the above\n", 280 | "parallel_trades = analyze_trade_parallelism(trades, '5m')\n", 281 | "\n", 282 | "parallel_trades.plot()" 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "metadata": {}, 288 | "source": [ 289 | "## Plot results\n", 290 | "\n", 291 | "Freqtrade offers interactive plotting capabilities based on plotly." 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": null, 297 | "metadata": {}, 298 | "outputs": [], 299 | "source": [ 300 | "from freqtrade.plot.plotting import generate_candlestick_graph\n", 301 | "# Limit graph period to keep plotly quick and reactive\n", 302 | "\n", 303 | "# Filter trades to one pair\n", 304 | "trades_red = trades.loc[trades['pair'] == pair]\n", 305 | "\n", 306 | "data_red = data['2019-06-01':'2019-06-10']\n", 307 | "# Generate candlestick graph\n", 308 | "graph = generate_candlestick_graph(pair=pair,\n", 309 | " data=data_red,\n", 310 | " trades=trades_red,\n", 311 | " indicators1=['sma20', 'ema50', 'ema55'],\n", 312 | " indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']\n", 313 | " )\n", 314 | "\n", 315 | "\n" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "metadata": {}, 322 | "outputs": [], 323 | "source": [ 324 | "# Show graph inline\n", 325 | "# graph.show()\n", 326 | "\n", 327 | "# Render graph in a seperate window\n", 328 | "graph.show(renderer=\"browser\")\n" 329 | ] 330 | }, 331 | { 332 | "cell_type": "markdown", 333 | "metadata": {}, 334 | "source": [ 335 | "## Plot average profit per trade as distribution graph" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": null, 341 | "metadata": {}, 342 | "outputs": [], 343 | "source": [ 344 | "import plotly.figure_factory as ff\n", 345 | "\n", 346 | "hist_data = [trades.profit_ratio]\n", 347 | "group_labels = ['profit_ratio'] # name of the dataset\n", 348 | "\n", 349 | "fig = ff.create_distplot(hist_data, group_labels,bin_size=0.01)\n", 350 | "fig.show()\n" 351 | ] 352 | }, 353 | { 354 | "cell_type": "markdown", 355 | "metadata": {}, 356 | "source": [ 357 | "Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data." 358 | ] 359 | } 360 | ], 361 | "metadata": { 362 | "file_extension": ".py", 363 | "kernelspec": { 364 | "display_name": "Python 3", 365 | "language": "python", 366 | "name": "python3" 367 | }, 368 | "language_info": { 369 | "codemirror_mode": { 370 | "name": "ipython", 371 | "version": 3 372 | }, 373 | "file_extension": ".py", 374 | "mimetype": "text/x-python", 375 | "name": "python", 376 | "nbconvert_exporter": "python", 377 | "pygments_lexer": "ipython3", 378 | "version": "3.8.5" 379 | }, 380 | "mimetype": "text/x-python", 381 | "name": "python", 382 | "npconvert_exporter": "python", 383 | "pygments_lexer": "ipython3", 384 | "toc": { 385 | "base_numbering": 1, 386 | "nav_menu": {}, 387 | "number_sections": true, 388 | "sideBar": true, 389 | "skip_h1_title": false, 390 | "title_cell": "Table of Contents", 391 | "title_sidebar": "Contents", 392 | "toc_cell": false, 393 | "toc_position": {}, 394 | "toc_section_display": true, 395 | "toc_window_display": false 396 | }, 397 | "varInspector": { 398 | "cols": { 399 | "lenName": 16, 400 | "lenType": 16, 401 | "lenVar": 40 402 | }, 403 | "kernels_config": { 404 | "python": { 405 | "delete_cmd_postfix": "", 406 | "delete_cmd_prefix": "del ", 407 | "library": "var_list.py", 408 | "varRefreshCmd": "print(var_dic_list())" 409 | }, 410 | "r": { 411 | "delete_cmd_postfix": ") ", 412 | "delete_cmd_prefix": "rm(", 413 | "library": "var_list.r", 414 | "varRefreshCmd": "cat(var_dic_list()) " 415 | } 416 | }, 417 | "types_to_exclude": [ 418 | "module", 419 | "function", 420 | "builtin_function_or_method", 421 | "instance", 422 | "_Feature" 423 | ], 424 | "window_display": false 425 | }, 426 | "version": 3 427 | }, 428 | "nbformat": 4, 429 | "nbformat_minor": 4 430 | } 431 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | --------------------------------------------------------------------------------