├── ai ├── __init__.py ├── blueprints │ ├── __init__.py │ ├── junior.py │ ├── blp5m1117.py │ ├── luckyantelope.py │ └── base.py └── blueprint.py ├── core ├── __init__.py ├── bots │ ├── __init__.py │ ├── enums.py │ ├── live.py │ ├── backtest.py │ ├── paper.py │ └── base.py ├── constants.py ├── tradeaction.py ├── wallet.py ├── common.py ├── plot.py ├── report.py └── engine.py ├── dojo ├── __init__.py └── dojo.py ├── lib ├── __init__.py └── indicators │ ├── __init__.py │ ├── epc.py │ ├── ropc.py │ ├── macd.py │ ├── elderray.py │ └── stoploss.py ├── stats ├── __init__.py └── stats.py ├── utils ├── __init__.py ├── fetch2gcp.sh ├── telegrambot.py ├── blueprints2gcp.py ├── postman.py └── walletlense.py ├── backfill ├── __init__.py ├── base.py ├── candles.py └── trades.py ├── exchanges ├── __init__.py ├── bittrex │ ├── __init__.py │ └── bittrexclient.py ├── poloniex │ ├── __init__.py │ └── polo.py ├── base.py └── exchange.py ├── strategies ├── __init__.py ├── ai │ ├── __init__.py │ ├── scikitbase.py │ └── luckyantelope.py ├── enums.py ├── base.py └── ema.py ├── .coveragerc ├── tests └── test.py ├── .coveralls.yml ├── images ├── mosquito.png ├── console_sample.png └── mosquito_plot.png ├── .travis.yml ├── .gitmodules ├── logging.ini ├── requirements.txt ├── .gitignore ├── lense.py ├── dojo.py ├── examples └── exchange.py ├── blueprint.py ├── backfill.py ├── .circleci └── config.yml ├── mosquito.py ├── mosquito.sample.ini ├── README.md └── market_stats.py /ai/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dojo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backfill/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/bots/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exchanges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ai/blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/indicators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategies/ai/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exchanges/bittrex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /exchanges/poloniex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | tests/* -------------------------------------------------------------------------------- /core/constants.py: -------------------------------------------------------------------------------- 1 | 2 | SECONDS_IN_DAY = 86400 3 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | print("some tests to come here") 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: lsmDGkMqa8uNiNjrvBM0U9Jlf9bVwmV4h -------------------------------------------------------------------------------- /images/mosquito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miro-ka/mosquito/HEAD/images/mosquito.png -------------------------------------------------------------------------------- /images/console_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miro-ka/mosquito/HEAD/images/console_sample.png -------------------------------------------------------------------------------- /images/mosquito_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miro-ka/mosquito/HEAD/images/mosquito_plot.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | 4 | - "3.8" 5 | - "3.9" 6 | - "3.9-dev" # 3.9 development branch 7 | 8 | before_install: 9 | 10 | # command to install dependencies 11 | install: 12 | 13 | script: 14 | - python3 tests/test.py 15 | 16 | after_success: 17 | coveralls -------------------------------------------------------------------------------- /strategies/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TradeState(Enum): 5 | """ 6 | Enum class for holding all available trade states 7 | """ 8 | none = 0 9 | buy = 1 10 | buying = 2 11 | bought = 3 12 | sell = 4 13 | selling = 5 14 | sold = 6 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "exchanges/poloniex/python-poloniex"] 2 | path = exchanges/poloniex/python-poloniex 3 | url = https://github.com/s4w3d0ff/python-poloniex.git 4 | [submodule "exchanges/bittrex/python-bittrex"] 5 | path = exchanges/bittrex/python-bittrex 6 | url = https://github.com/miti0/python-bittrex.git 7 | -------------------------------------------------------------------------------- /logging.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=DEBUG 17 | formatter=simpleFormatter 18 | args=(sys.stdout,) 19 | 20 | [formatter_simpleFormatter] 21 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 22 | datefmt= -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi>=2021.5.30 2 | chardet>=4.0.0 3 | charset-normalizer>=2.0.6 4 | ConfigArgParse>=1.5.3 5 | idna>=3.2 6 | numpy>=1.24.3 7 | pandas>=1.3.3 8 | pandas-ta>=0.3.14b0 9 | plotly>=5.3.1 10 | poloniexapi>=0.5.7 11 | pymongo>=3.12.0 12 | python-bittrex>=0.3.0 13 | python-dateutil>=2.8.2 14 | pytz>=2021.3 15 | requests>=2.26.0 16 | retrying>=1.3.3 17 | six>=1.16.0 18 | tenacity>=8.0.1 19 | termcolor>=1.1.0 20 | tzlocal>=3.0 21 | urllib3>=1.26.7 22 | websocket-client>=1.2.1 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Extensions 2 | *.html 3 | *.zip 4 | *.csv 5 | *.prof 6 | .DS_Store 7 | 8 | # Folders 9 | out/ 10 | notebooks/ 11 | __pycache__/ 12 | core/__pycache__/ 13 | core/bots/__pycache__/ 14 | images/mosquito_imgs/ 15 | .idea/ 16 | exchanges/__pycache__/ 17 | exchanges/poloniex/__pycache__/ 18 | strategies/__pycache__/ 19 | notebooks/.ipynb_checkpoints/* 20 | 21 | # Files 22 | config.ini 23 | mosquito.ini 24 | images/rendered_plain_small.png 25 | config.bumblebee.ini 26 | config.mosquito.ini 27 | mosquito_ai.ini 28 | notebooks/.ipynb_checkpoints 29 | -------------------------------------------------------------------------------- /core/bots/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TradeMode(Enum): 5 | """ 6 | Enum class for holding all available trade modes 7 | """ 8 | backtest = 0 9 | paper = 1 10 | live = 2 11 | 12 | 13 | class BuySellMode(Enum): 14 | """ 15 | Enum class holding buy/sell mode the bot should use 16 | """ 17 | all = 0 # Only 1 currency will be used for trading 18 | fixed = 1 # Currencies will be bought only for given amount 19 | user_defined = 2 # Buy/sell amount is specified by the user 20 | -------------------------------------------------------------------------------- /lib/indicators/epc.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def epc(array, distance=5): 4 | """ 5 | Calculates percentage change between 2 elements between give space distance 6 | close: price numpy.ndarray 7 | distance: space distance check 8 | """ 9 | 10 | dataset_size = array.size 11 | if dataset_size < distance-1: 12 | print('Error in ropc.py: passed not enough data! Required: ' + str(distance) + 13 | ' passed: ' + str(dataset_size)) 14 | return None 15 | 16 | return (array[-1]*100.0 / array[-distance]) - 100.0 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /utils/fetch2gcp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Simple scripts which fetched ticker data, runs blueprint and uploads data to gcp 4 | 5 | DAYS=2 6 | BUCKET=mosquito 7 | BUCKER_DIR=data/ 8 | PAIRS=BTC_ETH 9 | BLEUPRINT=blp5m1117 10 | 11 | 12 | echo fetching ticker data 13 | cd .. 14 | python3 backfill.py --days $DAYS --pairs $PAIRS 15 | 16 | echo generating blueprint 17 | python3 blueprint.py --pairs $PAIRS --days $DAYS --features $BLEUPRINT 18 | 19 | 20 | echo uploading to gcp 21 | cd utils 22 | python3 blueprints2gcp.py --bucket $BUCKET --bucket_dir $BUCKER_DIR 23 | 24 | echo done 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lense.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | from utils.walletlense import WalletLense 3 | 4 | """ 5 | Lense: Returns actual wallet statistics with simple daily digest (winners / losers) 6 | """ 7 | 8 | 9 | def main(): 10 | lense = WalletLense() 11 | lense.get_stats() 12 | 13 | 14 | if __name__ == "__main__": 15 | arg_parser = configargparse.get_argument_parser() 16 | arg_parser.add('-c', '--config', is_config_file=True, help='config file path', default='mosquito.ini') 17 | arg_parser.add('-v', '--verbosity', help='Verbosity', action='store_true') 18 | args = arg_parser.parse_known_args()[0] 19 | 20 | main() 21 | 22 | -------------------------------------------------------------------------------- /core/tradeaction.py: -------------------------------------------------------------------------------- 1 | from strategies.enums import TradeState 2 | from core.bots.enums import BuySellMode 3 | 4 | 5 | class TradeAction: 6 | """ 7 | Trade Action class 8 | """ 9 | 10 | def __init__(self, pair, 11 | action=TradeState.none, 12 | amount=0.0, 13 | rate=0.0, 14 | buy_sell_mode=BuySellMode.all): 15 | """ 16 | Definition of Trace Action 17 | """ 18 | 19 | self.pair = pair 20 | self.action = action 21 | self.amount = amount 22 | self.rate = rate 23 | self.buy_sell_mode = buy_sell_mode 24 | self.order_number = None 25 | 26 | -------------------------------------------------------------------------------- /dojo.py: -------------------------------------------------------------------------------- 1 | import time 2 | import configargparse 3 | from dojo.dojo import Dojo 4 | 5 | 6 | def run(): 7 | """ 8 | Start blueprint 9 | """ 10 | dojo = Dojo() 11 | start_time = time.time() 12 | args = arg_parser.parse_known_args()[0] 13 | dojo.train(blueprint=args.blueprint) 14 | end_time = time.time() 15 | time_delta = end_time - start_time 16 | print('Finished in ' + str(int(time_delta)) + ' sec.') 17 | 18 | 19 | if __name__ == '__main__': 20 | arg_parser = configargparse.get_argument_parser() 21 | arg_parser.add('-v', '--verbose', help='Verbosity', action='store_true') 22 | arg_parser.add('-b', '--blueprint', help='blueprint csv.file') 23 | run() 24 | -------------------------------------------------------------------------------- /examples/exchange.py: -------------------------------------------------------------------------------- 1 | import time 2 | import configargparse 3 | from exchanges.exchange import Exchange 4 | 5 | 6 | def trade_history(): 7 | """ 8 | Gets sample trades history 9 | """ 10 | exchange = Exchange() 11 | end = time.time() 12 | start = end - 3600 13 | data = exchange.get_market_history(start=start, 14 | end=end, 15 | currency_pair='BTC_ETH') 16 | print(data) 17 | 18 | 19 | if __name__ == "__main__": 20 | arg_parser = configargparse.get_argument_parser() 21 | arg_parser.add('-c', '--config', is_config_file=True, help='config file path', default='../mosquito.ini') 22 | options = arg_parser.parse_known_args()[0] 23 | 24 | trade_history() 25 | -------------------------------------------------------------------------------- /core/wallet.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | 3 | 4 | class Wallet: 5 | """ 6 | Class holding current status of wallet (assets and currencies) 7 | """ 8 | arg_parser = configargparse.get_argument_parser() 9 | arg_parser.add('--wallet_currency', help='Wallet currency (separated by comma)') 10 | arg_parser.add("--wallet_amount", help='Wallet amount (separated by comma)') 11 | 12 | def __init__(self): 13 | args = self.arg_parser.parse_known_args()[0] 14 | currency = args.wallet_currency.replace(" ", "").split(',') 15 | amount = args.wallet_amount.replace(" ", "").split(',') 16 | amount = [float(i) for i in amount] 17 | self.initial_balance = dict(zip(currency, amount)) 18 | self.current_balance = self.initial_balance.copy() 19 | -------------------------------------------------------------------------------- /blueprint.py: -------------------------------------------------------------------------------- 1 | import time 2 | import configargparse 3 | from ai.blueprint import Blueprint 4 | import logging 5 | 6 | 7 | def run(): 8 | """ 9 | Start blueprint 10 | """ 11 | blueprint = Blueprint() 12 | start_time = time.time() 13 | blueprint.run() 14 | end_time = time.time() 15 | time_delta = end_time - start_time 16 | logger = logging.getLogger(__name__) 17 | logger.info('Finished in ' + str(int(time_delta)) + ' sec.') 18 | 19 | 20 | if __name__ == '__main__': 21 | arg_parser = configargparse.get_argument_parser() 22 | arg_parser.add('--pairs', help='Pairs to run blueprint on. For ex. [BTC_ETH, BTC_* (to get all BTC_* prefixed pairs]') 23 | arg_parser.add('-c', '--config', is_config_file=True, help='config file path', default='mosquito.ini') 24 | run() 25 | -------------------------------------------------------------------------------- /backfill.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | from backfill.candles import Candles 3 | from backfill.trades import Trades 4 | 5 | 6 | def main(args): 7 | if args.full: 8 | trades_client = Trades() 9 | trades_client.run() 10 | candles_client = Candles() 11 | candles_client.run() 12 | return 13 | 14 | if args.backfilltrades: 15 | backfill_client = Trades() 16 | backfill_client.run() 17 | else: 18 | backfill_client = Candles() 19 | backfill_client.run() 20 | 21 | 22 | if __name__ == "__main__": 23 | arg_parser = configargparse.get_argument_parser() 24 | arg_parser.add('--full', 25 | help='Backfill candle and trades', 26 | action='store_true') 27 | 28 | options = arg_parser.parse_known_args()[0] 29 | main(options) 30 | -------------------------------------------------------------------------------- /lib/indicators/ropc.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def ropc(close, timeperiod=5): 4 | """ 5 | Calculates percentage change between n last elements 6 | close: price numpy.ndarray 7 | timeperiod: size to check 8 | """ 9 | 10 | dataset_size = close.size 11 | if dataset_size < timeperiod-1: 12 | print('Error in ropc.py: passed not enough data! Required: ' + str(timeperiod) + 13 | ' passed: ' + str(dataset_size)) 14 | return None 15 | 16 | close_list = close.tolist() 17 | prev_value = None 18 | price_diff_sum = 0.0 19 | for value in close_list: 20 | if not prev_value: 21 | prev_value = value 22 | continue 23 | value_diff = value - prev_value 24 | perc_change = (value_diff*100/prev_value) 25 | price_diff_sum += perc_change 26 | 27 | return price_diff_sum 28 | 29 | 30 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Define a job to be invoked later in a workflow. 6 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 7 | jobs: 8 | say-hello: 9 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 10 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 11 | docker: 12 | - image: cimg/base:stable 13 | # Add steps to the job 14 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 15 | steps: 16 | - checkout 17 | - run: 18 | name: "Say hello" 19 | command: "echo Hello, World!" 20 | 21 | # Invoke jobs via workflows 22 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 23 | workflows: 24 | say-hello-workflow: 25 | jobs: 26 | - say-hello 27 | -------------------------------------------------------------------------------- /utils/telegrambot.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | import logging 3 | import telegram 4 | 5 | 6 | def run(token, chat_id): 7 | """ 8 | Telegram bot connection 9 | """ 10 | 11 | bot = telegram.Bot(token=token) 12 | logger = logging.getLogger(__name__) 13 | bot.send_message(chat_id=chat_id, text="I'm sorry Dave I'm afraid I can't do that.") 14 | logger.info(bot.get_me()) 15 | """ 16 | 17 | file = open('mosquito_stats.html', 'w') 18 | file.write(body) 19 | file.close() 20 | 21 | token = '572035357:AAEZ0na7xvdIUk53o9OTfLzwZkX52_nTAY4' 22 | chat_id = '406903247' 23 | bot = telegram.Bot(token=token) 24 | bot.send_document(chat_id=chat_id, document=open('mosquito_stats.html', 'rb')) 25 | """ 26 | 27 | 28 | if __name__ == '__main__': 29 | arg_parser = configargparse.get_argument_parser() 30 | arg_parser.add('--telegram_token', help='Telegram token', required=True) 31 | arg_parser.add('--chat_id', help='Telegram Chat id', required=True) 32 | 33 | args = arg_parser.parse_known_args()[0] 34 | run(token=args.telegram_token, chat_id=args.chat_id) 35 | -------------------------------------------------------------------------------- /lib/indicators/macd.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | 4 | def macd(close, previous_macds=[], fast_period=12, slow_period=26, signal_period=9): 5 | """ 6 | MACD - Moving Average Convergence Divergence 7 | previous_macd: numpy.ndarray of previous MACDs 8 | Returns: 9 | - macd 10 | - macd_line 11 | """ 12 | dataset_size = close.size 13 | if dataset_size < slow_period-1: 14 | print('Error in macd.py: passed not enough data! Required: ' + str(slow_period) + 15 | ' passed: ' + str(dataset_size)) 16 | return None, None 17 | 18 | try: 19 | ema_slow = talib.EMA(close, timeperiod=slow_period)[-1] 20 | ema_fast = talib.EMA(close[-fast_period:], timeperiod=fast_period)[-1] 21 | macd_value = ema_fast - ema_slow 22 | 23 | # print('previous_macds:', previous_macds) 24 | if len(previous_macds) < signal_period: 25 | signal_line = None 26 | else: 27 | signal_line = talib.EMA(previous_macds[-signal_period:], timeperiod=signal_period)[-1] 28 | except Exception as e: 29 | print('Got Exception in macd.py. Details: ' + str(e) + '. Data: ' + str(previous_macds)) 30 | return None, None 31 | 32 | return macd_value, signal_line 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /mosquito.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | from core.engine import Engine 3 | 4 | 5 | def main(): 6 | engine = Engine() 7 | engine.run() 8 | 9 | 10 | def has_mandatory_fields(options): 11 | """ 12 | Checks if command arguments contain all mandatory arguments 13 | """ 14 | if not options.backtest and not options.live and not options.paper: 15 | return False 16 | return True 17 | 18 | 19 | if __name__ == "__main__": 20 | arg_parser = configargparse.get_argument_parser() 21 | arg_parser.add('-c', '--config', is_config_file=True, help='config file path', default='mosquito.ini') 22 | arg_parser.add('--backtest', help='Simulate your strategy on history ticker data', action='store_true') 23 | arg_parser.add("--paper", help="Simulate your strategy on real ticker", action='store_true') 24 | arg_parser.add("--live", help="REAL trading mode", action='store_true') 25 | arg_parser.add('-v', '--verbosity', help='Verbosity', action='store_true') 26 | arg_parser.add('--strategy', help='Strategy') 27 | arg_parser.add('--fixed_trade_amount', help='Fixed trade amount') 28 | args = arg_parser.parse_known_args()[0] 29 | 30 | if not has_mandatory_fields(args): 31 | print("Missing trade mode argument (backtest, paper or live). See --help for more details.") 32 | exit(0) 33 | 34 | main() 35 | 36 | -------------------------------------------------------------------------------- /strategies/ai/scikitbase.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | import configargparse 3 | from sklearn.externals import joblib 4 | from termcolor import colored 5 | 6 | 7 | class ScikitBase(ABC): 8 | """ 9 | Base class for AI strategies 10 | """ 11 | arg_parser = configargparse.get_argument_parser() 12 | arg_parser.add('-p', '--pipeline', help='trained model/pipeline (*.pkl file)', required=True) 13 | arg_parser.add('-f', '--feature_names', help='List of features list pipeline (*.pkl file)') 14 | pipeline = None 15 | 16 | def __init__(self): 17 | args = self.arg_parser.parse_known_args()[0] 18 | super(ScikitBase, self).__init__() 19 | self.pipeline = self.load_pipeline(args.pipeline) 20 | if args.feature_names: 21 | self.feature_names = self.load_pipeline(args.feature_names) 22 | 23 | @staticmethod 24 | def load_pipeline(pipeline_file): 25 | """ 26 | Loads scikit model/pipeline 27 | """ 28 | print(colored('Loading pipeline: ' + pipeline_file, 'green')) 29 | return joblib.load(pipeline_file) 30 | 31 | def fetch_pipeline_from_server(self): 32 | """ 33 | Method fetches pipeline from server/cloud 34 | """ 35 | # TODO 36 | pass 37 | 38 | def predict(self, df): 39 | """ 40 | Returns predictions based on the model/pipeline 41 | """ 42 | try: 43 | return self.pipeline.predict(df) 44 | except (ValueError, TypeError): 45 | print(colored('Got ValueError while using scikit model.. ', 'red')) 46 | return None 47 | 48 | -------------------------------------------------------------------------------- /utils/blueprints2gcp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import google.cloud.storage 3 | import configargparse 4 | import logging 5 | import glob 6 | 7 | 8 | def get_last_file(path): 9 | """ 10 | :param dir: 11 | :return: terates through all files that are under the given path and 12 | returns last created/edited file 13 | """ 14 | 15 | list_of_files = glob.glob(path + '*') 16 | return max(list_of_files, key=os.path.getctime) 17 | 18 | 19 | def run(root_dir, bucket_name, bucket_dir): 20 | """ 21 | Simple script which fetches last generated blueprint and uploads 22 | """ 23 | logger = logging.getLogger(__name__) 24 | logger.info('Starting to upload blueprints to gcp bucket: ' + bucket_name + 25 | ', bucket_dir: ' + bucket_dir) 26 | 27 | # Get last modified/created blueprint file 28 | last_blueprint_file = get_last_file(root_dir) 29 | logger.info('last_blueprint_file:' + last_blueprint_file) 30 | 31 | # Create a storage client. 32 | source_file_name = last_blueprint_file 33 | storage_client = google.cloud.storage.Client() 34 | bucket = storage_client.get_bucket(bucket_name) 35 | blob = bucket.blob(bucket_dir + os.path.basename(source_file_name)) 36 | blob.upload_from_filename(source_file_name) 37 | logger.info('File ' + source_file_name + ' uploaded to ' + str(bucket)) 38 | 39 | 40 | if __name__ == '__main__': 41 | arg_parser = configargparse.get_argument_parser() 42 | arg_parser.add('--path', help='Path to root dir from where the script should read files (default out/blueprints)') 43 | arg_parser.add('--bucket', help='GCP Bucket name and path', required=True) 44 | arg_parser.add('--bucket_dir', help='GCP Bucket directory') 45 | 46 | args = arg_parser.parse_known_args()[0] 47 | 48 | if args.path is None: 49 | args.path = '../out/blueprints/' 50 | run(root_dir=args.path, bucket_name=args.bucket, bucket_dir=args.bucket_dir) 51 | -------------------------------------------------------------------------------- /lib/indicators/elderray.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | 4 | def elderray(close): 5 | """ 6 | Elder ray Indicator (Bulls/Bears Power) 7 | Returns: 8 | 0 - No condition met 9 | 1 - Green Price Bar: (13-period EMA > previous 13-period EMA) and (MACD-Histogram > previous period's MACD-Histogram) 10 | 2 - Red Price Bar: (13-period EMA < previous 13-period EMA) and (MACD-Histogram < previous period's MACD-Histogram) 11 | 12 | Price bars are colored blue when conditions for a Red Price Bar or Green Price Bar are not met. The MACD-Histogram 13 | is based on MACD(12,26,9). 14 | """ 15 | 16 | min_dataset_size = 36 17 | dataset_size = close.size 18 | if dataset_size < min_dataset_size: 19 | print('Error in elderray.py: passed not enough data! Required: ' + str(min_dataset_size) + 20 | ' passed: ' + str(dataset_size)) 21 | return None 22 | 23 | # Calc EMA 24 | ema_period = 13 25 | ema = talib.EMA(close[-ema_period:], timeperiod=ema_period)[-1] 26 | ema_prev = talib.EMA(close[-ema_period-1:len(close)-1], 27 | timeperiod=ema_period)[-1] 28 | 29 | # Calc MACD 30 | macd_period = 34 31 | macd, macd_signal, _ = talib.MACD(close[-macd_period:], 32 | fastperiod=12, 33 | slowperiod=26, 34 | signalperiod=9) 35 | macd = macd[-1:] 36 | 37 | macd_prev, macd_signal_prev, _ = talib.MACD(close[-macd_period-1:len(close)-1], 38 | fastperiod=12, 39 | slowperiod=26, 40 | signalperiod=9) 41 | macd_prev = macd_prev[-1:] 42 | 43 | # Green Price Bar 44 | if ema > ema_prev and macd > macd_prev: 45 | return 1 46 | 47 | # Red Price Bar 48 | if ema < ema_prev and macd < macd_prev: 49 | return 2 50 | 51 | return 0 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/indicators/stoploss.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | 3 | 4 | class StopLoss: 5 | """ 6 | StopLoss class with normal and trailing stop-loss functionality 7 | :param: interval: Interval in minutes for checking price difference 8 | :param stop_loss_limit: Percentage value for stop-loss (how much should the price drop to send stop-loss signal 9 | :param trailing: If True stop_loss Trailing stop-loss will be applied. If False first price will be used 10 | for a static stop-loss limit 11 | :param ticker_size: Ticker size 12 | """ 13 | 14 | arg_parser = configargparse.get_argument_parser() 15 | arg_parser.add('--stoploss_interval', help='Stop-loss interval in minutes') 16 | base_price = None 17 | 18 | def __init__(self, ticker_size, interval=30, stop_loss_limit=-0.1, trailing=True, ): 19 | self.trailing = trailing 20 | self.checkpoint = int(interval/ticker_size) 21 | self.stop_loss_limit = stop_loss_limit 22 | 23 | def set_base_price(self, price): 24 | """ 25 | Sets base price, which is compared to trailing-stop 26 | :param price: 27 | """ 28 | self.base_price = price 29 | 30 | def calculate(self, price): 31 | """ 32 | 33 | :param price: numpy array of price values 34 | :return: Returns True if Stop-Loss met conditions 35 | """ 36 | 37 | # Check if array has data 38 | if len(price) < self.checkpoint: 39 | print('StopLoss: not enough data.') 40 | return False 41 | 42 | if not self.base_price: 43 | self.base_price = price[-1] 44 | print('StopLoss: setting base-price to:', self.base_price) 45 | return False 46 | 47 | last_price = price[-1] 48 | checkpoint_price = price[-self.checkpoint] 49 | percentage_change = last_price*100/checkpoint_price - 100.0 50 | if percentage_change <= self.stop_loss_limit: 51 | return True 52 | 53 | # Handle trailing 54 | if self.trailing: 55 | if last_price > self.base_price: 56 | self.base_price = last_price 57 | 58 | -------------------------------------------------------------------------------- /core/common.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from importlib import import_module 3 | from termcolor import colored 4 | 5 | 6 | def load_module(module_prefix, module_name): 7 | """ 8 | Loads strategy module based on given name. 9 | """ 10 | if module_name is None: 11 | print(colored('Not provided module,. please add it as an argument or in config file', 'red')) 12 | sys.exit() 13 | mod = import_module(module_prefix + module_name) 14 | module_class = getattr(mod, module_name.split('.')[-1].capitalize()) 15 | return module_class 16 | 17 | 18 | def handle_buffer_limits(df, max_size): 19 | """ 20 | Handles dataframe limits (drops df, if the df > max_size) 21 | """ 22 | df_size = len(df.index) 23 | if df_size > max_size: 24 | # print(colored('Max buffer memory exceeded, cleaning', 'yellow')) 25 | rows_to_delete = df_size - max_size 26 | df = df.ix[rows_to_delete:] 27 | df = df.reset_index(drop=True) 28 | return df 29 | 30 | 31 | def parse_pairs(exchange, in_pairs): 32 | """ 33 | Returns list of available pairs from exchange based on the given pairs string/list 34 | """ 35 | all_pairs = exchange.get_pairs() 36 | if in_pairs == 'all': 37 | print('setting_all_pairs') 38 | return all_pairs 39 | else: 40 | pairs = [] 41 | parsed_pairs = in_pairs.replace(" ", "").split(',') 42 | for in_pair in parsed_pairs: 43 | if '*' in in_pair: 44 | prefix = in_pair.replace('*', '') 45 | pairs_list = [p for p in all_pairs if prefix in p] 46 | pairs.extend(pairs_list) 47 | # remove duplicates 48 | # pairs = list(set(pairs)) 49 | else: 50 | pairs.append(in_pair) 51 | return pairs 52 | 53 | 54 | def get_dataset_count(df, group_by_field='pair'): 55 | """ 56 | Returns count of dataset and pairs_count (group by provided string) 57 | """ 58 | pairs_group = df.groupby([group_by_field]) 59 | # cnt = pairs_group.count() 60 | pairs_count = len(pairs_group.groups.keys()) 61 | dataset_cnt = pairs_group.size().iloc[0] 62 | return dataset_cnt, pairs_count 63 | -------------------------------------------------------------------------------- /utils/postman.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import configargparse 3 | from email.mime.text import MIMEText 4 | from email.mime.multipart import MIMEMultipart 5 | from premailer import transform 6 | 7 | 8 | class Postman: 9 | """ 10 | Simple email/postman module 11 | ! Currently supported only for gmail 12 | """ 13 | arg_parser = configargparse.get_argument_parser() 14 | arg_parser.add('--mail_username', help='Email username (supported only gmail)') 15 | arg_parser.add("--mail_password", help='Email password (supported only gmail)') 16 | arg_parser.add("--mail_recipients", help='Email recipients') 17 | 18 | def __init__(self): 19 | self.args = self.arg_parser.parse_known_args()[0] 20 | self.username = self.args.mail_username 21 | self.password = self.args.mail_password 22 | self.recipients = self.args.mail_recipients 23 | 24 | def send_mail(self, subject, body): 25 | """ 26 | Send email to configured account with given subject and body 27 | """ 28 | mail_from = self.username 29 | # mail_to = self.recipients if type(self.recipients) is list else [self.recipients] 30 | mail_to = self.recipients 31 | 32 | msg = MIMEMultipart('alternative') 33 | msg['Subject'] = subject 34 | msg['From'] = mail_from 35 | msg['To'] = mail_to 36 | 37 | # body = self.html_style() + body 38 | # msg.attach(MIMEText(body, 'html')) 39 | body = transform(body) 40 | #body = '
Hej
' 41 | msg.attach(MIMEText(body, 'html')) 42 | mail = smtplib.SMTP("smtp.gmail.com", 587) 43 | mail.ehlo() 44 | mail.starttls() 45 | mail.login(self.username, self.password) 46 | mail.sendmail(mail_from, mail_to, msg.as_string()) 47 | mail.close() 48 | print('mail successfully sent') 49 | 50 | @staticmethod 51 | def html_style(): 52 | """ 53 | Email css styles 54 | """ 55 | style = ''' 56 | 62 | ''' 63 | return style 64 | -------------------------------------------------------------------------------- /dojo/dojo.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import configargparse 3 | 4 | 5 | class Dojo: 6 | """ 7 | Main module responsible for training 8 | """ 9 | arg_parser = configargparse.get_argument_parser() 10 | 11 | def __init__(self): 12 | args = self.arg_parser.parse_known_args()[0] 13 | self.verbose = args.verbose 14 | 15 | def train(self, blueprint=None, automatic_search=True, models=None, minimum_score=0.7): 16 | """ 17 | Trains input data by automatic_search (GA) or with given models 18 | :param blueprint: multi-currency features csv file (for example generated by blueprint module) 19 | :param automatic_search: automatically find the best models 20 | :param models: list of models to be used. If automatic_search is True, models will be included into model search 21 | :param minimum_score: minimum_score the model needs to have so that it is returned 22 | :return: list of best models with their score 23 | """ 24 | if not blueprint: 25 | print("Required blueprint csv. argument value is missing, nothing to do here.") 26 | return 27 | 28 | print('Loading dataset') 29 | df_pair_groups = self.load_blueprint(blueprint) 30 | # Get trained models for every pair 31 | for pair, df in df_pair_groups: 32 | pair_models = self.train_pair(pair, df, automatic_search, models, minimum_score) 33 | # TODO: Store pair_models 34 | print(pair) 35 | 36 | @staticmethod 37 | def train_pair(pair, df, automatic_search, models, minimum_score): 38 | """ 39 | Function that trains given dataset for a specific pair 40 | """ 41 | print('Training model for pair:', pair) 42 | # TODO: Train model 43 | return pair, None 44 | 45 | @staticmethod 46 | def load_blueprint(blueprint_file): 47 | """ 48 | Loads blueprint csv file and returns paired groups (grouped by pair) 49 | """ 50 | print('Loading dataset') 51 | df = pd.read_csv(blueprint_file) 52 | df_pair_groups = df.groupby(['pair']) 53 | pairs_names = list(df_pair_groups.groups.keys()) 54 | print('Training total of: ', len(pairs_names), 'pairs and ', df.shape[0], 'records') 55 | return df_pair_groups 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /core/bots/live.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from core.bots.enums import TradeMode 3 | import time 4 | 5 | 6 | class Live(Base): 7 | """ 8 | Main class for Live Trading 9 | """ 10 | mode = TradeMode.live 11 | 12 | def __init__(self): 13 | super(Live, self).__init__(self.mode) 14 | self.counter = 0 15 | # open_orders = self.exchange.get_open_orders() 16 | # print(open_orders) 17 | 18 | def get_next(self, interval_in_min): 19 | """ 20 | Returns next state 21 | Interval: Interval in minutes 22 | """ 23 | interval_in_sec = interval_in_min*60 24 | epoch_now = int(time.time()) 25 | if self.last_tick_epoch > 0: 26 | next_ticker_time = (self.last_tick_epoch + interval_in_sec) 27 | delay_second = epoch_now - next_ticker_time 28 | if delay_second < 0: 29 | print('Going to sleep for: ', abs(delay_second), ' seconds.') 30 | time.sleep(abs(delay_second)) 31 | 32 | if not self.ticker_df.empty: 33 | self.ticker_df.drop(self.ticker_df.index, inplace=True) 34 | 35 | print('Fetching data for ' + str(len(self.pairs)) + ' ticker/tickers.', end='', flush=True) 36 | 37 | epoch_now = int(time.time()) 38 | epoch_start = epoch_now - interval_in_sec*5 # just to be sure get extra 5 datasets 39 | epoch_end = epoch_now 40 | for pair in self.pairs: 41 | new_df = self.exchange.get_candles_df(pair, epoch_start, epoch_end, interval_in_sec) 42 | # df = self.exchange.get_symbol_ticker(pair, interval_in_min) 43 | if self.ticker_df.empty: 44 | self.ticker_df = new_df.copy() 45 | else: 46 | self.ticker_df = self.ticker_df.append(new_df, ignore_index=True) 47 | print('.', end='', flush=True) 48 | 49 | print('..done') 50 | self.last_tick_epoch = epoch_now 51 | return self.ticker_df 52 | 53 | def get_balance(self): 54 | """ 55 | Returns wallet balance 56 | """ 57 | return self.exchange.get_balances() 58 | 59 | def trade(self, actions, wallet, trades, force_sell=True): 60 | """ 61 | Simulate currency buy/sell (places fictive buy/sell orders) 62 | """ 63 | # TODO: we need to deal with trades-buffer (trades) 64 | return self.exchange.trade(actions, wallet, TradeMode.live) 65 | -------------------------------------------------------------------------------- /ai/blueprints/junior.py: -------------------------------------------------------------------------------- 1 | import talib 2 | from .base import Base 3 | 4 | 5 | class Junior(Base): 6 | """ 7 | Mid-size blueprint - EMA, RCI, CCI, OBV 8 | """ 9 | 10 | def __init__(self, pairs): 11 | super(Junior, self).__init__('junior', pairs) 12 | self.min_history_ticks = 35 13 | 14 | @staticmethod 15 | def calculate_features(df): 16 | """ 17 | Method which calculates and generates features 18 | """ 19 | close = df['close'].values 20 | high = df['high'].values 21 | low = df['low'].values 22 | volume = df['volume'].values 23 | last_row = df.tail(1).copy() 24 | 25 | periods = [2, 4, 8, 12, 16, 20] 26 | for period in periods: 27 | # ************** Calc EMAs 28 | ema = talib.EMA(close[-period:], timeperiod=period)[-1] 29 | last_row['ema' + str(period)] = ema 30 | 31 | # ************** Calc OBVs 32 | obv = talib.OBV(close[-period:], volume[-period:])[-1] 33 | last_row['obv' + str(period)] = obv 34 | 35 | # ************** Calc RSIs 36 | rsi_periods = [5] 37 | for rsi_period in rsi_periods: 38 | rsi = talib.RSI(close[-rsi_period:], timeperiod=rsi_period-1)[-1] 39 | last_row['rsi' + str(rsi_period)] = rsi 40 | last_row['rsi_above_50' + str(rsi_period)] = int(rsi > 50.0) 41 | 42 | # ************** Calc CCIs 43 | cci_periods = [5] 44 | for cci_period in cci_periods: 45 | cci = talib.CCI(high[-cci_period:], 46 | low[-cci_period:], 47 | close[-cci_period:], 48 | timeperiod=cci_period)[-1] 49 | last_row['cci' + str(cci_period)] = cci 50 | 51 | # ************** Calc MACD 1 52 | macd_periods = [34] 53 | for macd_period in macd_periods: 54 | macd, macd_signal, _ = talib.MACD(close[-macd_period:], 55 | fastperiod=12, 56 | slowperiod=26, 57 | signalperiod=9) 58 | macd = macd[-1] 59 | signal_line = macd_signal[-1] 60 | last_row['macd_above_signal' + str(macd_period)] = int(macd > signal_line) 61 | last_row['macd_above_zero' + str(macd_period)] = int(macd > 0.0) 62 | 63 | return last_row 64 | -------------------------------------------------------------------------------- /backfill/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import configargparse 3 | from logging.config import fileConfig 4 | from abc import ABC, abstractmethod 5 | from pymongo import MongoClient 6 | from exchanges.exchange import Exchange 7 | 8 | 9 | class Base(ABC): 10 | """ 11 | Base class for data back-filling 12 | """ 13 | arg_parser = configargparse.get_argument_parser() 14 | arg_parser.add('-c', '--config', is_config_file=True, help='config file path', default='mosquito.ini') 15 | arg_parser.add('--pairs', help='Pairs to backfill. For ex. [BTC_ETH, BTC_* (to get all BTC_* prefixed pairs]') 16 | arg_parser.add("--all", help='Backfill data for ALL currencies', action='store_true') 17 | arg_parser.add("--days", help="Number of days to backfill", required=True, type=int, default=1) 18 | arg_parser.add('-v', '--verbosity', help='Verbosity', action='store_true') 19 | logging.config.fileConfig('logging.ini') 20 | 21 | def __init__(self): 22 | super(Base, self).__init__() 23 | args = self.arg_parser.parse_known_args()[0] 24 | self.exchange = Exchange() 25 | self.exchange_name = self.exchange.get_exchange_name() 26 | self.db = self.initialize_db(args) 27 | 28 | @staticmethod 29 | def initialize_db(args): 30 | """ 31 | DB Initializer 32 | """ 33 | db = args.db 34 | port = int(args.db_port) 35 | url = args.db_url 36 | # Init DB 37 | client = MongoClient(url, port) 38 | return client[db] 39 | 40 | def get_backfill_pairs(self, backfill_all_pairs=False, pairs_list=None): 41 | """ 42 | Returns list of exchange pairs that were ordered to backfill 43 | """ 44 | all_pairs = self.exchange.get_pairs() 45 | if backfill_all_pairs: 46 | return all_pairs 47 | elif pairs_list is not None: 48 | tmp_pairs = [pairs_list] 49 | pairs = [] 50 | # Handle * suffix pairs 51 | for pair in tmp_pairs: 52 | if '*' in pair: 53 | prefix = pair.replace('*', '') 54 | pairs_list = [p for p in all_pairs if prefix in p] 55 | pairs.extend(pairs_list) 56 | # remove duplicates 57 | pairs = list(set(pairs)) 58 | else: 59 | pairs.append(pair) 60 | return pairs 61 | 62 | @abstractmethod 63 | def run(self): 64 | """ 65 | Backfill/fetch data 66 | """ 67 | pass 68 | -------------------------------------------------------------------------------- /core/bots/backtest.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | import time 3 | import pandas as pd 4 | from core.bots.enums import TradeMode 5 | import configargparse 6 | 7 | 8 | DAY_IN_SECONDS = 86400 9 | 10 | 11 | class Backtest(Base): 12 | """ 13 | Main class for Backtest trading 14 | """ 15 | arg_parser = configargparse.get_argument_parser() 16 | arg_parser.add('--backtest_from', help='Backtest epoch start datetime') 17 | arg_parser.add("--backtest_to", help='Backtest epoch end datetime') 18 | arg_parser.add("--days", help='Number of history days the simulation should start from') 19 | 20 | mode = TradeMode.backtest 21 | sim_start = None 22 | sim_end = None 23 | sim_days = None 24 | 25 | def __init__(self, wallet): 26 | args = self.arg_parser.parse_known_args()[0] 27 | super(Backtest, self).__init__(self.mode) 28 | self.counter = 0 29 | if args.backtest_from: 30 | self.sim_start = int(args.backtest_from) 31 | if args.backtest_to: 32 | self.sim_end = int(args.backtest_to) 33 | if args.days: 34 | self.sim_days = int(args.days) 35 | self.sim_epoch_start = self.get_sim_epoch_start(self.sim_days, self.sim_start) 36 | self.current_epoch = self.sim_epoch_start 37 | self.balance = wallet 38 | 39 | @staticmethod 40 | def get_sim_epoch_start(sim_days, sim_start): 41 | if sim_start: 42 | return sim_start 43 | elif sim_days: 44 | epoch_now = int(time.time()) 45 | return epoch_now - (DAY_IN_SECONDS * sim_days) 46 | 47 | def get_next(self, interval_in_min): 48 | """ 49 | Returns next state of current_time + interval (in minutes) 50 | """ 51 | if self.sim_end and self.current_epoch > self.sim_end: 52 | return pd.DataFrame() 53 | self.ticker_df = self.exchange.get_offline_ticker(self.current_epoch, self.pairs) 54 | df_trades = self.exchange.get_offline_trades(self.current_epoch, self.pairs) 55 | 56 | if df_trades.empty: 57 | self.ticker_df = self.ticker_df 58 | else: 59 | self.ticker_df = self.ticker_df.merge(df_trades) 60 | 61 | self.current_epoch += interval_in_min*60 62 | return self.ticker_df 63 | 64 | def trade(self, actions, wallet, trades, force_sell=True): 65 | """ 66 | Simulate currency buy/sell (places fictive buy/sell orders) 67 | """ 68 | return super(Backtest, self).trade(actions, wallet, trades, force_sell=False) 69 | 70 | -------------------------------------------------------------------------------- /ai/blueprints/blp5m1117.py: -------------------------------------------------------------------------------- 1 | import talib 2 | from .base import Base 3 | 4 | 5 | class Blp5m1117(Base): 6 | """ 7 | Full blown blueprint - using 5m ticker 8 | """ 9 | 10 | def __init__(self, pairs): 11 | super(Blp5m1117, self).__init__('blp5m1117', pairs) 12 | self.min_history_ticks = 35 13 | 14 | @staticmethod 15 | def calculate_features(df): 16 | """ 17 | Method which calculates and generates features 18 | """ 19 | close = df['close'].values 20 | high = df['high'].values 21 | low = df['low'].values 22 | volume = df['volume'].values 23 | last_row = df.tail(1).copy() 24 | 25 | # ************** Calc EMAs 26 | ema_periods = [2, 4, 8, 12, 16, 20] 27 | for ema_period in ema_periods: 28 | ema = talib.EMA(close[-ema_period:], timeperiod=ema_period)[-1] 29 | last_row['ema' + str(ema_period)] = ema 30 | 31 | # ************** Calc RSIs 32 | rsi_periods = [5] 33 | for rsi_period in rsi_periods: 34 | rsi = talib.RSI(close[-rsi_period:], timeperiod=rsi_period-1)[-1] 35 | last_row['rsi' + str(rsi_period)] = rsi 36 | last_row['rsi_above_50' + str(rsi_period)] = int(rsi > 50.0) 37 | 38 | # ************** Calc CCIs 39 | cci_periods = [5] 40 | for cci_period in cci_periods: 41 | cci = talib.CCI(high[-cci_period:], 42 | low[-cci_period:], 43 | close[-cci_period:], 44 | timeperiod=cci_period)[-1] 45 | last_row['cci' + str(cci_period)] = cci 46 | 47 | # ************** Calc MACD 1 48 | macd_periods = [34] 49 | for macd_period in macd_periods: 50 | macd, macd_signal, _ = talib.MACD(close[-macd_period:], 51 | fastperiod=12, 52 | slowperiod=26, 53 | signalperiod=9) 54 | macd = macd[-1] 55 | signal_line = macd_signal[-1] 56 | last_row['macd_above_signal' + str(macd_period)] = int(macd > signal_line) 57 | last_row['macd_above_zero' + str(macd_period)] = int(macd > 0.0) 58 | 59 | # ************** Calc OBVs 60 | obv_periods = [2, 4, 8, 12, 16, 20] 61 | for obv_period in obv_periods: 62 | obv = talib.OBV(close[-obv_period:], volume[-obv_period:])[-1] 63 | last_row['obv' + str(obv_period)] = obv 64 | 65 | return last_row 66 | 67 | 68 | -------------------------------------------------------------------------------- /ai/blueprints/luckyantelope.py: -------------------------------------------------------------------------------- 1 | import talib 2 | from .base import Base 3 | 4 | 5 | class Luckyantelope(Base): 6 | """ 7 | Full blown blueprint - using 2h ticker 8 | """ 9 | 10 | def __init__(self, pairs): 11 | super(Luckyantelope, self).__init__('luckyantelope', pairs) 12 | self.min_history_ticks = 21 13 | 14 | @staticmethod 15 | def calculate_features(df): 16 | """ 17 | Method which calculates and generates features 18 | """ 19 | close = df['close'].values 20 | high = df['high'].values 21 | low = df['low'].values 22 | volume = df['volume'].values 23 | last_row = df.tail(1).copy() 24 | 25 | # ************** Calc EMAs 26 | ema_periods = [2, 4, 8, 12, 16, 20] 27 | for ema_period in ema_periods: 28 | ema = talib.EMA(close[-ema_period:], timeperiod=ema_period)[-1] 29 | last_row['ema' + str(ema_period)] = ema 30 | 31 | # ************** Calc RSIs 32 | rsi_periods = [5] 33 | for rsi_period in rsi_periods: 34 | rsi = talib.RSI(close[-rsi_period:], timeperiod=rsi_period-1)[-1] 35 | last_row['rsi' + str(rsi_period)] = rsi 36 | last_row['rsi_above_50' + str(rsi_period)] = int(rsi > 50.0) 37 | 38 | # ************** Calc CCIs 39 | cci_periods = [5] 40 | for cci_period in cci_periods: 41 | cci = talib.CCI(high[-cci_period:], 42 | low[-cci_period:], 43 | close[-cci_period:], 44 | timeperiod=cci_period)[-1] 45 | last_row['cci' + str(cci_period)] = cci 46 | 47 | # ************** Calc MACD 1 48 | macd_periods = [20] 49 | for macd_period in macd_periods: 50 | macd, macd_signal, _ = talib.MACD(close[-macd_period:], 51 | fastperiod=6, 52 | slowperiod=12, 53 | signalperiod=8) 54 | macd = macd[-1] 55 | signal_line = macd_signal[-1] 56 | last_row['macd_above_signal' + str(macd_period)] = int(macd > signal_line) 57 | last_row['macd_above_zero' + str(macd_period)] = int(macd > 0.0) 58 | 59 | # ************** Calc OBVs 60 | obv_periods = [2, 4, 8, 12, 16, 20] 61 | for obv_period in obv_periods: 62 | obv = talib.OBV(close[-obv_period:], volume[-obv_period:])[-1] 63 | last_row['obv' + str(obv_period)] = obv 64 | 65 | return last_row 66 | 67 | 68 | -------------------------------------------------------------------------------- /strategies/base.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | from termcolor import colored 3 | from abc import ABC, abstractmethod 4 | from .enums import TradeState as ts 5 | from strategies.enums import TradeState 6 | 7 | 8 | class Base(ABC): 9 | """ 10 | Base class for all strategies 11 | """ 12 | arg_parser = configargparse.get_argument_parser() 13 | action_request = ts.none 14 | actions = [] 15 | 16 | def __init__(self): 17 | super(Base, self).__init__() 18 | args = self.arg_parser.parse_known_args()[0] 19 | self.verbosity = args.verbosity 20 | self.min_history_ticks = 5 21 | self.group_by_field = 'pair' 22 | 23 | def get_min_history_ticks(self): 24 | """ 25 | Returns min_history_ticks 26 | """ 27 | return self.min_history_ticks 28 | 29 | @staticmethod 30 | def get_delimiter(df): 31 | if df.empty: 32 | print('Error: get_delimiter! Got empty df!') 33 | pair = df.iloc[-1].pair 34 | return '_' if '_' in pair else '-' 35 | 36 | @staticmethod 37 | def parse_pairs(pairs): 38 | return [x.strip() for x in pairs.split(',')] 39 | 40 | @abstractmethod 41 | def calculate(self, look_back, wallet): 42 | """ 43 | Main Strategy function, which takes recent history data and returns recommended list of actions 44 | """ 45 | None 46 | 47 | @staticmethod 48 | def get_price(trade_action, df, pair): 49 | """ 50 | Returns price based on on the given action and dataset. 51 | """ 52 | 53 | if df.empty: 54 | print(colored('get_price: got empty dataframe (pair): ' + pair + ', skipping!', 'red')) 55 | return 0.0 56 | 57 | pair_df = df.loc[df['pair'] == pair].sort_values('date') 58 | if pair_df.empty: 59 | print(colored('get_price: got empty dataframe for pair: ' + pair + ', skipping!', 'red')) 60 | return 0.0 61 | 62 | pair_df = pair_df.iloc[-1] 63 | close_price = float(pair_df.get('close')) 64 | price = None 65 | 66 | if trade_action == TradeState.buy: 67 | if 'lowestAsk' in pair_df: 68 | price = float(pair_df.get('lowestAsk')) 69 | elif trade_action == TradeState.sell: 70 | if 'highestBid' in pair_df: 71 | price = float(pair_df.get('highestBid')) 72 | 73 | # Check if we don't have nan 74 | if not price or price != price: 75 | if close_price != close_price: 76 | print(colored('got Nan price for pair: ' + pair + '. Dataframe: ' + str(pair_df), 'red')) 77 | return 0.0 78 | else: 79 | return close_price 80 | 81 | return price 82 | -------------------------------------------------------------------------------- /strategies/ema.py: -------------------------------------------------------------------------------- 1 | import configargparse 2 | import pandas as pd 3 | import pandas_ta as ta 4 | from .base import Base 5 | import core.common as common 6 | from .enums import TradeState 7 | from core.bots.enums import BuySellMode 8 | from core.tradeaction import TradeAction 9 | from lib.indicators.stoploss import StopLoss 10 | 11 | 12 | class Ema(Base): 13 | """ 14 | Ema strategy 15 | About: Buy when close_price > ema20, sell when close_price < ema20 and below death_cross 16 | """ 17 | arg_parser = configargparse.get_argument_parser() 18 | 19 | def __init__(self): 20 | args = self.arg_parser.parse_known_args()[0] 21 | super(Ema, self).__init__() 22 | self.name = 'ema' 23 | self.min_history_ticks = 30 24 | self.pair = self.parse_pairs(args.pairs)[0] 25 | self.buy_sell_mode = BuySellMode.all 26 | self.stop_loss = StopLoss(int(args.ticker_size)) 27 | 28 | def calculate(self, look_back, wallet): 29 | """ 30 | Main strategy logic (the meat of the strategy) 31 | """ 32 | (dataset_cnt, _) = common.get_dataset_count(look_back, self.group_by_field) 33 | 34 | # Wait until we have enough data 35 | if dataset_cnt < self.min_history_ticks: 36 | print('dataset_cnt:', dataset_cnt) 37 | return self.actions 38 | 39 | self.actions.clear() 40 | new_action = TradeState.none 41 | 42 | # Calculate indicators 43 | df = look_back.tail(self.min_history_ticks) 44 | close = df['close'] 45 | 46 | # ************** Calc EMA 47 | ema5 = ta.ema(close, length=5).values[-1] 48 | ema10 = ta.ema(close, length=10).values[-1] 49 | ema20 = ta.ema(close, length=20).values[-1] 50 | 51 | close_price = self.get_price(TradeState.none, df.tail(), self.pair) 52 | 53 | print('close_price:', close_price, 'ema:', ema20) 54 | if close_price < ema10 or close_price < ema20: 55 | new_action = TradeState.sell 56 | elif close_price > ema5 and close_price > ema10: 57 | new_action = TradeState.buy 58 | 59 | trade_price = self.get_price(new_action, df.tail(), self.pair) 60 | 61 | # Get stop-loss 62 | if new_action == TradeState.buy and self.stop_loss.calculate(close.values): 63 | print('stop-loss detected,..selling') 64 | new_action = TradeState.sell 65 | 66 | action = TradeAction(self.pair, 67 | new_action, 68 | amount=None, 69 | rate=trade_price, 70 | buy_sell_mode=self.buy_sell_mode) 71 | 72 | self.actions.append(action) 73 | return self.actions 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /backfill/candles.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from pymongo import ASCENDING 4 | from backfill.base import Base 5 | from core.constants import SECONDS_IN_DAY 6 | 7 | 8 | class Candles(Base): 9 | """ 10 | Back-fills ticker candle data 11 | """ 12 | 13 | def __init__(self): 14 | super(Candles, self).__init__() 15 | self.args = self.arg_parser.parse_known_args()[0] 16 | self.db_ticker = self.db.ticker 17 | self.db_ticker.create_index([('id', ASCENDING)], unique=True) 18 | 19 | def run(self): 20 | """ 21 | Run actual backfill job 22 | """ 23 | # Get list of all currencies 24 | logger = logging.getLogger(__name__) 25 | time_start = time.time() 26 | pairs = self.get_backfill_pairs(self.args.all, self.args.pairs) 27 | logger.info("Back-filling candles for total currencies:" + str(len(pairs))) 28 | 29 | # Get the candlestick data 30 | epoch_now = int(time.time()) 31 | 32 | for pair in pairs: 33 | for day in reversed(range(1, int(self.args.days) + 1)): 34 | epoch_from = epoch_now - (SECONDS_IN_DAY * day) 35 | epoch_to = epoch_now if day == 1 else epoch_now - (SECONDS_IN_DAY * (day - 1)) 36 | logger.info('Getting currency data: ' + pair + ', days left: ' + str(day)) 37 | candles = self.exchange.get_candles(pair, 38 | epoch_from, 39 | epoch_to, 40 | 300) # by default 5 minutes candles (minimum) 41 | 42 | logger.info(' (got total candles: ' + str(len(candles)) + ')') 43 | for candle in candles: 44 | if candle['date'] == 0: 45 | logger.warning('Found nothing for pair: ' + pair) 46 | continue 47 | # Convert strings to number (float or int) 48 | for key, value in candle.items(): 49 | if key == 'date': 50 | candle[key] = int(value) 51 | else: 52 | candle[key] = float(value) 53 | new_db_item = candle.copy() 54 | # Add identifier 55 | new_db_item['exchange'] = self.exchange_name 56 | new_db_item['pair'] = pair 57 | unique_id = self.exchange_name + '-' + pair + '-' + str(candle['date']) 58 | new_db_item['id'] = unique_id 59 | # Store to DB 60 | self.db_ticker.update_one({'id': unique_id}, {'$set': new_db_item}, upsert=True) 61 | 62 | time_end = time.time() 63 | duration_in_sec = int(time_end-time_start) 64 | logger.info("Backfill done in (sec) " + str(duration_in_sec)) 65 | -------------------------------------------------------------------------------- /core/bots/paper.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from core.bots.enums import TradeMode 3 | import time 4 | import configargparse 5 | 6 | 7 | class Paper(Base): 8 | """ 9 | Main class for Paper trading 10 | """ 11 | arg_parser = configargparse.get_argument_parser() 12 | arg_parser.add('--use_real_wallet', help='Use/not use fictive wallet (only for paper simulation)', 13 | action='store_true') 14 | mode = TradeMode.paper 15 | ticker_df = None 16 | 17 | def __init__(self, wallet): 18 | args = self.arg_parser.parse_known_args()[0] 19 | super(Paper, self).__init__(self.mode) 20 | self.use_real_wallet = args.use_real_wallet 21 | if not self.use_real_wallet: 22 | self.balance = wallet.copy() 23 | 24 | def get_next(self, interval_in_min): 25 | """ 26 | Returns next state 27 | Interval: Interval in minutes 28 | """ 29 | interval_in_sec = interval_in_min*60 30 | epoch_now = int(time.time()) 31 | if self.last_tick_epoch > 0: 32 | next_ticker_time = (self.last_tick_epoch + interval_in_sec) 33 | delay_second = epoch_now - next_ticker_time 34 | if delay_second < 0: 35 | print('Going to sleep for: ', abs(delay_second), ' seconds.') 36 | time.sleep(abs(delay_second)) 37 | 38 | if not self.ticker_df.empty: 39 | self.ticker_df.drop(self.ticker_df.index, inplace=True) 40 | 41 | epoch_now = int(time.time()) 42 | epoch_start = epoch_now - interval_in_sec*5 # just to be sure get extra 5 datasets 43 | epoch_end = epoch_now 44 | for pair in self.pairs: 45 | # print('getting candles for period:', str(epoch_start) + '---' + str(epoch_end) + '----' 46 | # + str(interval_in_sec) + pair) 47 | new_df = self.exchange.get_candles_df(pair, epoch_start, epoch_end, interval_in_sec) 48 | # rint('new_df____:', new_df) 49 | if self.ticker_df.empty: 50 | self.ticker_df = new_df.copy() 51 | else: 52 | self.ticker_df = self.ticker_df.append(new_df, ignore_index=True) 53 | # Remove duplicates 54 | # self.ticker_df.drop_duplicates(subset=['date', 'pair'], inplace=True, keep='last') 55 | 56 | self.last_tick_epoch = epoch_now 57 | return self.ticker_df.copy() 58 | 59 | def get_balance(self): 60 | """ 61 | Returns wallet balance 62 | """ 63 | if self.use_real_wallet: 64 | return self.exchange.get_balances() 65 | else: 66 | return self.balance.copy() 67 | 68 | def trade(self, actions, wallet, trades, force_sell=True): 69 | """ 70 | Simulate currency buy/sell (places fictive buy/sell orders) 71 | """ 72 | return super(Paper, self).trade(actions, wallet, trades, force_sell=True) 73 | -------------------------------------------------------------------------------- /core/plot.py: -------------------------------------------------------------------------------- 1 | import plotly.graph_objs as go 2 | import pandas as pd 3 | from plotly.offline import plot 4 | from tzlocal import get_localzone 5 | 6 | 7 | class Plot: 8 | """ 9 | Main plotting class 10 | """ 11 | 12 | def __init__(self): 13 | pass 14 | 15 | @staticmethod 16 | def draw(df, df_trades, pair, strategy_info): 17 | """ 18 | Candle-stick plot 19 | """ 20 | if df.empty: 21 | print('No data to plot!') 22 | return 23 | 24 | df = df[df['pair'] == pair] 25 | 26 | if df.empty: 27 | print('Plot: Empty dataframe, nothing to draw!') 28 | return 29 | 30 | pd.options.mode.chained_assignment = None 31 | 32 | df['date'] = pd.to_datetime(df['date'], unit='s', utc=True) 33 | df_trades['date'] = pd.to_datetime(df_trades['date'], unit='s', utc=True) 34 | local_tz = get_localzone() 35 | df = df.set_index(['date']) 36 | df.tz_convert(local_tz) 37 | # Convert datetime to current time-zone 38 | df_index = df.index.tz_localize(None) 39 | 40 | df_trades = df_trades.set_index(['date']) 41 | df_trades.tz_convert(local_tz) 42 | 43 | # plotly.offline.init_notebook_mode() 44 | 45 | trace = go.Candlestick(x=df_index, 46 | open=df.open, 47 | high=df.high, 48 | low=df.low, 49 | close=df.close) 50 | data = [trace] 51 | 52 | # Create buy/sell annotations 53 | annotations = [] 54 | for index, row in df_trades.iterrows(): 55 | d = dict(x=index.tz_localize(None), 56 | y=row['close_price'], 57 | xref='x', 58 | yref='y', 59 | ax=0, 60 | ay=40 if row['action'] == 'buy' else -40, 61 | showarrow=True, 62 | arrowhead=2, 63 | arrowsize=3, 64 | arrowwidth=1, 65 | arrowcolor='red' if row['action'] == 'sell' else 'green', 66 | bordercolor='#c7c7c7') 67 | annotations.append(d) 68 | 69 | # Unpack the report string 70 | title = '' 71 | for item in strategy_info: 72 | s = str(item) 73 | title = title + '
')
38 | winners, losers = self.get_winners_losers(df_candles)
39 | html_body.append(self.parse_winners_losers_to_html(winners, losers))
40 |
41 | # wallet_stats = self.get_wallet_stats(ticker)
42 | print('wallet stats:')
43 | html_body.append('')
44 | self.send_email(html_body)
45 |
46 | @staticmethod
47 | def df_to_html(df, header, bar_color='lightblue'):
48 | """
49 | Converts DataFrame to html text
50 | """
51 | df_header = '
2 |
3 | Flexible Trading Bot with main focus on Machine Learning and Genetic Algorithms, inspired by [zenbot.](https://github.com/carlos8f/zenbot)
4 |
5 | [](https://www.codacy.com/gh/miro-ka/mosquito/dashboard?utm_source=github.com&utm_medium=referral&utm_content=miro-ka/mosquito&utm_campaign=Badge_Grade)
6 | [](https://codebeat.co/projects/github-com-miti0-mosquito-master)
7 |
8 | ## About
9 |
10 | 
11 |
12 | Mosquito is a cryptocurrency trading bot written in Python, with main focus on modularity,
13 | so it is straight forward to plug-in new exchange.
14 |
15 | The idea to build a new bot came because of missing following easy-access features in all of available open-source bots:
16 | * **Multi-currency bot** - Be able to monitor and exchange several currencies in one strategy (no need to run 5 same strategies for 5 different currencies).
17 | * **Easy AI plug & play** - Possibility to easily plug any of the existing AI/ML libraries (for ex. scikit or keras)
18 |
19 | > **Please be AWARE that Mosquito is still in beta and under heavy development. Please use it in Live trading VERY carefully.**
20 |
21 |
22 | ### Supported Exchanges
23 | Mosquito currently supports following exchanges:
24 | * **Poloniex** - supporting *fillOrKill* and *immediateOrCancel* trading types. *postOnly* type is not supported. You can
25 | read more about trading types [here.](https://github.com/s4w3d0ff/python-poloniex/blob/master/poloniex/__init__.py)
26 | * **Bittrex** - supporting *Trade Limit Buy/Sell Orders*
27 | * **Kucoin** - work in progress
28 |
29 |
30 |
31 | ## Requirements
32 | * Python 3.9
33 | * mongodb
34 | * Depending on the exchange you want to use:
35 | * [Poloniex-api](https://github.com/s4w3d0ff/python-poloniex)
36 | * [Bittrex-api](https://github.com/miti0/python-bittrex)
37 |
38 |
39 |
40 | ## Quick Start
41 |
42 |
43 |
44 | ### Install
45 | 1. Clone repo
46 | ```
47 | git clone https://github.com/miti0/mosquito.git
48 | ```
49 | 2. Install requirements (ideally in separate virtual environment)
50 | ```
51 | pip install -r requirements.txt
52 | ```
53 | 3. Install [mongodb](https://www.mongodb.com/try/download/community)
54 |
55 | 4. Set-up mosquito.ini (if you want to use sample config, just rename mosquito.sample.ini to mosquito.ini)
56 |
57 | 5. Run desired command (full list of commands below)
58 |
59 | All parameters in the program can be overridden with input arguments.
60 | You can get list of all available arguments with:
61 |
62 | ```
63 | python mosquito.py --help
64 | ```
65 |
66 |
67 | ## Backfill
68 | Backfill gets history data from exchange and stores them to mongodb. Data can be after that used for testing your simulation strategies.
69 |
70 | usage: backfill.py [-h] [--pairs PAIRS] [--all] --days DAYS
71 |
72 | ```
73 | optional arguments:
74 | -h, --help show this help message and exit
75 | --pairs PAIRS PairS to backfill. For ex. [BTC_ETH, BTC_* (to get all BTC_*
76 | prefixed pairs]
77 | --all Backfill data for ALL currencies
78 | --days DAYS Number of days to backfill
79 | ```
80 |
81 |
82 | Example 1) Load historical data for BTC_ETH pair for the last 5 days:
83 | ```
84 | python backfill.py --days 5 --pairs USDT_BTC
85 | ```
86 |
87 | Example 2) Load historical data for ALL pairs for the last 2 days
88 | ```
89 | python backfill.py --days 3 --all
90 | ```
91 |
92 | Example 3) Load historical data for all pairs starting with BTC_ for the last day
93 | ```
94 | python backfill.py --days 1 --pairs BTC_*
95 | ```
96 |
97 |
98 |
99 | ## Trading
100 | This is the main module that handles passed strategy and places buy/sell orders.
101 |
102 | Architecture and logic of mosquito is made so, that it should be easy to set and tune all strategy parameters with program arguments. Below is a list of main arguments that can be either configured via the mosquito.ini config file or by passing the value/values as argument.
103 |
104 | ```
105 | -h, --help show this help message and exit
106 | --polo_api_key POLO_API_KEY
107 | Poloniex API key (default: None)
108 | --polo_secret POLO_SECRET
109 | Poloniex secret key (default: None)
110 | --polo_txn_fee POLO_TXN_FEE
111 | Poloniex txn. fee (default: None)
112 | --polo_buy_order POLO_BUY_ORDER
113 | Poloniex buy order type (default: None)
114 | --polo_sell_order POLO_SELL_ORDER
115 | Poloniex sell order type (default: None)
116 | --bittrex_api_key BITTREX_API_KEY
117 | Bittrex API key (default: None)
118 | --bittrex_secret BITTREX_SECRET
119 | Bittrex secret key (default: None)
120 | --bittrex_txn_fee BITTREX_TXN_FEE
121 | Bittrex txn. fee (default: None)
122 | --exchange EXCHANGE Exchange (default: None)
123 | --db_url DB_URL Mongo db url (default: None)
124 | --db_port DB_PORT Mongo db port (default: None)
125 | --db DB Mongo db (default: None)
126 | --pairs PAIRS Pairs (default: None)
127 | --use_real_wallet Use/not use fictive wallet (only for paper simulation)
128 | (default: False)
129 | --backtest_from BACKTEST_FROM
130 | Backtest epoch start datetime (default: None)
131 | --backtest_to BACKTEST_TO
132 | Backtest epoch end datetime (default: None)
133 | --backtest_days BACKTEST_DAYS
134 | Number of history days the simulation should start
135 | from (default: None)
136 | --wallet_currency WALLET_CURRENCY
137 | Wallet currency (separated by comma) (default: None)
138 | --wallet_amount WALLET_AMOUNT
139 | Wallet amount (separated by comma) (default: None)
140 | --backtest Simulate your strategy on history ticker data
141 | (default: False)
142 | --paper Simulate your strategy on real ticker (default: False)
143 | --live REAL trading mode (default: False)
144 | --plot Generate a candle stick plot at simulation end
145 | (default: False)
146 | --ticker_size TICKER_SIZE Simulation ticker_size (default: 5)
147 | --root_report_currency ROOT_REPORT_CURRENCY
148 | Root currency used in final plot (default: None)
149 | --buffer_size BUFFER_SIZE
150 | Buffer size in days (default: 30)
151 | --prefetch Prefetch data from history DB (default: False)
152 | --plot_pair PLOT_PAIR
153 | Plot pair (default: None)
154 | --all ALL Include all currencies/tickers (default: None)
155 | --days DAYS Days to pre-fill (default: None)
156 | -c CONFIG, --config CONFIG
157 | config file path (default: mosquito.ini)
158 | -v, --verbosity Verbosity (default: False)
159 | --strategy STRATEGY Strategy (default: None)
160 | --fixed_trade_amount FIXED_TRADE_AMOUNT
161 | Fixed trade amount (default: None)
162 |
163 | ```
164 |
165 | Currently Trading supports following modes:
166 | * **Backtest** - fast simulation mode using past data and placing fictive buy/sell orders.
167 | * **Paper** - mode simulating live ticker with placing fictive buy/sell orders.
168 | * **Live** - live trading with placing REAL buy/sell orders.
169 |
170 | > Backtest and Paper trading are using immediate buy/sell orders by using the last ticker
171 | closing price. This results to NOT 100% accurate strategy results, what you should be aware of.
172 |
173 |
174 | ### Backtest
175 | Fast simulation mode using past data and placing fictive buy/sell orders. Simulation configuration is done via
176 | *config.ini* file (some of the parameters can be overridden with command line arguments).
177 |
178 | Below is an example of running a backtest together with final buy/sell plot generated at the end of the simulation.
179 | ```
180 | python3 mosquito.py --backtest --plot
181 | ```
182 | > ! Please be aware that Backtest should 99% work, but it is currently under final verification test.
183 |
184 |
185 | ### Paper
186 | Trading mode that simulates live ticker with placing fictive buy/sell orders. Simulation configuration is done via
187 | *config.ini* file (some of the parameters can be overridden with command line arguments).
188 |
189 | Below is an example of running a backtest together with final buy/sell plot generated at the end of the simulation.
190 | ```
191 | python mosquito.py --paper
192 | ```
193 | > ! Please be aware that Paper should 99% work, but it is currently under final verification test.
194 |
195 |
196 | ### Live
197 | Live trading with placing REAL buy/sell orders. Configuration is done via *config.ini* file (some of the parameters can be overridden with command line arguments).
198 | Below is an example of running a backtest together with final buy/sell plot generated at the end of the simulation.
199 | ```
200 | python mosquito.py --live
201 | ```
202 | > ! Please be aware that Live should 99% work, but it is currently under final verification test.
203 |
204 |
205 |
206 | ## Plot and Statistics
207 | Mosquito has a simple plot utility for visualizing current pair combined with trading history.
208 | Visualization uses external library [plotly](https://plot.ly/). Below You can see an example visualizing ticker price plot, together with simulated buy/sell orders.
209 |
210 |
211 |
212 | Below is an example of Final Simulation Report summary:
213 | ```
214 | ****************************************************
215 | Final simulation report:
216 | ****************************************************
217 | Wallet at Start: | 50.0DGB |
218 | Wallet at End: | 51.3464723121DGB |
219 | Strategy result: -5.68%
220 | Buy & Hold: -8.16%
221 | Strategy vs Buy & Hold: 2.47%
222 | Total txn: 10
223 | Simulated (data time): 0 days, 4 hours and 55 minutes
224 | Transactions per hour: 2.03
225 | Simulation run time: 0 hours 1 minutes and 13 seconds
226 | ```
227 |
228 | ## AI
229 |
230 | ### Blueprint
231 | Blueprint is a part of AI package. Main function of the module is to generate datasets which can be used for training AI. Logic of Blueprint module is following:
232 |
233 | 1. Create a blueprint file/module which contains features, indicators and output parameters. As an example you can take a look at ai/blueprints/minimal.py or ai/blueprints/junior.py
234 |
235 | 2. Decide how many days you would like to run the Blueprint. Backfield data for that period.
236 |
237 | 3. Choose which pair/pairs you would like to include. Following combinations should work [BTC_ETH] - single pair, [BTC_ETH, BTC_LTC] - list of pairs, [BTC_*] - all pairs with prefix BTC
238 |
239 | 4. Start blueprint with following parameters (example below)
240 |
241 | ```
242 | python blueprint.py --features junior --days 200
243 | ```
244 |
245 | As a result you should see *.csv file in your Mosquito's **out/blueprints** folder, which should contain the dataset.
246 |
247 |
248 | ## Utilities
249 |
250 | ### Wallet Lense
251 | Simple module which sends up to 24h winners/losers market pairs summary by email in user specified intervals (sample below).
252 |
253 |
254 |
255 | #### Usage
256 | ```
257 | # You need to have configured email parameters in ini file, or pass them as input arguments.
258 | python lense.py
259 | ```
260 |
261 |
262 | ---
263 |
264 |
265 |
266 | ### License: GNU GENERAL PUBLIC LICENSE
267 | - Copyright (C) 2023 (miro-ka)
268 |
269 |
270 | The GNU General Public License is a free, copyleft license for
271 | software and other kinds of works.
272 |
273 | The licenses for most software and other practical works are designed
274 | to take away your freedom to share and change the works. By contrast,
275 | the GNU General Public License is intended to guarantee your freedom to
276 | share and change all versions of a program--to make sure it remains free
277 | software for all its users. We, the Free Software Foundation, use the
278 | GNU General Public License for most of our software; it applies also to
279 | any other work released this way by its authors. You can apply it to
280 | your programs, too.
281 |
282 |
283 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
284 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
285 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
286 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
287 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
288 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
289 | SOFTWARE.
290 |
--------------------------------------------------------------------------------
/market_stats.py:
--------------------------------------------------------------------------------
1 | """
2 | import configargparse
3 | from stats.stats import Stats
4 |
5 |
6 | def main():
7 | stats = Stats()
8 | stats.run()
9 |
10 |
11 | if __name__ == "__main__":
12 | arg_parser = configargparse.get_argument_parser()
13 | arg_parser.add('-c', '--config', is_config_file=True, help='config file path', default='mosquito.ini')
14 | arg_parser.add("--live", help="REAL trading mode", action='store_true')
15 | arg_parser.add('-v', '--verbosity', help='Verbosity', action='store_true')
16 | args = arg_parser.parse_known_args()[0]
17 |
18 | main()
19 | """
20 |
21 |
22 | # Works on python3 / requires: pandas, numpy, pymongo, bokeh
23 | # BTC: 1A7K4kgXLSSzvDRjvoGwomvhrNU4CKezEp
24 | # LTC: LWShTeRrZpYS4aJhb6JdP3R9tNFMnZiDo2
25 |
26 | import logging
27 | from operator import itemgetter
28 | from math import pi
29 | from time import time
30 |
31 | from pymongo import MongoClient
32 | import pandas as pd
33 | import numpy as np
34 | from bokeh.plotting import figure, show
35 | from bokeh.models import NumeralTickFormatter
36 | from bokeh.models import LinearAxis, Range1d
37 |
38 | logger = logging.getLogger(__name__)
39 |
40 |
41 | def rsi(df, window, targetcol='weightedAverage', colname='rsi'):
42 | """ Calculates the Relative Strength Index (RSI) from a pandas dataframe
43 | http://stackoverflow.com/a/32346692/3389859
44 | """
45 | series = df[targetcol]
46 | delta = series.diff().dropna()
47 | u = delta * 0
48 | d = u.copy()
49 | u[delta > 0] = delta[delta > 0]
50 | d[delta < 0] = -delta[delta < 0]
51 | # first value is sum of avg gains
52 | u[u.index[window - 1]] = np.mean(u[:window])
53 | u = u.drop(u.index[:(window - 1)])
54 | # first value is sum of avg losses
55 | d[d.index[window - 1]] = np.mean(d[:window])
56 | d = d.drop(d.index[:(window - 1)])
57 | rs = u.ewm(com=window - 1,
58 | ignore_na=False,
59 | min_periods=0,
60 | adjust=False).mean() / d.ewm(com=window - 1,
61 | ignore_na=False,
62 | min_periods=0,
63 | adjust=False).mean()
64 | df[colname] = 100 - 100 / (1 + rs)
65 | df[colname].fillna(df[colname].mean(), inplace=True)
66 | return df
67 |
68 |
69 | def sma(df, window, targetcol='close', colname='sma'):
70 | """ Calculates Simple Moving Average on a 'targetcol' in a pandas dataframe
71 | """
72 | df[colname] = df[targetcol].rolling(
73 | min_periods=1, window=window, center=False).mean()
74 | return df
75 |
76 |
77 | def ema(df, window, targetcol='close', colname='ema', **kwargs):
78 | """ Calculates Expodential Moving Average on a 'targetcol' in a pandas
79 | dataframe """
80 | df[colname] = df[targetcol].ewm(
81 | span=window,
82 | min_periods=kwargs.get('min_periods', 1),
83 | adjust=kwargs.get('adjust', True),
84 | ignore_na=kwargs.get('ignore_na', False)
85 | ).mean()
86 | df[colname].fillna(df[colname].mean(), inplace=True)
87 | return df
88 |
89 |
90 | def macd(df, fastcol='emafast', slowcol='sma', colname='macd'):
91 | """ Calculates the differance between 'fastcol' and 'slowcol' in a pandas
92 | dataframe """
93 | df[colname] = df[fastcol] - df[slowcol]
94 | return df
95 |
96 |
97 | def bbands(df, window, targetcol='close', stddev=2.0):
98 | """ Calculates Bollinger Bands for 'targetcol' of a pandas dataframe """
99 | if not 'sma' in df:
100 | df = sma(df, window, targetcol)
101 | df['sma'].fillna(df['sma'].mean(), inplace=True)
102 | df['bbtop'] = df['sma'] + stddev * df[targetcol].rolling(
103 | min_periods=1,
104 | window=window,
105 | center=False).std()
106 | df['bbtop'].fillna(df['bbtop'].mean(), inplace=True)
107 | df['bbbottom'] = df['sma'] - stddev * df[targetcol].rolling(
108 | min_periods=1,
109 | window=window,
110 | center=False).std()
111 | df['bbbottom'].fillna(df['bbbottom'].mean(), inplace=True)
112 | df['bbrange'] = df['bbtop'] - df['bbbottom']
113 | df['bbpercent'] = ((df[targetcol] - df['bbbottom']) / df['bbrange']) - 0.5
114 | return df
115 |
116 |
117 | def plotRSI(p, df, plotwidth=800, upcolor='green', downcolor='red'):
118 | # create y axis for rsi
119 | p.extra_y_ranges = {"rsi": Range1d(start=0, end=100)}
120 | p.add_layout(LinearAxis(y_range_name="rsi"), 'right')
121 |
122 | # create rsi 'zone' (30-70)
123 | p.patch(np.append(df['date'].values, df['date'].values[::-1]),
124 | np.append([30 for i in df['rsi'].values],
125 | [70 for i in df['rsi'].values[::-1]]),
126 | color='olive',
127 | fill_alpha=0.2,
128 | legend="rsi",
129 | y_range_name="rsi")
130 |
131 | candleWidth = (df.iloc[2]['date'].timestamp() -
132 | df.iloc[1]['date'].timestamp()) * plotwidth
133 | # plot green bars
134 | inc = df.rsi >= 50
135 | p.vbar(x=df.date[inc],
136 | width=candleWidth,
137 | top=df.rsi[inc],
138 | bottom=50,
139 | fill_color=upcolor,
140 | line_color=upcolor,
141 | alpha=0.5,
142 | y_range_name="rsi")
143 | # Plot red bars
144 | dec = df.rsi <= 50
145 | p.vbar(x=df.date[dec],
146 | width=candleWidth,
147 | top=50,
148 | bottom=df.rsi[dec],
149 | fill_color=downcolor,
150 | line_color=downcolor,
151 | alpha=0.5,
152 | y_range_name="rsi")
153 |
154 |
155 | def plotMACD(p, df, color='blue'):
156 | # plot macd
157 | p.line(df['date'], df['macd'], line_width=4,
158 | color=color, alpha=0.8, legend="macd")
159 | p.yaxis[0].formatter = NumeralTickFormatter(format='0.00000000')
160 |
161 |
162 | def plotCandlesticks(p, df, plotwidth=750, upcolor='green', downcolor='red'):
163 | candleWidth = (df.iloc[2]['date'].timestamp() -
164 | df.iloc[1]['date'].timestamp()) * plotwidth
165 | # Plot candle 'shadows'/wicks
166 | p.segment(x0=df.date,
167 | y0=df.high,
168 | x1=df.date,
169 | y1=df.low,
170 | color="black",
171 | line_width=2)
172 | # Plot green candles
173 | inc = df.close > df.open
174 | p.vbar(x=df.date[inc],
175 | width=candleWidth,
176 | top=df.open[inc],
177 | bottom=df.close[inc],
178 | fill_color=upcolor,
179 | line_width=0.5,
180 | line_color='black')
181 | # Plot red candles
182 | dec = df.open > df.close
183 | p.vbar(x=df.date[dec],
184 | width=candleWidth,
185 | top=df.open[dec],
186 | bottom=df.close[dec],
187 | fill_color=downcolor,
188 | line_width=0.5,
189 | line_color='black')
190 | # format price labels
191 | p.yaxis[0].formatter = NumeralTickFormatter(format='0.00000000')
192 |
193 |
194 | def plotVolume(p, df, plotwidth=800, upcolor='green', downcolor='red'):
195 | candleWidth = (df.iloc[2]['date'].timestamp() -
196 | df.iloc[1]['date'].timestamp()) * plotwidth
197 | # create new y axis for volume
198 | p.extra_y_ranges = {"volume": Range1d(start=min(df['volume'].values),
199 | end=max(df['volume'].values))}
200 | p.add_layout(LinearAxis(y_range_name="volume"), 'right')
201 | # Plot green candles
202 | inc = df.close > df.open
203 | p.vbar(x=df.date[inc],
204 | width=candleWidth,
205 | top=df.volume[inc],
206 | bottom=0,
207 | alpha=0.1,
208 | fill_color=upcolor,
209 | line_color=upcolor,
210 | y_range_name="volume")
211 |
212 | # Plot red candles
213 | dec = df.open > df.close
214 | p.vbar(x=df.date[dec],
215 | width=candleWidth,
216 | top=df.volume[dec],
217 | bottom=0,
218 | alpha=0.1,
219 | fill_color=downcolor,
220 | line_color=downcolor,
221 | y_range_name="volume")
222 |
223 |
224 | def plotBBands(p, df, color='navy'):
225 | # Plot bbands
226 | p.patch(np.append(df['date'].values, df['date'].values[::-1]),
227 | np.append(df['bbbottom'].values, df['bbtop'].values[::-1]),
228 | color=color,
229 | fill_alpha=0.1,
230 | legend="bband")
231 | # plot sma
232 | p.line(df['date'], df['sma'], color=color, alpha=0.9, legend="sma")
233 |
234 |
235 | def plotMovingAverages(p, df):
236 | # Plot moving averages
237 | p.line(df['date'], df['emaslow'],
238 | color='orange', alpha=0.9, legend="emaslow")
239 | p.line(df['date'], df['emafast'],
240 | color='red', alpha=0.9, legend="emafast")
241 |
242 |
243 | class Charter(object):
244 | """ Retrieves 5min candlestick data for a market and saves it in a mongo
245 | db collection. Can display data in a dataframe or bokeh plot."""
246 |
247 | def __init__(self, api):
248 | """
249 | api = poloniex api object
250 | """
251 | self.api = api
252 |
253 | def __call__(self, pair, frame=False):
254 | """ returns raw chart data from the mongo database, updates/fills the
255 | data if needed, the date column is the '_id' of each candle entry, and
256 | the date column has been removed. Use 'frame' to restrict the amount
257 | of data returned.
258 | Example: 'frame=api.YEAR' will return last years data
259 | """
260 | # use last pair and period if not specified
261 | if not frame:
262 | frame = self.api.YEAR * 10
263 | dbcolName = pair + 'chart'
264 | # get db connection
265 | db = MongoClient()['poloniex'][dbcolName]
266 | # get last candle
267 | try:
268 | last = sorted(
269 | list(db.find({"_id": {"$gt": time() - 60 * 20}})),
270 | key=itemgetter('_id'))[-1]
271 | except:
272 | last = False
273 | # no entrys found, get all 5min data from poloniex
274 | if not last:
275 | logger.warning('%s collection is empty!', dbcolName)
276 | new = self.api.returnChartData(pair,
277 | period=60 * 5,
278 | start=time() - self.api.YEAR * 13)
279 | else:
280 | new = self.api.returnChartData(pair,
281 | period=60 * 5,
282 | start=int(last['_id']))
283 | # add new candles
284 | updateSize = len(new)
285 | logger.info('Updating %s with %s new entrys!',
286 | dbcolName, str(updateSize))
287 |
288 | # show the progess
289 | for i in range(updateSize):
290 | print("\r%s/%s" % (str(i + 1), str(updateSize)), end=" complete ")
291 | date = new[i]['date']
292 | del new[i]['date']
293 | db.update_one({'_id': date}, {"$set": new[i]}, upsert=True)
294 | print('')
295 |
296 | logger.debug('Getting chart data from db')
297 | # return data from db (sorted just in case...)
298 | return sorted(
299 | list(db.find({"_id": {"$gt": time() - frame}})),
300 | key=itemgetter('_id'))
301 |
302 | def dataFrame(self, pair, frame=False, zoom=False, window=120):
303 | """ returns pandas DataFrame from raw db data with indicators.
304 | zoom = passed as the resample(rule) argument to 'merge' candles into a
305 | different timeframe
306 | window = number of candles to use when calculating indicators
307 | """
308 | data = self.__call__(pair, frame)
309 | # make dataframe
310 | df = pd.DataFrame(data)
311 | # set date column
312 | df['date'] = pd.to_datetime(df["_id"], unit='s')
313 | if zoom:
314 | df.set_index('date', inplace=True)
315 | df = df.resample(rule=zoom,
316 | closed='left',
317 | label='left').apply({'open': 'first',
318 | 'high': 'max',
319 | 'low': 'min',
320 | 'close': 'last',
321 | 'quoteVolume': 'sum',
322 | 'volume': 'sum',
323 | 'weightedAverage': 'mean'})
324 | df.reset_index(inplace=True)
325 |
326 | # calculate/add sma and bbands
327 | df = bbands(df, window)
328 | # add slow ema
329 | df = ema(df, window, colname='emaslow')
330 | # add fast ema
331 | df = ema(df, int(window // 3.5), colname='emafast')
332 | # add macd
333 | df = macd(df)
334 | # add rsi
335 | df = rsi(df, window // 5)
336 | # add candle body and shadow size
337 | df['bodysize'] = df['close'] - df['open']
338 | df['shadowsize'] = df['high'] - df['low']
339 | df['percentChange'] = df['close'].pct_change()
340 | df.dropna(inplace=True)
341 | return df
342 |
343 | def graph(self, pair, frame=False, zoom=False,
344 | window=120, plot_width=1000, min_y_border=40,
345 | border_color="whitesmoke", background_color="white",
346 | background_alpha=0.4, legend_location="top_left",
347 | tools="pan,wheel_zoom,reset"):
348 | """
349 | Plots market data using bokeh and returns a 2D array for gridplot
350 | """
351 | df = self.dataFrame(pair, frame, zoom, window)
352 | #
353 | # Start Candlestick Plot -------------------------------------------
354 | # create figure
355 | candlePlot = figure(
356 | x_axis_type=None,
357 | y_range=(min(df['low'].values) - (min(df['low'].values) * 0.2),
358 | max(df['high'].values) * 1.2),
359 | x_range=(df.tail(int(len(df) // 10)).date.min().timestamp() * 1000,
360 | df.date.max().timestamp() * 1000),
361 | tools=tools,
362 | title=pair,
363 | plot_width=plot_width,
364 | plot_height=int(plot_width // 2.7),
365 | toolbar_location="above")
366 | # add plots
367 | # plot volume
368 | plotVolume(candlePlot, df)
369 | # plot candlesticks
370 | plotCandlesticks(candlePlot, df)
371 | # plot bbands
372 | plotBBands(candlePlot, df)
373 | # plot moving aves
374 | plotMovingAverages(candlePlot, df)
375 | # set legend location
376 | candlePlot.legend.location = legend_location
377 | # set background color
378 | candlePlot.background_fill_color = background_color
379 | candlePlot.background_fill_alpha = background_alpha
380 | # set border color and size
381 | candlePlot.border_fill_color = border_color
382 | candlePlot.min_border_left = min_y_border
383 | candlePlot.min_border_right = candlePlot.min_border_left
384 | #
385 | # Start RSI/MACD Plot -------------------------------------------
386 | # create a new plot and share x range with candlestick plot
387 | rsiPlot = figure(plot_height=int(candlePlot.plot_height // 2.5),
388 | x_axis_type="datetime",
389 | y_range=(-(max(df['macd'].values) * 2),
390 | max(df['macd'].values) * 2),
391 | x_range=candlePlot.x_range,
392 | plot_width=candlePlot.plot_width,
393 | title=None,
394 | toolbar_location=None)
395 | # plot macd
396 | plotMACD(rsiPlot, df)
397 | # plot rsi
398 | plotRSI(rsiPlot, df)
399 | # set background color
400 | rsiPlot.background_fill_color = candlePlot.background_fill_color
401 | rsiPlot.background_fill_alpha = candlePlot.background_fill_alpha
402 | # set border color and size
403 | rsiPlot.border_fill_color = candlePlot.border_fill_color
404 | rsiPlot.min_border_left = candlePlot.min_border_left
405 | rsiPlot.min_border_right = candlePlot.min_border_right
406 | rsiPlot.min_border_bottom = 20
407 | # orient x labels
408 | rsiPlot.xaxis.major_label_orientation = pi / 4
409 | # set legend
410 | rsiPlot.legend.location = legend_location
411 | # set dataframe 'date' as index
412 | df.set_index('date', inplace=True)
413 | # return layout and df
414 | return [[candlePlot], [rsiPlot]], df
415 |
416 |
417 | if __name__ == '__main__':
418 | from poloniex import Poloniex
419 | from bokeh.layouts import gridplot
420 |
421 | logging.basicConfig(level=logging.DEBUG)
422 | logging.getLogger("poloniex").setLevel(logging.INFO)
423 | logging.getLogger('requests').setLevel(logging.ERROR)
424 |
425 | api = Poloniex(jsonNums=float)
426 |
427 | layout, df = Charter(api).graph('USDT_BTC', window=90,
428 | frame=api.YEAR * 12, zoom='1W')
429 | print(df.tail())
430 | p = gridplot(layout)
431 | show(p)
--------------------------------------------------------------------------------