├── .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})
--------------------------------------------------------------------------------