├── .gitignore ├── README.md ├── backtesting_scripts ├── coinbase_backtesting.py └── poloniex_backtesting.py ├── cloud_infrastructure └── aws_terraform │ ├── cloudwatch.tf │ ├── cloudwatch_iam.tf │ ├── lambda.tf │ ├── lambda_iam.tf │ ├── providers.tf │ ├── sns.tf │ └── variables.tf ├── deployment_scripts ├── deploy_cryptotradingbot.sh └── deploy_cryptotradingbot_coinbase.sh ├── project_architecture.png ├── trading_strategies ├── coinbase_cmo_trading_strategy │ ├── README.md │ ├── app │ │ ├── app.py │ │ └── payload │ │ │ ├── app.py │ │ │ ├── trading_strategies │ │ │ └── poloniex_cmo_trading_strategy │ │ │ │ └── config.py │ │ │ └── trading_tools │ │ │ ├── cmo_calculation.py │ │ │ └── poloniex_wrapper.py │ ├── config.py │ ├── docs │ │ └── cmo_trading_architecture.png │ ├── optimisation │ │ ├── analyse_optimisation.py │ │ ├── eth_backtesting_results.csv │ │ ├── eth_optimisation.csv │ │ ├── pairs_raw.txt │ │ ├── pairs_transform.py │ │ └── sol_optimisation.csv │ └── requirements.txt ├── poloniex_cmo_trading_strategy │ ├── README.md │ ├── app │ │ └── app.py │ ├── config.py │ ├── docs │ │ └── cmo_trading_architecture.png │ ├── optimisation │ │ ├── analyse_optimisation.py │ │ ├── initial_optimisation_results.csv │ │ ├── pairs_raw.txt │ │ └── pairs_transform.py │ └── requirements.txt └── template_strategy │ ├── README.md │ ├── app │ └── app.py │ ├── backtesting │ └── backtesting.py │ ├── config.py │ ├── docs │ ├── README.md │ └── cmo_trading_architecture.png │ └── optimisation │ ├── analyse_optimisation.py │ ├── crypto_pairs.py │ └── optimisation_results.csv └── trading_tools ├── coinbase_cmo_calculation.py ├── coinbase_pro_wrapper ├── authenticated_client.py ├── cbpro_auth.py ├── credits.md └── public_client.py ├── crypto_com_api_wrapper.py ├── poloniex_cmo_calculation.py ├── poloniex_wrapper.py ├── poloniex_wrapper_bwentzloff.py └── poloniex_wrapper_bwentzloff.py.bak /.gitignore: -------------------------------------------------------------------------------- 1 | trading_strategies/poloniex_cmo_trading_strategy/app/payload/ 2 | trading_strategies/poloniex_cmo_trading_strategy/app/payload.zip 3 | 4 | results.html 5 | 6 | .idea/ 7 | *.iml 8 | venv 9 | 10 | .terraform/ 11 | terraform.tfvars 12 | trading_strategies/poloniex_cmo_trading_strategy/backtesting/__pycache__/ 13 | trading_strategies/poloniex_cmo_trading_strategy/__pycache__/ 14 | trading_tools/__pycache__/ 15 | 16 | cloud_infrastructure/aws_terraform/terraform.tfstate 17 | cloud_infrastructure/aws_terraform/terraform.tfstate.backup 18 | 19 | notes.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypto Trading Bot 2 | 3 | ## Contents 4 | 0. [Introduction](#introduction) 5 | 1. [Template Architecture](#architecture) 6 | 1. [Trading Strategies](#strategies) 7 | 1. [Usage](#usage) 8 | 2. [Future Improvements](#futureimprovements) 9 | 3. [How to Contribute](#howtocontribute) 10 | 4. [Further Reading](#furtherreading) 11 | 5. [Donations](#donations) 12 | 13 | 14 | ## Introduction 15 | 16 | A project to automatically run technical analysis strategies on AWS to trade cryptocurrencies whilst you sleep. 17 | 18 | This project aims to have multiple trading strategies that can all be deployed into AWS using Terraform. 19 | 20 | You can watch YouTube videos about the project [here](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=4) 21 | 22 | 23 | ## Project Architecture 24 | 25 | #### Repository Structure 26 | 27 | - cloud_infrastructure: this folder contains all the infrastructure as code in order to deploy trading strategies. 28 | - trading_strategies: each folder within trading_strategies contains all the code required to backtest, optimise and develop your trading logic. The "app" logic is what gets deployed in the generic setup. 29 | - trading_tools: generic pieces of code that can be used across multiple trading strategies. 30 | 31 | #### Generic Trading Architecture Overview (may differ between trading strategies) 32 | 33 | - The cloud infrastructure is deployed and managed using infrastructure as code. In this example terraform deploys all the infrastructure into AWS. 34 | - Cloudwatch (AWS) is the trigger which kicks off the Lambda Function (AWS) at a pre-defined frequency. 35 | - The Lambda function contains the trading strategy to execute trades. 36 | - Every decision that the Lambda takes is logged into to Cloudwatch for auditing. 37 | 38 | 39 | 40 | 41 | 42 | ## Trading Strategies 43 | 44 | - [Polniex Chande Momentum Oscillator (CMO) Trading strategy](https://github.com/liamhartley/cryptotradingbot/blob/master/poloniex_cmo_trading_strategy/) 45 | 46 | 47 | ## Usage 48 | 49 | Please see each trading strategies README for specific usage instructions: 50 | 51 | - [Poloniex CMO Trading Strategy](https://github.com/liamhartley/cryptotradingbot/blob/master/poloniex_cmo_trading_strategy/README.md) 52 | 53 | 54 | 55 | ## Future Improvements 56 | 57 | Creation of more trading strategies. 58 | 59 | Please contribute any trading strategies back into this repository. 60 | 61 | 62 | 63 | ## How to Contribute 64 | 65 | 66 | Branch or fork off from the project to create a new feature and open a PR against master when complete. 67 | 68 | Please feel free to reach out to me to check if a feature isn't already in development or raise issues on GitHub. 69 | 70 | In the future I would love to see all sorts of strategies in this repo! 71 | 72 | 73 | ## Further Reading 74 | - [CMO by Investopedia](https://www.investopedia.com/terms/c/chandemomentumoscillator.asp) 75 | - [Poloniex Exchange](https://poloniex.com) 76 | 77 | 78 | ## Acknowledgements 79 | 80 | - [The Gemini crypto backtesting engine](https://github.com/anfederico/Gemini) by anfederico 81 | 82 | 83 | 84 | ## Donations 85 | 86 | If this project helped you learn Python or has helped you make some money then I would love any tips in crypto! 87 | 88 | - **BTC**: 36icgLxqAfFTuuruU7v1GQmFpUHjKnzJa8 89 | - **ETH**: 0xd084F627630Cb8C24895fAc531447256BD17b05e 90 | - **USDC**: 0xd084F627630Cb8C24895fAc531447256BD17b05e 91 | - **DOGE**: DS7JRhMmL9RdXruqz5ubDaR3R8SNtoUi6i 92 | 93 | [Alternatively you can buy me a coffee!](https://www.buymeacoffee.com/liamhartley) 94 | 95 | -------------------------------------------------------------------------------- /backtesting_scripts/coinbase_backtesting.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pandas as pd 4 | 5 | from gemini.gemini_core.gemini_master import Gemini 6 | from gemini.helpers import poloniex, analyze 7 | from trading_tools.coinbase_cmo_calculation import coinbase_cmo_logic_no_pandas 8 | from trading_strategies.coinbase_cmo_trading_strategy.config import LOGICAL_PARAMS 9 | from trading_tools.coinbase_pro_wrapper.public_client import PublicClient 10 | from trading_tools.poloniex_cmo_calculation import poloniex_cmo_logic_no_pandas 11 | 12 | # PERIOD_DICTIONARY = {86400: '1D', 21600: '6H', 3600: '1H', 900: '0.25H', 300: '5M', 60: '1M'} 13 | PERIOD_DICTIONARY = {86400: '1D'} 14 | 15 | # PERIOD_DICTIONARY = {86400: '1D', 14400: '4H', 7200: '2H', 1800: '0.5H', 900: '0.25H'} # Poloniex API 16 | # PERIOD_DICTIONARY = {86400: '1D', 7200: '2H'} # Poloniex API 17 | 18 | OUTPUT_FILEPATH = '/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/optimisation/eth_backtesting_results.csv' 19 | CMO_PERIODS = [7, 8, 9, 10, 11, 12] 20 | # CMO_PERIODS = [10, 11, 12] 21 | DAYS_HISTORY = 180 22 | OVERBOUGHT_VALUEs = [40, 45, 50, 55, 60] 23 | OVERSOLD_VALUEs = [-60, -55, -50, -45, -40] 24 | CAPITAL_BASE = 1000 25 | ENTRY_SIZE_LIST = [CAPITAL_BASE*0.05, CAPITAL_BASE*0.1, CAPITAL_BASE*0.15, CAPITAL_BASE*0.2, CAPITAL_BASE*0.25, CAPITAL_BASE*0.30, 26 | CAPITAL_BASE*0.35, CAPITAL_BASE*0.40, CAPITAL_BASE*0.45, CAPITAL_BASE*0.5] 27 | 28 | # PAIRS = ['ETH_USDC', 'USDC_SOL'] 29 | # PAIRS = ['ETH-USDC', 'SOL-USDC'] 30 | PAIRS = ['ETH-USDC'] 31 | # PAIRS = ['ETH_USDC'] 32 | # PAIRS = ['SOL_USDT'] 33 | 34 | # PAIRS = ['XRP_BTC'] 35 | # PAIRS = ['AMP_USDT'] 36 | # PAIRS = ['BTC_XRP'] 37 | 38 | # PAIRS = [ 39 | # 'BTC_USDT\n', 40 | # 'DOGE_BTC\n', 41 | # 'ETH_USDT\n', 42 | # 'DOGE_USDT\n', 43 | # 'ETH_BTC\n', 44 | # 'XRP_USDT\n', 45 | # 'TRX_USDT\n', 46 | # 'BTC_USDC\n', 47 | # 'XRP_BTC\n', 48 | # 'LTC_USDT\n', 49 | # 'SRM_USDT\n', 50 | # 'XMR_BTC\n', 51 | # 'LTC_BTC\n', 52 | # 'TRX_BTC\n', 53 | # 'ETC_USDT\n', 54 | # 'ETH_USDC\n', 55 | # 'ATOM_BTC\n', 56 | # 'EOS_BTC\n', 57 | # 'XEM_BTC\n', 58 | # 'EOS_USDT\n', 59 | # 'BCH_USDT\n', 60 | # 'ETC_BTC\n', 61 | # 'BCH_BTC\n', 62 | # 'XMR_USDT\n', 63 | # 'ATOM_USDC\n', 64 | # 'DOT_USDT\n', 65 | # 'ATOM_USDT\n', 66 | # 'BNB_USDT\n', 67 | # 'DOGE_USDC\n', 68 | # 'SC_USDT\n', 69 | # 'EOS_USDC\n', 70 | # 'LSK_BTC\n', 71 | # 'BTT_USDT\n', 72 | # 'DASH_BTC\n', 73 | # 'TUSD_USDT\n', 74 | # 'BCH_USDC\n', 75 | # 'AMP_USDT\n', 76 | # 'XRP_USDC\n', 77 | # 'JST_USDT\n', 78 | # 'DASH_USDT\n', 79 | # 'ETC_ETH\n', 80 | # 'ZEC_USDT\n', 81 | # 'KCS_USDT\n', 82 | # 'QTUM_BTC\n', 83 | # 'LSK_USDT\n', 84 | # 'SC_BTC\n' 85 | # ] 86 | 87 | 88 | def cmo_trading_strategy(gemini, data): 89 | base_currency = LOGICAL_PARAMS['PAIR'].split('-')[0] 90 | quote_currency = LOGICAL_PARAMS['PAIR'].split('-')[1] 91 | if len(data) >= CMO_PERIOD: 92 | # cmo = poloniex_cmo_logic_no_pandas(pair=quote_currency+'_'+base_currency, period=api_period) 93 | cmo = coinbase_cmo_logic_no_pandas(pair=LOGICAL_PARAMS['PAIR'], period=api_period) 94 | assert -100 <= cmo <= 100 95 | 96 | if cmo < OVERSOLD_VALUE: 97 | gemini.account.enter_position(type_="Long", 98 | entry_capital=ENTRY_SIZE, 99 | entry_price=data.iloc[-1]['high']) 100 | print(f'Open position @ {data.iloc[-1]["low"]}') 101 | elif cmo > OVERBOUGHT_VALUE and len(gemini.account.positions) > 0: 102 | gemini.account.close_position(position=gemini.account.positions[0], 103 | percent=1, 104 | price=data.iloc[-1]['low']) 105 | print(f'Close position @ {data.iloc[-1]["low"]}') 106 | 107 | 108 | if __name__ == '__main__': 109 | total_simulations = len(PAIRS) * len(PERIOD_DICTIONARY) * len(CMO_PERIODS) * len(ENTRY_SIZE_LIST) * len(OVERSOLD_VALUEs) * len(OVERSOLD_VALUEs) 110 | print(f'Total backtesting simulations: {total_simulations}\n') 111 | time.sleep(1) 112 | 113 | for PAIR in PAIRS: 114 | # PAIR = PAIR[:-1] 115 | for OVERSOLD_VALUE in OVERSOLD_VALUEs: 116 | for OVERBOUGHT_VALUE in OVERBOUGHT_VALUEs: 117 | for api_period, gemini_period in PERIOD_DICTIONARY.items(): 118 | for cmo_period in CMO_PERIODS: 119 | global CMO_PERIOD 120 | CMO_PERIOD = cmo_period 121 | for entry_size in ENTRY_SIZE_LIST: 122 | global ENTRY_SIZE 123 | ENTRY_SIZE = entry_size 124 | print(f"Pair: {PAIR}") 125 | params = { 126 | 'capital_base': CAPITAL_BASE, 127 | 'data_frequency': gemini_period, 128 | 'fee': { 129 | 'Long': 0.005, # as a percentage 130 | 'Short': 0.005 131 | } 132 | } 133 | 134 | # load backtesting data 135 | # data_df = poloniex.load_dataframe(pair=PAIR, period=poloniex_period, days_history=DAYS_HISTORY) 136 | data = PublicClient().get_product_historic_rates(product_id=PAIR, granularity=poloniex_period) 137 | headers = ['time', 'low', 'high', 'open', 'close', 'volume'], 138 | df = pd.DataFrame(data) 139 | df.columns = headers[0] 140 | df['time'] = pd.to_datetime(df['time'], unit='s') 141 | df = df.set_index('time') 142 | df = df.reindex(columns=['high', 'low', 'open', 'close', 'volume']) 143 | 144 | backtesting_engine = Gemini(logic=cmo_trading_strategy, sim_params=params, analyze=analyze.analyze_bokeh) 145 | 146 | # run the backtesting engine 147 | backtesting_engine.run(data=df) 148 | 149 | backtesting_engine.save_results_to_csv( 150 | filepath=OUTPUT_FILEPATH, 151 | additional_datapoints=[PAIR, 152 | CMO_PERIOD, 153 | OVERSOLD_VALUE, 154 | OVERBOUGHT_VALUE, 155 | ENTRY_SIZE, 156 | poloniex_period 157 | ] 158 | ) 159 | -------------------------------------------------------------------------------- /backtesting_scripts/poloniex_backtesting.py: -------------------------------------------------------------------------------- 1 | import time 2 | from gemini.gemini_core.gemini_master import Gemini 3 | from gemini.helpers import poloniex, analyze 4 | 5 | 6 | # PERIOD_DICTIONARY = {7200: '2H'} 7 | PERIOD_DICTIONARY = {86400: '1D', 14400: '4H', 7200: '2H', 1800: '0.5H', 900: '0.25H'} 8 | OUTPUT_FILEPATH = '/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/optimisation/sol_optimisation.csv' 9 | CMO_PERIODS = [7, 8, 9, 10] 10 | DAYS_HISTORY = 180 11 | OVERBOUGHT_VALUE = 50 12 | OVERSOLD_VALUE = -50 13 | CAPITAL_BASE = 1000 14 | ENTRY_SIZE_LIST = [CAPITAL_BASE*0.1, CAPITAL_BASE*0.15, CAPITAL_BASE*0.2, CAPITAL_BASE*0.25, CAPITAL_BASE*0.30, 15 | CAPITAL_BASE*0.35, CAPITAL_BASE*0.40, CAPITAL_BASE*0.45, CAPITAL_BASE*0.5, CAPITAL_BASE*0.55] 16 | 17 | # PAIRS = ['XRP_BTC'] 18 | # PAIRS = ['AMP_USDT'] 19 | PAIRS = ['SOL_USDT'] 20 | 21 | # PAIRS = [ 22 | # 'BTC_USDT\n', 23 | # 'DOGE_BTC\n', 24 | # 'ETH_USDT\n', 25 | # 'DOGE_USDT\n', 26 | # 'ETH_BTC\n', 27 | # 'XRP_USDT\n', 28 | # 'TRX_USDT\n', 29 | # 'BTC_USDC\n', 30 | # 'XRP_BTC\n', 31 | # 'LTC_USDT\n', 32 | # 'SRM_USDT\n', 33 | # 'XMR_BTC\n', 34 | # 'LTC_BTC\n', 35 | # 'TRX_BTC\n', 36 | # 'ETC_USDT\n', 37 | # 'ETH_USDC\n', 38 | # 'ATOM_BTC\n', 39 | # 'EOS_BTC\n', 40 | # 'XEM_BTC\n', 41 | # 'EOS_USDT\n', 42 | # 'BCH_USDT\n', 43 | # 'ETC_BTC\n', 44 | # 'BCH_BTC\n', 45 | # 'XMR_USDT\n', 46 | # 'ATOM_USDC\n', 47 | # 'DOT_USDT\n', 48 | # 'ATOM_USDT\n', 49 | # 'BNB_USDT\n', 50 | # 'DOGE_USDC\n', 51 | # 'SC_USDT\n', 52 | # 'EOS_USDC\n', 53 | # 'LSK_BTC\n', 54 | # 'BTT_USDT\n', 55 | # 'DASH_BTC\n', 56 | # 'TUSD_USDT\n', 57 | # 'BCH_USDC\n', 58 | # 'AMP_USDT\n', 59 | # 'XRP_USDC\n', 60 | # 'JST_USDT\n', 61 | # 'DASH_USDT\n', 62 | # 'ETC_ETH\n', 63 | # 'ZEC_USDT\n', 64 | # 'KCS_USDT\n', 65 | # 'QTUM_BTC\n', 66 | # 'LSK_USDT\n', 67 | # 'SC_BTC\n' 68 | # ] 69 | 70 | 71 | def cmo_logic(data) -> float: 72 | last_day = len(data) 73 | first_day = len(data) - CMO_PERIOD 74 | 75 | higher_close_price = 0 76 | lower_close_price = 0 77 | 78 | for ticker in range(first_day, last_day): 79 | if data['close'][ticker] > data['open'][ticker]: 80 | higher_close_price += 1 81 | elif data['close'][ticker] < data['open'][ticker]: 82 | lower_close_price += 1 83 | 84 | cmo = ((higher_close_price - lower_close_price) / (higher_close_price + lower_close_price)) * 100 85 | 86 | return cmo 87 | 88 | 89 | def cmo_trading_strategy(gemini, data): 90 | if len(data) >= CMO_PERIOD: 91 | cmo = cmo_logic(data) 92 | assert -100 <= cmo <= 100 93 | 94 | if cmo < OVERSOLD_VALUE: 95 | gemini.account.enter_position(type_="Long", 96 | entry_capital=ENTRY_SIZE, 97 | entry_price=data.iloc[-1]['high']) 98 | print(f'Open position @ {data.iloc[-1]["low"]}') 99 | elif cmo > OVERBOUGHT_VALUE and len(gemini.account.positions) > 0: 100 | gemini.account.close_position(position=gemini.account.positions[0], 101 | percent=1, 102 | price=data.iloc[-1]['low']) 103 | print(f'Close position @ {data.iloc[-1]["low"]}') 104 | 105 | 106 | if __name__ == '__main__': 107 | total_simulations = len(PAIRS) * len(PERIOD_DICTIONARY) * len(CMO_PERIODS) * len(ENTRY_SIZE_LIST) 108 | print(f'Total backtesting simulations: {total_simulations}\n') 109 | time.sleep(5) 110 | 111 | for PAIR in PAIRS: 112 | # PAIR = PAIR[:-1] 113 | for poloniex_period, gemini_period in PERIOD_DICTIONARY.items(): 114 | for cmo_period in CMO_PERIODS: 115 | global CMO_PERIOD 116 | CMO_PERIOD = cmo_period 117 | for entry_size in ENTRY_SIZE_LIST: 118 | global ENTRY_SIZE 119 | ENTRY_SIZE = entry_size 120 | print(f"Pair: {PAIR}") 121 | params = { 122 | 'capital_base': CAPITAL_BASE, 123 | 'data_frequency': gemini_period, 124 | 'fee': { 125 | 'Long': 0.00125, # as a percentage 126 | 'Short': 0.00125 127 | } 128 | } 129 | 130 | # load backtesting data 131 | data_df = poloniex.load_dataframe(pair=PAIR, period=poloniex_period, days_history=DAYS_HISTORY) 132 | 133 | backtesting_engine = Gemini(logic=cmo_trading_strategy, sim_params=params, analyze=analyze.analyze_bokeh) 134 | 135 | # run the backtesting engine 136 | backtesting_engine.run(data=data_df) 137 | 138 | backtesting_engine.save_results_to_csv( 139 | filepath=OUTPUT_FILEPATH, 140 | additional_datapoints=[PAIR, 141 | CMO_PERIOD, 142 | OVERSOLD_VALUE, 143 | OVERBOUGHT_VALUE, 144 | ENTRY_SIZE, 145 | poloniex_period 146 | ] 147 | ) 148 | -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/cloudwatch.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_event_rule" "cryptotradingbot" { 2 | name = "${var.PROJECT_NAME}-event-rule" 3 | description = "sends a trigger to execute trading logic" 4 | schedule_expression = var.TRADING_FREQUENCY 5 | is_enabled = true 6 | } 7 | 8 | resource "aws_cloudwatch_event_target" "trigger_cmo_strategy" { 9 | rule = aws_cloudwatch_event_rule.cryptotradingbot.name 10 | target_id = "cryptotradingbot" 11 | arn = aws_lambda_function.cryptotradingbot.arn 12 | } 13 | 14 | module "entering_position_log_metric_filter" { 15 | source = "terraform-aws-modules/cloudwatch/aws//modules/log-metric-filter" 16 | version = "~> 2.0" 17 | 18 | log_group_name = "/aws/lambda/${aws_lambda_function.cryptotradingbot.function_name}" 19 | 20 | name = "${var.PROJECT_NAME}-entering-position-metric" 21 | pattern = "entering position" 22 | 23 | metric_transformation_namespace = "${var.PROJECT_NAME}-entering-position-namespace" 24 | metric_transformation_name = "${var.PROJECT_NAME}-entering-position-metric-name" 25 | } 26 | 27 | module "entering_position_metric_alarm" { 28 | source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm" 29 | version = "~> 2.0" 30 | 31 | alarm_name = "${var.PROJECT_NAME}-entering-position-alarm" 32 | alarm_description = "Triggers when bot enters a trade" 33 | comparison_operator = "GreaterThanThreshold" 34 | evaluation_periods = 1 35 | threshold = 0 36 | period = 300 37 | unit = "None" 38 | 39 | namespace = "${var.PROJECT_NAME}-entering-position-namespace" 40 | metric_name = "${var.PROJECT_NAME}-entering-position-metric-name" 41 | statistic = "Maximum" 42 | treat_missing_data = "notBreaching" // or ignore 43 | 44 | alarm_actions = [aws_sns_topic.trade-notification-sns.arn] 45 | } 46 | 47 | module "closing_position_log_metric_filter" { 48 | source = "terraform-aws-modules/cloudwatch/aws//modules/log-metric-filter" 49 | version = "~> 2.0" 50 | 51 | log_group_name = "/aws/lambda/${aws_lambda_function.cryptotradingbot.function_name}" 52 | 53 | name = "${var.PROJECT_NAME}-closing-position-metric" 54 | pattern = "closing position" 55 | 56 | metric_transformation_namespace = "${var.PROJECT_NAME}-closing-position-namespace" 57 | metric_transformation_name = "${var.PROJECT_NAME}-closing-position-metric-name" 58 | } 59 | 60 | module "closing_position_metric_alarm" { 61 | source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm" 62 | version = "~> 2.0" 63 | 64 | alarm_name = "${var.PROJECT_NAME}-closing-position-alarm" 65 | alarm_description = "Triggers when bot closes a trade" 66 | comparison_operator = "GreaterThanThreshold" 67 | evaluation_periods = 1 68 | threshold = 0 69 | period = 300 70 | unit = "None" 71 | 72 | namespace = "${var.PROJECT_NAME}-closing-position-namespace" 73 | metric_name = "${var.PROJECT_NAME}-closing-position-metric-name" 74 | statistic = "Maximum" 75 | treat_missing_data = "notBreaching" // or ignore 76 | 77 | alarm_actions = [aws_sns_topic.trade-notification-sns.arn] 78 | } 79 | -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/cloudwatch_iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_permission" "allow_cloudwatch_to_call_cmo_strategy" { 2 | statement_id = "AllowExecutionFromCloudWatch" 3 | action = "lambda:InvokeFunction" 4 | function_name = aws_lambda_function.cryptotradingbot.function_name 5 | principal = "events.amazonaws.com" 6 | source_arn = aws_cloudwatch_event_rule.cryptotradingbot.arn 7 | } 8 | -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/lambda.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lambda_function" "cryptotradingbot" { 2 | filename = var.PAYLOAD_FUNCTION_FILEPATH 3 | function_name = "cryptotradingbot-${var.PROJECT_NAME}" 4 | handler = "app.lambda_handler" 5 | role = aws_iam_role.crypto_lambda_execution_role.arn 6 | runtime = "python3.7" 7 | timeout = "300" 8 | 9 | environment { 10 | variables = { 11 | POLONIEX_KEY = var.POLONIEX_KEY, 12 | POLONIEX_SECRET = var.POLONIEX_SECRET, 13 | COINBASE_API_KEY = var.COINBASE_API_KEY, 14 | COINBASE_API_SECRET = var.COINBASE_API_SECRET, 15 | COINBASE_PASSPHRASE = var.COINBASE_PASSPHRASE 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/lambda_iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "crypto_lambda_execution_role" { 2 | name = "${var.PROJECT_NAME}_lambda_execution_role" 3 | assume_role_policy = data.aws_iam_policy_document.crypto_lambda_trust_policy.json 4 | } 5 | 6 | data "aws_iam_policy_document" "crypto_lambda_trust_policy"{ 7 | statement { 8 | effect = "Allow" 9 | principals { 10 | type = "Service" 11 | identifiers = [ 12 | "lambda.amazonaws.com", 13 | "ec2.amazonaws.com" 14 | ] 15 | } 16 | actions = ["sts:AssumeRole"] 17 | } 18 | } 19 | 20 | data "aws_iam_policy_document" "crypto_lambda_execution_policy_document" { 21 | statement { 22 | sid = "ManageLambdaFunction" 23 | effect = "Allow" 24 | actions = [ 25 | "lambda:*", 26 | "ec2:*", 27 | "cloudwatch:*", 28 | "logs:*", 29 | "s3:*" 30 | ] 31 | resources = ["*"] 32 | } 33 | } 34 | 35 | resource "aws_iam_policy" "crypto_lambda_execution_policy" { 36 | name = "${var.PROJECT_NAME}_lambda_execution_policy" 37 | policy = data.aws_iam_policy_document.crypto_lambda_execution_policy_document.json 38 | } 39 | 40 | resource "aws_iam_role_policy_attachment" "crypto_lambda_execution_attachment" { 41 | role = aws_iam_role.crypto_lambda_execution_role.name 42 | policy_arn = aws_iam_policy.crypto_lambda_execution_policy.arn 43 | } 44 | -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | access_key = var.AWS_ACCESS_KEY 4 | secret_key = var.AWS_SECRET_KEY 5 | } -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/sns.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sns_topic" "trade-notification-sns" { 2 | name = "${var.PROJECT_NAME}-sns" 3 | } -------------------------------------------------------------------------------- /cloud_infrastructure/aws_terraform/variables.tf: -------------------------------------------------------------------------------- 1 | ### strategy specific variables 2 | 3 | # poloniex 4 | //variable "CLOSING_PATTERN" { 5 | // type = string 6 | // default = "closing position" 7 | //} 8 | // 9 | //variable "opening_PATTERN" { 10 | // type = string 11 | // default = "opening position" 12 | //} 13 | 14 | # coinbase 15 | variable "CLOSING_PATTERN" { 16 | type = string 17 | default = "'side': 'sell'" 18 | } 19 | 20 | variable "OPENING_PATTERN" { 21 | type = string 22 | default = "'side': 'buy'" 23 | } 24 | 25 | # generic variables - MUST BE DEFINED 26 | 27 | variable "PROJECT_NAME" { 28 | type = string 29 | } 30 | 31 | variable "PAYLOAD_FUNCTION_FILEPATH" { 32 | type = string 33 | } 34 | 35 | variable "TRADING_FREQUENCY" { 36 | type = string 37 | } 38 | 39 | variable "AWS_ACCESS_KEY" { 40 | type = string 41 | } 42 | 43 | variable "AWS_SECRET_KEY" { 44 | type = string 45 | } 46 | 47 | variable "region" { 48 | type = string 49 | default = "" 50 | } 51 | 52 | # strategy variables - OPTIONAL 53 | 54 | # poloniex strategies 55 | variable "POLONIEX_KEY" { 56 | type = string 57 | default = "" 58 | } 59 | 60 | variable "POLONIEX_SECRET" { 61 | type = string 62 | default = "" 63 | } 64 | 65 | # coinbase strategies 66 | variable "COINBASE_API_KEY" { 67 | type = string 68 | default = "" 69 | } 70 | 71 | variable "COINBASE_API_SECRET" { 72 | type = string 73 | default = "" 74 | } 75 | 76 | variable "COINBASE_PASSPHRASE" { 77 | type = string 78 | default = "" 79 | } 80 | -------------------------------------------------------------------------------- /deployment_scripts/deploy_cryptotradingbot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin bash 2 | echo $BASH_VERSION 3 | 4 | # Set the name of your custom workspace 5 | workspace_name="poloniex-cmo-1" 6 | 7 | # run this script in the root directory after defining the filepaths below to deploy your trading strategy 8 | lambda_zip_filepath="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/payload.zip" 9 | payload_filepath="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/payload/" 10 | infrastructure_filepath="/Users/liamhartley/PycharmProjects/cryptotradingbot/cloud_infrastructure/aws_terraform/" 11 | 12 | # TODO make this its own function and pass in variables 13 | ########## required trading script paths ########## 14 | declare -A scripts=( 15 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/app.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/payload/" 16 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/config.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/payload/trading_strategies/poloniex_cmo_trading_strategy/" 17 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_tools/poloniex_wrapper.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/payload/trading_tools/" 18 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_tools/cmo_calculation.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/app/payload/trading_tools/" 19 | ) 20 | 21 | # copy all scripts into the payload directory 22 | for script in "${!scripts[@]}" 23 | do 24 | mkdir -p "${scripts[$script]}" 25 | cp "$script" "${scripts[$script]}" 26 | echo "copied $script into ${scripts[$script]}" 27 | done 28 | 29 | ### zipping ### 30 | echo "zipping" 31 | cd $payload_filepath || exit 32 | rm -f $lambda_zip_filepath 33 | zip -r $lambda_zip_filepath * 34 | 35 | ### deploying cloud infrastructure ### 36 | echo "deploying cloud infrastructure" 37 | cd $infrastructure_filepath || exit 38 | 39 | # update the backend config to point to the respective workspace 40 | terraform workspace new $workspace_name || echo "Workspace $workspace_name already exists" 41 | terraform workspace select $workspace_name 42 | 43 | terraform init 44 | 45 | terraform apply -lock=false 46 | -------------------------------------------------------------------------------- /deployment_scripts/deploy_cryptotradingbot_coinbase.sh: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin bash 2 | echo $BASH_VERSION 3 | 4 | # Set the name of your custom workspace 5 | workspace_name="coinbase-cmo-1" 6 | 7 | # run this script in the root directory after defining the filepaths below to deploy your trading strategy 8 | lambda_zip_filepath="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload.zip" 9 | payload_filepath="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/" 10 | infrastructure_filepath="/Users/liamhartley/PycharmProjects/cryptotradingbot/cloud_infrastructure/aws_terraform/" 11 | 12 | # TODO make this its own function and pass in variables 13 | ########## required trading script paths ########## 14 | declare -A scripts=( 15 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/app.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/" 16 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/config.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_strategies/coinbase_cmo_trading_strategy/" 17 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_tools/poloniex_cmo_calculation.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_tools/" 18 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_tools/coinbase_pro_wrapper/authenticated_client.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_tools/coinbase_pro_wrapper/" 19 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_tools/coinbase_pro_wrapper/public_client.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_tools/coinbase_pro_wrapper/" 20 | ["/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_tools/coinbase_pro_wrapper/cbpro_auth.py"]="/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_tools/coinbase_pro_wrapper/" 21 | ) 22 | 23 | # copy all scripts into the payload directory 24 | for script in "${!scripts[@]}" 25 | do 26 | mkdir -p "${scripts[$script]}" 27 | cp "$script" "${scripts[$script]}" 28 | echo "copied $script into ${scripts[$script]}" 29 | done 30 | 31 | ### zipping ### 32 | echo "zipping" 33 | cd $payload_filepath || exit 34 | rm -f $lambda_zip_filepath 35 | zip -r $lambda_zip_filepath * 36 | 37 | ### deploying cloud infrastructure ### 38 | echo "deploying cloud infrastructure" 39 | cd $infrastructure_filepath || exit 40 | 41 | # update the backend config to point to the respective workspace 42 | terraform workspace new $workspace_name || echo "Workspace $workspace_name already exists" 43 | terraform workspace select $workspace_name 44 | 45 | terraform init 46 | 47 | terraform apply -lock=false 48 | -------------------------------------------------------------------------------- /project_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamhartley/cryptotradingbot/7c242f76169f4835ea541ff5bc409b7c32d6de52/project_architecture.png -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/README.md: -------------------------------------------------------------------------------- 1 | # Coinbase CMO Trading Strategy 2 | 3 | ## Contents 4 | 0. [Overview](#overview) 5 | 1. [Project Architecture](#projectarchitecture) 6 | 2. [Setup](#setup) 7 | 3. [Usage](#usage) 8 | 4. [How to Contribute](#howtocontribute) 9 | 5. [Further Reading](#furtherreading) 10 | 6. [Acknowledgements](#acknowledgements) 11 | 7. [Donations](#donations) 12 | 13 | 14 | ## Overview 15 | 16 | This solution allows you to run the strategy 24/7 on AWS. 17 | 18 | You can watch the entire development of this project on [this YouTube playlist](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=4). 19 | You can see this project being deployed in [this video](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=3). 20 | 21 | The trading strategy uses CMO to analyse when there is potentially too much trading momentum in one direction. 22 | 23 | If the asset is considered to be oversold (CMO < oversold threshold) then the bot will purchase the amount defined in the config file. 24 | 25 | The opposite is also true, overbought assets (CMO > overbought threshold) will cause the bot to sell an amount defined in the config. 26 | 27 | 28 | ## Project Architecture 29 | 30 | 31 | 32 | #### Architecture Overview 33 | 34 | - Terraform deploys all the infrastructure into AWS 35 | - Cloudwatch is the trigger which kicks off the Lambda function 36 | - The Lambda function contains the trading logic which is written in Python 37 | - This logic uses the Coinbase Pro API to connect to the exchange 38 | - Every decision that the Lambda takes is logged into to Cloudwatch for auditing 39 | 40 | 41 | 42 | 43 | ## Setup 44 | 45 | 46 | #### Pre-Requisites 47 | 48 | - [Python version 3.0.0 and above](https://www.python.org/downloads/) for backtesting and optimisation. 49 | - [Terraform version 0.13.0 and above](https://www.terraform.io/downloads.html) for cloud infrastructure deployment. 50 | - An [AWS](https://aws.amazon.com) account. 51 | - Packages inside the requirements.txt file. 52 | 53 | To install requirements file (inside project root): `pip install -r requirements.txt` 54 | 55 | To upgrade to the latest backtesting package: `pip install git+https://github.com/liamhartley/Gemini.git --upgrade` 56 | 57 | 58 | 59 | ## Usage 60 | 61 | #### Terraform AWS Deployment 62 | 63 | All of the AWS resources are managed by Terraform and it is best practice to make changes to the code and re-deploy instead of making changes in the GUI. 64 | 65 | - Create a terraform.tfvars file in the terraform directory to store your environment variables (defined in variables.tf in the Terraform folder) 66 | - Modify the filepaths in the "package_lambda.sh" script in /terraform for your machine 67 | - Navigate to the root directory of the project in the terminal 68 | - Run the "package_lambda.sh" script in /terraform 69 | - Run Terraform init in the terraform directory to download the required modules 70 | - Run Terraform plan in the Terraform directory to ensure that your plan is working as expected 71 | - Run Terraform apply in the Terraform directory to deploy the cloud infrastructure 72 | 73 | All logs will be outputted to Cloudwatch and the respective S3 bucket for debugging. 74 | 75 | #### Backtesting 76 | 77 | If you wish to re-run the backtesting/optimisation to identify which cryptocurrency pair you would like to trade then navigate to the backtesting.py script in the backtesting folder. 78 | Change the filepath of the csv output in the __main__ function to a local filepath and modify the config in the strategies root folder. 79 | Then run the backtesting with any permutation of the variables to build a dataset for optimisation. 80 | 81 | #### Optimisation 82 | 83 | Navigate to the analyse_optimisation.py script in the optimisation folder and change the filepath to be the same as the filepath in the optimsiation script. 84 | Then run any SQL query you like against this dataframe to identify the most suitable trading configuration. 85 | 86 | 87 | ## How to Contribute 88 | 89 | Branch off from the project to create a new feature and open a PR against master when complete. 90 | 91 | Please feel free to reach out to me to check if a feature isn't already in development. 92 | 93 | 94 | ## Further Reading 95 | - Read more about the project [my blog](https://medium.datadriveninvestor.com/deploying-a-bitcoin-trading-bot-eb9998dfc0f5) 96 | - [CMO by Investopedia](https://www.investopedia.com/terms/c/chandemomentumoscillator.asp) 97 | 98 | 99 | ## Acknowledgements 100 | - [The Gemini crypto backtesting engine](https://github.com/anfederico/Gemini) by anfederico 101 | 102 | 103 | ## Donations 104 | 105 | If this project helped you learn Python or has helped you make some money then I would love any tips in Bitcoin! 106 | 107 | **My wallet address is**: 36icgLxqAfFTuuruU7v1GQmFpUHjKnzJa8 108 | 109 | [Alternatively you can buy me a coffee!](https://www.buymeacoffee.com/liamhartley) 110 | 111 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/app/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from trading_strategies.coinbase_cmo_trading_strategy.config import LOGICAL_PARAMS 4 | from trading_tools.coinbase_pro_wrapper.authenticated_client import AuthenticatedClient 5 | from trading_tools.coinbase_pro_wrapper.public_client import PublicClient 6 | # from trading_tools.coinbase_cmo_calculation import coinbase_cmo_logic_no_pandas 7 | from trading_tools.poloniex_cmo_calculation import poloniex_cmo_logic_no_pandas 8 | 9 | 10 | def get_balance(coinbase_wrapper, ticker: str): 11 | ''' 12 | Function to parse the get_accounts output to find the balance for a given ticker 13 | :param ticker: the ticker for the cryptocurrency that you want the balnace for e.g. BTC 14 | :return: 15 | ''' 16 | 17 | all_accounts = coinbase_wrapper.get_accounts() 18 | 19 | response = None 20 | for account in all_accounts: 21 | if account['currency'] == ticker: 22 | response = account 23 | 24 | assert response is not None 25 | return response 26 | 27 | 28 | def close_positions(coinbase_wrapper, pair, rate, amount): 29 | ''' 30 | 31 | :param currency_pair: A string that defines the market, "ETH-USDC" for example. 32 | :param rate: The price. Units are market quote currency. Eg ETH-USDC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 33 | :param amount: The total amount offered in this buy order. 34 | :return: 35 | ''' 36 | print('----- closing position -----') 37 | print(f'entry_amount: {amount}') 38 | print(f'rate: {rate}') 39 | print(f'ticker: {pair}') 40 | 41 | # base_currency = LOGICAL_PARAMS['PAIR'].split('-')[0] 42 | quote_currency = LOGICAL_PARAMS['PAIR'].split('-')[1] 43 | 44 | # if we have any of our quote_currency 45 | if float(get_balance(coinbase_wrapper, ticker=quote_currency)['balance']) > 0 and LOGICAL_PARAMS["DRY_RUN"] is False: 46 | response = coinbase_wrapper.sell( 47 | # price=rate, not required for a market order 48 | size=round(amount, 8), 49 | order_type='market', 50 | product_id=pair 51 | ) 52 | elif LOGICAL_PARAMS["DRY_RUN"] is True: 53 | print(f"closing {LOGICAL_PARAMS['PAIR']}\nsale price: {rate}") 54 | response = {'id': '0f27c0ce-5705-43e6-972a-c611ace696be', 'size': '0.00265321', 'product_id': 'ETH-USDC', 55 | 'side': 'sell', 'stp': 'dc', 'type': 'market', 'post_only': False, 56 | 'created_at': '2021-12-23T13:54:24.581751Z', 'fill_fees': '0', 'filled_size': '0', 57 | 'executed_value': '0', 'status': 'pending', 'settled': False} 58 | else: 59 | response = None 60 | 61 | return response 62 | 63 | 64 | def enter_position(coinbase_wrapper, pair, rate, amount): 65 | ''' 66 | 67 | :param currency_pair: A string that defines the market, "ETH-USDC" for example. 68 | :param rate: The price. Units are market quote currency. Eg ETH-USDC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 69 | :param amount: The total amount offered in this buy order. 70 | :return: 71 | ''' 72 | print('----- entering position -----') 73 | print(f'entry_amount: {amount}') 74 | print(f'rate: {rate}') 75 | print(f'ticker: {pair}') 76 | 77 | if LOGICAL_PARAMS["DRY_RUN"] is False: 78 | response = coinbase_wrapper.buy(# price=str(rate), not required for a market order 79 | size=str(round(amount, 8)), 80 | order_type='market', 81 | product_id=pair) 82 | else: 83 | print(f"opening: {LOGICAL_PARAMS['PAIR']}\nsize: {amount}\npurchase price: {rate} ") 84 | response = {'id': 'a118497f-702e-4d11-a8d4-443e50a94d8d', 'size': '0.00265487', 'product_id': 'ETH-USDC', 'side': 'buy', 85 | 'stp': 'dc', 'funds': '105.45273631', 'type': 'market', 'post_only': False, 86 | 'created_at': '2021-12-23T13:44:40.753983Z', 'fill_fees': '0', 'filled_size': '0', 'executed_value': '0', 87 | 'status': 'pending', 'settled': False} 88 | 89 | return response 90 | 91 | 92 | def lambda_handler(event, context): 93 | 94 | coinbase_wrapper = AuthenticatedClient( 95 | key=os.getenv('COINBASE_API_KEY'), 96 | b64secret=os.getenv('COINBASE_API_SECRET'), 97 | passphrase=os.getenv('COINBASE_PASSPHRASE'), 98 | api_url="https://api.pro.coinbase.com" 99 | ) 100 | 101 | base_currency = LOGICAL_PARAMS['PAIR'].split('-')[0] 102 | quote_currency = LOGICAL_PARAMS['PAIR'].split('-')[1] 103 | assert base_currency+'-'+quote_currency == LOGICAL_PARAMS['PAIR'] 104 | 105 | cmo = poloniex_cmo_logic_no_pandas(pair=quote_currency+'_'+base_currency) 106 | 107 | product_details = PublicClient().get_product_ticker(product_id=LOGICAL_PARAMS['PAIR']) 108 | ask_price = float(product_details['ask']) 109 | trade_size = LOGICAL_PARAMS['INITIAL_CAPITAL'] * LOGICAL_PARAMS['ENTRY_SIZE'] # of base currency 110 | exit_amount = trade_size/ask_price 111 | # asset oversold 112 | if cmo < LOGICAL_PARAMS["OVERSOLD_VALUE"]: 113 | response = enter_position( 114 | coinbase_wrapper, 115 | pair=LOGICAL_PARAMS['PAIR'], 116 | rate=ask_price, 117 | amount=trade_size 118 | ) 119 | # asset overbought 120 | elif cmo > LOGICAL_PARAMS["OVERBOUGHT_VALUE"]: 121 | response = close_positions( 122 | coinbase_wrapper, 123 | pair=LOGICAL_PARAMS['PAIR'], 124 | rate=ask_price, 125 | amount=exit_amount 126 | ) 127 | else: 128 | response = 'no trades' 129 | 130 | print(f"{base_currency} balance: {get_balance(coinbase_wrapper, ticker=base_currency)['balance']}") 131 | print(f"{quote_currency} balance: {get_balance(coinbase_wrapper, ticker=quote_currency)['balance']}") 132 | 133 | print(f'CMO: {cmo}') 134 | print(f'response: {response}') 135 | print('Logical parameters:') 136 | for key in LOGICAL_PARAMS: 137 | print(f'{key}: {LOGICAL_PARAMS[key]}') 138 | 139 | 140 | if __name__ == '__main__': 141 | lambda_handler(0, 0) 142 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/app/payload/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from trading_strategies.coinbase_cmo_trading_strategy.config import LOGICAL_PARAMS 4 | from trading_tools.coinbase_pro_wrapper.authenticated_client import AuthenticatedClient 5 | from trading_tools.coinbase_pro_wrapper.public_client import PublicClient 6 | # from trading_tools.coinbase_cmo_calculation import coinbase_cmo_logic_no_pandas 7 | from trading_tools.poloniex_cmo_calculation import poloniex_cmo_logic_no_pandas 8 | 9 | 10 | def get_balance(coinbase_wrapper, ticker: str): 11 | ''' 12 | Function to parse the get_accounts output to find the balance for a given ticker 13 | :param ticker: the ticker for the cryptocurrency that you want the balnace for e.g. BTC 14 | :return: 15 | ''' 16 | 17 | all_accounts = coinbase_wrapper.get_accounts() 18 | 19 | response = None 20 | for account in all_accounts: 21 | if account['currency'] == ticker: 22 | response = account 23 | 24 | assert response is not None 25 | return response 26 | 27 | 28 | def close_positions(coinbase_wrapper, pair, rate, amount): 29 | ''' 30 | 31 | :param currency_pair: A string that defines the market, "ETH-USDC" for example. 32 | :param rate: The price. Units are market quote currency. Eg ETH-USDC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 33 | :param amount: The total amount offered in this buy order. 34 | :return: 35 | ''' 36 | print('----- closing position -----') 37 | print(f'entry_amount: {amount}') 38 | print(f'rate: {rate}') 39 | print(f'ticker: {pair}') 40 | 41 | # base_currency = LOGICAL_PARAMS['PAIR'].split('-')[0] 42 | quote_currency = LOGICAL_PARAMS['PAIR'].split('-')[1] 43 | 44 | # if we have any of our quote_currency 45 | if float(get_balance(coinbase_wrapper, ticker=quote_currency)['balance']) > 0 and LOGICAL_PARAMS["DRY_RUN"] is False: 46 | response = coinbase_wrapper.sell( 47 | # price=rate, not required for a market order 48 | size=round(amount, 8), 49 | order_type='market', 50 | product_id=pair 51 | ) 52 | elif LOGICAL_PARAMS["DRY_RUN"] is True: 53 | print(f"closing {LOGICAL_PARAMS['PAIR']}\nsale price: {rate}") 54 | response = {'id': '0f27c0ce-5705-43e6-972a-c611ace696be', 'size': '0.00265321', 'product_id': 'ETH-USDC', 55 | 'side': 'sell', 'stp': 'dc', 'type': 'market', 'post_only': False, 56 | 'created_at': '2021-12-23T13:54:24.581751Z', 'fill_fees': '0', 'filled_size': '0', 57 | 'executed_value': '0', 'status': 'pending', 'settled': False} 58 | else: 59 | response = None 60 | 61 | return response 62 | 63 | 64 | def enter_position(coinbase_wrapper, pair, rate, amount): 65 | ''' 66 | 67 | :param currency_pair: A string that defines the market, "ETH-USDC" for example. 68 | :param rate: The price. Units are market quote currency. Eg ETH-USDC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 69 | :param amount: The total amount offered in this buy order. 70 | :return: 71 | ''' 72 | print('----- entering position -----') 73 | print(f'entry_amount: {amount}') 74 | print(f'rate: {rate}') 75 | print(f'ticker: {pair}') 76 | 77 | if LOGICAL_PARAMS["DRY_RUN"] is False: 78 | response = coinbase_wrapper.buy(# price=str(rate), not required for a market order 79 | size=str(round(amount, 8)), 80 | order_type='market', 81 | product_id=pair) 82 | else: 83 | print(f"opening: {LOGICAL_PARAMS['PAIR']}\nsize: {amount}\npurchase price: {rate} ") 84 | response = {'id': 'a118497f-702e-4d11-a8d4-443e50a94d8d', 'size': '0.00265487', 'product_id': 'ETH-USDC', 'side': 'buy', 85 | 'stp': 'dc', 'funds': '105.45273631', 'type': 'market', 'post_only': False, 86 | 'created_at': '2021-12-23T13:44:40.753983Z', 'fill_fees': '0', 'filled_size': '0', 'executed_value': '0', 87 | 'status': 'pending', 'settled': False} 88 | 89 | return response 90 | 91 | 92 | def lambda_handler(event, context): 93 | 94 | coinbase_wrapper = AuthenticatedClient( 95 | key=os.getenv('COINBASE_API_KEY'), 96 | b64secret=os.getenv('COINBASE_API_SECRET'), 97 | passphrase=os.getenv('COINBASE_PASSPHRASE'), 98 | api_url="https://api.pro.coinbase.com" 99 | ) 100 | 101 | base_currency = LOGICAL_PARAMS['PAIR'].split('-')[0] 102 | quote_currency = LOGICAL_PARAMS['PAIR'].split('-')[1] 103 | assert base_currency+'-'+quote_currency == LOGICAL_PARAMS['PAIR'] 104 | 105 | cmo = poloniex_cmo_logic_no_pandas(pair=quote_currency+'_'+base_currency) 106 | 107 | product_details = PublicClient().get_product_ticker(product_id=LOGICAL_PARAMS['PAIR']) 108 | ask_price = float(product_details['ask']) 109 | trade_size = LOGICAL_PARAMS['INITIAL_CAPITAL'] * LOGICAL_PARAMS['ENTRY_SIZE'] # of base currency 110 | exit_amount = trade_size/ask_price 111 | # asset oversold 112 | if cmo < LOGICAL_PARAMS["OVERSOLD_VALUE"]: 113 | response = enter_position( 114 | coinbase_wrapper, 115 | pair=LOGICAL_PARAMS['PAIR'], 116 | rate=ask_price, 117 | amount=trade_size 118 | ) 119 | # asset overbought 120 | elif cmo > LOGICAL_PARAMS["OVERBOUGHT_VALUE"]: 121 | response = close_positions( 122 | coinbase_wrapper, 123 | pair=LOGICAL_PARAMS['PAIR'], 124 | rate=ask_price, 125 | amount=exit_amount 126 | ) 127 | else: 128 | response = 'no trades' 129 | 130 | print(f"{base_currency} balance: {get_balance(coinbase_wrapper, ticker=base_currency)['balance']}") 131 | print(f"{quote_currency} balance: {get_balance(coinbase_wrapper, ticker=quote_currency)['balance']}") 132 | 133 | print(f'CMO: {cmo}') 134 | print(f'response: {response}') 135 | print('Logical parameters:') 136 | for key in LOGICAL_PARAMS: 137 | print(f'{key}: {LOGICAL_PARAMS[key]}') 138 | 139 | 140 | if __name__ == '__main__': 141 | lambda_handler(0, 0) 142 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_strategies/poloniex_cmo_trading_strategy/config.py: -------------------------------------------------------------------------------- 1 | LOGICAL_PARAMS = { 2 | "CMO_PERIOD": 10, 3 | "PERIOD": 86400, 4 | "PAIR": "BTC_XRP", 5 | "OVERSOLD_VALUE": -50, 6 | "OVERBOUGHT_VALUE": 50, 7 | "DRY_RUN": False, 8 | "INITIAL_CAPITAL": 0.018, # in quote currency 9 | "ENTRY_SIZE": 0.1 # 10% 10 | } 11 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_tools/cmo_calculation.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from trading_strategies.poloniex_cmo_trading_strategy.config import LOGICAL_PARAMS 4 | 5 | 6 | def get_past(pair, period, days_history=30): 7 | """ 8 | Return historical charts data from poloniex.com 9 | :param pair: 10 | :param period: 11 | :param days_history: 12 | :return: 13 | """ 14 | end = int(time.time()) 15 | start = end - (24 * 60 * 60 * days_history) 16 | params = { 17 | 'command': 'returnChartData', 18 | 'currencyPair': pair, 19 | 'start': start, 20 | 'end': end, 21 | 'period': period 22 | } 23 | 24 | response = requests.get('https://poloniex.com/public', params=params) 25 | return response.json() 26 | 27 | 28 | def cmo_logic_no_pandas(): 29 | response_json = get_past( 30 | pair=LOGICAL_PARAMS["PAIR"], 31 | period=LOGICAL_PARAMS["PERIOD"], 32 | days_history=LOGICAL_PARAMS["CMO_PERIOD"] 33 | ) 34 | 35 | print(f'historical data: {response_json}') 36 | 37 | # if len(response_json)-1 == LOGICAL_PARAMS["CMO_PERIOD"]: 38 | # past_data = response_json[1:] 39 | # elif len(response_json) == LOGICAL_PARAMS["CMO_PERIOD"]: 40 | # past_data = response_json 41 | # else: 42 | # raise Exception("Invalid CMO check") 43 | 44 | higher_close_price = 0 45 | lower_close_price = 0 46 | 47 | previous_day = False 48 | 49 | for day in response_json: 50 | if previous_day is not False: 51 | if day['close'] > previous_day['close']: 52 | higher_close_price += 1 53 | elif day['close'] < previous_day['close']: 54 | lower_close_price += 1 55 | previous_day = day 56 | 57 | cmo = ((higher_close_price - lower_close_price) / (higher_close_price + lower_close_price)) * 100 58 | print(f'higher_close_price: {higher_close_price}') 59 | print(f'lower_close_price: {lower_close_price}') 60 | print(f'cmo: {cmo}') 61 | return cmo 62 | 63 | 64 | if __name__ == '__main__': 65 | cmo_logic_no_pandas() 66 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/app/payload/trading_tools/poloniex_wrapper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import requests 4 | import hashlib 5 | import hmac 6 | import urllib 7 | import ssl 8 | import json 9 | 10 | from trading_strategies.poloniex_cmo_trading_strategy.config import LOGICAL_PARAMS 11 | 12 | 13 | class Poloniex: 14 | def __init__(self, APIKey, Secret): 15 | self.APIKey = APIKey 16 | self.Secret = bytes(Secret.encode('utf8')) 17 | self.public_url = 'https://poloniex.com/public?command=' 18 | self.private_url = 'https://poloniex.com/tradingApi' 19 | 20 | def public_query(self, command): 21 | gcontext = ssl.SSLContext() 22 | data = urllib.request.urlopen(urllib.request.Request(self.public_url+command), context=gcontext) 23 | return json.loads(data.read()) 24 | 25 | def private_query(self, command): 26 | payload = { 27 | 'command': command, 28 | 'nonce': int(time.time() * 1000), 29 | } 30 | 31 | paybytes = urllib.parse.urlencode(payload).encode('utf8') 32 | sign = hmac.new(self.Secret, paybytes, hashlib.sha512).hexdigest() 33 | 34 | headers = { 35 | 'Key': self.APIKey, 36 | 'Sign': sign 37 | } 38 | 39 | r = requests.post(self.private_url, headers=headers, data=payload) 40 | return r.json() 41 | 42 | def trade(self, currency_pair, rate, amount, command): 43 | ''' 44 | 45 | :param currency_pair: A string that defines the market, "USDT_BTC" for example. 46 | :param rate: The price. Units are market quote currency. Eg USDT_BTC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 47 | :param amount: The total amount offered in this buy order. 48 | :return: 49 | ''' 50 | url = 'https://poloniex.com/tradingApi' 51 | payload = { 52 | 'command': command, 53 | 'nonce': int(time.time() * 1000), 54 | 'currencyPair': currency_pair, 55 | 'rate': rate, 56 | 'amount': amount 57 | } 58 | paybytes = urllib.parse.urlencode(payload).encode('utf8') 59 | sign = hmac.new(self.Secret, paybytes, hashlib.sha512).hexdigest() 60 | 61 | headers = { 62 | 'Key': self.APIKey, 63 | 'Sign': sign 64 | } 65 | 66 | r = requests.post(url, headers=headers, data=payload) 67 | return r.json() 68 | 69 | 70 | if __name__ == '__main__': 71 | 72 | # TODO write a decorator that logs you in - (still required??) 73 | poloniex_wrapper = Poloniex( 74 | APIKey=os.getenv('POLONIEX_KEY'), 75 | Secret=os.getenv('POLONIEX_SECRET') 76 | ) 77 | 78 | all_tickers = poloniex_wrapper.public_query(command='returnTicker') 79 | ticker = all_tickers[LOGICAL_PARAMS['PAIR']] 80 | rate = float(ticker['lowestAsk']) 81 | price = LOGICAL_PARAMS['INITIAL_CAPITAL'] * LOGICAL_PARAMS['ENTRY_SIZE'] # of base currency 82 | entry_amount = price/rate 83 | 84 | # response = poloniex_wrapper.trade( 85 | # currency_pair=LOGICAL_PARAMS['PAIR'], 86 | # rate=rate, 87 | # amount=entry_amount, 88 | # command='buy' 89 | # ) 90 | 91 | # response = poloniex_wrapper.trade( 92 | # currency_pair=LOGICAL_PARAMS['PAIR'], 93 | # rate=rate, 94 | # amount=entry_amount, 95 | # command='sell' 96 | # ) 97 | 98 | balance = poloniex_wrapper.private_query(command='returnBalances') 99 | 100 | print(balance) 101 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/config.py: -------------------------------------------------------------------------------- 1 | LOGICAL_PARAMS = { 2 | "CMO_PERIOD": 10, # number of days that CMO is calculated over 3 | "PERIOD": 14400, # period that data is received 4 | "PAIR": "ETH-USDC", # crypto pair to trade 5 | "OVERSOLD_VALUE": -50, # CMO threshold value to buy at 6 | "OVERBOUGHT_VALUE": 50, # CMO threshold value to sell at 7 | "DRY_RUN": True, # when True the bot will not execute trades 8 | "INITIAL_CAPITAL": 1000, # currency at deployment (in quote currency) 9 | "ENTRY_SIZE": 0.1 # size of trade to buy/sell (e.g. 0.1=10% of INITIAL_CAPITAL) 10 | } 11 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/docs/cmo_trading_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamhartley/cryptotradingbot/7c242f76169f4835ea541ff5bc409b7c32d6de52/trading_strategies/coinbase_cmo_trading_strategy/docs/cmo_trading_architecture.png -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/optimisation/analyse_optimisation.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pandasql as ps 3 | 4 | # 180 days multiple periods 5 | filepath = '/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/coinbase_cmo_trading_strategy/optimisation/sol_optimisation.csv' 6 | 7 | 8 | if __name__ == '__main__': 9 | df = pd.read_csv(filepath, header=0) 10 | optimised_df = ps.sqldf('select * from df order by net_profit desc') 11 | optimised_df_no_atom = ps.sqldf('select * from df where pair not like "ATOM%" order by net_profit desc') 12 | period_df = ps.sqldf('select poloniex_period, avg(net_profit) as net_profit, avg(sharpe_ratio), sum(fees_paid), avg(total_trades) from df group by poloniex_period order by net_profit desc') 13 | period_df_profit = ps.sqldf('select poloniex_period, avg(net_profit) as net_profit, avg(sharpe_ratio), sum(fees_paid), avg(total_trades) from df where net_profit > 0 group by poloniex_period order by net_profit desc') 14 | # df = df.drop(['cmo_period', 'oversold_value', 'overbought_value', 'entry_size', 'starting_capital'], axis=1) 15 | 16 | crypto_selection_df = ps.sqldf('select * from df where pair not like "ATOM%" and poloniex_period = 7200 order by net_profit desc') 17 | 18 | print(df) 19 | 20 | # # period analysis 21 | # ps.sqldf('select poloniex_period, avg(net_profit) as net_profit, avg(sharpe_ratio), sum(fees_paid), avg(total_trades) from df group by poloniex_period order by net_profit desc') 22 | -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/optimisation/pairs_raw.txt: -------------------------------------------------------------------------------- 1 | BTC/USDT 2 | Poloniex 3 | $ 59,413.84 4 | $ 62.17 million 5 | 2 6 | Poloniex icon 7 | DOGE/BTC 8 | Poloniex 9 | $ 0.7172 10 | $ 37.73 million 11 | 3 12 | Poloniex icon 13 | ETH/USDT 14 | Poloniex 15 | $ 3,602.94 16 | $ 37.37 million 17 | 4 18 | Poloniex icon 19 | DOGE/USDT 20 | Poloniex 21 | $ 0.7169 22 | $ 31.28 million 23 | 5 24 | Poloniex icon 25 | ETH/BTC 26 | Poloniex 27 | $ 3,605.45 28 | $ 19.44 million 29 | 6 30 | Poloniex icon 31 | XRP/USDT 32 | Poloniex 33 | $ 1.62 34 | $ 13.91 million 35 | 7 36 | Poloniex icon 37 | TRX/USDT 38 | Poloniex 39 | $ 0.1514 40 | $ 9.84 million 41 | 8 42 | Poloniex icon 43 | BTC/USDC 44 | Poloniex 45 | $ 59,439.65 46 | $ 9.21 million 47 | 9 48 | Poloniex icon 49 | XRP/BTC 50 | Poloniex 51 | $ 1.62 52 | $ 9.10 million 53 | 10 54 | Poloniex icon 55 | LTC/USDT 56 | Poloniex 57 | $ 348.54 58 | $ 7.07 million 59 | 11 60 | Poloniex icon 61 | SRM/USDT 62 | Poloniex 63 | $ 10.57 64 | $ 6.29 million 65 | 12 66 | Poloniex icon 67 | XMR/BTC 68 | Poloniex 69 | $ 480.96 70 | $ 6.00 million 71 | 13 72 | Poloniex icon 73 | LTC/BTC 74 | Poloniex 75 | $ 348.72 76 | $ 5.79 million 77 | 14 78 | Poloniex icon 79 | TRX/BTC 80 | Poloniex 81 | $ 0.1516 82 | $ 5.20 million 83 | 15 84 | Poloniex icon 85 | ETC/USDT 86 | Poloniex 87 | $ 84.56 88 | $ 5.08 million 89 | 16 90 | Poloniex icon 91 | ETH/USDC 92 | Poloniex 93 | $ 3,597.10 94 | $ 3.98 million 95 | 17 96 | Poloniex icon 97 | ATOM/BTC 98 | Poloniex 99 | $ 29.89 100 | $ 3.14 million 101 | 18 102 | Poloniex icon 103 | EOS/BTC 104 | Poloniex 105 | $ 10.59 106 | $ 2.78 million 107 | 19 108 | Poloniex icon 109 | XEM/BTC 110 | Poloniex 111 | $ 0.4056 112 | $ 2.76 million 113 | 20 114 | Poloniex icon 115 | EOS/USDT 116 | Poloniex 117 | $ 10.57 118 | $ 2.17 million 119 | 21 120 | Poloniex icon 121 | BCH/USDT 122 | Poloniex 123 | $ 1,436.32 124 | $ 2.15 million 125 | 22 126 | Poloniex icon 127 | ETC/BTC 128 | Poloniex 129 | $ 84.54 130 | $ 2.03 million 131 | 23 132 | Poloniex icon 133 | BCH/BTC 134 | Poloniex 135 | $ 1,439.00 136 | $ 1.95 million 137 | 24 138 | Poloniex icon 139 | XMR/USDT 140 | Poloniex 141 | $ 481.98 142 | $ 1.75 million 143 | 25 144 | Poloniex icon 145 | ATOM/USDC 146 | Poloniex 147 | $ 29.72 148 | $ 1.60 million 149 | 26 150 | Poloniex icon 151 | DOT/USDT 152 | Poloniex 153 | $ 40.41 154 | $ 1.57 million 155 | 27 156 | Poloniex icon 157 | ATOM/USDT 158 | Poloniex 159 | $ 29.74 160 | $ 1.50 million 161 | 28 162 | Poloniex icon 163 | BNB/USDT 164 | Poloniex 165 | $ 642.01 166 | $ 1.39 million 167 | 29 168 | Poloniex icon 169 | XLM/BTC 170 | Poloniex 171 | $ 0.6363 172 | $ 1.36 million 173 | 30 174 | Poloniex icon 175 | USDT/USDC 176 | Poloniex 177 | $ 1.01 178 | $ 1.18 million 179 | 31 180 | Poloniex icon 181 | USDJ/USDT 182 | Poloniex 183 | $ 1.09 184 | $ 1.14 million 185 | 32 186 | Poloniex icon 187 | DOGE/USDC 188 | Poloniex 189 | $ 0.7187 190 | $ 971,429.58 191 | 33 192 | Poloniex icon 193 | SC/USDT 194 | Poloniex 195 | $ 0.0423 196 | $ 952,106.77 197 | 34 198 | Poloniex icon 199 | EOS/USDC 200 | Poloniex 201 | $ 10.50 202 | $ 928,664.80 203 | 35 204 | Poloniex icon 205 | LSK/BTC 206 | Poloniex 207 | $ 8.91 208 | $ 859,547.21 209 | 36 210 | Poloniex icon 211 | BTT/USDT 212 | Poloniex 213 | $ 0.00792 214 | $ 856,592.96 215 | 37 216 | Poloniex icon 217 | XLM/USDT 218 | Poloniex 219 | $ 0.6368 220 | $ 847,826.92 221 | 38 222 | Poloniex icon 223 | DASH/BTC 224 | Poloniex 225 | $ 420.96 226 | $ 824,234.21 227 | 39 228 | Poloniex icon 229 | TUSD/USDT 230 | Poloniex 231 | $ 1.01 232 | $ 769,005.49 233 | 40 234 | Poloniex icon 235 | BCH/USDC 236 | Poloniex 237 | $ 1,434.83 238 | $ 717,741.90 239 | 41 240 | Poloniex icon 241 | AMP/USDT 242 | Poloniex 243 | $ 0.0738 244 | $ 693,140.91 245 | 42 246 | Poloniex icon 247 | XRP/USDC 248 | Poloniex 249 | $ 1.62 250 | $ 682,483.63 251 | 43 252 | Poloniex icon 253 | JST/USDT 254 | Poloniex 255 | $ 0.1455 256 | $ 667,452.11 257 | 44 258 | Poloniex icon 259 | DASH/USDT 260 | Poloniex 261 | $ 420.21 262 | $ 647,107.81 263 | 45 264 | Poloniex icon 265 | ETC/ETH 266 | Poloniex 267 | $ 82.75 268 | $ 626,853.08 269 | 46 270 | Poloniex icon 271 | ZEC/USDT 272 | Poloniex 273 | $ 313.67 274 | $ 581,176.88 275 | 47 276 | Poloniex icon 277 | KCS/USDT 278 | Poloniex 279 | $ 13.44 280 | $ 526,098.44 281 | 48 282 | Poloniex icon 283 | QTUM/BTC 284 | Poloniex 285 | $ 25.86 286 | $ 500,828.54 287 | 49 288 | Poloniex icon 289 | LSK/USDT 290 | Poloniex 291 | $ 9.04 292 | $ 499,157.04 293 | 50 294 | Poloniex icon 295 | SC/BTC 296 | Poloniex 297 | $ 0.0422 -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/optimisation/pairs_transform.py: -------------------------------------------------------------------------------- 1 | with open('/Users/liamhartley/PycharmProjects/cryptotradingbot/pairs_raw.txt') as f: 2 | pairs = [] 3 | lines = f.readlines() 4 | for line in lines: 5 | print(line) 6 | if '/' in line: 7 | pairs.append(line.replace('/','_')) 8 | 9 | print(pairs) -------------------------------------------------------------------------------- /trading_strategies/coinbase_cmo_trading_strategy/requirements.txt: -------------------------------------------------------------------------------- 1 | bokeh==2.3.0 2 | certifi==2020.12.5 3 | chardet==4.0.0 4 | cycler==0.10.0 5 | idna==2.10 6 | Jinja2==2.11.3 7 | kiwisolver==1.3.1 8 | MarkupSafe==1.1.1 9 | matplotlib==3.3.4 10 | numpy==1.19.5 11 | packaging==20.9 12 | pandas==1.1.5 13 | Pillow==8.1.1 14 | pyparsing==2.4.7 15 | python-dateutil==2.8.1 16 | pytz==2021.1 17 | PyYAML==5.4.1 18 | requests==2.25.1 19 | scipy==1.5.4 20 | six==1.15.0 21 | tornado==6.1 22 | typing-extensions==3.7.4.3 23 | urllib3==1.26.3 24 | pandasql==0.7.3 25 | boto3==1.17.75 26 | botocore==1.20.75 27 | jmespath==0.10.0 28 | s3transfer==0.4.2 29 | git+git://github.com/liamhartley/Gemini.git 30 | -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/README.md: -------------------------------------------------------------------------------- 1 | # Poloniex CMO Trading Strategy 2 | 3 | ## Contents 4 | 0. [Overview](#overview) 5 | 1. [Project Architecture](#projectarchitecture) 6 | 2. [Setup](#setup) 7 | 3. [Usage](#usage) 8 | 4. [How to Contribute](#howtocontribute) 9 | 5. [Further Reading](#furtherreading) 10 | 6. [Acknowledgements](#acknowledgements) 11 | 7. [Donations](#donations) 12 | 13 | 14 | ## Overview 15 | 16 | This solution allows you to run the strategy 24/7 on AWS. 17 | 18 | You can watch the entire development of this project on [this YouTube playlist](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=4). 19 | You can see this project being deployed in [this video](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=3). 20 | 21 | The trading strategy uses CMO to analyse when there is potentially too much trading momentum in one direction. 22 | 23 | If the asset is considered to be oversold (CMO < oversold threshold) then the bot will purchase the amount defined in the config file. 24 | 25 | The opposite is also true, overbought assets (CMO > overbought threshold) will cause the bot to sell an amount defined in the config. 26 | 27 | 28 | ## Project Architecture 29 | 30 | 31 | 32 | #### Architecture Overview 33 | 34 | - Terraform deploys all the infrastructure into AWS 35 | - Cloudwatch is the trigger which kicks off the Lambda function 36 | - The Lambda function contains the trading logic which is written in Python 37 | - This logic uses the Poloniex API to connect to the exchange 38 | - Every decision that the Lambda takes is logged into to Cloudwatch for auditing 39 | 40 | 41 | 42 | 43 | ## Setup 44 | 45 | 46 | #### Pre-Requisites 47 | 48 | - [Python version 3.0.0 and above](https://www.python.org/downloads/) for backtesting and optimisation. 49 | - [Terraform version 0.13.0 and above](https://www.terraform.io/downloads.html) for cloud infrastructure deployment. 50 | - An [AWS](https://aws.amazon.com) account. 51 | - Packages inside the requirements.txt file. 52 | 53 | To install requirements file (inside project root): `pip install -r requirements.txt` 54 | 55 | To upgrade to the latest backtesting package: `pip install git+https://github.com/liamhartley/Gemini.git --upgrade` 56 | 57 | 58 | 59 | ## Usage 60 | 61 | #### Terraform AWS Deployment 62 | 63 | All of the AWS resources are managed by Terraform and it is best practice to make changes to the code and re-deploy instead of making changes in the GUI. 64 | 65 | - Create a terraform.tfvars file in the terraform directory to store your environment variables (defined in variables.tf in the Terraform folder) 66 | - Modify the filepaths in the "package_lambda.sh" script in /terraform for your machine 67 | - Navigate to the root directory of the project in the terminal 68 | - Run the "package_lambda.sh" script in /terraform 69 | - Run Terraform init in the terraform directory to download the required modules 70 | - Run Terraform plan in the Terraform directory to ensure that your plan is working as expected 71 | - Run Terraform apply in the Terraform directory to deploy the cloud infrastructure 72 | 73 | All logs will be outputted to Cloudwatch and the respective S3 bucket for debugging. 74 | 75 | #### Backtesting 76 | 77 | If you wish to re-run the backtesting/optimisation to identify which cryptocurrency pair you would like to trade then navigate to the backtesting.py script in the backtesting folder. 78 | Change the filepath of the csv output in the __main__ function to a local filepath and modify the config in the strategies root folder. 79 | Then run the backtesting with any permutation of the variables to build a dataset for optimisation. 80 | 81 | #### Optimisation 82 | 83 | Navigate to the analyse_optimisation.py script in the optimisation folder and change the filepath to be the same as the filepath in the optimsiation script. 84 | Then run any SQL query you like against this dataframe to identify the most suitable trading configuration. 85 | 86 | 87 | ## How to Contribute 88 | 89 | Branch off from the project to create a new feature and open a PR against master when complete. 90 | 91 | Please feel free to reach out to me to check if a feature isn't already in development. 92 | 93 | 94 | ## Further Reading 95 | - Read more about the project [my blog](https://medium.datadriveninvestor.com/deploying-a-bitcoin-trading-bot-eb9998dfc0f5) 96 | - [CMO by Investopedia](https://www.investopedia.com/terms/c/chandemomentumoscillator.asp) 97 | - [Poloniex Exchange](https://poloniex.com) 98 | 99 | 100 | ## Acknowledgements 101 | - [The Gemini crypto backtesting engine](https://github.com/anfederico/Gemini) by anfederico 102 | - [The Poloniex wrapper](https://github.com/bwentzloff/trading-bot) by bwentzloff 103 | 104 | 105 | ## Donations 106 | 107 | If this project helped you learn Python or has helped you make some money then I would love any tips in Bitcoin! 108 | 109 | **My wallet address is**: 36icgLxqAfFTuuruU7v1GQmFpUHjKnzJa8 110 | 111 | [Alternatively you can buy me a coffee!](https://www.buymeacoffee.com/liamhartley) 112 | 113 | -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/app/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from trading_strategies.poloniex_cmo_trading_strategy.config import LOGICAL_PARAMS 4 | from trading_tools.poloniex_wrapper import Poloniex 5 | from trading_tools.poloniex_cmo_calculation import poloniex_cmo_logic_no_pandas 6 | 7 | 8 | def close_positions(poloniex_wrapper, pair, rate, amount): 9 | ''' 10 | 11 | :param currency_pair: A string that defines the market, "USDT_BTC" for example. 12 | :param rate: The price. Units are market quote currency. Eg USDT_BTC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 13 | :param amount: The total amount offered in this buy order. 14 | :return: 15 | ''' 16 | print('closing position') 17 | print(f'entry_amount: {amount}') 18 | print(f'rate: {rate}') 19 | print(f'ticker: {pair}') 20 | 21 | # base_currency = LOGICAL_PARAMS['PAIR'].split('_')[0] 22 | quote_currency = LOGICAL_PARAMS['PAIR'].split('_')[1] 23 | 24 | # if we have any of our quote_currency 25 | if float(poloniex_wrapper.private_query(command='returnBalances')[quote_currency]) > 0 and LOGICAL_PARAMS["DRY_RUN"] is False: 26 | # sell the entire balance of our base currency 27 | response = poloniex_wrapper.trade( 28 | currency_pair=pair, 29 | rate=rate, 30 | amount=amount, 31 | command='sell' 32 | ) 33 | elif LOGICAL_PARAMS["DRY_RUN"] is True: 34 | print(f"closing {LOGICAL_PARAMS['PAIR']}\nsale price: {pair['highestBid']}") 35 | response = {'orderNumber': '514845991795', 36 | 'resultingTrades': [ 37 | {'amount': '3.0', 38 | 'date': '2018-10-25 23:03:21', 39 | 'rate': '0.0002', 40 | 'total': '0.0006', 41 | 'tradeID': '251834', 42 | 'type': 'sell'} 43 | ], 44 | 'fee': '0.01000000', 45 | 'clientOrderId': '12345', 46 | 'currencyPair': 'BTC_ETH'} 47 | else: 48 | response = None 49 | 50 | return response 51 | 52 | 53 | def enter_position(poloniex_wrapper, pair, rate, amount): 54 | ''' 55 | 56 | :param currency_pair: A string that defines the market, "USDT_BTC" for example. 57 | :param rate: The price. Units are market quote currency. Eg USDT_BTC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 58 | :param amount: The total amount offered in this buy order. 59 | :return: 60 | ''' 61 | print('entering position') 62 | print(f'entry_amount: {amount}') 63 | print(f'rate: {rate}') 64 | print(f'ticker: {pair}') 65 | 66 | if LOGICAL_PARAMS["DRY_RUN"] is False: 67 | response = poloniex_wrapper.trade( 68 | currency_pair=pair, 69 | rate=rate, 70 | amount=amount, 71 | command='buy' 72 | ) 73 | else: 74 | print(f"opening: {LOGICAL_PARAMS['PAIR']}\nsize: {amount}\npurchase price: {pair['lowestAsk']} ") 75 | response = {'orderNumber': '514845991795', 76 | 'resultingTrades': [ 77 | {'amount': '3.0', 78 | 'date': '2018-10-25 23:03:21', 79 | 'rate': '0.0002', 80 | 'total': '0.0006', 81 | 'tradeID': '251834', 82 | 'type': 'buy'} 83 | ], 84 | 'fee': '0.01000000', 85 | 'clientOrderId': '12345', 86 | 'currencyPair': 'BTC_ETH'} 87 | 88 | return response 89 | 90 | 91 | def lambda_handler(event, context): 92 | 93 | poloniex_wrapper = Poloniex( 94 | APIKey=os.getenv('POLONIEX_KEY'), 95 | Secret=os.getenv('POLONIEX_SECRET') 96 | ) 97 | 98 | base_currency = LOGICAL_PARAMS['PAIR'].split('_')[0] 99 | quote_currency = LOGICAL_PARAMS['PAIR'].split('_')[1] 100 | assert base_currency+'_'+quote_currency == LOGICAL_PARAMS['PAIR'] 101 | 102 | cmo = poloniex_cmo_logic_no_pandas(pair=LOGICAL_PARAMS['PAIR']) 103 | 104 | all_tickers = poloniex_wrapper.public_query(command='returnTicker') 105 | ticker = all_tickers[LOGICAL_PARAMS['PAIR']] 106 | rate = float(ticker['lowestAsk']) 107 | price = LOGICAL_PARAMS['INITIAL_CAPITAL'] * LOGICAL_PARAMS['ENTRY_SIZE'] # of base currency 108 | entry_amount = price/rate 109 | 110 | # asset oversold 111 | if cmo < LOGICAL_PARAMS["OVERSOLD_VALUE"]: 112 | response = enter_position( 113 | poloniex_wrapper, 114 | pair=LOGICAL_PARAMS['PAIR'], 115 | rate=rate, 116 | amount=entry_amount 117 | ) 118 | # asset overbought 119 | elif cmo > LOGICAL_PARAMS["OVERBOUGHT_VALUE"]: 120 | response = close_positions( 121 | poloniex_wrapper, 122 | pair=LOGICAL_PARAMS['PAIR'], 123 | rate=rate, 124 | amount=entry_amount 125 | ) 126 | else: 127 | response = 'no trades' 128 | 129 | print(f"{base_currency} balance: {poloniex_wrapper.private_query(command='returnBalances')[base_currency]}") 130 | print(f"{quote_currency} balance: {poloniex_wrapper.private_query(command='returnBalances')[quote_currency]}") 131 | 132 | print(f'CMO: {cmo}') 133 | print(f'response: {response}') 134 | for key in LOGICAL_PARAMS: 135 | print(f'{key}: {LOGICAL_PARAMS[key]}') 136 | 137 | 138 | if __name__ == '__main__': 139 | lambda_handler(0, 0) 140 | -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/config.py: -------------------------------------------------------------------------------- 1 | LOGICAL_PARAMS = { 2 | "CMO_PERIOD": 7, # number of days that CMO is calculated over 3 | "PERIOD": 7200, # period that data is received 4 | "PAIR": "BTC_XRP", # crypto pair to trade 5 | "OVERSOLD_VALUE": -50, # CMO threshold value to buy at 6 | "OVERBOUGHT_VALUE": 39, # CMO threshold value to sell at 7 | "DRY_RUN": True, # when True the bot will not execute trades 8 | "INITIAL_CAPITAL": 0.018, # currency at deployment (in quote currency) 9 | "ENTRY_SIZE": 0.1 # size of trade to buy/sell (e.g. 0.1=10% of INITIAL_CAPITAL) 10 | } 11 | -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/docs/cmo_trading_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamhartley/cryptotradingbot/7c242f76169f4835ea541ff5bc409b7c32d6de52/trading_strategies/poloniex_cmo_trading_strategy/docs/cmo_trading_architecture.png -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/optimisation/analyse_optimisation.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pandasql as ps 3 | 4 | # 180 days multiple periods 5 | filepath = '/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/optimisation/optimisation_results_period_extended.csv' 6 | # # 90 days multiple periods 7 | # filepath = '/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/optimisation/optimisation_results_period.csv' 8 | # initial analysis 9 | # filepath = "/Users/liamhartley/PycharmProjects/cryptotradingbot/trading_strategies/poloniex_cmo_trading_strategy/optimisation/initial_optimisation_results.csv" 10 | 11 | if __name__ == '__main__': 12 | df = pd.read_csv(filepath, header=0) 13 | optimised_df = ps.sqldf('select * from df order by net_profit desc') 14 | optimised_df_no_atom = ps.sqldf('select * from df where pair not like "ATOM%" order by net_profit desc') 15 | period_df = ps.sqldf('select poloniex_period, avg(net_profit) as net_profit, avg(sharpe_ratio), sum(fees_paid), avg(total_trades) from df group by poloniex_period order by net_profit desc') 16 | period_df_profit = ps.sqldf('select poloniex_period, avg(net_profit) as net_profit, avg(sharpe_ratio), sum(fees_paid), avg(total_trades) from df where net_profit > 0 group by poloniex_period order by net_profit desc') 17 | # df = df.drop(['cmo_period', 'oversold_value', 'overbought_value', 'entry_size', 'starting_capital'], axis=1) 18 | 19 | crypto_selection_df = ps.sqldf('select * from df where pair not like "ATOM%" and poloniex_period = 7200 order by net_profit desc') 20 | 21 | print(df) 22 | 23 | # # period analysis 24 | # ps.sqldf('select poloniex_period, avg(net_profit) as net_profit, avg(sharpe_ratio), sum(fees_paid), avg(total_trades) from df group by poloniex_period order by net_profit desc') 25 | -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/optimisation/pairs_raw.txt: -------------------------------------------------------------------------------- 1 | BTC/USDT 2 | Poloniex 3 | $ 59,413.84 4 | $ 62.17 million 5 | 2 6 | Poloniex icon 7 | DOGE/BTC 8 | Poloniex 9 | $ 0.7172 10 | $ 37.73 million 11 | 3 12 | Poloniex icon 13 | ETH/USDT 14 | Poloniex 15 | $ 3,602.94 16 | $ 37.37 million 17 | 4 18 | Poloniex icon 19 | DOGE/USDT 20 | Poloniex 21 | $ 0.7169 22 | $ 31.28 million 23 | 5 24 | Poloniex icon 25 | ETH/BTC 26 | Poloniex 27 | $ 3,605.45 28 | $ 19.44 million 29 | 6 30 | Poloniex icon 31 | XRP/USDT 32 | Poloniex 33 | $ 1.62 34 | $ 13.91 million 35 | 7 36 | Poloniex icon 37 | TRX/USDT 38 | Poloniex 39 | $ 0.1514 40 | $ 9.84 million 41 | 8 42 | Poloniex icon 43 | BTC/USDC 44 | Poloniex 45 | $ 59,439.65 46 | $ 9.21 million 47 | 9 48 | Poloniex icon 49 | XRP/BTC 50 | Poloniex 51 | $ 1.62 52 | $ 9.10 million 53 | 10 54 | Poloniex icon 55 | LTC/USDT 56 | Poloniex 57 | $ 348.54 58 | $ 7.07 million 59 | 11 60 | Poloniex icon 61 | SRM/USDT 62 | Poloniex 63 | $ 10.57 64 | $ 6.29 million 65 | 12 66 | Poloniex icon 67 | XMR/BTC 68 | Poloniex 69 | $ 480.96 70 | $ 6.00 million 71 | 13 72 | Poloniex icon 73 | LTC/BTC 74 | Poloniex 75 | $ 348.72 76 | $ 5.79 million 77 | 14 78 | Poloniex icon 79 | TRX/BTC 80 | Poloniex 81 | $ 0.1516 82 | $ 5.20 million 83 | 15 84 | Poloniex icon 85 | ETC/USDT 86 | Poloniex 87 | $ 84.56 88 | $ 5.08 million 89 | 16 90 | Poloniex icon 91 | ETH/USDC 92 | Poloniex 93 | $ 3,597.10 94 | $ 3.98 million 95 | 17 96 | Poloniex icon 97 | ATOM/BTC 98 | Poloniex 99 | $ 29.89 100 | $ 3.14 million 101 | 18 102 | Poloniex icon 103 | EOS/BTC 104 | Poloniex 105 | $ 10.59 106 | $ 2.78 million 107 | 19 108 | Poloniex icon 109 | XEM/BTC 110 | Poloniex 111 | $ 0.4056 112 | $ 2.76 million 113 | 20 114 | Poloniex icon 115 | EOS/USDT 116 | Poloniex 117 | $ 10.57 118 | $ 2.17 million 119 | 21 120 | Poloniex icon 121 | BCH/USDT 122 | Poloniex 123 | $ 1,436.32 124 | $ 2.15 million 125 | 22 126 | Poloniex icon 127 | ETC/BTC 128 | Poloniex 129 | $ 84.54 130 | $ 2.03 million 131 | 23 132 | Poloniex icon 133 | BCH/BTC 134 | Poloniex 135 | $ 1,439.00 136 | $ 1.95 million 137 | 24 138 | Poloniex icon 139 | XMR/USDT 140 | Poloniex 141 | $ 481.98 142 | $ 1.75 million 143 | 25 144 | Poloniex icon 145 | ATOM/USDC 146 | Poloniex 147 | $ 29.72 148 | $ 1.60 million 149 | 26 150 | Poloniex icon 151 | DOT/USDT 152 | Poloniex 153 | $ 40.41 154 | $ 1.57 million 155 | 27 156 | Poloniex icon 157 | ATOM/USDT 158 | Poloniex 159 | $ 29.74 160 | $ 1.50 million 161 | 28 162 | Poloniex icon 163 | BNB/USDT 164 | Poloniex 165 | $ 642.01 166 | $ 1.39 million 167 | 29 168 | Poloniex icon 169 | XLM/BTC 170 | Poloniex 171 | $ 0.6363 172 | $ 1.36 million 173 | 30 174 | Poloniex icon 175 | USDT/USDC 176 | Poloniex 177 | $ 1.01 178 | $ 1.18 million 179 | 31 180 | Poloniex icon 181 | USDJ/USDT 182 | Poloniex 183 | $ 1.09 184 | $ 1.14 million 185 | 32 186 | Poloniex icon 187 | DOGE/USDC 188 | Poloniex 189 | $ 0.7187 190 | $ 971,429.58 191 | 33 192 | Poloniex icon 193 | SC/USDT 194 | Poloniex 195 | $ 0.0423 196 | $ 952,106.77 197 | 34 198 | Poloniex icon 199 | EOS/USDC 200 | Poloniex 201 | $ 10.50 202 | $ 928,664.80 203 | 35 204 | Poloniex icon 205 | LSK/BTC 206 | Poloniex 207 | $ 8.91 208 | $ 859,547.21 209 | 36 210 | Poloniex icon 211 | BTT/USDT 212 | Poloniex 213 | $ 0.00792 214 | $ 856,592.96 215 | 37 216 | Poloniex icon 217 | XLM/USDT 218 | Poloniex 219 | $ 0.6368 220 | $ 847,826.92 221 | 38 222 | Poloniex icon 223 | DASH/BTC 224 | Poloniex 225 | $ 420.96 226 | $ 824,234.21 227 | 39 228 | Poloniex icon 229 | TUSD/USDT 230 | Poloniex 231 | $ 1.01 232 | $ 769,005.49 233 | 40 234 | Poloniex icon 235 | BCH/USDC 236 | Poloniex 237 | $ 1,434.83 238 | $ 717,741.90 239 | 41 240 | Poloniex icon 241 | AMP/USDT 242 | Poloniex 243 | $ 0.0738 244 | $ 693,140.91 245 | 42 246 | Poloniex icon 247 | XRP/USDC 248 | Poloniex 249 | $ 1.62 250 | $ 682,483.63 251 | 43 252 | Poloniex icon 253 | JST/USDT 254 | Poloniex 255 | $ 0.1455 256 | $ 667,452.11 257 | 44 258 | Poloniex icon 259 | DASH/USDT 260 | Poloniex 261 | $ 420.21 262 | $ 647,107.81 263 | 45 264 | Poloniex icon 265 | ETC/ETH 266 | Poloniex 267 | $ 82.75 268 | $ 626,853.08 269 | 46 270 | Poloniex icon 271 | ZEC/USDT 272 | Poloniex 273 | $ 313.67 274 | $ 581,176.88 275 | 47 276 | Poloniex icon 277 | KCS/USDT 278 | Poloniex 279 | $ 13.44 280 | $ 526,098.44 281 | 48 282 | Poloniex icon 283 | QTUM/BTC 284 | Poloniex 285 | $ 25.86 286 | $ 500,828.54 287 | 49 288 | Poloniex icon 289 | LSK/USDT 290 | Poloniex 291 | $ 9.04 292 | $ 499,157.04 293 | 50 294 | Poloniex icon 295 | SC/BTC 296 | Poloniex 297 | $ 0.0422 -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/optimisation/pairs_transform.py: -------------------------------------------------------------------------------- 1 | with open('/Users/liamhartley/PycharmProjects/cryptotradingbot/pairs_raw.txt') as f: 2 | pairs = [] 3 | lines = f.readlines() 4 | for line in lines: 5 | print(line) 6 | if '/' in line: 7 | pairs.append(line.replace('/','_')) 8 | 9 | print(pairs) -------------------------------------------------------------------------------- /trading_strategies/poloniex_cmo_trading_strategy/requirements.txt: -------------------------------------------------------------------------------- 1 | bokeh==2.3.0 2 | certifi==2020.12.5 3 | chardet==4.0.0 4 | cycler==0.10.0 5 | idna==2.10 6 | Jinja2==2.11.3 7 | kiwisolver==1.3.1 8 | MarkupSafe==1.1.1 9 | matplotlib==3.3.4 10 | numpy==1.19.5 11 | packaging==20.9 12 | pandas==1.1.5 13 | Pillow==8.1.1 14 | pyparsing==2.4.7 15 | python-dateutil==2.8.1 16 | pytz==2021.1 17 | PyYAML==5.4.1 18 | requests==2.25.1 19 | scipy==1.5.4 20 | six==1.15.0 21 | tornado==6.1 22 | typing-extensions==3.7.4.3 23 | urllib3==1.26.3 24 | pandasql==0.7.3 25 | boto3==1.17.75 26 | botocore==1.20.75 27 | jmespath==0.10.0 28 | s3transfer==0.4.2 29 | git+git://github.com/liamhartley/Gemini.git 30 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/README.md: -------------------------------------------------------------------------------- 1 | # XYZ Trading Strategy 2 | 3 | ## Contents 4 | 0. [Overview](#overview) 5 | 1. [Project Architecture](#projectarchitecture) 6 | 2. [Setup](#setup) 7 | 3. [Usage](#usage) 8 | 4. [How to Contribute](#howtocontribute) 9 | 5. [Further Reading](#furtherreading) 10 | 6. [Acknowledgements](#acknowledgements) 11 | 7. [Donations](#donations) 12 | 13 | 14 | ## Overview 15 | 16 | This solution allows you to run the strategy 24/7 on AWS. 17 | 18 | You can watch the entire development of this project on [this YouTube playlist](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=4). 19 | You can see this project being deployed in [this video](https://www.youtube.com/watch?v=ee0JCfeFw1o&list=PLobCEGRAX3hZ0KqKoZ1RTlYZF-VguIhtC&index=3). 20 | 21 | The trading strategy uses XXX 22 | 23 | 24 | 25 | 26 | ## Project Architecture 27 | 28 | 29 | [comment]: <> (example of how to reference an image in the repository ) 30 | 31 | --- 32 | 33 | I reccomend using draw.io for your architecture diagrams. 34 | 35 | #### Architecture Overview 36 | - X 37 | - Y 38 | - X 39 | 40 | 41 | --- 42 | 43 | 44 | ## Setup 45 | 46 | 47 | #### Pre-Requisites 48 | 49 | - [Python version 3.0.0 and above](https://www.python.org/downloads/) for backtesting and optimisation. 50 | - [Terraform version 0.13.0 and above](https://www.terraform.io/downloads.html) for cloud infrastructure deployment. 51 | - An [AWS](https://aws.amazon.com) account. 52 | - Packages inside the requirements.txt file. 53 | 54 | To install requirements file (inside project root): `pip install -r requirements.txt` 55 | 56 | To upgrade to the latest backtesting package: `pip install git+https://github.com/liamhartley/Gemini.git --upgrade` 57 | 58 | 59 | 60 | ## Usage 61 | # TODO continue proof read here 62 | #### Terraform AWS Deployment 63 | 64 | All of the AWS resources are managed by Terraform and it is best practice to make changes to the code and re-deploy instead of making changes in the GUI. 65 | 66 | - Create a terraform.tfvars file in the terraform directory to store your environment variables (defined in variables.tf in the Terraform folder) 67 | - Modify the filepaths in the "package_lambda.sh" script in /terraform for your machine 68 | - Navigate to the root directory of the project in the terminal 69 | - Run the "package_lambda.sh" script in /terraform 70 | - Run Terraform init in the terraform directory to download the required modules 71 | - Run Terraform plan in the Terraform directory to ensure that your plan is working as expected 72 | - Run Terraform apply in the Terraform directory to deploy the cloud infrastructure 73 | 74 | All logs will be outputted to Cloudwatch and the respective S3 bucket for debugging. 75 | 76 | #### Backtesting 77 | 78 | If you wish to re-run the backtesting/optimisation to identify which cryptocurrency pair you would like to trade then navigate to the backtesting.py script in the backtesting folder. 79 | Change the filepath of the csv output in the __main__ function to a local filepath and modify the config in the strategies root folder. 80 | Then run the backtesting with any permutation of the variables to build a dataset for optimisation. 81 | 82 | #### Optimisation 83 | 84 | Navigate to the analyse_optimisation.py script in the optimisation folder and change the filepath to be the same as the filepath in the optimsiation script. 85 | Then run any SQL query you like against this dataframe to identify the most suitable trading configuration. 86 | 87 | 88 | ## How to Contribute 89 | 90 | Branch off from the project to create a new feature and open a PR against master when complete. 91 | 92 | 93 | ## Further Reading 94 | - X 95 | - Y 96 | - Z 97 | 98 | 99 | ## Acknowledgements 100 | - X 101 | - Y 102 | - Z 103 | 104 | 105 | ## Donations 106 | 107 | If this project helped you learn Python or has helped you make some money then I would love any tips in Dogecoin! 108 | 109 | **My wallet address is**: DS7JRhMmL9RdXruqz5ubDaR3R8SNtoUi6i 110 | 111 | [Alternatively you can buy me a coffee!](https://www.buymeacoffee.com/liamhartley) 112 | 113 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/app/app.py: -------------------------------------------------------------------------------- 1 | # App code is your trading logic that is deployed into the AWS Lambda function 2 | # See other strategies for examples 3 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/backtesting/backtesting.py: -------------------------------------------------------------------------------- 1 | # Backtesting is your trading logic that backtests your strategy locally 2 | # See other strategies for examples 3 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/config.py: -------------------------------------------------------------------------------- 1 | # Example config file to define your parameters across backtesting, optimisation and deployment 2 | 3 | LOGICAL_PARAMS = { 4 | "PERIOD": 86400, # 1 day 5 | "PAIR": "BTC_XRP", 6 | "DRY_RUN": True, 7 | "INITIAL_CAPITAL": 100, # in quote currency 8 | "ENTRY_SIZE": 0.1 # 10% 9 | } 10 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/docs/README.md: -------------------------------------------------------------------------------- 1 | Space for additional documentation and diagrams. 2 | 3 | I use https://app.diagrams.net for my architecture diagrams. -------------------------------------------------------------------------------- /trading_strategies/template_strategy/docs/cmo_trading_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamhartley/cryptotradingbot/7c242f76169f4835ea541ff5bc409b7c32d6de52/trading_strategies/template_strategy/docs/cmo_trading_architecture.png -------------------------------------------------------------------------------- /trading_strategies/template_strategy/optimisation/analyse_optimisation.py: -------------------------------------------------------------------------------- 1 | # Optimisation scripts analyse the data generated in backtesting 2 | # See below for example on how to use SQL on a Pandas DataFrame 3 | 4 | # import pandas as pd 5 | # import pandasql as ps 6 | # 7 | # filepath = '/Users/liamhartley/PycharmProjects/cryptotradingbot/poloniex_cmo_trading_strategy/optimisation/initial_optimisation_results.csv' 8 | # 9 | # if __name__ == '__main__': 10 | # df = pd.read_csv(filepath, header=0) 11 | # optimised_df = ps.sqldf('select * from df order by net_profit') 12 | # print(df) 13 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/optimisation/crypto_pairs.py: -------------------------------------------------------------------------------- 1 | # Example list of pairs to loop into backtesting to generate data for optimsation 2 | 3 | PAIRS = [ 4 | 'BTC_USDT\n', 5 | 'DOGE_BTC\n', 6 | 'ETH_USDT\n', 7 | 'DOGE_USDT\n', 8 | 'ETH_BTC\n', 9 | 'XRP_USDT\n', 10 | 'TRX_USDT\n', 11 | 'BTC_USDC\n', 12 | 'XRP_BTC\n', 13 | 'LTC_USDT\n', 14 | 'SRM_USDT\n', 15 | 'XMR_BTC\n', 16 | 'LTC_BTC\n', 17 | 'TRX_BTC\n', 18 | 'ETC_USDT\n', 19 | 'ETH_USDC\n', 20 | 'ATOM_BTC\n', 21 | 'EOS_BTC\n', 22 | 'XEM_BTC\n', 23 | 'EOS_USDT\n', 24 | 'BCH_USDT\n', 25 | 'ETC_BTC\n', 26 | 'BCH_BTC\n', 27 | 'XMR_USDT\n', 28 | 'ATOM_USDC\n', 29 | 'DOT_USDT\n', 30 | 'ATOM_USDT\n', 31 | 'BNB_USDT\n', 32 | 'USDT_USDC\n', 33 | 'USDJ_USDT\n', 34 | 'DOGE_USDC\n', 35 | 'SC_USDT\n', 36 | 'EOS_USDC\n', 37 | 'LSK_BTC\n', 38 | 'BTT_USDT\n', 39 | 'DASH_BTC\n', 40 | 'TUSD_USDT\n', 41 | 'BCH_USDC\n', 42 | 'AMP_USDT\n', 43 | 'XRP_USDC\n', 44 | 'JST_USDT\n', 45 | 'DASH_USDT\n', 46 | 'ETC_ETH\n', 47 | 'ZEC_USDT\n', 48 | 'KCS_USDT\n', 49 | 'QTUM_BTC\n', 50 | 'LSK_USDT\n', 51 | 'SC_BTC\n' 52 | ] 53 | 54 | -------------------------------------------------------------------------------- /trading_strategies/template_strategy/optimisation/optimisation_results.csv: -------------------------------------------------------------------------------- 1 | pair,cmo_period,oversold_value,overbought_value,entry_size,starting_capital,final_equity,net_profit,max_drawdown,sharpe_ratio,sortino_ratio,alpha,beta,fees_paid,starting_capital,final_benchmark_equity,final_benchmark_profit,benchmark_max_drawdown,longs,sells,shorts,covers,total_trades 2 | -------------------------------------------------------------------------------- /trading_tools/coinbase_cmo_calculation.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | import datetime 4 | from trading_strategies.coinbase_cmo_trading_strategy.config import LOGICAL_PARAMS 5 | from trading_tools.coinbase_pro_wrapper.public_client import PublicClient 6 | 7 | 8 | def coinbase_cmo_logic_no_pandas(pair: str, period: int): 9 | ''' 10 | Calulcate CMO over a given period for a given pair and CMO period. 11 | :param pair: currency pair to trade 12 | :param period: time period, NOT CMO period 13 | :return: 14 | ''' 15 | datetime.datetime.now().isoformat() 16 | start_time = (datetime.datetime.now() - datetime.timedelta(seconds=LOGICAL_PARAMS['PERIOD']) - datetime.timedelta(seconds=LOGICAL_PARAMS['PERIOD'] * LOGICAL_PARAMS['CMO_PERIOD'])).isoformat() 17 | response_json = PublicClient().get_product_historic_rates(product_id=pair, granularity=period, start=start_time, end=datetime.datetime.now().isoformat()) 18 | 19 | print(f'historical data: {response_json}') 20 | 21 | higher_close_price = 0 22 | lower_close_price = 0 23 | 24 | previous_day = False 25 | 26 | for day in response_json: 27 | if previous_day is not False: 28 | if day[4] > previous_day[4]: 29 | lower_close_price += day[4] - previous_day[4] 30 | elif day[4] < previous_day[4]: 31 | higher_close_price += previous_day[4] - day[4] 32 | previous_day = day 33 | 34 | cmo = ((higher_close_price - lower_close_price) / (higher_close_price + lower_close_price)) * 100 35 | print(f'higher_close_price: {higher_close_price}') 36 | print(f'lower_close_price: {lower_close_price}') 37 | return cmo 38 | 39 | 40 | if __name__ == '__main__': 41 | coinbase_cmo_logic_no_pandas(pair='ETH-USDC', period=21600) 42 | -------------------------------------------------------------------------------- /trading_tools/coinbase_pro_wrapper/authenticated_client.py: -------------------------------------------------------------------------------- 1 | # 2 | # cbpro/AuthenticatedClient.py 3 | # Daniel Paquin 4 | # 5 | # For authenticated requests to the Coinbase exchange 6 | 7 | import hmac 8 | import hashlib 9 | import time 10 | import requests 11 | import base64 12 | import json 13 | from requests.auth import AuthBase 14 | from trading_tools.coinbase_pro_wrapper.public_client import PublicClient 15 | from trading_tools.coinbase_pro_wrapper.cbpro_auth import CBProAuth 16 | 17 | 18 | class AuthenticatedClient(PublicClient): 19 | """ Provides access to Private Endpoints on the cbpro API. 20 | 21 | All requests default to the live `api_url`: 'https://api.pro.coinbase.com'. 22 | To test your application using the sandbox modify the `api_url`. 23 | 24 | Attributes: 25 | url (str): The api url for this client instance to use. 26 | auth (CBProAuth): Custom authentication handler for each request. 27 | session (requests.Session): Persistent HTTP connection object. 28 | """ 29 | def __init__(self, key, b64secret, passphrase, 30 | api_url="https://api.pro.coinbase.com"): 31 | """ Create an instance of the AuthenticatedClient class. 32 | 33 | Args: 34 | key (str): Your API key. 35 | b64secret (str): The secret key matching your API key. 36 | passphrase (str): Passphrase chosen when setting up key. 37 | api_url (Optional[str]): API URL. Defaults to cbpro API. 38 | """ 39 | super(AuthenticatedClient, self).__init__(api_url) 40 | self.auth = CBProAuth(key, b64secret, passphrase) 41 | self.session = requests.Session() 42 | 43 | def get_account(self, account_id): 44 | """ Get information for a single account. 45 | 46 | Use this endpoint when you know the account_id. 47 | 48 | Args: 49 | account_id (str): Account id for account you want to get. 50 | 51 | Returns: 52 | dict: Account information. Example:: 53 | { 54 | "id": "a1b2c3d4", 55 | "balance": "1.100", 56 | "holds": "0.100", 57 | "available": "1.00", 58 | "currency": "USD" 59 | } 60 | """ 61 | return self._send_message('get', '/accounts/' + account_id) 62 | 63 | def get_accounts(self): 64 | """ Get a list of trading all accounts. 65 | 66 | When you place an order, the funds for the order are placed on 67 | hold. They cannot be used for other orders or withdrawn. Funds 68 | will remain on hold until the order is filled or canceled. The 69 | funds on hold for each account will be specified. 70 | 71 | Returns: 72 | list: Info about all accounts. Example:: 73 | [ 74 | { 75 | "id": "71452118-efc7-4cc4-8780-a5e22d4baa53", 76 | "currency": "BTC", 77 | "balance": "0.0000000000000000", 78 | "available": "0.0000000000000000", 79 | "hold": "0.0000000000000000", 80 | "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" 81 | }, 82 | { 83 | ... 84 | } 85 | ] 86 | 87 | * Additional info included in response for margin accounts. 88 | """ 89 | return self.get_account('') 90 | 91 | def get_account_history(self, account_id, **kwargs): 92 | """ List account activity. Account activity either increases or 93 | decreases your account balance. 94 | 95 | Entry type indicates the reason for the account change. 96 | * transfer: Funds moved to/from Coinbase to cbpro 97 | * match: Funds moved as a result of a trade 98 | * fee: Fee as a result of a trade 99 | * rebate: Fee rebate as per our fee schedule 100 | 101 | If an entry is the result of a trade (match, fee), the details 102 | field will contain additional information about the trade. 103 | 104 | Args: 105 | account_id (str): Account id to get history of. 106 | kwargs (dict): Additional HTTP request parameters. 107 | 108 | Returns: 109 | list: History information for the account. Example:: 110 | [ 111 | { 112 | "id": "100", 113 | "created_at": "2014-11-07T08:19:27.028459Z", 114 | "amount": "0.001", 115 | "balance": "239.669", 116 | "type": "fee", 117 | "details": { 118 | "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", 119 | "trade_id": "74", 120 | "product_id": "BTC-USD" 121 | } 122 | }, 123 | { 124 | ... 125 | } 126 | ] 127 | """ 128 | endpoint = '/accounts/{}/ledger'.format(account_id) 129 | return self._send_paginated_message(endpoint, params=kwargs) 130 | 131 | def get_account_holds(self, account_id, **kwargs): 132 | """ Get holds on an account. 133 | 134 | This method returns a generator which may make multiple HTTP requests 135 | while iterating through it. 136 | 137 | Holds are placed on an account for active orders or 138 | pending withdraw requests. 139 | 140 | As an order is filled, the hold amount is updated. If an order 141 | is canceled, any remaining hold is removed. For a withdraw, once 142 | it is completed, the hold is removed. 143 | 144 | The `type` field will indicate why the hold exists. The hold 145 | type is 'order' for holds related to open orders and 'transfer' 146 | for holds related to a withdraw. 147 | 148 | The `ref` field contains the id of the order or transfer which 149 | created the hold. 150 | 151 | Args: 152 | account_id (str): Account id to get holds of. 153 | kwargs (dict): Additional HTTP request parameters. 154 | 155 | Returns: 156 | generator(list): Hold information for the account. Example:: 157 | [ 158 | { 159 | "id": "82dcd140-c3c7-4507-8de4-2c529cd1a28f", 160 | "account_id": "e0b3f39a-183d-453e-b754-0c13e5bab0b3", 161 | "created_at": "2014-11-06T10:34:47.123456Z", 162 | "updated_at": "2014-11-06T10:40:47.123456Z", 163 | "amount": "4.23", 164 | "type": "order", 165 | "ref": "0a205de4-dd35-4370-a285-fe8fc375a273", 166 | }, 167 | { 168 | ... 169 | } 170 | ] 171 | 172 | """ 173 | endpoint = '/accounts/{}/holds'.format(account_id) 174 | return self._send_paginated_message(endpoint, params=kwargs) 175 | 176 | 177 | def convert_stablecoin(self, amount, from_currency, to_currency): 178 | """ Convert stablecoin. 179 | 180 | Args: 181 | amount (Decimal): The amount to convert. 182 | from_currency (str): Currency type (eg. 'USDC') 183 | to_currency (str): Currency type (eg. 'USD'). 184 | 185 | Returns: 186 | dict: Conversion details. Example:: 187 | { 188 | "id": "8942caee-f9d5-4600-a894-4811268545db", 189 | "amount": "10000.00", 190 | "from_account_id": "7849cc79-8b01-4793-9345-bc6b5f08acce", 191 | "to_account_id": "105c3e58-0898-4106-8283-dc5781cda07b", 192 | "from": "USD", 193 | "to": "USDC" 194 | } 195 | 196 | """ 197 | params = {'from': from_currency, 198 | 'to': to_currency, 199 | 'amount': amount} 200 | return self._send_message('post', '/conversions', data=json.dumps(params)) 201 | 202 | 203 | def place_order(self, product_id, side, order_type=None, **kwargs): 204 | """ Place an order. 205 | 206 | The three order types (limit, market, and stop) can be placed using this 207 | method. Specific methods are provided for each order type, but if a 208 | more generic interface is desired this method is available. 209 | 210 | Args: 211 | product_id (str): Product to order (eg. 'BTC-USD') 212 | side (str): Order side ('buy' or 'sell) 213 | order_type (str): Order type ('limit', or 'market') 214 | **client_oid (str): Order ID selected by you to identify your order. 215 | This should be a UUID, which will be broadcast in the public 216 | feed for `received` messages. 217 | **stp (str): Self-trade prevention flag. cbpro doesn't allow self- 218 | trading. This behavior can be modified with this flag. 219 | Options: 220 | 'dc' Decrease and Cancel (default) 221 | 'co' Cancel oldest 222 | 'cn' Cancel newest 223 | 'cb' Cancel both 224 | **overdraft_enabled (Optional[bool]): If true funding above and 225 | beyond the account balance will be provided by margin, as 226 | necessary. 227 | **funding_amount (Optional[Decimal]): Amount of margin funding to be 228 | provided for the order. Mutually exclusive with 229 | `overdraft_enabled`. 230 | **kwargs: Additional arguments can be specified for different order 231 | types. See the limit/market/stop order methods for details. 232 | 233 | Returns: 234 | dict: Order details. Example:: 235 | { 236 | "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", 237 | "price": "0.10000000", 238 | "size": "0.01000000", 239 | "product_id": "BTC-USD", 240 | "side": "buy", 241 | "stp": "dc", 242 | "type": "limit", 243 | "time_in_force": "GTC", 244 | "post_only": false, 245 | "created_at": "2016-12-08T20:02:28.53864Z", 246 | "fill_fees": "0.0000000000000000", 247 | "filled_size": "0.00000000", 248 | "executed_value": "0.0000000000000000", 249 | "status": "pending", 250 | "settled": false 251 | } 252 | 253 | """ 254 | # Margin parameter checks 255 | if kwargs.get('overdraft_enabled') is not None and \ 256 | kwargs.get('funding_amount') is not None: 257 | raise ValueError('Margin funding must be specified through use of ' 258 | 'overdraft or by setting a funding amount, but not' 259 | ' both') 260 | 261 | # Limit order checks 262 | if order_type == 'limit': 263 | if kwargs.get('cancel_after') is not None and \ 264 | kwargs.get('time_in_force') != 'GTT': 265 | raise ValueError('May only specify a cancel period when time ' 266 | 'in_force is `GTT`') 267 | if kwargs.get('post_only') is not None and kwargs.get('time_in_force') in \ 268 | ['IOC', 'FOK']: 269 | raise ValueError('post_only is invalid when time in force is ' 270 | '`IOC` or `FOK`') 271 | 272 | # Market and stop order checks 273 | if order_type == 'market' or kwargs.get('stop'): 274 | if not (kwargs.get('size') is None) ^ (kwargs.get('funds') is None): 275 | raise ValueError('Either `size` or `funds` must be specified ' 276 | 'for market/stop orders (but not both).') 277 | 278 | # Build params dict 279 | params = {'product_id': product_id, 280 | 'side': side, 281 | 'type': order_type} 282 | params.update(kwargs) 283 | return self._send_message('post', '/orders', data=json.dumps(params)) 284 | 285 | def buy(self, product_id, order_type, **kwargs): 286 | """Place a buy order. 287 | 288 | This is included to maintain backwards compatibility with older versions 289 | of cbpro-Python. For maximum support from docstrings and function 290 | signatures see the order type-specific functions place_limit_order, 291 | place_market_order, and place_stop_order. 292 | 293 | Args: 294 | product_id (str): Product to order (eg. 'BTC-USD') 295 | order_type (str): Order type ('limit', 'market', or 'stop') 296 | **kwargs: Additional arguments can be specified for different order 297 | types. 298 | 299 | Returns: 300 | dict: Order details. See `place_order` for example. 301 | 302 | """ 303 | return self.place_order(product_id, 'buy', order_type, **kwargs) 304 | 305 | def sell(self, product_id, order_type, **kwargs): 306 | """Place a sell order. 307 | 308 | This is included to maintain backwards compatibility with older versions 309 | of cbpro-Python. For maximum support from docstrings and function 310 | signatures see the order type-specific functions place_limit_order, 311 | place_market_order, and place_stop_order. 312 | 313 | Args: 314 | product_id (str): Product to order (eg. 'BTC-USD') 315 | order_type (str): Order type ('limit', 'market', or 'stop') 316 | **kwargs: Additional arguments can be specified for different order 317 | types. 318 | 319 | Returns: 320 | dict: Order details. See `place_order` for example. 321 | 322 | """ 323 | return self.place_order(product_id, 'sell', order_type, **kwargs) 324 | 325 | def place_limit_order(self, product_id, side, price, size, 326 | client_oid=None, 327 | stp=None, 328 | time_in_force=None, 329 | cancel_after=None, 330 | post_only=None, 331 | overdraft_enabled=None, 332 | funding_amount=None): 333 | """Place a limit order. 334 | 335 | Args: 336 | product_id (str): Product to order (eg. 'BTC-USD') 337 | side (str): Order side ('buy' or 'sell) 338 | price (Decimal): Price per cryptocurrency 339 | size (Decimal): Amount of cryptocurrency to buy or sell 340 | client_oid (Optional[str]): User-specified Order ID 341 | stp (Optional[str]): Self-trade prevention flag. See `place_order` 342 | for details. 343 | time_in_force (Optional[str]): Time in force. Options: 344 | 'GTC' Good till canceled 345 | 'GTT' Good till time (set by `cancel_after`) 346 | 'IOC' Immediate or cancel 347 | 'FOK' Fill or kill 348 | cancel_after (Optional[str]): Cancel after this period for 'GTT' 349 | orders. Options are 'min', 'hour', or 'day'. 350 | post_only (Optional[bool]): Indicates that the order should only 351 | make liquidity. If any part of the order results in taking 352 | liquidity, the order will be rejected and no part of it will 353 | execute. 354 | overdraft_enabled (Optional[bool]): If true funding above and 355 | beyond the account balance will be provided by margin, as 356 | necessary. 357 | funding_amount (Optional[Decimal]): Amount of margin funding to be 358 | provided for the order. Mutually exclusive with 359 | `overdraft_enabled`. 360 | 361 | Returns: 362 | dict: Order details. See `place_order` for example. 363 | 364 | """ 365 | params = {'product_id': product_id, 366 | 'side': side, 367 | 'order_type': 'limit', 368 | 'price': price, 369 | 'size': size, 370 | 'client_oid': client_oid, 371 | 'stp': stp, 372 | 'time_in_force': time_in_force, 373 | 'cancel_after': cancel_after, 374 | 'post_only': post_only, 375 | 'overdraft_enabled': overdraft_enabled, 376 | 'funding_amount': funding_amount} 377 | params = dict((k, v) for k, v in params.items() if v is not None) 378 | 379 | return self.place_order(**params) 380 | 381 | def place_market_order(self, product_id, side, size=None, funds=None, 382 | client_oid=None, 383 | stp=None, 384 | overdraft_enabled=None, 385 | funding_amount=None): 386 | """ Place market order. 387 | 388 | Args: 389 | product_id (str): Product to order (eg. 'BTC-USD') 390 | side (str): Order side ('buy' or 'sell) 391 | size (Optional[Decimal]): Desired amount in crypto. Specify this or 392 | `funds`. 393 | funds (Optional[Decimal]): Desired amount of quote currency to use. 394 | Specify this or `size`. 395 | client_oid (Optional[str]): User-specified Order ID 396 | stp (Optional[str]): Self-trade prevention flag. See `place_order` 397 | for details. 398 | overdraft_enabled (Optional[bool]): If true funding above and 399 | beyond the account balance will be provided by margin, as 400 | necessary. 401 | funding_amount (Optional[Decimal]): Amount of margin funding to be 402 | provided for the order. Mutually exclusive with 403 | `overdraft_enabled`. 404 | 405 | Returns: 406 | dict: Order details. See `place_order` for example. 407 | 408 | """ 409 | params = {'product_id': product_id, 410 | 'side': side, 411 | 'order_type': 'market', 412 | 'size': size, 413 | 'funds': funds, 414 | 'client_oid': client_oid, 415 | 'stp': stp, 416 | 'overdraft_enabled': overdraft_enabled, 417 | 'funding_amount': funding_amount} 418 | params = dict((k, v) for k, v in params.items() if v is not None) 419 | 420 | return self.place_order(**params) 421 | 422 | def place_stop_order(self, product_id, stop_type, price, size=None, funds=None, 423 | client_oid=None, 424 | stp=None, 425 | overdraft_enabled=None, 426 | funding_amount=None): 427 | """ Place stop order. 428 | 429 | Args: 430 | product_id (str): Product to order (eg. 'BTC-USD') 431 | stop_type(str): Stop type ('entry' or 'loss') 432 | loss: Triggers when the last trade price changes to a value at or below the stop_price. 433 | entry: Triggers when the last trade price changes to a value at or above the stop_price 434 | price (Decimal): Desired price at which the stop order triggers. 435 | size (Optional[Decimal]): Desired amount in crypto. Specify this or 436 | `funds`. 437 | funds (Optional[Decimal]): Desired amount of quote currency to use. 438 | Specify this or `size`. 439 | client_oid (Optional[str]): User-specified Order ID 440 | stp (Optional[str]): Self-trade prevention flag. See `place_order` 441 | for details. 442 | overdraft_enabled (Optional[bool]): If true funding above and 443 | beyond the account balance will be provided by margin, as 444 | necessary. 445 | funding_amount (Optional[Decimal]): Amount of margin funding to be 446 | provided for the order. Mutually exclusive with 447 | `overdraft_enabled`. 448 | 449 | Returns: 450 | dict: Order details. See `place_order` for example. 451 | 452 | """ 453 | 454 | if stop_type == 'loss': 455 | side = 'sell' 456 | elif stop_type == 'entry': 457 | side = 'buy' 458 | else: 459 | raise ValueError('Invalid stop_type for stop order: ' + stop_type) 460 | 461 | params = {'product_id': product_id, 462 | 'side': side, 463 | 'price': price, 464 | 'order_type': None, 465 | 'stop': stop_type, 466 | 'stop_price': price, 467 | 'size': size, 468 | 'funds': funds, 469 | 'client_oid': client_oid, 470 | 'stp': stp, 471 | 'overdraft_enabled': overdraft_enabled, 472 | 'funding_amount': funding_amount} 473 | params = dict((k, v) for k, v in params.items() if v is not None) 474 | 475 | return self.place_order(**params) 476 | 477 | def cancel_order(self, order_id): 478 | """ Cancel a previously placed order. 479 | 480 | If the order had no matches during its lifetime its record may 481 | be purged. This means the order details will not be available 482 | with get_order(order_id). If the order could not be canceled 483 | (already filled or previously canceled, etc), then an error 484 | response will indicate the reason in the message field. 485 | 486 | **Caution**: The order id is the server-assigned order id and 487 | not the optional client_oid. 488 | 489 | Args: 490 | order_id (str): The order_id of the order you want to cancel 491 | 492 | Returns: 493 | list: Containing the order_id of cancelled order. Example:: 494 | [ "c5ab5eae-76be-480e-8961-00792dc7e138" ] 495 | 496 | """ 497 | return self._send_message('delete', '/orders/' + order_id) 498 | 499 | def cancel_all(self, product_id=None): 500 | """ With best effort, cancel all open orders. 501 | 502 | Args: 503 | product_id (Optional[str]): Only cancel orders for this 504 | product_id 505 | 506 | Returns: 507 | list: A list of ids of the canceled orders. Example:: 508 | [ 509 | "144c6f8e-713f-4682-8435-5280fbe8b2b4", 510 | "debe4907-95dc-442f-af3b-cec12f42ebda", 511 | "cf7aceee-7b08-4227-a76c-3858144323ab", 512 | "dfc5ae27-cadb-4c0c-beef-8994936fde8a", 513 | "34fecfbf-de33-4273-b2c6-baf8e8948be4" 514 | ] 515 | 516 | """ 517 | if product_id is not None: 518 | params = {'product_id': product_id} 519 | else: 520 | params = None 521 | return self._send_message('delete', '/orders', params=params) 522 | 523 | def get_order(self, order_id): 524 | """ Get a single order by order id. 525 | 526 | If the order is canceled the response may have status code 404 527 | if the order had no matches. 528 | 529 | **Caution**: Open orders may change state between the request 530 | and the response depending on market conditions. 531 | 532 | Args: 533 | order_id (str): The order to get information of. 534 | 535 | Returns: 536 | dict: Containing information on order. Example:: 537 | { 538 | "created_at": "2017-06-18T00:27:42.920136Z", 539 | "executed_value": "0.0000000000000000", 540 | "fill_fees": "0.0000000000000000", 541 | "filled_size": "0.00000000", 542 | "id": "9456f388-67a9-4316-bad1-330c5353804f", 543 | "post_only": true, 544 | "price": "1.00000000", 545 | "product_id": "BTC-USD", 546 | "settled": false, 547 | "side": "buy", 548 | "size": "1.00000000", 549 | "status": "pending", 550 | "stp": "dc", 551 | "time_in_force": "GTC", 552 | "type": "limit" 553 | } 554 | 555 | """ 556 | return self._send_message('get', '/orders/' + order_id) 557 | 558 | def get_orders(self, product_id=None, status=None, **kwargs): 559 | """ List your current open orders. 560 | 561 | This method returns a generator which may make multiple HTTP requests 562 | while iterating through it. 563 | 564 | Only open or un-settled orders are returned. As soon as an 565 | order is no longer open and settled, it will no longer appear 566 | in the default request. 567 | 568 | Orders which are no longer resting on the order book, will be 569 | marked with the 'done' status. There is a small window between 570 | an order being 'done' and 'settled'. An order is 'settled' when 571 | all of the fills have settled and the remaining holds (if any) 572 | have been removed. 573 | 574 | For high-volume trading it is strongly recommended that you 575 | maintain your own list of open orders and use one of the 576 | streaming market data feeds to keep it updated. You should poll 577 | the open orders endpoint once when you start trading to obtain 578 | the current state of any open orders. 579 | 580 | Args: 581 | product_id (Optional[str]): Only list orders for this 582 | product 583 | status (Optional[list/str]): Limit list of orders to 584 | this status or statuses. Passing 'all' returns orders 585 | of all statuses. 586 | ** Options: 'open', 'pending', 'active', 'done', 587 | 'settled' 588 | ** default: ['open', 'pending', 'active'] 589 | 590 | Returns: 591 | list: Containing information on orders. Example:: 592 | [ 593 | { 594 | "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", 595 | "price": "0.10000000", 596 | "size": "0.01000000", 597 | "product_id": "BTC-USD", 598 | "side": "buy", 599 | "stp": "dc", 600 | "type": "limit", 601 | "time_in_force": "GTC", 602 | "post_only": false, 603 | "created_at": "2016-12-08T20:02:28.53864Z", 604 | "fill_fees": "0.0000000000000000", 605 | "filled_size": "0.00000000", 606 | "executed_value": "0.0000000000000000", 607 | "status": "open", 608 | "settled": false 609 | }, 610 | { 611 | ... 612 | } 613 | ] 614 | 615 | """ 616 | params = kwargs 617 | if product_id is not None: 618 | params['product_id'] = product_id 619 | if status is not None: 620 | params['status'] = status 621 | return self._send_paginated_message('/orders', params=params) 622 | 623 | def get_fills(self, product_id=None, order_id=None, **kwargs): 624 | """ Get a list of recent fills. 625 | 626 | As of 8/23/18 - Requests without either order_id or product_id 627 | will be rejected 628 | 629 | This method returns a generator which may make multiple HTTP requests 630 | while iterating through it. 631 | 632 | Fees are recorded in two stages. Immediately after the matching 633 | engine completes a match, the fill is inserted into our 634 | datastore. Once the fill is recorded, a settlement process will 635 | settle the fill and credit both trading counterparties. 636 | 637 | The 'fee' field indicates the fees charged for this fill. 638 | 639 | The 'liquidity' field indicates if the fill was the result of a 640 | liquidity provider or liquidity taker. M indicates Maker and T 641 | indicates Taker. 642 | 643 | Args: 644 | product_id (str): Limit list to this product_id 645 | order_id (str): Limit list to this order_id 646 | kwargs (dict): Additional HTTP request parameters. 647 | 648 | Returns: 649 | list: Containing information on fills. Example:: 650 | [ 651 | { 652 | "trade_id": 74, 653 | "product_id": "BTC-USD", 654 | "price": "10.00", 655 | "size": "0.01", 656 | "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", 657 | "created_at": "2014-11-07T22:19:28.578544Z", 658 | "liquidity": "T", 659 | "fee": "0.00025", 660 | "settled": true, 661 | "side": "buy" 662 | }, 663 | { 664 | ... 665 | } 666 | ] 667 | 668 | """ 669 | if (product_id is None) and (order_id is None): 670 | raise ValueError('Either product_id or order_id must be specified.') 671 | 672 | params = {} 673 | if product_id: 674 | params['product_id'] = product_id 675 | if order_id: 676 | params['order_id'] = order_id 677 | params.update(kwargs) 678 | 679 | return self._send_paginated_message('/fills', params=params) 680 | 681 | def get_fundings(self, status=None, **kwargs): 682 | """ Every order placed with a margin profile that draws funding 683 | will create a funding record. 684 | 685 | This method returns a generator which may make multiple HTTP requests 686 | while iterating through it. 687 | 688 | Args: 689 | status (list/str): Limit funding records to these statuses. 690 | ** Options: 'outstanding', 'settled', 'rejected' 691 | kwargs (dict): Additional HTTP request parameters. 692 | 693 | Returns: 694 | list: Containing information on margin funding. Example:: 695 | [ 696 | { 697 | "id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", 698 | "order_id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", 699 | "profile_id": "d881e5a6-58eb-47cd-b8e2-8d9f2e3ec6f6", 700 | "amount": "1057.6519956381537500", 701 | "status": "settled", 702 | "created_at": "2017-03-17T23:46:16.663397Z", 703 | "currency": "USD", 704 | "repaid_amount": "1057.6519956381537500", 705 | "default_amount": "0", 706 | "repaid_default": false 707 | }, 708 | { 709 | ... 710 | } 711 | ] 712 | 713 | """ 714 | params = {} 715 | if status is not None: 716 | params['status'] = status 717 | params.update(kwargs) 718 | return self._send_paginated_message('/funding', params=params) 719 | 720 | def repay_funding(self, amount, currency): 721 | """ Repay funding. Repays the older funding records first. 722 | 723 | Args: 724 | amount (int): Amount of currency to repay 725 | currency (str): The currency, example USD 726 | 727 | Returns: 728 | Not specified by cbpro. 729 | 730 | """ 731 | params = { 732 | 'amount': amount, 733 | 'currency': currency # example: USD 734 | } 735 | return self._send_message('post', '/funding/repay', 736 | data=json.dumps(params)) 737 | 738 | def margin_transfer(self, margin_profile_id, transfer_type, currency, 739 | amount): 740 | """ Transfer funds between your standard profile and a margin profile. 741 | 742 | Args: 743 | margin_profile_id (str): Margin profile ID to withdraw or deposit 744 | from. 745 | transfer_type (str): 'deposit' or 'withdraw' 746 | currency (str): Currency to transfer (eg. 'USD') 747 | amount (Decimal): Amount to transfer 748 | 749 | Returns: 750 | dict: Transfer details. Example:: 751 | { 752 | "created_at": "2017-01-25T19:06:23.415126Z", 753 | "id": "80bc6b74-8b1f-4c60-a089-c61f9810d4ab", 754 | "user_id": "521c20b3d4ab09621f000011", 755 | "profile_id": "cda95996-ac59-45a3-a42e-30daeb061867", 756 | "margin_profile_id": "45fa9e3b-00ba-4631-b907-8a98cbdf21be", 757 | "type": "deposit", 758 | "amount": "2", 759 | "currency": "USD", 760 | "account_id": "23035fc7-0707-4b59-b0d2-95d0c035f8f5", 761 | "margin_account_id": "e1d9862c-a259-4e83-96cd-376352a9d24d", 762 | "margin_product_id": "BTC-USD", 763 | "status": "completed", 764 | "nonce": 25 765 | } 766 | 767 | """ 768 | params = {'margin_profile_id': margin_profile_id, 769 | 'type': transfer_type, 770 | 'currency': currency, # example: USD 771 | 'amount': amount} 772 | return self._send_message('post', '/profiles/margin-transfer', 773 | data=json.dumps(params)) 774 | 775 | def get_position(self): 776 | """ Get An overview of your margin profile. 777 | 778 | Returns: 779 | dict: Details about funding, accounts, and margin call. 780 | 781 | """ 782 | return self._send_message('get', '/position') 783 | 784 | def close_position(self, repay_only): 785 | """ Close position. 786 | 787 | Args: 788 | repay_only (bool): Undocumented by cbpro. 789 | 790 | Returns: 791 | Undocumented 792 | 793 | """ 794 | params = {'repay_only': repay_only} 795 | return self._send_message('post', '/position/close', 796 | data=json.dumps(params)) 797 | 798 | def deposit(self, amount, currency, payment_method_id): 799 | """ Deposit funds from a payment method. 800 | 801 | See AuthenticatedClient.get_payment_methods() to receive 802 | information regarding payment methods. 803 | 804 | Args: 805 | amount (Decmial): The amount to deposit. 806 | currency (str): The type of currency. 807 | payment_method_id (str): ID of the payment method. 808 | 809 | Returns: 810 | dict: Information about the deposit. Example:: 811 | { 812 | "id": "593533d2-ff31-46e0-b22e-ca754147a96a", 813 | "amount": "10.00", 814 | "currency": "USD", 815 | "payout_at": "2016-08-20T00:31:09Z" 816 | } 817 | 818 | """ 819 | params = {'amount': amount, 820 | 'currency': currency, 821 | 'payment_method_id': payment_method_id} 822 | return self._send_message('post', '/deposits/payment-method', 823 | data=json.dumps(params)) 824 | 825 | def coinbase_deposit(self, amount, currency, coinbase_account_id): 826 | """ Deposit funds from a coinbase account. 827 | 828 | You can move funds between your Coinbase accounts and your cbpro 829 | trading accounts within your daily limits. Moving funds between 830 | Coinbase and cbpro is instant and free. 831 | 832 | See AuthenticatedClient.get_coinbase_accounts() to receive 833 | information regarding your coinbase_accounts. 834 | 835 | Args: 836 | amount (Decimal): The amount to deposit. 837 | currency (str): The type of currency. 838 | coinbase_account_id (str): ID of the coinbase account. 839 | 840 | Returns: 841 | dict: Information about the deposit. Example:: 842 | { 843 | "id": "593533d2-ff31-46e0-b22e-ca754147a96a", 844 | "amount": "10.00", 845 | "currency": "BTC", 846 | } 847 | 848 | """ 849 | params = {'amount': amount, 850 | 'currency': currency, 851 | 'coinbase_account_id': coinbase_account_id} 852 | return self._send_message('post', '/deposits/coinbase-account', 853 | data=json.dumps(params)) 854 | 855 | def withdraw(self, amount, currency, payment_method_id): 856 | """ Withdraw funds to a payment method. 857 | 858 | See AuthenticatedClient.get_payment_methods() to receive 859 | information regarding payment methods. 860 | 861 | Args: 862 | amount (Decimal): The amount to withdraw. 863 | currency (str): Currency type (eg. 'BTC') 864 | payment_method_id (str): ID of the payment method. 865 | 866 | Returns: 867 | dict: Withdraw details. Example:: 868 | { 869 | "id":"593533d2-ff31-46e0-b22e-ca754147a96a", 870 | "amount": "10.00", 871 | "currency": "USD", 872 | "payout_at": "2016-08-20T00:31:09Z" 873 | } 874 | 875 | """ 876 | params = {'amount': amount, 877 | 'currency': currency, 878 | 'payment_method_id': payment_method_id} 879 | return self._send_message('post', '/withdrawals/payment-method', 880 | data=json.dumps(params)) 881 | 882 | def coinbase_withdraw(self, amount, currency, coinbase_account_id): 883 | """ Withdraw funds to a coinbase account. 884 | 885 | You can move funds between your Coinbase accounts and your cbpro 886 | trading accounts within your daily limits. Moving funds between 887 | Coinbase and cbpro is instant and free. 888 | 889 | See AuthenticatedClient.get_coinbase_accounts() to receive 890 | information regarding your coinbase_accounts. 891 | 892 | Args: 893 | amount (Decimal): The amount to withdraw. 894 | currency (str): The type of currency (eg. 'BTC') 895 | coinbase_account_id (str): ID of the coinbase account. 896 | 897 | Returns: 898 | dict: Information about the deposit. Example:: 899 | { 900 | "id":"593533d2-ff31-46e0-b22e-ca754147a96a", 901 | "amount":"10.00", 902 | "currency": "BTC", 903 | } 904 | 905 | """ 906 | params = {'amount': amount, 907 | 'currency': currency, 908 | 'coinbase_account_id': coinbase_account_id} 909 | return self._send_message('post', '/withdrawals/coinbase-account', 910 | data=json.dumps(params)) 911 | 912 | def crypto_withdraw(self, amount, currency, crypto_address): 913 | """ Withdraw funds to a crypto address. 914 | 915 | Args: 916 | amount (Decimal): The amount to withdraw 917 | currency (str): The type of currency (eg. 'BTC') 918 | crypto_address (str): Crypto address to withdraw to. 919 | 920 | Returns: 921 | dict: Withdraw details. Example:: 922 | { 923 | "id":"593533d2-ff31-46e0-b22e-ca754147a96a", 924 | "amount":"10.00", 925 | "currency": "BTC", 926 | } 927 | 928 | """ 929 | params = {'amount': amount, 930 | 'currency': currency, 931 | 'crypto_address': crypto_address} 932 | return self._send_message('post', '/withdrawals/crypto', 933 | data=json.dumps(params)) 934 | 935 | def get_payment_methods(self): 936 | """ Get a list of your payment methods. 937 | 938 | Returns: 939 | list: Payment method details. 940 | 941 | """ 942 | return self._send_message('get', '/payment-methods') 943 | 944 | def get_coinbase_accounts(self): 945 | """ Get a list of your coinbase accounts. 946 | 947 | Returns: 948 | list: Coinbase account details. 949 | 950 | """ 951 | return self._send_message('get', '/coinbase-accounts') 952 | 953 | def create_report(self, report_type, start_date, end_date, product_id=None, 954 | account_id=None, report_format='pdf', email=None): 955 | """ Create report of historic information about your account. 956 | 957 | The report will be generated when resources are available. Report status 958 | can be queried via `get_report(report_id)`. 959 | 960 | Args: 961 | report_type (str): 'fills' or 'account' 962 | start_date (str): Starting date for the report in ISO 8601 963 | end_date (str): Ending date for the report in ISO 8601 964 | product_id (Optional[str]): ID of the product to generate a fills 965 | report for. Required if account_type is 'fills' 966 | account_id (Optional[str]): ID of the account to generate an account 967 | report for. Required if report_type is 'account'. 968 | report_format (Optional[str]): 'pdf' or 'csv'. Default is 'pdf'. 969 | email (Optional[str]): Email address to send the report to. 970 | 971 | Returns: 972 | dict: Report details. Example:: 973 | { 974 | "id": "0428b97b-bec1-429e-a94c-59232926778d", 975 | "type": "fills", 976 | "status": "pending", 977 | "created_at": "2015-01-06T10:34:47.000Z", 978 | "completed_at": undefined, 979 | "expires_at": "2015-01-13T10:35:47.000Z", 980 | "file_url": undefined, 981 | "params": { 982 | "start_date": "2014-11-01T00:00:00.000Z", 983 | "end_date": "2014-11-30T23:59:59.000Z" 984 | } 985 | } 986 | 987 | """ 988 | params = {'type': report_type, 989 | 'start_date': start_date, 990 | 'end_date': end_date, 991 | 'format': report_format} 992 | if product_id is not None: 993 | params['product_id'] = product_id 994 | if account_id is not None: 995 | params['account_id'] = account_id 996 | if email is not None: 997 | params['email'] = email 998 | 999 | return self._send_message('post', '/reports', 1000 | data=json.dumps(params)) 1001 | 1002 | def get_report(self, report_id): 1003 | """ Get report status. 1004 | 1005 | Use to query a specific report once it has been requested. 1006 | 1007 | Args: 1008 | report_id (str): Report ID 1009 | 1010 | Returns: 1011 | dict: Report details, including file url once it is created. 1012 | 1013 | """ 1014 | return self._send_message('get', '/reports/' + report_id) 1015 | 1016 | def get_trailing_volume(self): 1017 | """ Get your 30-day trailing volume for all products. 1018 | 1019 | This is a cached value that's calculated every day at midnight UTC. 1020 | 1021 | Returns: 1022 | list: 30-day trailing volumes. Example:: 1023 | [ 1024 | { 1025 | "product_id": "BTC-USD", 1026 | "exchange_volume": "11800.00000000", 1027 | "volume": "100.00000000", 1028 | "recorded_at": "1973-11-29T00:05:01.123456Z" 1029 | }, 1030 | { 1031 | ... 1032 | } 1033 | ] 1034 | 1035 | """ 1036 | return self._send_message('get', '/users/self/trailing-volume') 1037 | 1038 | def get_fees(self): 1039 | """ Get your maker & taker fee rates and 30-day trailing volume. 1040 | 1041 | Returns: 1042 | dict: Fee information and USD volume:: 1043 | { 1044 | "maker_fee_rate": "0.0015", 1045 | "taker_fee_rate": "0.0025", 1046 | "usd_volume": "25000.00" 1047 | } 1048 | """ 1049 | return self._send_message('get', '/fees') 1050 | -------------------------------------------------------------------------------- /trading_tools/coinbase_pro_wrapper/cbpro_auth.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import time 4 | import base64 5 | from requests.auth import AuthBase 6 | 7 | 8 | class CBProAuth(AuthBase): 9 | # Provided by CBPro: https://docs.pro.coinbase.com/#signing-a-message 10 | def __init__(self, api_key, secret_key, passphrase): 11 | self.api_key = api_key 12 | self.secret_key = secret_key 13 | self.passphrase = passphrase 14 | 15 | def __call__(self, request): 16 | timestamp = str(time.time()) 17 | message = ''.join([timestamp, request.method, 18 | request.path_url, (request.body or '')]) 19 | request.headers.update(get_auth_headers(timestamp, message, 20 | self.api_key, 21 | self.secret_key, 22 | self.passphrase)) 23 | return request 24 | 25 | 26 | def get_auth_headers(timestamp, message, api_key, secret_key, passphrase): 27 | message = message.encode('ascii') 28 | hmac_key = base64.b64decode(secret_key) 29 | signature = hmac.new(hmac_key, message, hashlib.sha256) 30 | signature_b64 = base64.b64encode(signature.digest()).decode('utf-8') 31 | return { 32 | 'Content-Type': 'Application/JSON', 33 | 'CB-ACCESS-SIGN': signature_b64, 34 | 'CB-ACCESS-TIMESTAMP': timestamp, 35 | 'CB-ACCESS-KEY': api_key, 36 | 'CB-ACCESS-PASSPHRASE': passphrase 37 | } 38 | -------------------------------------------------------------------------------- /trading_tools/coinbase_pro_wrapper/credits.md: -------------------------------------------------------------------------------- 1 | All code leveraged from this amazing repository https://github.com/danpaquin/coinbasepro-python -------------------------------------------------------------------------------- /trading_tools/coinbase_pro_wrapper/public_client.py: -------------------------------------------------------------------------------- 1 | # 2 | # cbpro/PublicClient.py 3 | # Daniel Paquin 4 | # 5 | # For public requests to the Coinbase exchange 6 | 7 | import requests 8 | 9 | 10 | class PublicClient(object): 11 | """cbpro public client API. 12 | 13 | All requests default to the `product_id` specified at object 14 | creation if not otherwise specified. 15 | 16 | Attributes: 17 | url (Optional[str]): API URL. Defaults to cbpro API. 18 | 19 | """ 20 | 21 | def __init__(self, api_url='https://api.pro.coinbase.com', timeout=30): 22 | """Create cbpro API public client. 23 | 24 | Args: 25 | api_url (Optional[str]): API URL. Defaults to cbpro API. 26 | 27 | """ 28 | self.url = api_url.rstrip('/') 29 | self.auth = None 30 | self.session = requests.Session() 31 | 32 | def get_products(self): 33 | """Get a list of available currency pairs for trading. 34 | 35 | Returns: 36 | list: Info about all currency pairs. Example:: 37 | [ 38 | { 39 | "id": "BTC-USD", 40 | "display_name": "BTC/USD", 41 | "base_currency": "BTC", 42 | "quote_currency": "USD", 43 | "base_min_size": "0.01", 44 | "base_max_size": "10000.00", 45 | "quote_increment": "0.01" 46 | } 47 | ] 48 | 49 | """ 50 | return self._send_message('get', '/products') 51 | 52 | def get_product_order_book(self, product_id, level=1): 53 | """Get a list of open orders for a product. 54 | 55 | The amount of detail shown can be customized with the `level` 56 | parameter: 57 | * 1: Only the best bid and ask 58 | * 2: Top 50 bids and asks (aggregated) 59 | * 3: Full order book (non aggregated) 60 | 61 | Level 1 and Level 2 are recommended for polling. For the most 62 | up-to-date data, consider using the websocket stream. 63 | 64 | **Caution**: Level 3 is only recommended for users wishing to 65 | maintain a full real-time order book using the websocket 66 | stream. Abuse of Level 3 via polling will cause your access to 67 | be limited or blocked. 68 | 69 | Args: 70 | product_id (str): Product 71 | level (Optional[int]): Order book level (1, 2, or 3). 72 | Default is 1. 73 | 74 | Returns: 75 | dict: Order book. Example for level 1:: 76 | { 77 | "sequence": "3", 78 | "bids": [ 79 | [ price, size, num-orders ], 80 | ], 81 | "asks": [ 82 | [ price, size, num-orders ], 83 | ] 84 | } 85 | 86 | """ 87 | params = {'level': level} 88 | return self._send_message('get', 89 | '/products/{}/book'.format(product_id), 90 | params=params) 91 | 92 | def get_product_ticker(self, product_id): 93 | """Snapshot about the last trade (tick), best bid/ask and 24h volume. 94 | 95 | **Caution**: Polling is discouraged in favor of connecting via 96 | the websocket stream and listening for match messages. 97 | 98 | Args: 99 | product_id (str): Product 100 | 101 | Returns: 102 | dict: Ticker info. Example:: 103 | { 104 | "trade_id": 4729088, 105 | "price": "333.99", 106 | "size": "0.193", 107 | "bid": "333.98", 108 | "ask": "333.99", 109 | "volume": "5957.11914015", 110 | "time": "2015-11-14T20:46:03.511254Z" 111 | } 112 | 113 | """ 114 | return self._send_message('get', 115 | '/products/{}/ticker'.format(product_id)) 116 | 117 | def get_product_trades(self, product_id, before='', after='', limit=None, result=None): 118 | """List the latest trades for a product. 119 | 120 | This method returns a generator which may make multiple HTTP requests 121 | while iterating through it. 122 | 123 | Args: 124 | product_id (str): Product 125 | before (Optional[str]): start time in ISO 8601 126 | after (Optional[str]): end time in ISO 8601 127 | limit (Optional[int]): the desired number of trades (can be more than 100, 128 | automatically paginated) 129 | results (Optional[list]): list of results that is used for the pagination 130 | Returns: 131 | list: Latest trades. Example:: 132 | [{ 133 | "time": "2014-11-07T22:19:28.578544Z", 134 | "trade_id": 74, 135 | "price": "10.00000000", 136 | "size": "0.01000000", 137 | "side": "buy" 138 | }, { 139 | "time": "2014-11-07T01:08:43.642366Z", 140 | "trade_id": 73, 141 | "price": "100.00000000", 142 | "size": "0.01000000", 143 | "side": "sell" 144 | }] 145 | """ 146 | return self._send_paginated_message('/products/{}/trades' 147 | .format(product_id)) 148 | 149 | def get_product_historic_rates(self, product_id, start=None, end=None, 150 | granularity=None): 151 | """Historic rates for a product. 152 | 153 | Rates are returned in grouped buckets based on requested 154 | `granularity`. If start, end, and granularity aren't provided, 155 | the exchange will assume some (currently unknown) default values. 156 | 157 | Historical rate data may be incomplete. No data is published for 158 | intervals where there are no ticks. 159 | 160 | **Caution**: Historical rates should not be polled frequently. 161 | If you need real-time information, use the trade and book 162 | endpoints along with the websocket feed. 163 | 164 | The maximum number of data points for a single request is 200 165 | candles. If your selection of start/end time and granularity 166 | will result in more than 200 data points, your request will be 167 | rejected. If you wish to retrieve fine granularity data over a 168 | larger time range, you will need to make multiple requests with 169 | new start/end ranges. 170 | 171 | Args: 172 | product_id (str): Product 173 | start (Optional[str]): Start time in ISO 8601 174 | end (Optional[str]): End time in ISO 8601 175 | granularity (Optional[int]): Desired time slice in seconds 176 | 177 | Returns: 178 | list: Historic candle data. Example: 179 | [ 180 | [ time, low, high, open, close, volume ], 181 | [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], 182 | ... 183 | ] 184 | 185 | """ 186 | params = {} 187 | if start is not None: 188 | params['start'] = start 189 | if end is not None: 190 | params['end'] = end 191 | if granularity is not None: 192 | acceptedGrans = [60, 300, 900, 3600, 21600, 86400] 193 | if granularity not in acceptedGrans: 194 | raise ValueError( 'Specified granularity is {}, must be in approved values: {}'.format( 195 | granularity, acceptedGrans) ) 196 | 197 | params['granularity'] = granularity 198 | return self._send_message('get', 199 | '/products/{}/candles'.format(product_id), 200 | params=params) 201 | 202 | def get_product_24hr_stats(self, product_id): 203 | """Get 24 hr stats for the product. 204 | 205 | Args: 206 | product_id (str): Product 207 | 208 | Returns: 209 | dict: 24 hour stats. Volume is in base currency units. 210 | Open, high, low are in quote currency units. Example:: 211 | { 212 | "open": "34.19000000", 213 | "high": "95.70000000", 214 | "low": "7.06000000", 215 | "volume": "2.41000000" 216 | } 217 | 218 | """ 219 | return self._send_message('get', 220 | '/products/{}/stats'.format(product_id)) 221 | 222 | def get_currencies(self): 223 | """List known currencies. 224 | 225 | Returns: 226 | list: List of currencies. Example:: 227 | [{ 228 | "id": "BTC", 229 | "name": "Bitcoin", 230 | "min_size": "0.00000001" 231 | }, { 232 | "id": "USD", 233 | "name": "United States Dollar", 234 | "min_size": "0.01000000" 235 | }] 236 | 237 | """ 238 | return self._send_message('get', '/currencies') 239 | 240 | def get_time(self): 241 | """Get the API server time. 242 | 243 | Returns: 244 | dict: Server time in ISO and epoch format (decimal seconds 245 | since Unix epoch). Example:: 246 | { 247 | "iso": "2015-01-07T23:47:25.201Z", 248 | "epoch": 1420674445.201 249 | } 250 | 251 | """ 252 | return self._send_message('get', '/time') 253 | 254 | def _send_message(self, method, endpoint, params=None, data=None): 255 | """Send API request. 256 | 257 | Args: 258 | method (str): HTTP method (get, post, delete, etc.) 259 | endpoint (str): Endpoint (to be added to base URL) 260 | params (Optional[dict]): HTTP request parameters 261 | data (Optional[str]): JSON-encoded string payload for POST 262 | 263 | Returns: 264 | dict/list: JSON response 265 | 266 | """ 267 | url = self.url + endpoint 268 | r = self.session.request(method, url, params=params, data=data, 269 | auth=self.auth, timeout=30) 270 | return r.json() 271 | 272 | def _send_paginated_message(self, endpoint, params=None): 273 | """ Send API message that results in a paginated response. 274 | 275 | The paginated responses are abstracted away by making API requests on 276 | demand as the response is iterated over. 277 | 278 | Paginated API messages support 3 additional parameters: `before`, 279 | `after`, and `limit`. `before` and `after` are mutually exclusive. To 280 | use them, supply an index value for that endpoint (the field used for 281 | indexing varies by endpoint - get_fills() uses 'trade_id', for example). 282 | `before`: Only get data that occurs more recently than index 283 | `after`: Only get data that occurs further in the past than index 284 | `limit`: Set amount of data per HTTP response. Default (and 285 | maximum) of 100. 286 | 287 | Args: 288 | endpoint (str): Endpoint (to be added to base URL) 289 | params (Optional[dict]): HTTP request parameters 290 | 291 | Yields: 292 | dict: API response objects 293 | 294 | """ 295 | if params is None: 296 | params = dict() 297 | url = self.url + endpoint 298 | while True: 299 | r = self.session.get(url, params=params, auth=self.auth, timeout=30) 300 | results = r.json() 301 | for result in results: 302 | yield result 303 | # If there are no more pages, we're done. Otherwise update `after` 304 | # param to get next page. 305 | # If this request included `before` don't get any more pages - the 306 | # cbpro API doesn't support multiple pages in that case. 307 | if not r.headers.get('cb-after') or \ 308 | params.get('before') is not None: 309 | break 310 | else: 311 | params['after'] = r.headers['cb-after'] 312 | -------------------------------------------------------------------------------- /trading_tools/crypto_com_api_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf8 3 | 4 | # Full credit goes to Igor Jakovljevic for the code located here https://github.com/IgorJakovljevic/crypto-com-api 5 | 6 | import requests 7 | import hashlib 8 | import datetime 9 | 10 | 11 | class CryptoComApi(): 12 | def __init__(self, time_offset=0, api_key="", secret_key=""): 13 | self.url = "https://api.crypto.com" 14 | self.api_key = api_key 15 | self.secret_key = secret_key 16 | self.time_offset = time_offset 17 | 18 | def get_symbols(self): 19 | """ Queries all transaction pairs and precision supported by the system 20 | Returns: 21 | [list] -- list of all transaction pairs and precision supported by the system 22 | """ 23 | 24 | return requests.get(self.url + "/v1/symbols").json()['data'] 25 | 26 | def get_ticker(self, symbol): 27 | """ Gets the current market quotes 28 | Arguments: 29 | symbol {string} -- Market mark e.g. btcusdt 30 | Returns: 31 | [dict] -- Returns current market quotes of the given symbol. 32 | """ 33 | 34 | request_url = f"{self.url}/v1/ticker?symbol={symbol}" 35 | return requests.get(request_url).json()['data'] 36 | 37 | def get_trades(self, symbol): 38 | """ Obtains market transaction records. 39 | Arguments: 40 | symbol {string} -- Market mark e.g. btcusdt 41 | Returns: 42 | [list] -- Returns a list of market transaction records 43 | """ 44 | request_url = f"{self.url}/v1/trades?symbol={symbol}" 45 | return requests.get(request_url).json()['data'] 46 | 47 | def get_market_trades(self): 48 | """ Gets the latest transaction price of each pair of currencies 49 | Arguments: 50 | symbol {string} -- Market mark e.g. btcusdt 51 | Returns: 52 | [list] -- List of latest transaction price of each pair of currencies 53 | """ 54 | request_url = f"{self.url}/v1/ticker/price" 55 | return requests.get(request_url).json()['data'] 56 | 57 | def get_orders(self, symbol, step="step1"): 58 | """ Gets the list of orders from buyers and sellers for the market 59 | Arguments: 60 | symbol {[string]} -- Market mark, ethbtc, See below for details 61 | Keyword Arguments: 62 | step {str} -- The depth type -- options: step0, step1, step2 (Merger depth0-2). 63 | step0time is the highest accuracy (default: {"step1"}) 64 | Returns: 65 | [list] -- List of orders from buyers and sellers for the market 66 | """ 67 | request_url = f"{self.url}/v1/depth?symbol={symbol}&type={step}" 68 | return requests.get(request_url).json()['data'] 69 | 70 | def get_k_lines(self, symbol, period, format_data=False): 71 | """ Gets K-line data for symbol for a given period 72 | Arguments: 73 | symbol {string} -- Market mark e.g. bchbtc 74 | period {int} -- Given in minutes. Possible values are [1, 5, 15, 30, 60, 1440, 10080, 43200] 75 | which corresponds to 1min, 5min, 15min, 30min, 1hour, 1day, 1week, 1month. 76 | Keyword Arguments: 77 | format_data {bool} -- If set to true the output elements are formated as a dictionary (default: {False}) 78 | Returns: 79 | [list] -- Returns K-line data for symbol for a given period 80 | """ 81 | request_url = f"{self.url}/v1/klines?symbol={symbol}&period={period}" 82 | data = requests.get(request_url).json()['data'] 83 | if (not format_data): 84 | return data 85 | 86 | def parse_obj(val): 87 | ret = dict() 88 | ret["ts"] = val[0] 89 | ret["open"] = val[1] 90 | ret["high"] = val[2] 91 | ret["min"] = val[3] 92 | ret["close"] = val[4] 93 | ret["volume"] = val[5] 94 | return ret 95 | 96 | return [parse_obj(x) for x in data] 97 | 98 | def sign(self, params: dict): 99 | sign_str = "" 100 | # sort params alphabetically and add to signing string 101 | for param in sorted(params.keys()): 102 | sign_str += param + str(params[param]) 103 | # at the end add the secret 104 | sign_str += str(self.secret_key) 105 | hash = hashlib.sha256(sign_str.encode()).hexdigest() 106 | return hash 107 | 108 | def mandatory_post_params(self): 109 | data = dict() 110 | data['api_key'] = self.api_key 111 | data['time'] = int(datetime.datetime.now().timestamp() * 1000) - int(self.time_offset) 112 | return data 113 | 114 | def get_account(self): 115 | data = self.mandatory_post_params() 116 | data['sign'] = self.sign(data) 117 | 118 | request_url = f"{self.url}/v1/account" 119 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 120 | return requests.post(request_url, data=data, headers=headers).json()['data'] 121 | 122 | def get_all_orders(self, symbol, page=None, pageSize=None, startDate=None, endDate=None): 123 | """ Gets all orders of the user 124 | Arguments: 125 | symbol {string} -- Market mark e.g. bchbtc 126 | Keyword Arguments: 127 | page {int} -- Page number 128 | pageSize {int} -- pageSize 129 | startDate {string} -- Start time, accurate to seconds "yyyy-MM-dd mm:hh:ss" 130 | endDate {string} -- End time, accurate to seconds "yyyy-MM-dd mm:hh:ss" 131 | Returns: 132 | {'count': int, 'resultList': list} -- Returns object with a result list and number of elements 133 | """ 134 | data = self.mandatory_post_params() 135 | data['symbol'] = symbol 136 | 137 | if (page is not None): 138 | data['page'] = page 139 | if (pageSize is not None): 140 | data['pageSize'] = pageSize 141 | if (startDate is not None): 142 | data['startDate'] = startDate 143 | if (endDate is not None): 144 | data['endDate'] = endDate 145 | 146 | data['sign'] = self.sign(data) 147 | 148 | request_url = f"{self.url}/v1/allOrders" 149 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 150 | return requests.post(request_url, data=data, headers=headers).json()['data'] 151 | 152 | def get_open_orders(self, symbol, page=None, pageSize=None): 153 | """ Gets all open orders of the user 154 | Arguments: 155 | symbol {string} -- Market mark e.g. bchbtc 156 | Keyword Arguments: 157 | page {int} -- Page number 158 | pageSize {int} -- pageSize 159 | Returns: 160 | {'count': int, 'resultList': list} -- Returns object with a result list and number of elements 161 | """ 162 | 163 | data = self.mandatory_post_params() 164 | data['symbol'] = symbol 165 | 166 | if (page is not None): 167 | data['page'] = page 168 | if (pageSize is not None): 169 | data['pageSize'] = pageSize 170 | 171 | data['sign'] = self.sign(data) 172 | 173 | request_url = f"{self.url}/v1/openOrders" 174 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 175 | return requests.post(request_url, data=data, headers=headers).json()['data'] 176 | 177 | def get_trades(self, symbol, page=None, pageSize=None, startDate=None, endDate=None, sort=None): 178 | """ Gets all trades of the user 179 | Arguments: 180 | symbol {string} -- Market mark e.g. bchbtc 181 | Keyword Arguments: 182 | page {int} -- Page number 183 | pageSize {int} -- pageSize 184 | startDate {string} -- Start time, accurate to seconds "yyyy-MM-dd mm:hh:ss" 185 | endDate {string} -- End time, accurate to seconds "yyyy-MM-dd mm:hh:ss" 186 | sort {int} -- 1 gives reverse order 187 | Returns: 188 | {'count': int, 'resultList': list} -- Returns object with a result list and number of elements 189 | """ 190 | 191 | data = self.mandatory_post_params() 192 | data['symbol'] = symbol 193 | 194 | if (page is not None): 195 | data['page'] = page 196 | if (pageSize is not None): 197 | data['pageSize'] = pageSize 198 | if (startDate is not None): 199 | data['startDate'] = startDate 200 | if (endDate is not None): 201 | data['endDate'] = endDate 202 | if (sort is not None): 203 | data['sort'] = sort 204 | 205 | data['sign'] = self.sign(data) 206 | 207 | request_url = f"{self.url}/v1/openOrders" 208 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 209 | return requests.post(request_url, data=data, headers=headers).json()['data'] 210 | 211 | def get_order(self, symbol, order_id): 212 | """ Gets specific order of the user 213 | Arguments: 214 | symbol {string} -- Market mark e.g. bchbtc 215 | order_id {string} -- Id of the bid, not the order 216 | Returns: 217 | object -- Contains data about the whole order 218 | """ 219 | 220 | data = self.mandatory_post_params() 221 | data['symbol'] = symbol 222 | data['order_id'] = order_id 223 | 224 | data['sign'] = self.sign(data) 225 | 226 | request_url = f"{self.url}/v1/showOrder" 227 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 228 | return requests.post(request_url, data=data, headers=headers).json()['data'] 229 | 230 | def cancel_order(self, symbol, order_id): 231 | """ Cancels specific order of the user 232 | Arguments: 233 | symbol {string} -- Market mark e.g. bchbtc 234 | order_id {string} -- Id of the bid, not the order 235 | Returns: 236 | object -- Contains data about the whole order 237 | """ 238 | 239 | data = self.mandatory_post_params() 240 | data['symbol'] = symbol 241 | data['order_id'] = order_id 242 | 243 | data['sign'] = self.sign(data) 244 | 245 | request_url = f"{self.url}/v1/orders/cancel" 246 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 247 | return requests.post(request_url, data=data, headers=headers).json()['data'] 248 | 249 | def cancel_all_orders(self, symbol): 250 | """ Cancels specific order of the user 251 | Arguments: 252 | symbol {string} -- Market mark e.g. bchbtc 253 | order_id {string} -- Id of the bid, not the order 254 | Returns: 255 | object -- Contains data about the whole order 256 | """ 257 | 258 | data = self.mandatory_post_params() 259 | data['symbol'] = symbol 260 | 261 | data['sign'] = self.sign(data) 262 | 263 | request_url = f"{self.url}/v1/cancelAllOrders" 264 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 265 | return requests.post(request_url, data=data, headers=headers).json()['data'] 266 | 267 | def order(self, symbol, side, order_type=1, volume=0, price=None, fee_is_user_exchange_coin=1): 268 | """ Create a new order 269 | Arguments: 270 | symbol {string} -- Market mark e.g. bchbtc 271 | side {string} -- BUY, SELL 272 | type {int} -- Type of list: 1 for limit order (user sets a price), 2 for market order (best available price) 273 | volume {float} -- Purchase quantity (Polysemy, multiplexing fields) type=1 represents the quantity of sales and purchases type=2: buy means the total price, Selling represents the total number. Trading restrictions user/me-User information. 274 | 275 | Keyword Arguments: 276 | fee_is_user_exchange_coin {int} -- this parameter indicates whether to use the platform currency to pay the handling fee, 0 means no, 1 means yes. 0 when the exchange has the platform currency. 277 | price {float} -- Authorized unit price. If type=2 then no need for this parameter. 278 | Returns: 279 | object -- Contains data about the whole order 280 | """ 281 | 282 | data = self.mandatory_post_params() 283 | 284 | if (price is not None): 285 | data['price'] = price 286 | else: 287 | return None 288 | 289 | data['symbol'] = symbol 290 | data['volume'] = volume 291 | data['type'] = order_type 292 | data['side'] = side 293 | 294 | if (fee_is_user_exchange_coin is not None): 295 | data['fee_is_user_exchange_coin'] = fee_is_user_exchange_coin 296 | 297 | data['sign'] = self.sign(data) 298 | 299 | request_url = f"{self.url}/v1/order" 300 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 301 | result = requests.post(request_url, data=data, headers=headers).json() 302 | return result['data'] 303 | -------------------------------------------------------------------------------- /trading_tools/poloniex_cmo_calculation.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | # from trading_strategies.poloniex_cmo_trading_strategy.config import LOGICAL_PARAMS 4 | from trading_strategies.coinbase_cmo_trading_strategy.config import LOGICAL_PARAMS 5 | 6 | 7 | def get_past(pair, period, days_history=30): 8 | """ 9 | Return historical charts data from poloniex.com 10 | :param pair: 11 | :param period: 12 | :param days_history: 13 | :return: 14 | """ 15 | end = int(time.time()) 16 | start = end - (24 * 60 * 60 * days_history) 17 | params = { 18 | 'command': 'returnChartData', 19 | 'currencyPair': pair, 20 | 'start': start, 21 | 'end': end, 22 | 'period': period 23 | } 24 | 25 | response = requests.get('https://poloniex.com/public', params=params) 26 | return response.json() 27 | 28 | 29 | def poloniex_cmo_logic_no_pandas(pair: str, period: int): 30 | response_json = get_past( 31 | pair=pair, 32 | period=period, 33 | days_history=LOGICAL_PARAMS["CMO_PERIOD"] 34 | ) 35 | # Get the last x days of data with respect to the cmo period (-1s for 0 index and having one extra day) 36 | response_json = response_json[len(response_json) - 1 - LOGICAL_PARAMS["CMO_PERIOD"] - 1:len(response_json) - 1] 37 | print(f'historical data: {response_json}') 38 | 39 | higher_close_price = 0 40 | lower_close_price = 0 41 | 42 | previous_day = False 43 | 44 | for day in response_json: 45 | if previous_day is not False: 46 | if day['close'] > previous_day['close']: 47 | higher_close_price += day['close'] - previous_day['close'] 48 | elif day['close'] < previous_day['close']: 49 | lower_close_price += previous_day['close'] - day['close'] 50 | previous_day = day 51 | 52 | cmo = ((higher_close_price - lower_close_price) / (higher_close_price + lower_close_price)) * 100 53 | print(f'higher_close_price: {higher_close_price}') 54 | print(f'lower_close_price: {lower_close_price}') 55 | print(f'CMO: {cmo}') 56 | return cmo 57 | 58 | 59 | if __name__ == '__main__': 60 | poloniex_cmo_logic_no_pandas() 61 | -------------------------------------------------------------------------------- /trading_tools/poloniex_wrapper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import requests 4 | import hashlib 5 | import hmac 6 | import urllib 7 | import ssl 8 | import json 9 | 10 | from trading_strategies.poloniex_cmo_trading_strategy.config import LOGICAL_PARAMS 11 | 12 | 13 | class Poloniex: 14 | def __init__(self, APIKey, Secret): 15 | self.APIKey = APIKey 16 | self.Secret = bytes(Secret.encode('utf8')) 17 | self.public_url = 'https://poloniex.com/public?command=' 18 | self.private_url = 'https://poloniex.com/tradingApi' 19 | 20 | def public_query(self, command): 21 | gcontext = ssl.SSLContext() 22 | data = urllib.request.urlopen(urllib.request.Request(self.public_url+command), context=gcontext) 23 | return json.loads(data.read()) 24 | 25 | def private_query(self, command): 26 | payload = { 27 | 'command': command, 28 | 'nonce': int(time.time() * 1000), 29 | } 30 | 31 | paybytes = urllib.parse.urlencode(payload).encode('utf8') 32 | sign = hmac.new(self.Secret, paybytes, hashlib.sha512).hexdigest() 33 | 34 | headers = { 35 | 'Key': self.APIKey, 36 | 'Sign': sign 37 | } 38 | 39 | r = requests.post(self.private_url, headers=headers, data=payload) 40 | return r.json() 41 | 42 | def trade(self, currency_pair, rate, amount, command): 43 | ''' 44 | 45 | :param currency_pair: A string that defines the market, "USDT_BTC" for example. 46 | :param rate: The price. Units are market quote currency. Eg USDT_BTC market, the value of this field would be around 10,000. Naturally this will be dated quickly but should give the idea. 47 | :param amount: The total amount offered in this buy order. 48 | :return: 49 | ''' 50 | url = 'https://poloniex.com/tradingApi' 51 | payload = { 52 | 'command': command, 53 | 'nonce': int(time.time() * 1000), 54 | 'currencyPair': currency_pair, 55 | 'rate': rate, 56 | 'amount': amount 57 | } 58 | paybytes = urllib.parse.urlencode(payload).encode('utf8') 59 | sign = hmac.new(self.Secret, paybytes, hashlib.sha512).hexdigest() 60 | 61 | headers = { 62 | 'Key': self.APIKey, 63 | 'Sign': sign 64 | } 65 | 66 | r = requests.post(url, headers=headers, data=payload) 67 | return r.json() 68 | 69 | 70 | if __name__ == '__main__': 71 | 72 | # TODO write a decorator that logs you in - (still required??) 73 | poloniex_wrapper = Poloniex( 74 | APIKey=os.getenv('POLONIEX_KEY'), 75 | Secret=os.getenv('POLONIEX_SECRET') 76 | ) 77 | 78 | all_tickers = poloniex_wrapper.public_query(command='returnTicker') 79 | ticker = all_tickers[LOGICAL_PARAMS['PAIR']] 80 | rate = float(ticker['lowestAsk']) 81 | price = LOGICAL_PARAMS['INITIAL_CAPITAL'] * LOGICAL_PARAMS['ENTRY_SIZE'] # of base currency 82 | entry_amount = price/rate 83 | 84 | # response = poloniex_wrapper.trade( 85 | # currency_pair=LOGICAL_PARAMS['PAIR'], 86 | # rate=rate, 87 | # amount=entry_amount, 88 | # command='buy' 89 | # ) 90 | 91 | # response = poloniex_wrapper.trade( 92 | # currency_pair=LOGICAL_PARAMS['PAIR'], 93 | # rate=rate, 94 | # amount=entry_amount, 95 | # command='sell' 96 | # ) 97 | 98 | balance = poloniex_wrapper.private_query(command='returnBalances') 99 | 100 | print(balance) 101 | -------------------------------------------------------------------------------- /trading_tools/poloniex_wrapper_bwentzloff.py: -------------------------------------------------------------------------------- 1 | # This code was taken directly from bwentzloff 2 | # The code was lifted in order to be quickly adapated for this project and Python3 3 | # Original code: https://github.com/bwentzloff/trading-bot 4 | 5 | import urllib.request, urllib.parse, urllib.error 6 | import urllib.request, urllib.error, urllib.parse 7 | import json 8 | import time 9 | import hmac, hashlib 10 | 11 | 12 | def createTimeStamp(datestr, format="%Y-%m-%d %H:%M:%S"): 13 | return time.mktime(time.strptime(datestr, format)) 14 | 15 | 16 | class Poloniex: 17 | def __init__(self, APIKey, Secret): 18 | self.APIKey = APIKey 19 | self.Secret = Secret 20 | 21 | def post_process(self, before): 22 | after = before 23 | 24 | # Add timestamps if there isnt one but is a datetime 25 | if ('return' in after): 26 | if (isinstance(after['return'], list)): 27 | for x in range(0, len(after['return'])): 28 | if (isinstance(after['return'][x], dict)): 29 | if ('datetime' in after['return'][x] and 'timestamp' not in after['return'][x]): 30 | after['return'][x]['timestamp'] = float(createTimeStamp(after['return'][x]['datetime'])) 31 | 32 | return after 33 | 34 | def api_query(self, command, req={}): 35 | import ssl 36 | gcontext = ssl.SSLContext() # Only for gangstars 37 | if (command == "returnTicker" or command == "return24Volume"): 38 | ret = urllib.request.urlopen(urllib.request.Request('https://poloniex.com/public?command=' + command)) 39 | return json.loads(ret.read()) 40 | elif (command == "returnOrderBook"): 41 | ret = urllib.request.urlopen(urllib.request.Request( 42 | 'https://poloniex.com/public?command=' + command + '¤cyPair=' + str(req['currencyPair']))) 43 | return json.loads(ret.read()) 44 | elif (command == "returnMarketTradeHistory"): 45 | ret = urllib.request.urlopen(urllib.request.Request( 46 | 'https://poloniex.com/public?command=' + "returnTradeHistory" + '¤cyPair=' + str( 47 | req['currencyPair'])), context=gcontext) 48 | return json.loads(ret.read()) 49 | elif (command == "returnChartData"): 50 | ret = urllib.request.urlopen(urllib.request.Request( 51 | 'https://poloniex.com/public?command=returnChartData¤cyPair=' + str( 52 | req['currencyPair']) + '&start=' + str(req['start']) + '&end=' + str(req['end']) + '&period=' + str( 53 | req['period']))) 54 | return json.loads(ret.read()) 55 | else: 56 | req['command'] = command 57 | req['nonce'] = int(time.time() * 1000) 58 | post_data = urllib.parse.urlencode(req) 59 | # TODO fix this 60 | sign = hmac.new(self.Secret, post_data, hashlib.sha512).hexdigest() 61 | headers = { 62 | 'Sign': sign, 63 | 'Key': self.APIKey 64 | } 65 | 66 | ret = urllib.request.urlopen(urllib.request.Request('https://poloniex.com/tradingApi', post_data, headers)) 67 | jsonRet = json.loads(ret.read()) 68 | return self.post_process(jsonRet) 69 | 70 | def returnTicker(self): 71 | return self.api_query("returnTicker") 72 | 73 | def return24Volume(self): 74 | return self.api_query("return24Volume") 75 | 76 | def returnOrderBook(self, currencyPair): 77 | return self.api_query("returnOrderBook", {'currencyPair': currencyPair}) 78 | 79 | def returnMarketTradeHistory(self, currencyPair): 80 | return self.api_query("returnMarketTradeHistory", {'currencyPair': currencyPair}) 81 | 82 | # Returns all of your balances. 83 | # Outputs: 84 | # {"BTC":"0.59098578","LTC":"3.31117268", ... } 85 | def returnBalances(self): 86 | return self.api_query('returnBalances') 87 | 88 | # Returns your open orders for a given market, specified by the "currencyPair" POST parameter, e.g. "BTC_XCP" 89 | # Inputs: 90 | # currencyPair The currency pair e.g. "BTC_XCP" 91 | # Outputs: 92 | # orderNumber The order number 93 | # type sell or buy 94 | # rate Price the order is selling or buying at 95 | # Amount Quantity of order 96 | # total Total value of order (price * quantity) 97 | def returnOpenOrders(self, currencyPair): 98 | return self.api_query('returnOpenOrders', {"currencyPair": currencyPair}) 99 | 100 | # Returns your trade history for a given market, specified by the "currencyPair" POST parameter 101 | # Inputs: 102 | # currencyPair The currency pair e.g. "BTC_XCP" 103 | # Outputs: 104 | # date Date in the form: "2014-02-19 03:44:59" 105 | # rate Price the order is selling or buying at 106 | # amount Quantity of order 107 | # total Total value of order (price * quantity) 108 | # type sell or buy 109 | def returnTradeHistory(self, currencyPair): 110 | return self.api_query('returnTradeHistory', {"currencyPair": currencyPair}) 111 | 112 | # Places a buy order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number. 113 | # Inputs: 114 | # currencyPair The curreny pair 115 | # rate price the order is buying at 116 | # amount Amount of coins to buy 117 | # Outputs: 118 | # orderNumber The order number 119 | def buy(self, currencyPair, rate, amount): 120 | return self.api_query('buy', {"currencyPair": currencyPair, "rate": rate, "amount": amount}) 121 | 122 | # Places a sell order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number. 123 | # Inputs: 124 | # currencyPair The curreny pair 125 | # rate price the order is selling at 126 | # amount Amount of coins to sell 127 | # Outputs: 128 | # orderNumber The order number 129 | def sell(self, currencyPair, rate, amount): 130 | return self.api_query('sell', {"currencyPair": currencyPair, "rate": rate, "amount": amount}) 131 | 132 | # Cancels an order you have placed in a given market. Required POST parameters are "currencyPair" and "orderNumber". 133 | # Inputs: 134 | # currencyPair The curreny pair 135 | # orderNumber The order number to cancel 136 | # Outputs: 137 | # succes 1 or 0 138 | def cancel(self, currencyPair, orderNumber): 139 | return self.api_query('cancelOrder', {"currencyPair": currencyPair, "orderNumber": orderNumber}) 140 | 141 | # Immediately places a withdrawal for a given currency, with no email confirmation. In order to use this method, the withdrawal privilege must be enabled for your API key. Required POST parameters are "currency", "amount", and "address". Sample output: {"response":"Withdrew 2398 NXT."} 142 | # Inputs: 143 | # currency The currency to withdraw 144 | # amount The amount of this coin to withdraw 145 | # address The withdrawal address 146 | # Outputs: 147 | # response Text containing message about the withdrawal 148 | def withdraw(self, currency, amount, address): 149 | return self.api_query('withdraw', {"currency": currency, "amount": amount, "address": address}) -------------------------------------------------------------------------------- /trading_tools/poloniex_wrapper_bwentzloff.py.bak: -------------------------------------------------------------------------------- 1 | # This code was taken directly from bwentzloff 2 | # The code was lifted in order to be quickly adapated for this project and Python3 3 | # Original code: https://github.com/bwentzloff/trading-bot 4 | 5 | import urllib 6 | import urllib2 7 | import json 8 | import time 9 | import hmac, hashlib 10 | 11 | 12 | def createTimeStamp(datestr, format="%Y-%m-%d %H:%M:%S"): 13 | return time.mktime(time.strptime(datestr, format)) 14 | 15 | 16 | class Poloniex: 17 | def __init__(self, APIKey, Secret): 18 | self.APIKey = APIKey 19 | self.Secret = Secret 20 | 21 | def post_process(self, before): 22 | after = before 23 | 24 | # Add timestamps if there isnt one but is a datetime 25 | if ('return' in after): 26 | if (isinstance(after['return'], list)): 27 | for x in xrange(0, len(after['return'])): 28 | if (isinstance(after['return'][x], dict)): 29 | if ('datetime' in after['return'][x] and 'timestamp' not in after['return'][x]): 30 | after['return'][x]['timestamp'] = float(createTimeStamp(after['return'][x]['datetime'])) 31 | 32 | return after 33 | 34 | def api_query(self, command, req={}): 35 | 36 | if (command == "returnTicker" or command == "return24Volume"): 37 | ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/public?command=' + command)) 38 | return json.loads(ret.read()) 39 | elif (command == "returnOrderBook"): 40 | ret = urllib2.urlopen(urllib2.Request( 41 | 'https://poloniex.com/public?command=' + command + '¤cyPair=' + str(req['currencyPair']))) 42 | return json.loads(ret.read()) 43 | elif (command == "returnMarketTradeHistory"): 44 | ret = urllib2.urlopen(urllib2.Request( 45 | 'https://poloniex.com/public?command=' + "returnTradeHistory" + '¤cyPair=' + str( 46 | req['currencyPair']))) 47 | return json.loads(ret.read()) 48 | elif (command == "returnChartData"): 49 | ret = urllib2.urlopen(urllib2.Request( 50 | 'https://poloniex.com/public?command=returnChartData¤cyPair=' + str( 51 | req['currencyPair']) + '&start=' + str(req['start']) + '&end=' + str(req['end']) + '&period=' + str( 52 | req['period']))) 53 | return json.loads(ret.read()) 54 | else: 55 | req['command'] = command 56 | req['nonce'] = int(time.time() * 1000) 57 | post_data = urllib.urlencode(req) 58 | 59 | sign = hmac.new(self.Secret, post_data, hashlib.sha512).hexdigest() 60 | headers = { 61 | 'Sign': sign, 62 | 'Key': self.APIKey 63 | } 64 | 65 | ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/tradingApi', post_data, headers)) 66 | jsonRet = json.loads(ret.read()) 67 | return self.post_process(jsonRet) 68 | 69 | def returnTicker(self): 70 | return self.api_query("returnTicker") 71 | 72 | def return24Volume(self): 73 | return self.api_query("return24Volume") 74 | 75 | def returnOrderBook(self, currencyPair): 76 | return self.api_query("returnOrderBook", {'currencyPair': currencyPair}) 77 | 78 | def returnMarketTradeHistory(self, currencyPair): 79 | return self.api_query("returnMarketTradeHistory", {'currencyPair': currencyPair}) 80 | 81 | # Returns all of your balances. 82 | # Outputs: 83 | # {"BTC":"0.59098578","LTC":"3.31117268", ... } 84 | def returnBalances(self): 85 | return self.api_query('returnBalances') 86 | 87 | # Returns your open orders for a given market, specified by the "currencyPair" POST parameter, e.g. "BTC_XCP" 88 | # Inputs: 89 | # currencyPair The currency pair e.g. "BTC_XCP" 90 | # Outputs: 91 | # orderNumber The order number 92 | # type sell or buy 93 | # rate Price the order is selling or buying at 94 | # Amount Quantity of order 95 | # total Total value of order (price * quantity) 96 | def returnOpenOrders(self, currencyPair): 97 | return self.api_query('returnOpenOrders', {"currencyPair": currencyPair}) 98 | 99 | # Returns your trade history for a given market, specified by the "currencyPair" POST parameter 100 | # Inputs: 101 | # currencyPair The currency pair e.g. "BTC_XCP" 102 | # Outputs: 103 | # date Date in the form: "2014-02-19 03:44:59" 104 | # rate Price the order is selling or buying at 105 | # amount Quantity of order 106 | # total Total value of order (price * quantity) 107 | # type sell or buy 108 | def returnTradeHistory(self, currencyPair): 109 | return self.api_query('returnTradeHistory', {"currencyPair": currencyPair}) 110 | 111 | # Places a buy order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number. 112 | # Inputs: 113 | # currencyPair The curreny pair 114 | # rate price the order is buying at 115 | # amount Amount of coins to buy 116 | # Outputs: 117 | # orderNumber The order number 118 | def buy(self, currencyPair, rate, amount): 119 | return self.api_query('buy', {"currencyPair": currencyPair, "rate": rate, "amount": amount}) 120 | 121 | # Places a sell order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number. 122 | # Inputs: 123 | # currencyPair The curreny pair 124 | # rate price the order is selling at 125 | # amount Amount of coins to sell 126 | # Outputs: 127 | # orderNumber The order number 128 | def sell(self, currencyPair, rate, amount): 129 | return self.api_query('sell', {"currencyPair": currencyPair, "rate": rate, "amount": amount}) 130 | 131 | # Cancels an order you have placed in a given market. Required POST parameters are "currencyPair" and "orderNumber". 132 | # Inputs: 133 | # currencyPair The curreny pair 134 | # orderNumber The order number to cancel 135 | # Outputs: 136 | # succes 1 or 0 137 | def cancel(self, currencyPair, orderNumber): 138 | return self.api_query('cancelOrder', {"currencyPair": currencyPair, "orderNumber": orderNumber}) 139 | 140 | # Immediately places a withdrawal for a given currency, with no email confirmation. In order to use this method, the withdrawal privilege must be enabled for your API key. Required POST parameters are "currency", "amount", and "address". Sample output: {"response":"Withdrew 2398 NXT."} 141 | # Inputs: 142 | # currency The currency to withdraw 143 | # amount The amount of this coin to withdraw 144 | # address The withdrawal address 145 | # Outputs: 146 | # response Text containing message about the withdrawal 147 | def withdraw(self, currency, amount, address): 148 | return self.api_query('withdraw', {"currency": currency, "amount": amount, "address": address}) --------------------------------------------------------------------------------