├── bt.png ├── README.md ├── btc_algo.py └── cbpro.py /bt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samchaaa/simple_btc_algo/HEAD/bt.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple_btc_algo 2 | 3 | ## Summary: Long-only 50/100 period moving average crossover 4 | 5 | ![Image](https://raw.githubusercontent.com/samchaaa/simple_btc_algo/main/bt.png) 6 | 7 | Uses cbpro (from https://github.com/danpaquin/coinbasepro-python/), with minor modifications. 8 | 9 | Just a simple long-only moving average crossover for BTC-USD on the Coinbase Pro exchange (formerly GDAX). Requests previous 100 hourly bars from the Coinbase Pro public API, then checks for a 50/100 period simple moving average crossover, once per hour. 10 | 11 | Default size is 0.001 BTC, so the algo will by default trade just 0.001 per hour, until you are either 100% long or 100% flat. 12 | 13 | Intended to be traded unleveraged, currently no risk management. 14 | 15 | Algorithm is set up to implement immediately, just input your credentials and run. 16 | 17 | **Subscribe to https://samchaaa.substack.com/ for more in depth code, studies, and ready-to-implement algorithms.** 18 | -------------------------------------------------------------------------------- /btc_algo.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta 3 | import numpy as np 4 | 5 | from cbpro import * 6 | 7 | api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 8 | passphrase = 'xxxxxxxxxxxxx' 9 | secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 10 | 11 | import logging 12 | logging.basicConfig(filename='./btc_algo.log', format='%(name)s - %(message)s') 13 | logging.warning('{} logging started'.format(datetime.now().strftime("%x %X"))) 14 | 15 | class History(): 16 | 17 | """ Gets data and returns signal. """ 18 | 19 | def __init__(self): 20 | self.pc = PublicClient() 21 | self.avg1 = 50 22 | self.avg2 = 100 23 | 24 | def signal(self): 25 | self.startdate = (datetime.now() - timedelta(seconds=60*60*200)).strftime("%Y-%m-%dT%H:%M") 26 | self.enddate = datetime.now().strftime("%Y-%m-%dT%H:%M") 27 | 28 | self.data = self.pc.get_product_historic_rates( 29 | 'BTC-USD', 30 | start=self.startdate, 31 | end=self.enddate, 32 | granularity=3600 33 | ) 34 | 35 | self.data.sort(key=lambda x: x[0]) 36 | 37 | if np.mean([x[4] for x in self.data[-self.avg1:]]) > np.mean([x[4] for x in self.data[-self.avg2:]]): 38 | return True 39 | else: 40 | return False 41 | 42 | class Account(): 43 | 44 | """ Authenticates, checks balances, places orders. """ 45 | 46 | def __init__(self): 47 | self.auth_client = AuthenticatedClient(api_key, secret, passphrase) 48 | self.size = 0.001 49 | 50 | def is_balanceUSD(self): 51 | self.account = self.auth_client.get_accounts() 52 | if float([x for x in self.account if x['currency'] == 'USD'][0]['available']): 53 | return True 54 | else: 55 | return False 56 | 57 | def is_balanceBTC(self): 58 | self.account = self.auth_client.get_accounts() 59 | if float([x for x in self.account if x['currency'] == 'BTC'][0]['available']): 60 | return True 61 | else: 62 | return False 63 | 64 | def buy(self): 65 | return self.auth_client.place_market_order( 66 | 'BTC-USD', 67 | 'buy', 68 | size=self.size 69 | ) 70 | 71 | def sell(self): 72 | return self.auth_client.place_market_order( 73 | 'BTC-USD', 74 | 'sell', 75 | size=self.size 76 | ) 77 | 78 | def run(): 79 | 80 | print('initiating run()') 81 | 82 | auth_client = Account() 83 | 84 | # Checks if top of the hour, if so returns signals. 85 | while True: 86 | if datetime.now().minute == 0: 87 | if History().signal: 88 | if auth_client.is_balanceUSD(): 89 | buy = auth_client.buy() 90 | logging.warning('{} - {}'.format(datetime.now(), buy)) 91 | else: 92 | if auth_client.is_balanceBTC(): 93 | sell = auth_client.sell() 94 | logging.warning('{} - {}'.format(datetime.now(), sell)) 95 | time.sleep((60*60) - datetime.now().minute*60 - (datetime.now().microsecond/1000000)) 96 | else: 97 | time.sleep((60*60) - datetime.now().minute*60 - (datetime.now().microsecond/1000000)) 98 | 99 | if __name__ == '__main__': 100 | run() -------------------------------------------------------------------------------- /cbpro.py: -------------------------------------------------------------------------------- 1 | import requests, time, json 2 | import hmac 3 | import hashlib 4 | import base64 5 | from requests.auth import AuthBase 6 | 7 | """ 8 | 9 | From https://github.com/danpaquin/coinbasepro-python/ 10 | 11 | Modifications: 12 | - outdated versions of buy() and sell() commented out (under AuthenticatedClient) 13 | - param 'stop_price' added to place_stop_order (under AuthenticatedClient) 14 | 15 | """ 16 | 17 | 18 | class CBProAuth(AuthBase): 19 | # Provided by CBPro: https://docs.pro.coinbase.com/#signing-a-message 20 | def __init__(self, api_key, secret_key, passphrase): 21 | self.api_key = api_key 22 | self.secret_key = secret_key 23 | self.passphrase = passphrase 24 | 25 | def __call__(self, request): 26 | timestamp = str(time.time()) 27 | message = ''.join([timestamp, request.method, 28 | request.path_url, (request.body or '')]) 29 | request.headers.update(get_auth_headers(timestamp, message, 30 | self.api_key, 31 | self.secret_key, 32 | self.passphrase)) 33 | return request 34 | 35 | 36 | def get_auth_headers(timestamp, message, api_key, secret_key, passphrase): 37 | message = message.encode('ascii') 38 | hmac_key = base64.b64decode(secret_key) 39 | signature = hmac.new(hmac_key, message, hashlib.sha256) 40 | signature_b64 = base64.b64encode(signature.digest()).decode('utf-8') 41 | return { 42 | 'Content-Type': 'Application/JSON', 43 | 'CB-ACCESS-SIGN': signature_b64, 44 | 'CB-ACCESS-TIMESTAMP': timestamp, 45 | 'CB-ACCESS-KEY': api_key, 46 | 'CB-ACCESS-PASSPHRASE': passphrase 47 | } 48 | 49 | class PublicClient(object): 50 | """cbpro public client API. 51 | All requests default to the `product_id` specified at object 52 | creation if not otherwise specified. 53 | Attributes: 54 | url (Optional[str]): API URL. Defaults to cbpro API. 55 | """ 56 | 57 | def __init__(self, api_url='https://api.pro.coinbase.com', timeout=30): 58 | """Create cbpro API public client. 59 | Args: 60 | api_url (Optional[str]): API URL. Defaults to cbpro API. 61 | """ 62 | self.url = api_url.rstrip('/') 63 | self.auth = None 64 | self.session = requests.Session() 65 | 66 | def get_products(self): 67 | """Get a list of available currency pairs for trading. 68 | Returns: 69 | list: Info about all currency pairs. Example:: 70 | [ 71 | { 72 | "id": "BTC-USD", 73 | "display_name": "BTC/USD", 74 | "base_currency": "BTC", 75 | "quote_currency": "USD", 76 | "base_min_size": "0.01", 77 | "base_max_size": "10000.00", 78 | "quote_increment": "0.01" 79 | } 80 | ] 81 | """ 82 | return self._send_message('get', '/products') 83 | 84 | def get_product_order_book(self, product_id, level=1): 85 | """Get a list of open orders for a product. 86 | The amount of detail shown can be customized with the `level` 87 | parameter: 88 | * 1: Only the best bid and ask 89 | * 2: Top 50 bids and asks (aggregated) 90 | * 3: Full order book (non aggregated) 91 | Level 1 and Level 2 are recommended for polling. For the most 92 | up-to-date data, consider using the websocket stream. 93 | **Caution**: Level 3 is only recommended for users wishing to 94 | maintain a full real-time order book using the websocket 95 | stream. Abuse of Level 3 via polling will cause your access to 96 | be limited or blocked. 97 | Args: 98 | product_id (str): Product 99 | level (Optional[int]): Order book level (1, 2, or 3). 100 | Default is 1. 101 | Returns: 102 | dict: Order book. Example for level 1:: 103 | { 104 | "sequence": "3", 105 | "bids": [ 106 | [ price, size, num-orders ], 107 | ], 108 | "asks": [ 109 | [ price, size, num-orders ], 110 | ] 111 | } 112 | """ 113 | params = {'level': level} 114 | return self._send_message('get', 115 | '/products/{}/book'.format(product_id), 116 | params=params) 117 | 118 | def get_product_ticker(self, product_id): 119 | """Snapshot about the last trade (tick), best bid/ask and 24h volume. 120 | **Caution**: Polling is discouraged in favor of connecting via 121 | the websocket stream and listening for match messages. 122 | Args: 123 | product_id (str): Product 124 | Returns: 125 | dict: Ticker info. Example:: 126 | { 127 | "trade_id": 4729088, 128 | "price": "333.99", 129 | "size": "0.193", 130 | "bid": "333.98", 131 | "ask": "333.99", 132 | "volume": "5957.11914015", 133 | "time": "2015-11-14T20:46:03.511254Z" 134 | } 135 | """ 136 | return self._send_message('get', 137 | '/products/{}/ticker'.format(product_id)) 138 | 139 | def get_product_trades(self, product_id, before='', after='', limit=None, result=None): 140 | """List the latest trades for a product. 141 | This method returns a generator which may make multiple HTTP requests 142 | while iterating through it. 143 | Args: 144 | product_id (str): Product 145 | before (Optional[str]): start time in ISO 8601 146 | after (Optional[str]): end time in ISO 8601 147 | limit (Optional[int]): the desired number of trades (can be more than 100, 148 | automatically paginated) 149 | results (Optional[list]): list of results that is used for the pagination 150 | Returns: 151 | list: Latest trades. Example:: 152 | [{ 153 | "time": "2014-11-07T22:19:28.578544Z", 154 | "trade_id": 74, 155 | "price": "10.00000000", 156 | "size": "0.01000000", 157 | "side": "buy" 158 | }, { 159 | "time": "2014-11-07T01:08:43.642366Z", 160 | "trade_id": 73, 161 | "price": "100.00000000", 162 | "size": "0.01000000", 163 | "side": "sell" 164 | }] 165 | """ 166 | return self._send_paginated_message('/products/{}/trades' 167 | .format(product_id)) 168 | 169 | def get_product_historic_rates(self, product_id, start=None, end=None, 170 | granularity=None): 171 | """Historic rates for a product. 172 | Rates are returned in grouped buckets based on requested 173 | `granularity`. If start, end, and granularity aren't provided, 174 | the exchange will assume some (currently unknown) default values. 175 | Historical rate data may be incomplete. No data is published for 176 | intervals where there are no ticks. 177 | **Caution**: Historical rates should not be polled frequently. 178 | If you need real-time information, use the trade and book 179 | endpoints along with the websocket feed. 180 | The maximum number of data points for a single request is 200 181 | candles. If your selection of start/end time and granularity 182 | will result in more than 200 data points, your request will be 183 | rejected. If you wish to retrieve fine granularity data over a 184 | larger time range, you will need to make multiple requests with 185 | new start/end ranges. 186 | Args: 187 | product_id (str): Product 188 | start (Optional[str]): Start time in ISO 8601 189 | end (Optional[str]): End time in ISO 8601 190 | granularity (Optional[int]): Desired time slice in seconds 191 | Returns: 192 | list: Historic candle data. Example: 193 | [ 194 | [ time, low, high, open, close, volume ], 195 | [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], 196 | ... 197 | ] 198 | """ 199 | params = {} 200 | if start is not None: 201 | params['start'] = start 202 | if end is not None: 203 | params['end'] = end 204 | if granularity is not None: 205 | acceptedGrans = [60, 300, 900, 3600, 21600, 86400] 206 | if granularity not in acceptedGrans: 207 | raise ValueError( 'Specified granularity is {}, must be in approved values: {}'.format( 208 | granularity, acceptedGrans) ) 209 | 210 | params['granularity'] = granularity 211 | return self._send_message('get', 212 | '/products/{}/candles'.format(product_id), 213 | params=params) 214 | 215 | def get_product_24hr_stats(self, product_id): 216 | """Get 24 hr stats for the product. 217 | Args: 218 | product_id (str): Product 219 | Returns: 220 | dict: 24 hour stats. Volume is in base currency units. 221 | Open, high, low are in quote currency units. Example:: 222 | { 223 | "open": "34.19000000", 224 | "high": "95.70000000", 225 | "low": "7.06000000", 226 | "volume": "2.41000000" 227 | } 228 | """ 229 | return self._send_message('get', 230 | '/products/{}/stats'.format(product_id)) 231 | 232 | def get_currencies(self): 233 | """List known currencies. 234 | Returns: 235 | list: List of currencies. Example:: 236 | [{ 237 | "id": "BTC", 238 | "name": "Bitcoin", 239 | "min_size": "0.00000001" 240 | }, { 241 | "id": "USD", 242 | "name": "United States Dollar", 243 | "min_size": "0.01000000" 244 | }] 245 | """ 246 | return self._send_message('get', '/currencies') 247 | 248 | def get_time(self): 249 | """Get the API server time. 250 | Returns: 251 | dict: Server time in ISO and epoch format (decimal seconds 252 | since Unix epoch). Example:: 253 | { 254 | "iso": "2015-01-07T23:47:25.201Z", 255 | "epoch": 1420674445.201 256 | } 257 | """ 258 | return self._send_message('get', '/time') 259 | 260 | def _send_message(self, method, endpoint, params=None, data=None): 261 | """Send API request. 262 | Args: 263 | method (str): HTTP method (get, post, delete, etc.) 264 | endpoint (str): Endpoint (to be added to base URL) 265 | params (Optional[dict]): HTTP request parameters 266 | data (Optional[str]): JSON-encoded string payload for POST 267 | Returns: 268 | dict/list: JSON response 269 | """ 270 | url = self.url + endpoint 271 | r = self.session.request(method, url, params=params, data=data, 272 | auth=self.auth, timeout=30) 273 | return r.json() 274 | 275 | def _send_paginated_message(self, endpoint, params=None): 276 | """ Send API message that results in a paginated response. 277 | The paginated responses are abstracted away by making API requests on 278 | demand as the response is iterated over. 279 | Paginated API messages support 3 additional parameters: `before`, 280 | `after`, and `limit`. `before` and `after` are mutually exclusive. To 281 | use them, supply an index value for that endpoint (the field used for 282 | indexing varies by endpoint - get_fills() uses 'trade_id', for example). 283 | `before`: Only get data that occurs more recently than index 284 | `after`: Only get data that occurs further in the past than index 285 | `limit`: Set amount of data per HTTP response. Default (and 286 | maximum) of 100. 287 | Args: 288 | endpoint (str): Endpoint (to be added to base URL) 289 | params (Optional[dict]): HTTP request parameters 290 | Yields: 291 | dict: API response objects 292 | """ 293 | if params is None: 294 | params = dict() 295 | url = self.url + endpoint 296 | while True: 297 | r = self.session.get(url, params=params, auth=self.auth, timeout=30) 298 | results = r.json() 299 | for result in results: 300 | yield result 301 | # If there are no more pages, we're done. Otherwise update `after` 302 | # param to get next page. 303 | # If this request included `before` don't get any more pages - the 304 | # cbpro API doesn't support multiple pages in that case. 305 | if not r.headers.get('cb-after') or \ 306 | params.get('before') is not None: 307 | break 308 | else: 309 | params['after'] = r.headers['cb-after'] 310 | 311 | class AuthenticatedClient(PublicClient): 312 | """ Provides access to Private Endpoints on the cbpro API. 313 | All requests default to the live `api_url`: 'https://api.pro.coinbase.com'. 314 | To test your application using the sandbox modify the `api_url`. 315 | Attributes: 316 | url (str): The api url for this client instance to use. 317 | auth (CBProAuth): Custom authentication handler for each request. 318 | session (requests.Session): Persistent HTTP connection object. 319 | """ 320 | def __init__(self, key, b64secret, passphrase, 321 | api_url="https://api.pro.coinbase.com"): 322 | """ Create an instance of the AuthenticatedClient class. 323 | Args: 324 | key (str): Your API key. 325 | b64secret (str): The secret key matching your API key. 326 | passphrase (str): Passphrase chosen when setting up key. 327 | api_url (Optional[str]): API URL. Defaults to cbpro API. 328 | """ 329 | super(AuthenticatedClient, self).__init__(api_url) 330 | self.auth = CBProAuth(key, b64secret, passphrase) 331 | self.session = requests.Session() 332 | 333 | def get_account(self, account_id): 334 | """ Get information for a single account. 335 | Use this endpoint when you know the account_id. 336 | Args: 337 | account_id (str): Account id for account you want to get. 338 | Returns: 339 | dict: Account information. Example:: 340 | { 341 | "id": "a1b2c3d4", 342 | "balance": "1.100", 343 | "holds": "0.100", 344 | "available": "1.00", 345 | "currency": "USD" 346 | } 347 | """ 348 | return self._send_message('get', '/accounts/' + account_id) 349 | 350 | def get_accounts(self): 351 | """ Get a list of trading all accounts. 352 | When you place an order, the funds for the order are placed on 353 | hold. They cannot be used for other orders or withdrawn. Funds 354 | will remain on hold until the order is filled or canceled. The 355 | funds on hold for each account will be specified. 356 | Returns: 357 | list: Info about all accounts. Example:: 358 | [ 359 | { 360 | "id": "71452118-efc7-4cc4-8780-a5e22d4baa53", 361 | "currency": "BTC", 362 | "balance": "0.0000000000000000", 363 | "available": "0.0000000000000000", 364 | "hold": "0.0000000000000000", 365 | "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" 366 | }, 367 | { 368 | ... 369 | } 370 | ] 371 | * Additional info included in response for margin accounts. 372 | """ 373 | return self.get_account('') 374 | 375 | def get_account_history(self, account_id, **kwargs): 376 | """ List account activity. Account activity either increases or 377 | decreases your account balance. 378 | Entry type indicates the reason for the account change. 379 | * transfer: Funds moved to/from Coinbase to cbpro 380 | * match: Funds moved as a result of a trade 381 | * fee: Fee as a result of a trade 382 | * rebate: Fee rebate as per our fee schedule 383 | If an entry is the result of a trade (match, fee), the details 384 | field will contain additional information about the trade. 385 | Args: 386 | account_id (str): Account id to get history of. 387 | kwargs (dict): Additional HTTP request parameters. 388 | Returns: 389 | list: History information for the account. Example:: 390 | [ 391 | { 392 | "id": "100", 393 | "created_at": "2014-11-07T08:19:27.028459Z", 394 | "amount": "0.001", 395 | "balance": "239.669", 396 | "type": "fee", 397 | "details": { 398 | "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", 399 | "trade_id": "74", 400 | "product_id": "BTC-USD" 401 | } 402 | }, 403 | { 404 | ... 405 | } 406 | ] 407 | """ 408 | endpoint = '/accounts/{}/ledger'.format(account_id) 409 | return self._send_paginated_message(endpoint, params=kwargs) 410 | 411 | def get_account_holds(self, account_id, **kwargs): 412 | """ Get holds on an account. 413 | This method returns a generator which may make multiple HTTP requests 414 | while iterating through it. 415 | Holds are placed on an account for active orders or 416 | pending withdraw requests. 417 | As an order is filled, the hold amount is updated. If an order 418 | is canceled, any remaining hold is removed. For a withdraw, once 419 | it is completed, the hold is removed. 420 | The `type` field will indicate why the hold exists. The hold 421 | type is 'order' for holds related to open orders and 'transfer' 422 | for holds related to a withdraw. 423 | The `ref` field contains the id of the order or transfer which 424 | created the hold. 425 | Args: 426 | account_id (str): Account id to get holds of. 427 | kwargs (dict): Additional HTTP request parameters. 428 | Returns: 429 | generator(list): Hold information for the account. Example:: 430 | [ 431 | { 432 | "id": "82dcd140-c3c7-4507-8de4-2c529cd1a28f", 433 | "account_id": "e0b3f39a-183d-453e-b754-0c13e5bab0b3", 434 | "created_at": "2014-11-06T10:34:47.123456Z", 435 | "updated_at": "2014-11-06T10:40:47.123456Z", 436 | "amount": "4.23", 437 | "type": "order", 438 | "ref": "0a205de4-dd35-4370-a285-fe8fc375a273", 439 | }, 440 | { 441 | ... 442 | } 443 | ] 444 | """ 445 | endpoint = '/accounts/{}/holds'.format(account_id) 446 | return self._send_paginated_message(endpoint, params=kwargs) 447 | 448 | def place_order(self, product_id, side, order_type, **kwargs): 449 | """ Place an order. 450 | The three order types (limit, market, and stop) can be placed using this 451 | method. Specific methods are provided for each order type, but if a 452 | more generic interface is desired this method is available. 453 | Args: 454 | product_id (str): Product to order (eg. 'BTC-USD') 455 | side (str): Order side ('buy' or 'sell) 456 | order_type (str): Order type ('limit', 'market', or 'stop') 457 | **client_oid (str): Order ID selected by you to identify your order. 458 | This should be a UUID, which will be broadcast in the public 459 | feed for `received` messages. 460 | **stp (str): Self-trade prevention flag. cbpro doesn't allow self- 461 | trading. This behavior can be modified with this flag. 462 | Options: 463 | 'dc' Decrease and Cancel (default) 464 | 'co' Cancel oldest 465 | 'cn' Cancel newest 466 | 'cb' Cancel both 467 | **overdraft_enabled (Optional[bool]): If true funding above and 468 | beyond the account balance will be provided by margin, as 469 | necessary. 470 | **funding_amount (Optional[Decimal]): Amount of margin funding to be 471 | provided for the order. Mutually exclusive with 472 | `overdraft_enabled`. 473 | **kwargs: Additional arguments can be specified for different order 474 | types. See the limit/market/stop order methods for details. 475 | Returns: 476 | dict: Order details. Example:: 477 | { 478 | "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", 479 | "price": "0.10000000", 480 | "size": "0.01000000", 481 | "product_id": "BTC-USD", 482 | "side": "buy", 483 | "stp": "dc", 484 | "type": "limit", 485 | "time_in_force": "GTC", 486 | "post_only": false, 487 | "created_at": "2016-12-08T20:02:28.53864Z", 488 | "fill_fees": "0.0000000000000000", 489 | "filled_size": "0.00000000", 490 | "executed_value": "0.0000000000000000", 491 | "status": "pending", 492 | "settled": false 493 | } 494 | """ 495 | # Margin parameter checks 496 | if kwargs.get('overdraft_enabled') is not None and \ 497 | kwargs.get('funding_amount') is not None: 498 | raise ValueError('Margin funding must be specified through use of ' 499 | 'overdraft or by setting a funding amount, but not' 500 | ' both') 501 | 502 | # Limit order checks 503 | if order_type == 'limit': 504 | if kwargs.get('cancel_after') is not None and \ 505 | kwargs.get('time_in_force') != 'GTT': 506 | raise ValueError('May only specify a cancel period when time ' 507 | 'in_force is `GTT`') 508 | if kwargs.get('post_only') is not None and kwargs.get('time_in_force') in \ 509 | ['IOC', 'FOK']: 510 | raise ValueError('post_only is invalid when time in force is ' 511 | '`IOC` or `FOK`') 512 | 513 | # Market and stop order checks 514 | if order_type == 'market' or order_type == 'stop': 515 | if not (kwargs.get('size') is None) ^ (kwargs.get('funds') is None): 516 | raise ValueError('Either `size` or `funds` must be specified ' 517 | 'for market/stop orders (but not both).') 518 | 519 | # Build params dict 520 | params = {'product_id': product_id, 521 | 'side': side, 522 | 'type': order_type} 523 | params.update(kwargs) 524 | return self._send_message('post', '/orders', data=json.dumps(params)) 525 | 526 | # def buy(self, product_id, order_type, **kwargs): 527 | # """Place a buy order. 528 | # This is included to maintain backwards compatibility with older versions 529 | # of cbpro-Python. For maximum support from docstrings and function 530 | # signatures see the order type-specific functions place_limit_order, 531 | # place_market_order, and place_stop_order. 532 | # Args: 533 | # product_id (str): Product to order (eg. 'BTC-USD') 534 | # order_type (str): Order type ('limit', 'market', or 'stop') 535 | # **kwargs: Additional arguments can be specified for different order 536 | # types. 537 | # Returns: 538 | # dict: Order details. See `place_order` for example. 539 | # """ 540 | # return self.place_order(product_id, 'buy', order_type, **kwargs) 541 | 542 | # def sell(self, product_id, order_type, **kwargs): 543 | # """Place a sell order. 544 | # This is included to maintain backwards compatibility with older versions 545 | # of cbpro-Python. For maximum support from docstrings and function 546 | # signatures see the order type-specific functions place_limit_order, 547 | # place_market_order, and place_stop_order. 548 | # Args: 549 | # product_id (str): Product to order (eg. 'BTC-USD') 550 | # order_type (str): Order type ('limit', 'market', or 'stop') 551 | # **kwargs: Additional arguments can be specified for different order 552 | # types. 553 | # Returns: 554 | # dict: Order details. See `place_order` for example. 555 | # """ 556 | # return self.place_order(product_id, 'sell', order_type, **kwargs) 557 | 558 | def place_limit_order(self, product_id, side, price, size, 559 | client_oid=None, 560 | stp=None, 561 | time_in_force=None, 562 | cancel_after=None, 563 | post_only=None, 564 | overdraft_enabled=None, 565 | funding_amount=None): 566 | """Place a limit order. 567 | Args: 568 | product_id (str): Product to order (eg. 'BTC-USD') 569 | side (str): Order side ('buy' or 'sell) 570 | price (Decimal): Price per cryptocurrency 571 | size (Decimal): Amount of cryptocurrency to buy or sell 572 | client_oid (Optional[str]): User-specified Order ID 573 | stp (Optional[str]): Self-trade prevention flag. See `place_order` 574 | for details. 575 | time_in_force (Optional[str]): Time in force. Options: 576 | 'GTC' Good till canceled 577 | 'GTT' Good till time (set by `cancel_after`) 578 | 'IOC' Immediate or cancel 579 | 'FOK' Fill or kill 580 | cancel_after (Optional[str]): Cancel after this period for 'GTT' 581 | orders. Options are 'min', 'hour', or 'day'. 582 | post_only (Optional[bool]): Indicates that the order should only 583 | make liquidity. If any part of the order results in taking 584 | liquidity, the order will be rejected and no part of it will 585 | execute. 586 | overdraft_enabled (Optional[bool]): If true funding above and 587 | beyond the account balance will be provided by margin, as 588 | necessary. 589 | funding_amount (Optional[Decimal]): Amount of margin funding to be 590 | provided for the order. Mutually exclusive with 591 | `overdraft_enabled`. 592 | Returns: 593 | dict: Order details. See `place_order` for example. 594 | """ 595 | params = {'product_id': product_id, 596 | 'side': side, 597 | 'order_type': 'limit', 598 | 'price': price, 599 | 'size': size, 600 | 'client_oid': client_oid, 601 | 'stp': stp, 602 | 'time_in_force': time_in_force, 603 | 'cancel_after': cancel_after, 604 | 'post_only': post_only, 605 | 'overdraft_enabled': overdraft_enabled, 606 | 'funding_amount': funding_amount} 607 | params = dict((k, v) for k, v in params.items() if v is not None) 608 | 609 | return self.place_order(**params) 610 | 611 | def place_market_order(self, product_id, side, size=None, funds=None, 612 | client_oid=None, 613 | stp=None, 614 | overdraft_enabled=None, 615 | funding_amount=None): 616 | """ Place market order. 617 | Args: 618 | product_id (str): Product to order (eg. 'BTC-USD') 619 | side (str): Order side ('buy' or 'sell) 620 | size (Optional[Decimal]): Desired amount in crypto. Specify this or 621 | `funds`. 622 | funds (Optional[Decimal]): Desired amount of quote currency to use. 623 | Specify this or `size`. 624 | client_oid (Optional[str]): User-specified Order ID 625 | stp (Optional[str]): Self-trade prevention flag. See `place_order` 626 | for details. 627 | overdraft_enabled (Optional[bool]): If true funding above and 628 | beyond the account balance will be provided by margin, as 629 | necessary. 630 | funding_amount (Optional[Decimal]): Amount of margin funding to be 631 | provided for the order. Mutually exclusive with 632 | `overdraft_enabled`. 633 | Returns: 634 | dict: Order details. See `place_order` for example. 635 | """ 636 | params = {'product_id': product_id, 637 | 'side': side, 638 | 'order_type': 'market', 639 | 'size': size, 640 | 'funds': funds, 641 | 'client_oid': client_oid, 642 | 'stp': stp, 643 | 'overdraft_enabled': overdraft_enabled, 644 | 'funding_amount': funding_amount} 645 | params = dict((k, v) for k, v in params.items() if v is not None) 646 | 647 | return self.place_order(**params) 648 | 649 | def place_stop_order(self, product_id, side, price, stop, stop_price, 650 | size=None, funds=None, 651 | client_oid=None, 652 | stp=None, 653 | overdraft_enabled=None, 654 | funding_amount=None): 655 | """ Place stop order. 656 | Args: 657 | product_id (str): Product to order (eg. 'BTC-USD') 658 | side (str): Order side ('buy' or 'sell) 659 | price (Decimal): Desired price at which the stop order triggers. 660 | size (Optional[Decimal]): Desired amount in crypto. Specify this or 661 | `funds`. 662 | funds (Optional[Decimal]): Desired amount of quote currency to use. 663 | Specify this or `size`. 664 | client_oid (Optional[str]): User-specified Order ID 665 | stp (Optional[str]): Self-trade prevention flag. See `place_order` 666 | for details. 667 | overdraft_enabled (Optional[bool]): If true funding above and 668 | beyond the account balance will be provided by margin, as 669 | necessary. 670 | funding_amount (Optional[Decimal]): Amount of margin funding to be 671 | provided for the order. Mutually exclusive with 672 | `overdraft_enabled`. 673 | Returns: 674 | dict: Order details. See `place_order` for example. 675 | """ 676 | params = {'product_id': product_id, 677 | 'side': side, 678 | 'price': price, 679 | # 'order_type': 'stop', 680 | 'size': size, 681 | 'funds': funds, 682 | 'client_oid': client_oid, 683 | 'stop': stop, 684 | 'stop_price': stop_price, 685 | 'stp': stp, 686 | 'overdraft_enabled': overdraft_enabled, 687 | 'funding_amount': funding_amount} 688 | params = dict((k, v) for k, v in params.items() if v is not None) 689 | 690 | return self.place_order(**params) 691 | 692 | def cancel_order(self, order_id): 693 | """ Cancel a previously placed order. 694 | If the order had no matches during its lifetime its record may 695 | be purged. This means the order details will not be available 696 | with get_order(order_id). If the order could not be canceled 697 | (already filled or previously canceled, etc), then an error 698 | response will indicate the reason in the message field. 699 | **Caution**: The order id is the server-assigned order id and 700 | not the optional client_oid. 701 | Args: 702 | order_id (str): The order_id of the order you want to cancel 703 | Returns: 704 | list: Containing the order_id of cancelled order. Example:: 705 | [ "c5ab5eae-76be-480e-8961-00792dc7e138" ] 706 | """ 707 | return self._send_message('delete', '/orders/' + order_id) 708 | 709 | def cancel_all(self, product_id=None): 710 | """ With best effort, cancel all open orders. 711 | Args: 712 | product_id (Optional[str]): Only cancel orders for this 713 | product_id 714 | Returns: 715 | list: A list of ids of the canceled orders. Example:: 716 | [ 717 | "144c6f8e-713f-4682-8435-5280fbe8b2b4", 718 | "debe4907-95dc-442f-af3b-cec12f42ebda", 719 | "cf7aceee-7b08-4227-a76c-3858144323ab", 720 | "dfc5ae27-cadb-4c0c-beef-8994936fde8a", 721 | "34fecfbf-de33-4273-b2c6-baf8e8948be4" 722 | ] 723 | """ 724 | if product_id is not None: 725 | params = {'product_id': product_id} 726 | else: 727 | params = None 728 | return self._send_message('delete', '/orders', params=params) 729 | 730 | def get_order(self, order_id): 731 | """ Get a single order by order id. 732 | If the order is canceled the response may have status code 404 733 | if the order had no matches. 734 | **Caution**: Open orders may change state between the request 735 | and the response depending on market conditions. 736 | Args: 737 | order_id (str): The order to get information of. 738 | Returns: 739 | dict: Containing information on order. Example:: 740 | { 741 | "created_at": "2017-06-18T00:27:42.920136Z", 742 | "executed_value": "0.0000000000000000", 743 | "fill_fees": "0.0000000000000000", 744 | "filled_size": "0.00000000", 745 | "id": "9456f388-67a9-4316-bad1-330c5353804f", 746 | "post_only": true, 747 | "price": "1.00000000", 748 | "product_id": "BTC-USD", 749 | "settled": false, 750 | "side": "buy", 751 | "size": "1.00000000", 752 | "status": "pending", 753 | "stp": "dc", 754 | "time_in_force": "GTC", 755 | "type": "limit" 756 | } 757 | """ 758 | return self._send_message('get', '/orders/' + order_id) 759 | 760 | def get_orders(self, product_id=None, status=None, **kwargs): 761 | """ List your current open orders. 762 | This method returns a generator which may make multiple HTTP requests 763 | while iterating through it. 764 | Only open or un-settled orders are returned. As soon as an 765 | order is no longer open and settled, it will no longer appear 766 | in the default request. 767 | Orders which are no longer resting on the order book, will be 768 | marked with the 'done' status. There is a small window between 769 | an order being 'done' and 'settled'. An order is 'settled' when 770 | all of the fills have settled and the remaining holds (if any) 771 | have been removed. 772 | For high-volume trading it is strongly recommended that you 773 | maintain your own list of open orders and use one of the 774 | streaming market data feeds to keep it updated. You should poll 775 | the open orders endpoint once when you start trading to obtain 776 | the current state of any open orders. 777 | Args: 778 | product_id (Optional[str]): Only list orders for this 779 | product 780 | status (Optional[list/str]): Limit list of orders to 781 | this status or statuses. Passing 'all' returns orders 782 | of all statuses. 783 | ** Options: 'open', 'pending', 'active', 'done', 784 | 'settled' 785 | ** default: ['open', 'pending', 'active'] 786 | Returns: 787 | list: Containing information on orders. Example:: 788 | [ 789 | { 790 | "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", 791 | "price": "0.10000000", 792 | "size": "0.01000000", 793 | "product_id": "BTC-USD", 794 | "side": "buy", 795 | "stp": "dc", 796 | "type": "limit", 797 | "time_in_force": "GTC", 798 | "post_only": false, 799 | "created_at": "2016-12-08T20:02:28.53864Z", 800 | "fill_fees": "0.0000000000000000", 801 | "filled_size": "0.00000000", 802 | "executed_value": "0.0000000000000000", 803 | "status": "open", 804 | "settled": false 805 | }, 806 | { 807 | ... 808 | } 809 | ] 810 | """ 811 | params = kwargs 812 | if product_id is not None: 813 | params['product_id'] = product_id 814 | if status is not None: 815 | params['status'] = status 816 | return self._send_paginated_message('/orders', params=params) 817 | 818 | def get_fills(self, product_id=None, order_id=None, **kwargs): 819 | """ Get a list of recent fills. 820 | As of 8/23/18 - Requests without either order_id or product_id 821 | will be rejected 822 | This method returns a generator which may make multiple HTTP requests 823 | while iterating through it. 824 | Fees are recorded in two stages. Immediately after the matching 825 | engine completes a match, the fill is inserted into our 826 | datastore. Once the fill is recorded, a settlement process will 827 | settle the fill and credit both trading counterparties. 828 | The 'fee' field indicates the fees charged for this fill. 829 | The 'liquidity' field indicates if the fill was the result of a 830 | liquidity provider or liquidity taker. M indicates Maker and T 831 | indicates Taker. 832 | Args: 833 | product_id (str): Limit list to this product_id 834 | order_id (str): Limit list to this order_id 835 | kwargs (dict): Additional HTTP request parameters. 836 | Returns: 837 | list: Containing information on fills. Example:: 838 | [ 839 | { 840 | "trade_id": 74, 841 | "product_id": "BTC-USD", 842 | "price": "10.00", 843 | "size": "0.01", 844 | "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", 845 | "created_at": "2014-11-07T22:19:28.578544Z", 846 | "liquidity": "T", 847 | "fee": "0.00025", 848 | "settled": true, 849 | "side": "buy" 850 | }, 851 | { 852 | ... 853 | } 854 | ] 855 | """ 856 | if (product_id is None) and (order_id is None): 857 | raise ValueError('Either product_id or order_id must be specified.') 858 | 859 | params = {} 860 | if product_id: 861 | params['product_id'] = product_id 862 | if order_id: 863 | params['order_id'] = order_id 864 | params.update(kwargs) 865 | 866 | return self._send_paginated_message('/fills', params=params) 867 | 868 | def get_fundings(self, status=None, **kwargs): 869 | """ Every order placed with a margin profile that draws funding 870 | will create a funding record. 871 | This method returns a generator which may make multiple HTTP requests 872 | while iterating through it. 873 | Args: 874 | status (list/str): Limit funding records to these statuses. 875 | ** Options: 'outstanding', 'settled', 'rejected' 876 | kwargs (dict): Additional HTTP request parameters. 877 | Returns: 878 | list: Containing information on margin funding. Example:: 879 | [ 880 | { 881 | "id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", 882 | "order_id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", 883 | "profile_id": "d881e5a6-58eb-47cd-b8e2-8d9f2e3ec6f6", 884 | "amount": "1057.6519956381537500", 885 | "status": "settled", 886 | "created_at": "2017-03-17T23:46:16.663397Z", 887 | "currency": "USD", 888 | "repaid_amount": "1057.6519956381537500", 889 | "default_amount": "0", 890 | "repaid_default": false 891 | }, 892 | { 893 | ... 894 | } 895 | ] 896 | """ 897 | params = {} 898 | if status is not None: 899 | params['status'] = status 900 | params.update(kwargs) 901 | return self._send_paginated_message('/funding', params=params) 902 | 903 | def repay_funding(self, amount, currency): 904 | """ Repay funding. Repays the older funding records first. 905 | Args: 906 | amount (int): Amount of currency to repay 907 | currency (str): The currency, example USD 908 | Returns: 909 | Not specified by cbpro. 910 | """ 911 | params = { 912 | 'amount': amount, 913 | 'currency': currency # example: USD 914 | } 915 | return self._send_message('post', '/funding/repay', 916 | data=json.dumps(params)) 917 | 918 | def margin_transfer(self, margin_profile_id, transfer_type, currency, 919 | amount): 920 | """ Transfer funds between your standard profile and a margin profile. 921 | Args: 922 | margin_profile_id (str): Margin profile ID to withdraw or deposit 923 | from. 924 | transfer_type (str): 'deposit' or 'withdraw' 925 | currency (str): Currency to transfer (eg. 'USD') 926 | amount (Decimal): Amount to transfer 927 | Returns: 928 | dict: Transfer details. Example:: 929 | { 930 | "created_at": "2017-01-25T19:06:23.415126Z", 931 | "id": "80bc6b74-8b1f-4c60-a089-c61f9810d4ab", 932 | "user_id": "521c20b3d4ab09621f000011", 933 | "profile_id": "cda95996-ac59-45a3-a42e-30daeb061867", 934 | "margin_profile_id": "45fa9e3b-00ba-4631-b907-8a98cbdf21be", 935 | "type": "deposit", 936 | "amount": "2", 937 | "currency": "USD", 938 | "account_id": "23035fc7-0707-4b59-b0d2-95d0c035f8f5", 939 | "margin_account_id": "e1d9862c-a259-4e83-96cd-376352a9d24d", 940 | "margin_product_id": "BTC-USD", 941 | "status": "completed", 942 | "nonce": 25 943 | } 944 | """ 945 | params = {'margin_profile_id': margin_profile_id, 946 | 'type': transfer_type, 947 | 'currency': currency, # example: USD 948 | 'amount': amount} 949 | return self._send_message('post', '/profiles/margin-transfer', 950 | data=json.dumps(params)) 951 | 952 | def get_position(self): 953 | """ Get An overview of your margin profile. 954 | Returns: 955 | dict: Details about funding, accounts, and margin call. 956 | """ 957 | return self._send_message('get', '/position') 958 | 959 | def close_position(self, repay_only): 960 | """ Close position. 961 | Args: 962 | repay_only (bool): Undocumented by cbpro. 963 | Returns: 964 | Undocumented 965 | """ 966 | params = {'repay_only': repay_only} 967 | return self._send_message('post', '/position/close', 968 | data=json.dumps(params)) --------------------------------------------------------------------------------