├── src ├── __init__.py ├── account │ ├── __init__.py │ ├── summary.py │ ├── details.py │ ├── configure.py │ ├── instruments.py │ ├── README.md │ ├── changes.py │ └── account.py ├── common │ ├── __init__.py │ ├── args.py │ ├── input.py │ ├── view.py │ └── config.py ├── order │ ├── __init__.py │ ├── get.py │ ├── list_pending.py │ ├── set_client_extensions.py │ ├── market.py │ ├── stop_loss.py │ ├── take_profit.py │ ├── trailing_stop_loss.py │ ├── stop.py │ ├── entry.py │ ├── limit.py │ ├── cancel.py │ ├── view.py │ ├── README.md │ └── args.py ├── pricing │ ├── __init__.py │ ├── view.py │ ├── README.md │ ├── stream.py │ └── get.py ├── trade │ ├── __init__.py │ ├── view.py │ ├── README.md │ ├── close.py │ ├── set_client_extensions.py │ └── get.py ├── instrument │ ├── __init__.py │ ├── README.md │ ├── view.py │ ├── candles.py │ └── candles_poll.py ├── position │ ├── __init__.py │ ├── close.py │ └── view.py ├── transaction │ ├── __init__.py │ ├── get.py │ ├── stream.py │ ├── poll.py │ └── range.py ├── configure.py └── market_order_full_example.py ├── .gitignore ├── requirements └── base.txt ├── Makefile ├── LICENSE.txt ├── setup.py └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pricing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/trade/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/instrument/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/position/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/transaction/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | env/ 3 | env-python2/ 4 | build/ 5 | *.sw* 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | pyyaml==3.11 2 | tabulate==0.7.5 3 | requests==2.11.1 4 | ujson==1.35 5 | v20==3.0.14.0 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bootstrap 2 | bootstrap: bootstrap-python3 3 | 4 | bootstrap-python3: 5 | virtualenv -p python3 env 6 | env/bin/pip install -r requirements/base.txt 7 | 8 | bootstrap-python2: 9 | virtualenv env-python2 10 | env-python2/bin/pip install -r requirements/base.txt 11 | 12 | -------------------------------------------------------------------------------- /src/pricing/view.py: -------------------------------------------------------------------------------- 1 | def price_to_string(price): 2 | return "{} ({}) {}/{}".format( 3 | price.instrument, 4 | price.time, 5 | price.bids[0].price, 6 | price.asks[0].price 7 | ) 8 | 9 | def heartbeat_to_string(heartbeat): 10 | return "HEARTBEAT ({})".format( 11 | heartbeat.time 12 | ) 13 | -------------------------------------------------------------------------------- /src/common/args.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import argparse 3 | import v20.transaction 4 | 5 | def instrument(i): 6 | return i.replace("/", "_") 7 | 8 | def date_time(fmt="%Y-%m-%d %H:%M:%S"): 9 | def parse(s): 10 | try: 11 | return datetime.strptime(s, fmt) 12 | except ValueError: 13 | msg = "Not a valid date: '{0}'.".format(s) 14 | raise argparse.ArgumentTypeError(msg) 15 | 16 | return parse 17 | -------------------------------------------------------------------------------- /src/transaction/get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | import time 7 | 8 | 9 | def main(): 10 | """ 11 | Poll Transactions for the active Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | common.config.add_argument(parser) 17 | 18 | parser.add_argument( 19 | 'id', 20 | help="The ID of the Transaction to get" 21 | ) 22 | 23 | args = parser.parse_args() 24 | 25 | api = args.config.create_context() 26 | 27 | account_id = args.config.active_account 28 | 29 | response = api.transaction.get(account_id, args.id) 30 | 31 | print(response.get("transaction", 200)) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /src/pricing/README.md: -------------------------------------------------------------------------------- 1 | # Pricing Scripts 2 | 3 | ## Get/Poll Account Prices 4 | 5 | The script to get current prices for the active Account implemented in 6 | `get.py`. It may be used to repeated poll for changes to the prices. It can be 7 | executed directly or with the provided entry point alias: 8 | 9 | ```bash 10 | (env)user@host: ~/v20-python-samples$ python src/pricing/get.py 11 | (env)user@host: ~/v20-python-samples$ v20-pricing-get 12 | ``` 13 | ## Stream Account Prices 14 | 15 | The script to stream Prices for the active Account is implemented in 16 | `stream.py`. It can be executed directly or with the provided entry point 17 | alias: 18 | 19 | ```bash 20 | (env)user@host: ~/v20-python-samples$ python src/pricing/stream.py 21 | (env)user@host: ~/v20-python-samples$ v20-pricing-stream 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /src/instrument/README.md: -------------------------------------------------------------------------------- 1 | # Instrument Scripts 2 | 3 | ## Fetch Instrument Candlesticks 4 | 5 | The script to fetch instrument candlesticks is implemented in `candles.py`. It 6 | can be executed directly or with the provided entry point alias: 7 | 8 | ```bash 9 | (env)user@host: ~/v20-python-samples$ python src/instrument/candles.py 10 | (env)user@host: ~/v20-python-samples$ v20-instrument-candles 11 | ``` 12 | 13 | ## Poll Instrument Candlesticks 14 | 15 | The script to poll instrument candlesticks is implemented in `candles_poll.py`. 16 | It uses curses to redraw the current candle while it is being updated, and 17 | moves on to the next candle when the current candle is completed. It can be 18 | executed directly or with the provided entry point alias: 19 | 20 | ```bash 21 | (env)user@host: ~/v20-python-samples$ python src/instrument/candles_poll.py 22 | (env)user@host: ~/v20-python-samples$ v20-instrument-candles-poll 23 | ``` 24 | -------------------------------------------------------------------------------- /src/transaction/stream.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | 6 | 7 | def main(): 8 | """ 9 | Stream Transactions for the active Account 10 | """ 11 | 12 | parser = argparse.ArgumentParser() 13 | 14 | common.config.add_argument(parser) 15 | 16 | parser.add_argument( 17 | '--show-heartbeats', 18 | action='store_true', 19 | default=False, 20 | help="display heartbeats" 21 | ) 22 | 23 | args = parser.parse_args() 24 | 25 | account_id = args.config.active_account 26 | 27 | api = args.config.create_streaming_context() 28 | 29 | response = api.transaction.stream(account_id) 30 | 31 | for msg_type, msg in response.parts(): 32 | if msg.type == "HEARTBEAT" and not args.show_heartbeats: 33 | continue 34 | 35 | print(msg.summary()) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /src/trade/view.py: -------------------------------------------------------------------------------- 1 | import common.view 2 | 3 | 4 | def print_trades_map(trades_map): 5 | """ 6 | Print a map of Trade Summaries in table format. 7 | 8 | Args: 9 | orders_map: The map of id->Trade to print 10 | """ 11 | 12 | print_trades( 13 | sorted( 14 | trades_map.values(), 15 | key=lambda t: t.id 16 | ) 17 | ) 18 | 19 | 20 | def print_trades(trades): 21 | """ 22 | Print a collection or Trades in table format. 23 | 24 | Args: 25 | trades: The list of Trades to print 26 | """ 27 | 28 | # 29 | # Print the Trades in a table with their ID, state, summary, upl and pl 30 | # 31 | common.view.print_collection( 32 | "{} Trades".format(len(trades)), 33 | trades, 34 | [ 35 | ("ID", lambda t: t.id), 36 | ("State", lambda t: t.state), 37 | ("Summary", lambda t: t.summary()), 38 | ("Unrealized P/L", lambda t: t.unrealizedPL), 39 | ("P/L", lambda t: t.realizedPL) 40 | ] 41 | ) 42 | 43 | print("") 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 OANDA Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/trade/README.md: -------------------------------------------------------------------------------- 1 | # Trade Scripts 2 | 3 | ## Get specific Trade or all open Trades 4 | 5 | The script to get a specific Trade or all open Trades in an Account is 6 | implemented in `get.py`. It can be executed directly or with the provided 7 | entry point alias: 8 | 9 | ```bash 10 | (env)user@host: ~/v20-python-samples$ python src/trade/get.py 11 | (env)user@host: ~/v20-python-samples$ v20-trade-get 12 | ``` 13 | 14 | ## Close an open Trade 15 | 16 | The script to close (fully or paritally) an open Trade in an Account is 17 | implemented in `close.py`. It can be executed directly or with the provided entry 18 | point alias: 19 | 20 | ```bash 21 | (env)user@host: ~/v20-python-samples$ python src/trade/close.py 22 | (env)user@host: ~/v20-python-samples$ v20-trade-close 23 | ``` 24 | 25 | ## Set Trade Client Extensions 26 | 27 | The script to set the client extensions for an open Trade in the active Account 28 | is implemented in `set_client_extensions.py`. It can be executed directly or 29 | with the provided entry point alias: 30 | 31 | ```bash 32 | (env)user@host: ~/v20-python-samples$ python src/trade/set_client_extensions.py 33 | (env)user@host: ~/v20-python-samples$ v20-trade-set-client-extensions 34 | ``` 35 | -------------------------------------------------------------------------------- /src/transaction/poll.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | import time 7 | 8 | 9 | def main(): 10 | """ 11 | Poll Transactions for the active Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | common.config.add_argument(parser) 17 | 18 | parser.add_argument( 19 | '--poll-interval', 20 | type=float, 21 | default=2, 22 | help="The interval between polls" 23 | ) 24 | 25 | args = parser.parse_args() 26 | 27 | api = args.config.create_context() 28 | 29 | kwargs = {} 30 | 31 | account_id = args.config.active_account 32 | 33 | response = api.account.summary(account_id) 34 | 35 | last_transaction_id = response.get("lastTransactionID", 200) 36 | 37 | while True: 38 | time.sleep(args.poll_interval) 39 | 40 | response = api.transaction.since( 41 | account_id, 42 | id=last_transaction_id 43 | ) 44 | 45 | for transaction in response.get("transactions", 200): 46 | print(transaction.title()) 47 | 48 | last_transaction_id = response.get("lastTransactionID", 200) 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /src/order/get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | 7 | 8 | def main(): 9 | """ 10 | Get the details of an Order in an Account 11 | """ 12 | 13 | parser = argparse.ArgumentParser() 14 | 15 | # 16 | # Add the command line argument to parse to the v20 config 17 | # 18 | common.config.add_argument(parser) 19 | 20 | parser.add_argument( 21 | "orderid", 22 | help=( 23 | "The ID of the Order to get. If prepended " 24 | "with an '@', this will be interpreted as a client Order ID" 25 | ) 26 | ) 27 | 28 | args = parser.parse_args() 29 | 30 | # 31 | # Create the api context based on the contents of the 32 | # v20 config file 33 | # 34 | api = args.config.create_context() 35 | 36 | # 37 | # Submit the request to create the Market Order 38 | # 39 | response = api.order.get( 40 | args.config.active_account, 41 | args.orderid 42 | ) 43 | 44 | print("Response: {} ({})".format(response.status, response.reason)) 45 | print("") 46 | 47 | order = response.get("order", 200) 48 | 49 | print(order) 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /src/account/summary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .account import Account 6 | 7 | 8 | def main(): 9 | """ 10 | Create an API context, and use it to fetch and display an Account summary. 11 | 12 | The configuration for the context and Account to fetch is parsed from the 13 | config file provided as an argument. 14 | """ 15 | 16 | parser = argparse.ArgumentParser() 17 | 18 | # 19 | # The config object is initialized by the argument parser, and contains 20 | # the REST APID host, port, accountID, etc. 21 | # 22 | common.config.add_argument(parser) 23 | 24 | args = parser.parse_args() 25 | 26 | account_id = args.config.active_account 27 | 28 | # 29 | # The v20 config object creates the v20.Context for us based on the 30 | # contents of the config file. 31 | # 32 | api = args.config.create_context() 33 | 34 | # 35 | # Fetch the details of the Account found in the config file 36 | # 37 | response = api.account.summary(account_id) 38 | 39 | # 40 | # Extract the Account representation from the response. 41 | # 42 | account = Account( 43 | response.get("account", "200") 44 | ) 45 | 46 | account.dump() 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /src/account/details.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .account import Account 6 | 7 | def main(): 8 | """ 9 | Create an API context, and use it to fetch and display the state of an 10 | Account. 11 | 12 | The configuration for the context and Account to fetch is parsed from the 13 | config file provided as an argument. 14 | """ 15 | 16 | parser = argparse.ArgumentParser() 17 | 18 | # 19 | # The config object is initialized by the argument parser, and contains 20 | # the REST APID host, port, accountID, etc. 21 | # 22 | common.config.add_argument(parser) 23 | 24 | args = parser.parse_args() 25 | 26 | account_id = args.config.active_account 27 | 28 | # 29 | # The v20 config object creates the v20.Context for us based on the 30 | # contents of the config file. 31 | # 32 | api = args.config.create_context() 33 | 34 | # 35 | # Fetch the details of the Account found in the config file 36 | # 37 | response = api.account.get(account_id) 38 | 39 | # 40 | # Extract the Account representation from the response. 41 | # 42 | account = Account( 43 | response.get("account", "200") 44 | ) 45 | 46 | account.dump() 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /src/configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import common.config 4 | import common.input 5 | 6 | def main(): 7 | """ 8 | Load an existing v20.conf file, update it interactively, and save it 9 | back to a file. 10 | """ 11 | 12 | config = common.config.Config() 13 | 14 | filename = common.input.get_string( 15 | "Enter existing v20.conf filename to load", 16 | common.config.default_config_path() 17 | ) 18 | 19 | try: 20 | config.load(filename) 21 | except: 22 | print("Config file '{}' doesn't exist, starting with defaults.".format( 23 | filename 24 | )) 25 | print 26 | 27 | print("") 28 | print("------------ Intitial v20 configuration ------------") 29 | print(str(config)) 30 | print("----------------------------------------------------") 31 | print("") 32 | 33 | config.update_from_input() 34 | 35 | print("") 36 | print("-------------- New v20 configuration --------------") 37 | print(str(config)) 38 | print("---------------------------------------------------") 39 | print("") 40 | 41 | dump = common.input.get_yn( 42 | "Dump v20 configuration to {}?".format(filename), 43 | True 44 | ) 45 | 46 | if dump: 47 | config.dump(filename) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /src/order/list_pending.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .view import print_orders 6 | 7 | 8 | def main(): 9 | parser = argparse.ArgumentParser() 10 | common.config.add_argument(parser) 11 | 12 | parser.add_argument( 13 | "--summary", 14 | dest="summary", 15 | action="store_true", 16 | help="Print a summary of the orders", 17 | default=True 18 | ) 19 | 20 | parser.add_argument( 21 | "--verbose", "-v", 22 | dest="summary", 23 | help="Print details of the orders", 24 | action="store_false" 25 | ) 26 | 27 | args = parser.parse_args() 28 | 29 | account_id = args.config.active_account 30 | 31 | api = args.config.create_context() 32 | 33 | response = api.order.list_pending(account_id) 34 | 35 | orders = response.get("orders", 200) 36 | 37 | if len(orders) == 0: 38 | print("Account {} has no pending Orders".format(account_id)) 39 | return 40 | 41 | orders = sorted(orders, key=lambda o: int(o.id)) 42 | 43 | if not args.summary: 44 | print("-" * 80) 45 | 46 | for order in orders: 47 | if args.summary: 48 | print(order.title()) 49 | else: 50 | print(order.yaml(True)) 51 | print("-" * 80) 52 | 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /src/transaction/range.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | import time 7 | 8 | 9 | def main(): 10 | """ 11 | Poll Transactions for the active Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | common.config.add_argument(parser) 17 | 18 | parser.add_argument( 19 | 'fromid', 20 | help="The ID of the first Transaction to get" 21 | ) 22 | 23 | parser.add_argument( 24 | 'toid', 25 | help="The ID of the last Transaction to get" 26 | ) 27 | 28 | parser.add_argument( 29 | '--type', 30 | action="append", 31 | help=( 32 | "Type filter for range request. This can be any Transaction type " 33 | "name or one of the groupings ORDER FUNDING ADMIN" 34 | ) 35 | ) 36 | 37 | args = parser.parse_args() 38 | 39 | api = args.config.create_context() 40 | 41 | filter = None 42 | 43 | if args.type is not None: 44 | filter = ",".join(args.type) 45 | 46 | account_id = args.config.active_account 47 | 48 | response = api.transaction.range( 49 | account_id, 50 | fromID=args.fromid, 51 | toID=args.toid, 52 | type=filter 53 | ) 54 | 55 | for transaction in response.get("transactions", 200): 56 | print(transaction.title()) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /src/order/set_client_extensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | from .args import OrderArguments 7 | 8 | 9 | def main(): 10 | """ 11 | Set the client extensions of an open Trade in an Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | # 17 | # Add the command line argument to parse to the v20 config 18 | # 19 | common.config.add_argument(parser) 20 | 21 | parser.add_argument( 22 | "orderid", 23 | help=( 24 | "The ID of the Order to get. If prepended " 25 | "with an '@', this will be interpreted as a client Order ID" 26 | ) 27 | ) 28 | 29 | extnArgs = OrderArguments(parser) 30 | extnArgs.add_client_order_extensions() 31 | extnArgs.add_client_trade_extensions() 32 | 33 | args = parser.parse_args() 34 | 35 | # 36 | # Create the api context based on the contents of the 37 | # v20 config file 38 | # 39 | api = args.config.create_context() 40 | 41 | extnArgs.parse_arguments(args) 42 | 43 | # 44 | # Submit the request to create the Market Order 45 | # 46 | response = api.order.set_client_extensions( 47 | args.config.active_account, 48 | args.orderid, 49 | **extnArgs.parsed_args 50 | ) 51 | 52 | print("Response: {} ({})".format(response.status, response.reason)) 53 | print("") 54 | 55 | print(response.get( 56 | "orderClientExtensionsModifyTransaction", 200 57 | )) 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /src/trade/close.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | from order.view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Close an open Trade in an Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | # 17 | # Add the command line argument to parse to the v20 config 18 | # 19 | common.config.add_argument(parser) 20 | 21 | parser.add_argument( 22 | "tradeid", 23 | help=( 24 | "The ID of the Trade to close. If prepended " 25 | "with an '@', this will be interpreted as a client Trade ID" 26 | ) 27 | ) 28 | 29 | parser.add_argument( 30 | "--units", 31 | default="ALL", 32 | help=( 33 | "The amount of the Trade to close. Either the string 'ALL' " 34 | "indicating a full Trade close, or the number of units of the " 35 | "Trade to close. This number must always be positive and may " 36 | "not exceed the magnitude of the Trade's open units" 37 | ) 38 | ) 39 | 40 | args = parser.parse_args() 41 | 42 | account_id = args.config.active_account 43 | 44 | # 45 | # Create the api context based on the contents of the 46 | # v20 config file 47 | # 48 | api = args.config.create_context() 49 | 50 | response = api.trade.close( 51 | account_id, 52 | args.tradeid, 53 | units=args.units 54 | ) 55 | 56 | print( 57 | "Response: {} ({})\n".format( 58 | response.status, 59 | response.reason 60 | ) 61 | ) 62 | 63 | print_order_create_response_transactions(response) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /src/order/market.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments 6 | from v20.order import MarketOrderRequest 7 | from .view import print_order_create_response_transactions 8 | 9 | 10 | def main(): 11 | """ 12 | Create a Market Order in an Account based on the provided command-line 13 | arguments. 14 | """ 15 | 16 | parser = argparse.ArgumentParser() 17 | 18 | # 19 | # Add the command line argument to parse to the v20 config 20 | # 21 | common.config.add_argument(parser) 22 | 23 | # 24 | # Add the command line arguments required for a Market Order 25 | # 26 | marketOrderArgs = OrderArguments(parser) 27 | marketOrderArgs.add_instrument() 28 | marketOrderArgs.add_units() 29 | marketOrderArgs.add_time_in_force(["FOK", "IOC"]) 30 | marketOrderArgs.add_price_bound() 31 | marketOrderArgs.add_position_fill() 32 | marketOrderArgs.add_take_profit_on_fill() 33 | marketOrderArgs.add_stop_loss_on_fill() 34 | marketOrderArgs.add_trailing_stop_loss_on_fill() 35 | marketOrderArgs.add_client_order_extensions() 36 | marketOrderArgs.add_client_trade_extensions() 37 | 38 | args = parser.parse_args() 39 | 40 | # 41 | # Create the api context based on the contents of the 42 | # v20 config file 43 | # 44 | api = args.config.create_context() 45 | 46 | # 47 | # Extract the Market order parameters from the parsed arguments 48 | # 49 | marketOrderArgs.parse_arguments(args) 50 | 51 | # 52 | # Submit the request to create the Market Order 53 | # 54 | response = api.order.market( 55 | args.config.active_account, 56 | **marketOrderArgs.parsed_args 57 | ) 58 | 59 | print("Response: {} ({})".format(response.status, response.reason)) 60 | print("") 61 | 62 | print_order_create_response_transactions(response) 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /src/account/configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import select 5 | import argparse 6 | import common.config 7 | from common.view import print_response_entity 8 | from .account import Account 9 | 10 | 11 | def main(): 12 | """ 13 | Create an API context, and use it to fetch an Account state and then 14 | continually poll for changes to it. 15 | 16 | The configuration for the context and Account to fetch is parsed from the 17 | config file provided as an argument. 18 | """ 19 | 20 | parser = argparse.ArgumentParser() 21 | 22 | # 23 | # The config object is initialized by the argument parser, and contains 24 | # the REST APID host, port, accountID, etc. 25 | # 26 | common.config.add_argument(parser) 27 | 28 | parser.add_argument( 29 | "--margin-rate", 30 | default=None, 31 | help="The new default margin rate for the Account" 32 | ) 33 | 34 | parser.add_argument( 35 | "--alias", 36 | default=None, 37 | help="The new alias for the Account" 38 | ) 39 | 40 | args = parser.parse_args() 41 | 42 | account_id = args.config.active_account 43 | 44 | # 45 | # The v20 config object creates the v20.Context for us based on the 46 | # contents of the config file. 47 | # 48 | api = args.config.create_context() 49 | 50 | kwargs = {} 51 | 52 | if args.alias is not None: 53 | kwargs["alias"] = args.alias 54 | 55 | if args.margin_rate is not None: 56 | kwargs["marginRate"] = args.margin_rate 57 | 58 | # 59 | # Fetch the details of the Account found in the config file 60 | # 61 | response = api.account.configure(account_id, **kwargs) 62 | 63 | if response.status == 200: 64 | print("Success") 65 | print("") 66 | 67 | print_response_entity( 68 | response, 69 | "200", 70 | "Configure Transaction", 71 | "configureTransaction" 72 | ) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /src/pricing/stream.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | from .view import price_to_string, heartbeat_to_string 7 | 8 | 9 | def main(): 10 | """ 11 | Stream the prices for a list of Instruments for the active Account. 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | common.config.add_argument(parser) 17 | 18 | parser.add_argument( 19 | '--instrument', "-i", 20 | type=common.args.instrument, 21 | required=True, 22 | action="append", 23 | help="Instrument to get prices for" 24 | ) 25 | 26 | parser.add_argument( 27 | '--snapshot', 28 | action="store_true", 29 | default=True, 30 | help="Request an initial snapshot" 31 | ) 32 | 33 | parser.add_argument( 34 | '--no-snapshot', 35 | dest="snapshot", 36 | action="store_false", 37 | help="Do not request an initial snapshot" 38 | ) 39 | 40 | parser.add_argument( 41 | '--show-heartbeats', "-s", 42 | action='store_true', 43 | default=False, 44 | help="display heartbeats" 45 | ) 46 | 47 | args = parser.parse_args() 48 | 49 | account_id = args.config.active_account 50 | 51 | api = args.config.create_streaming_context() 52 | 53 | # api.set_convert_decimal_number_to_native(False) 54 | 55 | # api.set_stream_timeout(3) 56 | 57 | # 58 | # Subscribe to the pricing stream 59 | # 60 | response = api.pricing.stream( 61 | account_id, 62 | snapshot=args.snapshot, 63 | instruments=",".join(args.instrument), 64 | ) 65 | 66 | # 67 | # Print out each price as it is received 68 | # 69 | for msg_type, msg in response.parts(): 70 | if msg_type == "pricing.Heartbeat" and args.show_heartbeats: 71 | print(heartbeat_to_string(msg)) 72 | elif msg_type == "pricing.Price": 73 | print(price_to_string(msg)) 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /src/account/instruments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | 7 | 8 | def main(): 9 | """ 10 | Create an API context, and use it to fetch and display the tradeable 11 | instruments for and Account. 12 | 13 | The configuration for the context and Account to fetch is parsed from the 14 | config file provided as an argument. 15 | """ 16 | 17 | parser = argparse.ArgumentParser() 18 | 19 | # 20 | # The config object is initialized by the argument parser, and contains 21 | # the REST APID host, port, accountID, etc. 22 | # 23 | common.config.add_argument(parser) 24 | 25 | args = parser.parse_args() 26 | 27 | account_id = args.config.active_account 28 | 29 | # 30 | # The v20 config object creates the v20.Context for us based on the 31 | # contents of the config file. 32 | # 33 | api = args.config.create_context() 34 | 35 | # 36 | # Fetch the tradeable instruments for the Account found in the config file 37 | # 38 | response = api.account.instruments(account_id) 39 | 40 | # 41 | # Extract the list of Instruments from the response. 42 | # 43 | instruments = response.get("instruments", "200") 44 | 45 | instruments.sort(key=lambda i: i.name) 46 | 47 | def marginFmt(instrument): 48 | return "{:.0f}:1 ({})".format( 49 | 1.0 / float(instrument.marginRate), 50 | instrument.marginRate 51 | ) 52 | 53 | def pipFmt(instrument): 54 | location = float(10 ** instrument.pipLocation) 55 | return "{:.4f}".format (location) 56 | 57 | # 58 | # Print the details of the Account's tradeable instruments 59 | # 60 | common.view.print_collection( 61 | "{} Instruments".format(len(instruments)), 62 | instruments, 63 | [ 64 | ("Name", lambda i: i.name), 65 | ("Type", lambda i: i.type), 66 | ("Pip", pipFmt), 67 | ("Margin Rate", marginFmt), 68 | ] 69 | ) 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /src/market_order_full_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.args 5 | from order.view import print_order_create_response_transactions 6 | import v20 7 | 8 | 9 | def main(): 10 | """ 11 | Create a Market Order in an Account based on the provided command-line 12 | arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add arguments for API connection 19 | # 20 | parser.add_argument( 21 | "--hostname", 22 | default="api-fxpractice.oanda.com", 23 | help="v20 REST Server hostname" 24 | ) 25 | 26 | parser.add_argument( 27 | "--port", 28 | type=int, 29 | default=443, 30 | help="v20 REST Server port" 31 | ) 32 | 33 | # 34 | # Add Account arguments 35 | # 36 | parser.add_argument( 37 | "accountid", 38 | help="v20 Account ID" 39 | ) 40 | 41 | parser.add_argument( 42 | "token", 43 | help="v20 Auth Token" 44 | ) 45 | 46 | # 47 | # Add arguments for minimal Market Order 48 | # 49 | parser.add_argument( 50 | "instrument", 51 | type=common.args.instrument, 52 | help="The instrument to place the Market Order for" 53 | ) 54 | 55 | parser.add_argument( 56 | "units", 57 | help="The number of units for the Market Order" 58 | ) 59 | 60 | args = parser.parse_args() 61 | 62 | # 63 | # Create the API context based on the provided arguments 64 | # 65 | api = v20.Context( 66 | args.hostname, 67 | args.port, 68 | token=args.token 69 | ) 70 | 71 | # 72 | # Submit the request to create the Market Order 73 | # 74 | response = api.order.market( 75 | args.accountid, 76 | instrument=args.instrument, 77 | units=args.units 78 | ) 79 | 80 | # 81 | # Process the response 82 | # 83 | print("Response: {} ({})".format(response.status, response.reason)) 84 | 85 | print("") 86 | 87 | print_order_create_response_transactions(response) 88 | 89 | 90 | if __name__ == "__main__": 91 | main() 92 | -------------------------------------------------------------------------------- /src/order/stop_loss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments, add_replace_order_id_argument 6 | from .view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Create or replace an OANDA Stop Loss Order in an Account based on the 12 | provided command-line arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | # 23 | # Add the argument to support replacing an existing argument 24 | # 25 | add_replace_order_id_argument(parser) 26 | 27 | # 28 | # Add the command line arguments required for a Limit Order 29 | # 30 | orderArgs = OrderArguments(parser) 31 | orderArgs.add_trade_id() 32 | orderArgs.add_price() 33 | orderArgs.add_time_in_force(["GTD", "GFD", "GTC"]) 34 | orderArgs.add_client_order_extensions() 35 | 36 | args = parser.parse_args() 37 | 38 | # 39 | # Create the api context based on the contents of the 40 | # v20 config file 41 | # 42 | api = args.config.create_context() 43 | 44 | # 45 | # Extract the Limit Order parameters from the parsed arguments 46 | # 47 | orderArgs.parse_arguments(args) 48 | 49 | if args.replace_order_id is not None: 50 | # 51 | # Submit the request to cancel and replace a Stop Loss Order 52 | # 53 | response = api.order.stop_loss_replace( 54 | args.config.active_account, 55 | args.replace_order_id, 56 | **orderArgs.parsed_args 57 | ) 58 | else: 59 | # 60 | # Submit the request to create a Stop Loss Order 61 | # 62 | response = api.order.stop_loss( 63 | args.config.active_account, 64 | **orderArgs.parsed_args 65 | ) 66 | 67 | print("Response: {} ({})".format(response.status, response.reason)) 68 | print("") 69 | 70 | print_order_create_response_transactions(response) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='v20-python-samples', 5 | version='0.1.1', 6 | packages=find_packages('src'), 7 | package_dir={'': 'src'}, 8 | entry_points={ 9 | 'console_scripts': [ 10 | 'v20-configure = configure:main', 11 | 'v20-market-order-full-example = market_order_full_example:main', 12 | 'v20-account-details = account.details:main', 13 | 'v20-account-summary = account.summary:main', 14 | 'v20-account-instruments = account.instruments:main', 15 | 'v20-account-changes = account.changes:main', 16 | 'v20-account-configure = account.configure:main', 17 | 'v20-instrument-candles = instrument.candles:main', 18 | 'v20-instrument-candles-poll = instrument.candles_poll:main', 19 | 'v20-order-get = order.get:main', 20 | 'v20-order-list-pending = order.list_pending:main', 21 | 'v20-order-cancel = order.cancel:main', 22 | 'v20-order-set-client-extensions = order.set_client_extensions:main', 23 | 'v20-order-market = order.market:main', 24 | 'v20-order-entry = order.entry:main', 25 | 'v20-order-limit = order.limit:main', 26 | 'v20-order-stop = order.stop:main', 27 | 'v20-order-take-profit = order.take_profit:main', 28 | 'v20-order-stop-loss = order.stop_loss:main', 29 | 'v20-order-trailing-stop-loss = order.trailing_stop_loss:main', 30 | 'v20-pricing-get = pricing.get:main', 31 | 'v20-pricing-stream = pricing.stream:main', 32 | 'v20-transaction-stream = transaction.stream:main', 33 | 'v20-transaction-poll = transaction.poll:main', 34 | 'v20-transaction-get = transaction.get:main', 35 | 'v20-transaction-range = transaction.range:main', 36 | 'v20-trade-get = trade.get:main', 37 | 'v20-trade-close = trade.close:main', 38 | 'v20-trade-set-client-extensions = trade.set_client_extensions:main', 39 | 'v20-position-close = position.close:main', 40 | ] 41 | } 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /src/order/take_profit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments, add_replace_order_id_argument 6 | from .view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Create or replace an OANDA Take Profit Order in an Account based on the 12 | provided command-line arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | # 23 | # Add the argument to support replacing an existing argument 24 | # 25 | add_replace_order_id_argument(parser) 26 | 27 | # 28 | # Add the command line arguments required for a Limit Order 29 | # 30 | orderArgs = OrderArguments(parser) 31 | orderArgs.add_trade_id() 32 | orderArgs.add_price() 33 | orderArgs.add_time_in_force(["GTD", "GFD", "GTC"]) 34 | orderArgs.add_client_order_extensions() 35 | 36 | args = parser.parse_args() 37 | 38 | # 39 | # Create the api context based on the contents of the 40 | # v20 config file 41 | # 42 | api = args.config.create_context() 43 | 44 | # 45 | # Extract the Limit Order parameters from the parsed arguments 46 | # 47 | orderArgs.parse_arguments(args) 48 | 49 | if args.replace_order_id is not None: 50 | # 51 | # Submit the request to cancel and replace a Take Profit Order 52 | # 53 | response = api.order.take_profit_replace( 54 | args.config.active_account, 55 | args.replace_order_id, 56 | **orderArgs.parsed_args 57 | ) 58 | else: 59 | # 60 | # Submit the request to create a Take Profit Order 61 | # 62 | response = api.order.take_profit( 63 | args.config.active_account, 64 | **orderArgs.parsed_args 65 | ) 66 | 67 | print("Response: {} ({})".format(response.status, response.reason)) 68 | print("") 69 | 70 | print_order_create_response_transactions(response) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /src/instrument/view.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from datetime import datetime 4 | 5 | 6 | class CandlePrinter(object): 7 | def __init__(self): 8 | self.width = { 9 | 'time' : 19, 10 | 'type' : 4, 11 | 'price' : 8, 12 | 'volume' : 6, 13 | } 14 | # setattr(self.width, "time", 19) 15 | self.time_width = 19 16 | 17 | def print_header(self): 18 | print("{:<{width[time]}} {:<{width[type]}} {:<{width[price]}} {:<{width[price]}} {:<{width[price]}} {:<{width[price]}} {:<{width[volume]}}".format( 19 | "Time", 20 | "Type", 21 | "Open", 22 | "High", 23 | "Low", 24 | "Close", 25 | "Volume", 26 | width=self.width 27 | )) 28 | 29 | print("{} {} {} {} {} {} {}".format( 30 | "=" * self.width['time'], 31 | "=" * self.width['type'], 32 | "=" * self.width['price'], 33 | "=" * self.width['price'], 34 | "=" * self.width['price'], 35 | "=" * self.width['price'], 36 | "=" * self.width['volume'] 37 | )) 38 | 39 | def print_candle(self, candle): 40 | try: 41 | time = str( 42 | datetime.strptime( 43 | candle.time, 44 | "%Y-%m-%dT%H:%M:%S.000000000Z" 45 | ) 46 | ) 47 | except: 48 | time = candle.time.split(".")[0] 49 | 50 | volume = candle.volume 51 | 52 | for price in ["mid", "bid", "ask"]: 53 | c = getattr(candle, price, None) 54 | 55 | if c is None: 56 | continue 57 | 58 | print("{:>{width[time]}} {:>{width[type]}} {:>{width[price]}} {:>{width[price]}} {:>{width[price]}} {:>{width[price]}} {:>{width[volume]}}".format( 59 | time, 60 | price, 61 | c.o, 62 | c.h, 63 | c.l, 64 | c.c, 65 | volume, 66 | width=self.width 67 | )) 68 | 69 | volume = "" 70 | time = "" 71 | -------------------------------------------------------------------------------- /src/position/close.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | import common.args 7 | from order.view import print_order_create_response_transactions 8 | 9 | 10 | def main(): 11 | """ 12 | Close an open Trade in an Account 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | parser.add_argument( 23 | "instrument", 24 | type=common.args.instrument, 25 | help=( 26 | "The Instrument of the Position to close. If prepended " 27 | "with an '@', this will be interpreted as a client Trade ID" 28 | ) 29 | ) 30 | 31 | parser.add_argument( 32 | "--long-units", 33 | default=None, 34 | help=( 35 | "The amount of the long Position to close. Either the string 'ALL' " 36 | "indicating a full Position close, the string 'NONE', or the " 37 | "number of units of the Position to close" 38 | ) 39 | ) 40 | 41 | parser.add_argument( 42 | "--short-units", 43 | default=None, 44 | help=( 45 | "The amount of the short Position to close. Either the string " 46 | "'ALL' indicating a full Position close, the string 'NONE', or the " 47 | "number of units of the Position to close" 48 | ) 49 | ) 50 | 51 | args = parser.parse_args() 52 | 53 | account_id = args.config.active_account 54 | 55 | # 56 | # Create the api context based on the contents of the 57 | # v20 config file 58 | # 59 | api = args.config.create_context() 60 | 61 | response = api.position.close( 62 | account_id, 63 | args.instrument, 64 | longUnits=args.long_units, 65 | shortUnits=args.short_units 66 | ) 67 | 68 | print( 69 | "Response: {} ({})\n".format( 70 | response.status, 71 | response.reason 72 | ) 73 | ) 74 | 75 | print_order_create_response_transactions(response) 76 | 77 | 78 | if __name__ == "__main__": 79 | main() 80 | -------------------------------------------------------------------------------- /src/order/trailing_stop_loss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments, add_replace_order_id_argument 6 | from .view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Create or replace an OANDA Trailing Stop Loss Order in an Account based on 12 | the provided command-line arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | # 23 | # Add the argument to support replacing an existing argument 24 | # 25 | add_replace_order_id_argument(parser) 26 | 27 | # 28 | # Add the command line arguments required for a Limit Order 29 | # 30 | orderArgs = OrderArguments(parser) 31 | orderArgs.add_trade_id() 32 | orderArgs.add_distance() 33 | orderArgs.add_time_in_force(["GTD", "GFD", "GTC"]) 34 | orderArgs.add_client_order_extensions() 35 | 36 | args = parser.parse_args() 37 | 38 | # 39 | # Create the api context based on the contents of the 40 | # v20 config file 41 | # 42 | api = args.config.create_context() 43 | 44 | # 45 | # Extract the Limit Order parameters from the parsed arguments 46 | # 47 | orderArgs.parse_arguments(args) 48 | 49 | if args.replace_order_id is not None: 50 | # 51 | # Submit the request to cancel and replace a Trailing Stop Loss Order 52 | # 53 | response = api.order.trailing_stop_loss_replace( 54 | args.config.active_account, 55 | args.replace_order_id, 56 | **orderArgs.parsed_args 57 | ) 58 | else: 59 | # 60 | # Submit the request to create a Trailing Stop Loss Order 61 | # 62 | response = api.order.trailing_stop_loss( 63 | args.config.active_account, 64 | **orderArgs.parsed_args 65 | ) 66 | 67 | print("Response: {} ({})".format(response.status, response.reason)) 68 | print("") 69 | 70 | print_order_create_response_transactions(response) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /src/trade/set_client_extensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | import v20.transaction 7 | 8 | 9 | def main(): 10 | """ 11 | Set the client extensions for an open Trade in an Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | # 17 | # Add the command line argument to parse to the v20 config 18 | # 19 | common.config.add_argument(parser) 20 | 21 | parser.add_argument( 22 | "tradeid", 23 | help=( 24 | "The ID of the Trade to set the client extensions for. If " 25 | "prepended with an '@', this will be interpreted as a client Order " 26 | "ID" 27 | ) 28 | ) 29 | 30 | parser.add_argument( 31 | "--client-id", 32 | help="The client-provided ID to assign to the Trade" 33 | ) 34 | 35 | parser.add_argument( 36 | "--tag", 37 | help="The client-provided tag to assign to the Trade" 38 | ) 39 | 40 | parser.add_argument( 41 | "--comment", 42 | help="The client-provided comment to assign to the Trade" 43 | ) 44 | 45 | args = parser.parse_args() 46 | 47 | if (args.client_id is None and 48 | args.tag is None and 49 | args.comment is None): 50 | parser.error("must provide at least one client extension to be set") 51 | 52 | clientExtensions = v20.transaction.ClientExtensions( 53 | id=args.client_id, 54 | comment=args.comment, 55 | tag=args.tag 56 | ) 57 | 58 | # 59 | # Create the api context based on the contents of the 60 | # v20 config file 61 | # 62 | api = args.config.create_context() 63 | 64 | account_id = args.config.active_account 65 | 66 | # 67 | # Submit the request to create the Market Order 68 | # 69 | response = api.trade.set_client_extensions( 70 | account_id, 71 | args.tradeid, 72 | clientExtensions=clientExtensions 73 | ) 74 | 75 | print("Response: {} ({})".format(response.status, response.reason)) 76 | print("") 77 | 78 | print( 79 | response.get( 80 | "tradeClientExtensionsModifyTransaction", 200 81 | ) 82 | ) 83 | 84 | 85 | if __name__ == "__main__": 86 | main() 87 | -------------------------------------------------------------------------------- /src/order/stop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments, add_replace_order_id_argument 6 | from .view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Create or replace an OANDA Stop Order in an Account based on the provided 12 | command-line arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | # 23 | # Add the argument to support replacing an existing argument 24 | # 25 | add_replace_order_id_argument(parser) 26 | 27 | # 28 | # Add the command line arguments required for a Stop Order 29 | # 30 | orderArgs = OrderArguments(parser) 31 | orderArgs.add_instrument() 32 | orderArgs.add_units() 33 | orderArgs.add_price() 34 | orderArgs.add_price_bound() 35 | orderArgs.add_time_in_force() 36 | orderArgs.add_position_fill() 37 | orderArgs.add_take_profit_on_fill() 38 | orderArgs.add_stop_loss_on_fill() 39 | orderArgs.add_trailing_stop_loss_on_fill() 40 | orderArgs.add_client_order_extensions() 41 | orderArgs.add_client_trade_extensions() 42 | 43 | args = parser.parse_args() 44 | 45 | # 46 | # Create the api context based on the contents of the 47 | # v20 config file 48 | # 49 | api = args.config.create_context() 50 | 51 | # 52 | # Extract the Stop Order parameters from the parsed arguments 53 | # 54 | orderArgs.parse_arguments(args) 55 | 56 | if args.replace_order_id is not None: 57 | # 58 | # Submit the request to cancel and replace a Stop Order 59 | # 60 | response = api.order.stop_replace( 61 | args.config.active_account, 62 | args.replace_order_id, 63 | **orderArgs.parsed_args 64 | ) 65 | else: 66 | # 67 | # Submit the request to create a Stop Order 68 | # 69 | response = api.order.stop( 70 | args.config.active_account, 71 | **orderArgs.parsed_args 72 | ) 73 | 74 | print("Response: {} ({})".format(response.status, response.reason)) 75 | print("") 76 | 77 | print_order_create_response_transactions(response) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /src/position/view.py: -------------------------------------------------------------------------------- 1 | import common.view 2 | 3 | 4 | def position_side_formatter(side_name): 5 | """ 6 | Create a formatter that extracts and formats the long or short side from a 7 | Position 8 | 9 | Args: 10 | side_name: "long" or "short" indicating which side of the position to 11 | format 12 | """ 13 | def f(p): 14 | """The formatting function for the long or short side""" 15 | 16 | side = getattr(p, side_name) 17 | 18 | if side is None: 19 | return "" 20 | if side.units == "0": 21 | return "" 22 | 23 | return "{} @ {}".format(side.units, side.averagePrice) 24 | 25 | return f 26 | 27 | 28 | def print_positions_map(positions_map, open_only=True): 29 | """ 30 | Print a map of Positions in table format. 31 | 32 | Args: 33 | positions: The map of instrument->Positions to print 34 | open_only: Flag that controls if only open Positions are displayed 35 | """ 36 | 37 | print_positions( 38 | sorted( 39 | positions_map.values(), 40 | key=lambda p: p.instrument 41 | ), 42 | open_only 43 | ) 44 | 45 | 46 | def print_positions(positions, open_only=True): 47 | """ 48 | Print a list of Positions in table format. 49 | 50 | Args: 51 | positions: The list of Positions to print 52 | open_only: Flag that controls if only open Positions are displayed 53 | """ 54 | 55 | filtered_positions = [ 56 | p for p in positions 57 | if not open_only or p.long.units != "0" or p.short.units != "0" 58 | ] 59 | 60 | if len(filtered_positions) == 0: 61 | return 62 | 63 | # 64 | # Print the Trades in a table with their Instrument, realized PL, 65 | # unrealized PL long postion summary and shor position summary 66 | # 67 | common.view.print_collection( 68 | "{} {}Positions".format( 69 | len(filtered_positions), 70 | "Open " if open_only else "" 71 | ), 72 | filtered_positions, 73 | [ 74 | ("Instrument", lambda p: p.instrument), 75 | ("P/L", lambda p: p.pl), 76 | ("Unrealized P/L", lambda p: p.unrealizedPL), 77 | ("Long", position_side_formatter("long")), 78 | ("Short", position_side_formatter("short")), 79 | ] 80 | ) 81 | 82 | print("") 83 | -------------------------------------------------------------------------------- /src/trade/get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | 7 | 8 | def main(): 9 | """ 10 | Get details of a specific Trade or all open Trades in an Account 11 | """ 12 | 13 | parser = argparse.ArgumentParser() 14 | 15 | # 16 | # Add the command line argument to parse to the v20 config 17 | # 18 | common.config.add_argument(parser) 19 | 20 | parser.add_argument( 21 | "--trade-id", "-t", 22 | help=( 23 | "The ID of the Trade to get. If prepended " 24 | "with an '@', this will be interpreted as a client Trade ID" 25 | ) 26 | ) 27 | 28 | parser.add_argument( 29 | "--all", "-a", 30 | action="store_true", 31 | default=False, 32 | help="Flag to get all open Trades in the Account" 33 | ) 34 | 35 | parser.add_argument( 36 | "--summary", "-s", 37 | dest="summary", 38 | action="store_true", 39 | help="Print Trade summary instead of full details", 40 | default=True 41 | ) 42 | 43 | parser.add_argument( 44 | "--verbose", "-v", 45 | dest="summary", 46 | help="Print Trade details instead of summary", 47 | action="store_false" 48 | ) 49 | 50 | args = parser.parse_args() 51 | 52 | if args.trade_id is None and not args.all: 53 | parser.error("Must provide --trade-id or --all") 54 | 55 | account_id = args.config.active_account 56 | 57 | # 58 | # Create the api context based on the contents of the 59 | # v20 config file 60 | # 61 | api = args.config.create_context() 62 | 63 | if args.all: 64 | response = api.trade.list_open(account_id) 65 | 66 | if not args.summary: 67 | print("-" * 80) 68 | 69 | for trade in reversed(response.get("trades", 200)): 70 | if args.summary: 71 | print(trade.title()) 72 | else: 73 | print(trade.yaml(True)) 74 | print("-" * 80) 75 | 76 | return 77 | 78 | if args.trade_id: 79 | response = api.trade.get(account_id, args.trade_id) 80 | 81 | trade = response.get("trade", 200) 82 | 83 | if args.summary: 84 | print(trade.title()) 85 | else: 86 | print(trade.yaml(True)) 87 | 88 | return 89 | 90 | 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /src/order/entry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments, add_replace_order_id_argument 6 | from .view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Create or replace an OANDA Entry Order in an Account based on the provided 12 | command-line arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | # 23 | # Add the argument to support replacing an existing argument 24 | # 25 | add_replace_order_id_argument(parser) 26 | 27 | # 28 | # Add the command line arguments required for an Entry Order 29 | # 30 | orderArgs = OrderArguments(parser) 31 | orderArgs.add_instrument() 32 | orderArgs.add_units() 33 | orderArgs.add_price() 34 | orderArgs.add_price_bound() 35 | orderArgs.add_time_in_force(["GTD", "GFD", "GTC"]) 36 | orderArgs.add_position_fill() 37 | orderArgs.add_take_profit_on_fill() 38 | orderArgs.add_stop_loss_on_fill() 39 | orderArgs.add_trailing_stop_loss_on_fill() 40 | orderArgs.add_client_order_extensions() 41 | orderArgs.add_client_trade_extensions() 42 | 43 | args = parser.parse_args() 44 | 45 | # 46 | # Create the api context based on the contents of the 47 | # v20 config file 48 | # 49 | api = args.config.create_context() 50 | 51 | # 52 | # Extract the Entry Order parameters from the parsed arguments 53 | # 54 | orderArgs.parse_arguments(args) 55 | 56 | if args.replace_order_id is not None: 57 | # 58 | # Submit the request to cancel and replace an Entry Order 59 | # 60 | response = api.order.market_if_touched_replace( 61 | args.config.active_account, 62 | args.replace_order_id, 63 | **orderArgs.parsed_args 64 | ) 65 | else: 66 | # 67 | # Submit the request to create an Entry Order 68 | # 69 | response = api.order.market_if_touched( 70 | args.config.active_account, 71 | **orderArgs.parsed_args 72 | ) 73 | 74 | print("Response: {} ({})".format(response.status, response.reason)) 75 | print("") 76 | 77 | print_order_create_response_transactions(response) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /src/order/limit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | from .args import OrderArguments, add_replace_order_id_argument 6 | from .view import print_order_create_response_transactions 7 | 8 | 9 | def main(): 10 | """ 11 | Create or replace an OANDA Limit Order in an Account based on the provided 12 | command-line arguments. 13 | """ 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | # 18 | # Add the command line argument to parse to the v20 config 19 | # 20 | common.config.add_argument(parser) 21 | 22 | # 23 | # Add the argument to support replacing an existing argument 24 | # 25 | add_replace_order_id_argument(parser) 26 | 27 | # 28 | # Add the command line arguments required for a Limit Order 29 | # 30 | orderArgs = OrderArguments(parser) 31 | orderArgs.add_instrument() 32 | orderArgs.add_units() 33 | orderArgs.add_price() 34 | orderArgs.add_time_in_force() 35 | orderArgs.add_position_fill() 36 | orderArgs.add_take_profit_on_fill() 37 | orderArgs.add_stop_loss_on_fill() 38 | orderArgs.add_trailing_stop_loss_on_fill() 39 | orderArgs.add_client_order_extensions() 40 | orderArgs.add_client_trade_extensions() 41 | 42 | args = parser.parse_args() 43 | 44 | # 45 | # Create the api context based on the contents of the 46 | # v20 config file 47 | # 48 | api = args.config.create_context() 49 | 50 | # 51 | # Use the api context's datetime formatter when serializing data 52 | # 53 | orderArgs.set_datetime_formatter(lambda dt: api.datetime_to_str(dt)) 54 | 55 | # 56 | # Extract the Limit Order parameters from the parsed arguments 57 | # 58 | orderArgs.parse_arguments(args) 59 | 60 | if args.replace_order_id is not None: 61 | # 62 | # Submit the request to cancel and replace a Limit Order 63 | # 64 | response = api.order.limit_replace( 65 | args.config.active_account, 66 | args.replace_order_id, 67 | **orderArgs.parsed_args 68 | ) 69 | else: 70 | # 71 | # Submit the request to create a Limit Order 72 | # 73 | response = api.order.limit( 74 | args.config.active_account, 75 | **orderArgs.parsed_args 76 | ) 77 | 78 | print("Response: {} ({})".format(response.status, response.reason)) 79 | print("") 80 | 81 | print_order_create_response_transactions(response) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /src/account/README.md: -------------------------------------------------------------------------------- 1 | # Account Samples 2 | 3 | The Account scripts are sample programs that interact with the v20 REST API 4 | Account endpoints described at 5 | http://developer.oanda.com/rest-live-v20/account-ep/. 6 | 7 | ## Account Summary 8 | 9 | The Account Summary script is implemented in `summary.py`, and is used 10 | to fetch and display the summary of the `active_account` found in the v20 11 | configuration file. It can be executed directly or with the provided entry 12 | point alias: 13 | 14 | ```bash 15 | (env)user@host: ~/v20-python-samples$ python src/account/summary.py 16 | (env)user@host: ~/v20-python-samples$ v20-account-summary 17 | ``` 18 | 19 | ## Account Details 20 | 21 | The Account Details script is implemented in `details.py`, and is used to fetch 22 | and display the full details (including open Trades, open Positions and pending 23 | Orders) of the `active_account` found in the v20 configuration file. It can be 24 | executed directly or with the provided entry point alias: 25 | 26 | ```bash 27 | (env)user@host: ~/v20-python-samples$ python src/account/details.py 28 | (env)user@host: ~/v20-python-samples$ v20-account-details 29 | ``` 30 | 31 | ## Account Instruments 32 | 33 | The Account Instruments script is implemented in `instruments.py`, and is used 34 | to fetch and display the list of tradeable instruments for the `active_account` 35 | found in the v20 configuration file. It can be executed directly or with the 36 | provided entry point alias: 37 | 38 | ```bash 39 | (env)user@host: ~/v20-python-samples$ python src/account/instruments.py 40 | (env)user@host: ~/v20-python-samples$ v20-account-instruments 41 | ``` 42 | 43 | ## Polling for Account Changes 44 | 45 | The Account Changes script is implemented in `changes.py`, and is used to fetch 46 | and display the current Account state, and then poll repeatedly for changes to 47 | it. This script provides a reference implementation for how OANDA recommends 48 | that Account state be managed. It can be executed directly or with the provided 49 | entry point alias: 50 | 51 | ```bash 52 | (env)user@host: ~/v20-python-samples$ python src/account/changes.py 53 | (env)user@host: ~/v20-python-samples$ v20-account-changes 54 | ``` 55 | 56 | ## Account Configuration 57 | 58 | The Account Configuration script is implemented in `configure.py`, and is used 59 | to modify client Account configuration (alias or default margin rate). It can 60 | be executed directly or with the provided entry point alias: 61 | 62 | ```bash 63 | (env)user@host: ~/v20-python-samples$ python src/account/configuration.py 64 | (env)user@host: ~/v20-python-samples$ v20-account-configuration 65 | ``` 66 | -------------------------------------------------------------------------------- /src/account/changes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import select 5 | import argparse 6 | import common.config 7 | from .account import Account 8 | 9 | def main(): 10 | """ 11 | Create an API context, and use it to fetch an Account state and then 12 | continually poll for changes to it. 13 | 14 | The configuration for the context and Account to fetch is parsed from the 15 | config file provided as an argument. 16 | """ 17 | 18 | parser = argparse.ArgumentParser() 19 | 20 | # 21 | # The config object is initialized by the argument parser, and contains 22 | # the REST APID host, port, accountID, etc. 23 | # 24 | common.config.add_argument(parser) 25 | 26 | parser.add_argument( 27 | "--poll-interval", 28 | type=int, 29 | default=5, 30 | help="The number of seconds between polls for Account changes" 31 | ) 32 | 33 | args = parser.parse_args() 34 | 35 | account_id = args.config.active_account 36 | 37 | # 38 | # The v20 config object creates the v20.Context for us based on the 39 | # contents of the config file. 40 | # 41 | api = args.config.create_context() 42 | 43 | # 44 | # Fetch the details of the Account found in the config file 45 | # 46 | response = api.account.get(account_id) 47 | 48 | # 49 | # Extract the Account representation from the response and use 50 | # it to create an Account wrapper 51 | # 52 | account = Account( 53 | response.get("account", "200") 54 | ) 55 | 56 | def dump(): 57 | account.dump() 58 | 59 | print("Press to see current state for Account {}".format( 60 | account.details.id 61 | )) 62 | 63 | dump() 64 | 65 | while True: 66 | i, o, e = select.select([sys.stdin], [], [], args.poll_interval) 67 | 68 | if i: 69 | sys.stdin.readline() 70 | dump() 71 | 72 | # 73 | # Poll for all changes to the account since the last 74 | # Account Transaction ID that was seen 75 | # 76 | response = api.account.changes( 77 | account_id, 78 | sinceTransactionID=account.details.lastTransactionID 79 | ) 80 | 81 | account.apply_changes( 82 | response.get( 83 | "changes", 84 | "200" 85 | ) 86 | ) 87 | 88 | account.apply_state( 89 | response.get( 90 | "state", 91 | "200" 92 | ) 93 | ) 94 | 95 | account.details.lastTransactionID = response.get( 96 | "lastTransactionID", 97 | "200" 98 | ) 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /src/pricing/get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | from .view import price_to_string 7 | import time 8 | 9 | 10 | def main(): 11 | """ 12 | Get the prices for a list of Instruments for the active Account. 13 | Repeatedly poll for newer prices if requested. 14 | """ 15 | 16 | parser = argparse.ArgumentParser() 17 | 18 | common.config.add_argument(parser) 19 | 20 | parser.add_argument( 21 | '--instrument', "-i", 22 | type=common.args.instrument, 23 | required=True, 24 | action="append", 25 | help="Instrument to get prices for" 26 | ) 27 | 28 | parser.add_argument( 29 | '--poll', "-p", 30 | action="store_true", 31 | default=False, 32 | help="Flag used to poll repeatedly for price updates" 33 | ) 34 | 35 | parser.add_argument( 36 | '--poll-interval', 37 | type=float, 38 | default=2, 39 | help="The interval between polls. Only relevant polling is enabled" 40 | ) 41 | 42 | args = parser.parse_args() 43 | 44 | account_id = args.config.active_account 45 | 46 | api = args.config.create_context() 47 | 48 | latest_price_time = None 49 | 50 | def poll(latest_price_time): 51 | """ 52 | Fetch and display all prices since than the latest price time 53 | 54 | Args: 55 | latest_price_time: The time of the newest Price that has been seen 56 | 57 | Returns: 58 | The updated latest price time 59 | """ 60 | 61 | response = api.pricing.get( 62 | account_id, 63 | instruments=",".join(args.instrument), 64 | since=latest_price_time, 65 | includeUnitsAvailable=False 66 | ) 67 | 68 | # 69 | # Print out all prices newer than the lastest time 70 | # seen in a price 71 | # 72 | for price in response.get("prices", 200): 73 | if latest_price_time is None or price.time > latest_price_time: 74 | print(price_to_string(price)) 75 | 76 | # 77 | # Stash and return the current latest price time 78 | # 79 | for price in response.get("prices", 200): 80 | if latest_price_time is None or price.time > latest_price_time: 81 | latest_price_time = price.time 82 | 83 | return latest_price_time 84 | 85 | # 86 | # Fetch the current snapshot of prices 87 | # 88 | latest_price_time = poll(latest_price_time) 89 | 90 | # 91 | # Poll for of prices 92 | # 93 | while args.poll: 94 | time.sleep(args.poll_interval) 95 | latest_price_time = poll(latest_price_time) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /src/order/cancel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.view 6 | from common.input import get_yn 7 | from .view import print_orders 8 | 9 | def main(): 10 | """ 11 | Cancel one or more Pending Orders in an Account 12 | """ 13 | 14 | parser = argparse.ArgumentParser() 15 | 16 | # 17 | # Add the command line argument to parse to the v20 config 18 | # 19 | common.config.add_argument(parser) 20 | 21 | parser.add_argument( 22 | "--order-id", "-o", 23 | help=( 24 | "The ID of the Order to cancel. If prepended " 25 | "with an '@', this will be interpreted as a client Order ID" 26 | ) 27 | ) 28 | 29 | parser.add_argument( 30 | "--all", "-a", 31 | action="store_true", 32 | default=False, 33 | help="Flag to cancel all Orders in the Account" 34 | ) 35 | 36 | args = parser.parse_args() 37 | 38 | account_id = args.config.active_account 39 | 40 | # 41 | # Create the api context based on the contents of the 42 | # v20 config file 43 | # 44 | api = args.config.create_context() 45 | 46 | if args.all: 47 | # 48 | # Get the list of all pending Orders 49 | # 50 | response = api.order.list_pending(account_id) 51 | 52 | orders = response.get("orders", 200) 53 | 54 | if len(orders) == 0: 55 | print("Account {} has no pending Orders to cancel".format( 56 | account_id 57 | )) 58 | return 59 | 60 | print_orders(orders) 61 | 62 | if not get_yn("Cancel all Orders?"): 63 | return 64 | 65 | # 66 | # Loop through the pending Orders and cancel each one 67 | # 68 | for order in orders: 69 | response = api.order.cancel(account_id, order.id) 70 | 71 | orderCancelTransaction = response.get("orderCancelTransaction", 200) 72 | 73 | print(orderCancelTransaction.title()) 74 | 75 | elif args.order_id is not None: 76 | 77 | # 78 | # Submit the request to create the Market Order 79 | # 80 | response = api.order.cancel( 81 | account_id, 82 | args.order_id 83 | ) 84 | 85 | print("Response: {} ({})".format(response.status, response.reason)) 86 | print("") 87 | 88 | common.view.print_response_entity( 89 | response, 200, "Order Cancel", "orderCancelTransaction" 90 | ) 91 | 92 | common.view.print_response_entity( 93 | response, 404, "Order Cancel Reject", "orderCancelRejectTransaction" 94 | ) 95 | else: 96 | parser.error("Must provide --order-id or --all") 97 | 98 | 99 | if __name__ == "__main__": 100 | main() 101 | -------------------------------------------------------------------------------- /src/common/input.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import getpass 4 | import sys 5 | 6 | 7 | def get_string(prompt, default=None): 8 | prompt = "{}{}: ".format( 9 | prompt, 10 | "" if default is None else " [{}]".format(default) 11 | ) 12 | 13 | try: i = raw_input 14 | except NameError: i = input 15 | 16 | value = None 17 | 18 | while value is None or len(value) == 0: 19 | try: 20 | value = i(prompt) or default 21 | except KeyboardInterrupt: 22 | print("") 23 | sys.exit() 24 | except EOFError: 25 | print("") 26 | sys.exit() 27 | except: 28 | pass 29 | 30 | return value 31 | 32 | 33 | def get_password(prompt): 34 | while True: 35 | try: 36 | password = getpass.getpass("{}: ".format(prompt)) 37 | if len(password) > 0: 38 | return password 39 | except KeyboardInterrupt: 40 | print("") 41 | sys.exit() 42 | except EOFError: 43 | print("") 44 | sys.exit() 45 | except: 46 | pass 47 | 48 | 49 | def get_yn(prompt, default=True): 50 | choice = None 51 | 52 | choices = "[yn]" 53 | 54 | if default is True: 55 | choices = choices.replace("y", "Y") 56 | elif default is False: 57 | choices = choices.replace("n", "N") 58 | 59 | prompt = "{} {}: ".format( 60 | prompt, 61 | choices 62 | ) 63 | 64 | try: i = raw_input 65 | except NameError: i = input 66 | 67 | while choice is None: 68 | try: 69 | s = i(prompt) 70 | 71 | if len(s) == 0 and default is not None: 72 | return default 73 | 74 | if len(s) > 1: 75 | continue 76 | 77 | s = s.lower() 78 | 79 | if s == "y": 80 | return True 81 | 82 | if s == "n": 83 | return False 84 | 85 | except KeyboardInterrupt: 86 | print("") 87 | sys.exit() 88 | except EOFError: 89 | print("") 90 | sys.exit() 91 | except: 92 | pass 93 | 94 | 95 | def get_from_list(choices, title, prompt, default=0): 96 | choice = None 97 | 98 | prompt = "{}{}: ".format( 99 | prompt, 100 | "" if default is None else " [{}]".format(default) 101 | ) 102 | 103 | if title is not None: 104 | print(title) 105 | 106 | for i, c in enumerate(choices): 107 | print("[{}] {}".format(i, c)) 108 | 109 | try: i = raw_input 110 | except NameError: i = input 111 | 112 | while choice is None: 113 | try: 114 | s = i(prompt) or default 115 | 116 | i = int(s) 117 | 118 | if i >= 0 and i < len(choices): 119 | choice = choices[i] 120 | except KeyboardInterrupt: 121 | print("") 122 | sys.exit() 123 | except EOFError: 124 | print("") 125 | sys.exit() 126 | except: 127 | pass 128 | 129 | return choice 130 | -------------------------------------------------------------------------------- /src/common/view.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from tabulate import tabulate 4 | 5 | 6 | def print_title(s): 7 | """ 8 | Print a string as a title with a strong underline 9 | 10 | Args: 11 | s: string to print as a title 12 | """ 13 | print(s) 14 | print(len(s) * "=") 15 | print("") 16 | 17 | 18 | def print_subtitle(s): 19 | """ 20 | Print a string as a subtitle with an underline 21 | 22 | Args: 23 | s: string to print as a title 24 | """ 25 | print(s) 26 | print(len(s) * "-") 27 | print("") 28 | 29 | 30 | def print_entity(entity, title=None, headers=True): 31 | """ 32 | Print an entity as a title along with the tabular representation 33 | of the entity. 34 | 35 | Args: 36 | title: The title to print 37 | entity: The entity to print 38 | """ 39 | 40 | if title is not None and len(title) > 0: 41 | print_title(title) 42 | 43 | headers = ["Name", "Value"] 44 | headers=[] 45 | tablefmt = "rst" 46 | body = [] 47 | 48 | for field in entity.fields(): 49 | name = field.displayName 50 | value = field.value 51 | if field.typeClass.startswith("array"): 52 | value = "[{}]".format(len(field.value)) 53 | elif field.typeClass.startswith("object"): 54 | value = "<{}>".format(field.typeName) 55 | body.append([name, value]) 56 | 57 | getattr(sys.stdout, 'buffer', sys.stdout).write( 58 | tabulate 59 | ( 60 | body, 61 | headers, 62 | tablefmt=tablefmt 63 | ).encode('utf-8') 64 | ) 65 | print("") 66 | 67 | 68 | def print_collection(title, entities, columns): 69 | """ 70 | Print a collection of entities with specified headers and formatters 71 | 72 | Args: 73 | title: The title to pring 74 | entites: The collection to print, one per row in the table 75 | columns: Tuple of column header name and column row formatter to be 76 | applied to each entity in the collection 77 | """ 78 | 79 | if len(entities) == 0: 80 | return 81 | 82 | if title is not None and len(title) > 0: 83 | print_title(title) 84 | 85 | headers = [c[0] for c in columns] 86 | tablefmt = "rst" 87 | body = [] 88 | 89 | for e in entities: 90 | body.append([c[1](e) for c in columns]) 91 | 92 | getattr(sys.stdout, 'buffer', sys.stdout).write( 93 | tabulate 94 | ( 95 | body, 96 | headers, 97 | tablefmt=tablefmt, 98 | ).encode('utf-8') 99 | ) 100 | print("") 101 | 102 | 103 | 104 | def print_response_entity( 105 | response, 106 | expected_status, 107 | title, 108 | transaction_name 109 | ): 110 | """ 111 | Print a Transaction from a response object if the Transaction exists and 112 | the response has the expected HTTP status code. 113 | 114 | If the Transaction doesn't exist in the response, this function silently 115 | fails and nothing is printed. 116 | 117 | Args: 118 | response: The response object to extract the Transaction from 119 | expected_status: The status that the response is expected to have 120 | title: The title to use for the rendered Transction 121 | transaction_name: The name of the Transaction expected 122 | """ 123 | 124 | try: 125 | transaction = response.get(transaction_name, expected_status) 126 | print_entity(transaction, title=title) 127 | print("") 128 | except: 129 | pass 130 | -------------------------------------------------------------------------------- /src/order/view.py: -------------------------------------------------------------------------------- 1 | import common.view 2 | 3 | 4 | def print_orders_map(orders_map): 5 | """ 6 | Print a map of Order Summaries in table format. 7 | 8 | Args: 9 | orders_map: The map of id->Order to print 10 | """ 11 | 12 | print_orders( 13 | sorted( 14 | orders_map.values(), 15 | key=lambda o: o.id 16 | ) 17 | ) 18 | 19 | 20 | def print_orders(orders): 21 | """ 22 | Print a collection or Orders in table format. 23 | 24 | Args: 25 | orders: The list or Orders to print 26 | """ 27 | 28 | # 29 | # Mapping from Order type to human-readable name 30 | # 31 | order_names = { 32 | "STOP" : "Stop", 33 | "LIMIT" : "Limit", 34 | "MARKET" : "Market", 35 | "MARKET_IF_TOUCHED" : "Entry", 36 | "ONE_CANCELS_ALL" : "One Cancels All", 37 | "TAKE_PROFIT" : "Take Profit", 38 | "STOP_LOSS" : "Stop Loss", 39 | "TRAILING_STOP_LOSS" : "Trailing Stop Loss" 40 | } 41 | 42 | # 43 | # Print the Orders in a table with their ID, type, state, and summary 44 | # 45 | common.view.print_collection( 46 | "{} Orders".format(len(orders)), 47 | orders, 48 | [ 49 | ("ID", lambda o: o.id), 50 | ("Type", lambda o: order_names.get(o.type, o.type)), 51 | ("State", lambda o: o.state), 52 | ("Summary", lambda o: o.summary()), 53 | ] 54 | ) 55 | 56 | print("") 57 | 58 | 59 | def print_order_create_response_transactions(response): 60 | """ 61 | Print out the transactions found in the order create response 62 | """ 63 | 64 | common.view.print_response_entity( 65 | response, None, 66 | "Order Create", 67 | "orderCreateTransaction" 68 | ) 69 | 70 | common.view.print_response_entity( 71 | response, None, 72 | "Long Order Create", 73 | "longOrderCreateTransaction" 74 | ) 75 | 76 | common.view.print_response_entity( 77 | response, None, 78 | "Short Order Create", 79 | "shortOrderCreateTransaction" 80 | ) 81 | 82 | common.view.print_response_entity( 83 | response, None, 84 | "Order Fill", 85 | "orderFillTransaction" 86 | ) 87 | 88 | common.view.print_response_entity( 89 | response, None, 90 | "Long Order Fill", 91 | "longOrderFillTransaction" 92 | ) 93 | 94 | common.view.print_response_entity( 95 | response, None, 96 | "Short Order Fill", 97 | "shortOrderFillTransaction" 98 | ) 99 | 100 | common.view.print_response_entity( 101 | response, None, 102 | "Order Cancel", 103 | "orderCancelTransaction" 104 | ) 105 | 106 | common.view.print_response_entity( 107 | response, None, 108 | "Long Order Cancel", 109 | "longOrderCancelTransaction" 110 | ) 111 | 112 | common.view.print_response_entity( 113 | response, None, 114 | "Short Order Cancel", 115 | "shortOrderCancelTransaction" 116 | ) 117 | 118 | common.view.print_response_entity( 119 | response, None, 120 | "Order Reissue", 121 | "orderReissueTransaction" 122 | ) 123 | 124 | common.view.print_response_entity( 125 | response, None, 126 | "Order Reject", 127 | "orderRejectTransaction" 128 | ) 129 | 130 | common.view.print_response_entity( 131 | response, None, 132 | "Order Reissue Reject", 133 | "orderReissueRejectTransaction" 134 | ) 135 | 136 | common.view.print_response_entity( 137 | response, None, 138 | "Replacing Order Cancel", 139 | "replacingOrderCancelTransaction" 140 | ) 141 | -------------------------------------------------------------------------------- /src/order/README.md: -------------------------------------------------------------------------------- 1 | # Order Scripts 2 | 3 | ## Get Order 4 | 5 | The script to get the details of a single Order is implemented in `get.py`. It 6 | may be used to fetch an Order in any state (pending, cancelled, filled). It can 7 | be executed directly or with the provided entry point alias: 8 | 9 | ```bash 10 | (env)user@host: ~/v20-python-samples$ python src/order/get.py 11 | (env)user@host: ~/v20-python-samples$ v20-order-get 12 | ``` 13 | ## Get Pending Orders 14 | 15 | The script to get the details of all currently pending Order in the active 16 | Account is implemented in `list_pending.py`. It can be executed directly or 17 | with the provided entry point alias: 18 | 19 | ```bash 20 | (env)user@host: ~/v20-python-samples$ python src/order/list_pending.py 21 | (env)user@host: ~/v20-python-samples$ v20-order-list-pending 22 | ``` 23 | 24 | ## Cancel Order(s) 25 | 26 | The script to cancel a pending Order in the active Account is implemented in 27 | `cancel.py`. It can be used to cancel a single Order or all currently pending 28 | Orders. It can be executed directly or with the provided entry point alias: 29 | 30 | ```bash 31 | (env)user@host: ~/v20-python-samples$ python src/order/cancel.py 32 | (env)user@host: ~/v20-python-samples$ v20-order-cancel 33 | ``` 34 | 35 | ## Set Order Client Extensions 36 | 37 | The script to set the client extensions for a pending Order in the active 38 | Account is implemented in `set_client_extensions.py`. It can be executed 39 | directly or with the provided entry point alias: 40 | 41 | ```bash 42 | (env)user@host: ~/v20-python-samples$ python src/order/set_client_extensions.py 43 | (env)user@host: ~/v20-python-samples$ v20-order-set-client-extensions 44 | ``` 45 | 46 | ## Create Market Order 47 | 48 | The script to create a Market Order in the active Account is implemented in 49 | `market.py`. It can be executed directly or with the provided entry point 50 | alias: 51 | 52 | ```bash 53 | (env)user@host: ~/v20-python-samples$ python src/order/market.py 54 | (env)user@host: ~/v20-python-samples$ v20-order-market 55 | ``` 56 | 57 | ## Create/Replace Entry Order 58 | 59 | The script to create or replace an Entry Order in the active Account is 60 | implemented in `entry.py`. It can be executed directly or with the provided 61 | entry point alias: 62 | 63 | ```bash 64 | (env)user@host: ~/v20-python-samples$ python src/order/entry.py 65 | (env)user@host: ~/v20-python-samples$ v20-order-entry 66 | ``` 67 | 68 | ## Create/Replace Limit Order 69 | 70 | The script to create or replace a Limit Order in the active Account is 71 | implemented in `limit.py`. It can be executed directly or with the provided 72 | entry point alias: 73 | 74 | ```bash 75 | (env)user@host: ~/v20-python-samples$ python src/order/limit.py 76 | (env)user@host: ~/v20-python-samples$ v20-order-limit 77 | ``` 78 | 79 | ## Create/Replace Stop Order 80 | 81 | The script to create or replace a Stop Order in the active Account is 82 | implemented in `stop.py`. It can be executed directly or with the provided 83 | entry point alias: 84 | 85 | ```bash 86 | (env)user@host: ~/v20-python-samples$ python src/order/stop.py 87 | (env)user@host: ~/v20-python-samples$ v20-order-stop 88 | ``` 89 | 90 | ## Create/Replace Take Profit Order 91 | 92 | The script to create or replace a Take Profit Order in the active Account is 93 | implemented in `take_profit.py`. It can be executed directly or with the 94 | provided entry point alias: 95 | 96 | ```bash 97 | (env)user@host: ~/v20-python-samples$ python src/order/take_profit.py 98 | (env)user@host: ~/v20-python-samples$ v20-order-take-profit 99 | ``` 100 | 101 | ## Create/Replace Stop Loss Order 102 | 103 | The script to create or replace a Stop Loss Order in the active Account is 104 | implemented in `stop_loss.py`. It can be executed directly or with the provided 105 | entry point alias: 106 | 107 | ```bash 108 | (env)user@host: ~/v20-python-samples$ python src/order/stop_loss.py 109 | (env)user@host: ~/v20-python-samples$ v20-order-stop-loss 110 | ``` 111 | 112 | ## Create/Replace Trailing Stop Loss Order 113 | 114 | The script to create or replace a Trailing Stop Loss Order in the active 115 | Account is implemented in `trailing_stop_loss.py`. It can be executed directly 116 | or with the provided entry point alias: 117 | 118 | ```bash 119 | (env)user@host: ~/v20-python-samples$ python src/order/trailing_stop_loss.py 120 | (env)user@host: ~/v20-python-samples$ v20-order-trailing-stop-loss 121 | ``` 122 | -------------------------------------------------------------------------------- /src/instrument/candles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | from .view import CandlePrinter 7 | from datetime import datetime 8 | 9 | 10 | def main(): 11 | """ 12 | Create an API context, and use it to fetch candles for an instrument. 13 | 14 | The configuration for the context is parsed from the config file provided 15 | as an argumentV 16 | """ 17 | 18 | parser = argparse.ArgumentParser() 19 | 20 | # 21 | # The config object is initialized by the argument parser, and contains 22 | # the REST APID host, port, accountID, etc. 23 | # 24 | common.config.add_argument(parser) 25 | 26 | parser.add_argument( 27 | "instrument", 28 | type=common.args.instrument, 29 | help="The instrument to get candles for" 30 | ) 31 | 32 | parser.add_argument( 33 | "--mid", 34 | action='store_true', 35 | help="Get midpoint-based candles" 36 | ) 37 | 38 | parser.add_argument( 39 | "--bid", 40 | action='store_true', 41 | help="Get bid-based candles" 42 | ) 43 | 44 | parser.add_argument( 45 | "--ask", 46 | action='store_true', 47 | help="Get ask-based candles" 48 | ) 49 | 50 | parser.add_argument( 51 | "--smooth", 52 | action='store_true', 53 | help="'Smooth' the candles" 54 | ) 55 | 56 | parser.set_defaults(mid=False, bid=False, ask=False) 57 | 58 | parser.add_argument( 59 | "--granularity", 60 | default=None, 61 | help="The candles granularity to fetch" 62 | ) 63 | 64 | parser.add_argument( 65 | "--count", 66 | default=None, 67 | help="The number of candles to fetch" 68 | ) 69 | 70 | date_format = "%Y-%m-%d %H:%M:%S" 71 | 72 | parser.add_argument( 73 | "--from-time", 74 | default=None, 75 | type=common.args.date_time(), 76 | help="The start date for the candles to be fetched. Format is 'YYYY-MM-DD HH:MM:SS'" 77 | ) 78 | 79 | parser.add_argument( 80 | "--to-time", 81 | default=None, 82 | type=common.args.date_time(), 83 | help="The end date for the candles to be fetched. Format is 'YYYY-MM-DD HH:MM:SS'" 84 | ) 85 | 86 | parser.add_argument( 87 | "--alignment-timezone", 88 | default=None, 89 | help="The timezone to used for aligning daily candles" 90 | ) 91 | 92 | args = parser.parse_args() 93 | 94 | account_id = args.config.active_account 95 | 96 | # 97 | # The v20 config object creates the v20.Context for us based on the 98 | # contents of the config file. 99 | # 100 | api = args.config.create_context() 101 | 102 | kwargs = {} 103 | 104 | if args.granularity is not None: 105 | kwargs["granularity"] = args.granularity 106 | 107 | if args.smooth is not None: 108 | kwargs["smooth"] = args.smooth 109 | 110 | if args.count is not None: 111 | kwargs["count"] = args.count 112 | 113 | if args.from_time is not None: 114 | kwargs["fromTime"] = api.datetime_to_str(args.from_time) 115 | 116 | if args.to_time is not None: 117 | kwargs["toTime"] = api.datetime_to_str(args.to_time) 118 | 119 | if args.alignment_timezone is not None: 120 | kwargs["alignmentTimezone"] = args.alignment_timezone 121 | 122 | price = "mid" 123 | 124 | if args.mid: 125 | kwargs["price"] = "M" + kwargs.get("price", "") 126 | price = "mid" 127 | 128 | if args.bid: 129 | kwargs["price"] = "B" + kwargs.get("price", "") 130 | price = "bid" 131 | 132 | if args.ask: 133 | kwargs["price"] = "A" + kwargs.get("price", "") 134 | price = "ask" 135 | 136 | # 137 | # Fetch the candles 138 | # 139 | response = api.instrument.candles(args.instrument, **kwargs) 140 | 141 | if response.status != 200: 142 | print(response) 143 | print(response.body) 144 | return 145 | 146 | print("Instrument: {}".format(response.get("instrument", 200))) 147 | print("Granularity: {}".format(response.get("granularity", 200))) 148 | 149 | printer = CandlePrinter() 150 | 151 | printer.print_header() 152 | 153 | candles = response.get("candles", 200) 154 | 155 | for candle in response.get("candles", 200): 156 | printer.print_candle(candle) 157 | 158 | 159 | if __name__ == "__main__": 160 | main() 161 | -------------------------------------------------------------------------------- /src/instrument/candles_poll.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import common.config 5 | import common.args 6 | from datetime import datetime 7 | import curses 8 | import random 9 | import time 10 | 11 | 12 | class CandlePrinter(): 13 | def __init__(self, stdscr): 14 | self.stdscr = stdscr 15 | 16 | self.stdscr.clear() 17 | (h, w) = self.stdscr.getmaxyx() 18 | self.height = h 19 | self.width = w 20 | 21 | self.field_width = { 22 | 'time' : 19, 23 | 'price' : 8, 24 | 'volume' : 6, 25 | } 26 | 27 | def set_instrument(self, instrument): 28 | self.instrument = instrument 29 | 30 | def set_granularity(self, granularity): 31 | self.granularity = granularity 32 | 33 | def set_candles(self, candles): 34 | self.candles = candles 35 | 36 | def update_candles(self, candles): 37 | new = candles[0] 38 | last = self.candles[-1] 39 | 40 | # Candles haven't changed 41 | if new.time == last.time and new.volume == last.time: 42 | return False 43 | 44 | # Update last candle 45 | self.candles[-1] = candles.pop(0) 46 | 47 | # Add the newer candles 48 | self.candles.extend(candles) 49 | 50 | # Get rid of the oldest candles 51 | self.candles = self.candles[-self.max_candle_count():] 52 | 53 | return True 54 | 55 | def max_candle_count(self): 56 | return self.height - 3 57 | 58 | def last_candle_time(self): 59 | return self.candles[-1].time 60 | 61 | def render(self): 62 | title = "{} ({})".format(self.instrument, self.granularity) 63 | 64 | header = ( 65 | "{:<{width[time]}} {:>{width[price]}} " 66 | "{:>{width[price]}} {:>{width[price]}} {:>{width[price]}} " 67 | "{:<{width[volume]}}" 68 | ).format( 69 | "Time", 70 | "Open", 71 | "High", 72 | "Low", 73 | "Close", 74 | "Volume", 75 | width=self.field_width 76 | ) 77 | 78 | x = int((len(header) - len(title)) / 2) 79 | 80 | self.stdscr.addstr( 81 | 0, 82 | x, 83 | title, 84 | curses.A_BOLD 85 | ) 86 | 87 | self.stdscr.addstr(2, 0, header, curses.A_UNDERLINE) 88 | 89 | y = 3 90 | 91 | for candle in self.candles: 92 | time = candle.time.split(".")[0] 93 | volume = candle.volume 94 | 95 | for price in ["mid", "bid", "ask"]: 96 | c = getattr(candle, price, None) 97 | 98 | if c is None: 99 | continue 100 | 101 | candle_str = ( 102 | "{:>{width[time]}} {:>{width[price]}} " 103 | "{:>{width[price]}} {:>{width[price]}} " 104 | "{:>{width[price]}} {:>{width[volume]}}" 105 | ).format( 106 | time, 107 | c.o, 108 | c.h, 109 | c.l, 110 | c.c, 111 | volume, 112 | width=self.field_width 113 | ) 114 | 115 | self.stdscr.addstr(y, 0, candle_str) 116 | 117 | y += 1 118 | 119 | break 120 | 121 | self.stdscr.move(0, 0) 122 | 123 | self.stdscr.refresh() 124 | 125 | 126 | 127 | def main(): 128 | """ 129 | Create an API context, and use it to fetch candles for an instrument. 130 | 131 | The configuration for the context is parsed from the config file provided 132 | as an argumentV 133 | """ 134 | 135 | parser = argparse.ArgumentParser() 136 | 137 | # 138 | # The config object is initialized by the argument parser, and contains 139 | # the REST APID host, port, accountID, etc. 140 | # 141 | common.config.add_argument(parser) 142 | 143 | parser.add_argument( 144 | "instrument", 145 | type=common.args.instrument, 146 | help="The instrument to get candles for" 147 | ) 148 | 149 | parser.add_argument( 150 | "--granularity", 151 | default=None, 152 | help="The candles granularity to fetch" 153 | ) 154 | 155 | args = parser.parse_args() 156 | 157 | account_id = args.config.active_account 158 | 159 | # 160 | # The v20 config object creates the v20.Context for us based on the 161 | # contents of the config file. 162 | # 163 | api = args.config.create_context() 164 | 165 | def poll_candles(stdscr): 166 | kwargs = {} 167 | 168 | if args.granularity is not None: 169 | kwargs["granularity"] = args.granularity 170 | 171 | # 172 | # Fetch the candles 173 | # 174 | printer = CandlePrinter(stdscr) 175 | 176 | # 177 | # The printer decides how many candles can be displayed based on the size 178 | # of the terminal 179 | # 180 | kwargs["count"] = printer.max_candle_count() 181 | 182 | response = api.instrument.candles(args.instrument, **kwargs) 183 | 184 | if response.status != 200: 185 | print(response) 186 | print(response.body) 187 | return 188 | 189 | # 190 | # Get the initial batch of candlesticks to display 191 | # 192 | instrument = response.get("instrument", 200) 193 | 194 | granularity = response.get("granularity", 200) 195 | 196 | printer.set_instrument(instrument) 197 | 198 | printer.set_granularity(granularity) 199 | 200 | printer.set_candles( 201 | response.get("candles", 200) 202 | ) 203 | 204 | printer.render() 205 | 206 | # 207 | # Poll for candles updates every second and redraw 208 | # the results 209 | # 210 | while True: 211 | time.sleep(1) 212 | 213 | kwargs = { 214 | 'granularity': granularity, 215 | 'fromTime': printer.last_candle_time() 216 | } 217 | 218 | response = api.instrument.candles(args.instrument, **kwargs) 219 | 220 | candles = response.get("candles", 200) 221 | 222 | if printer.update_candles(candles): 223 | printer.render() 224 | 225 | curses.wrapper(poll_candles) 226 | 227 | if __name__ == "__main__": 228 | main() 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v20-python-samples 2 | 3 | This repo contains a suite of Python sample code that desmonstrates the use of 4 | OANDA's v20 REST API along with OANDA's v20 bindings for Python. 5 | 6 | ## Setup 7 | 8 | The following procedure describes how to create a virtualenv appropriate for 9 | running the v20 sample code: 10 | 11 | ```bash 12 | # 13 | # Set up the virtualenv and install required packages. By default the 14 | # virtualenv will be setup to use python3. If python2 is desired, use the make 15 | # target "bootstrap-python2" and the virtualenv will be created under 16 | # "env-python2" 17 | # 18 | user@host: ~/v20-python-samples$ make bootstrap 19 | 20 | # 21 | # Enter the virtualenv 22 | # 23 | user@host: ~/v20-python-samples$ source env/bin/activate 24 | 25 | # 26 | # Create the v20-* launch entry points in the virtualenv. These entry points 27 | # are aliases for the scripts which use the v20 REST API to interact with an 28 | # account (e.g. v20-market-order, v20-trades-list, etc.) 29 | # 30 | (env)user@host: ~/v20-python-samples$ python setup.py develop 31 | ``` 32 | 33 | ## Entering the v20 environment 34 | 35 | The v20-python-samples virtualenv must be activated to ensure that the current 36 | enviroment is set up correctly to run the sample code. This is done using the 37 | virualenv's activate script: 38 | 39 | ```bash 40 | user@host: ~/v20-python-samples$ source env/bin/activate 41 | (env)user@host: ~/v20-python-samples$ 42 | ``` 43 | 44 | The "(env)" prefix found in the prompt indicates that we are using the 45 | virtualenv "env". To leave the virtualenv, run the deactivate function: 46 | 47 | ```bash 48 | (env)user@host: ~/v20-python-samples$ deactivate 49 | user@host: ~/v20-python-samples$ 50 | ``` 51 | 52 | 53 | ## Configuration-free Example 54 | 55 | Most of the examples provided use a v20.conf discussed below. For a full 56 | example of how to create and use a v20 API context without the configuration 57 | wrapper, please examine `src/market_order_full_example.py`. This program 58 | enables the creation of a limited Market Order solely based on command line 59 | arguments. 60 | 61 | 62 | ## Configuration 63 | 64 | Using OANDA's v20 REST API requires configuration to set up connections and 65 | interact with the endpoints. This configuration includes: 66 | 67 | * API hostname 68 | * API port 69 | * API token 70 | * username of client 71 | * Account ID of account being manipulated 72 | 73 | To simplify the management of this configuration, the v20 Python sample code 74 | requires that a configuration file be created. All of the sample code loads 75 | this configuration file prior to connecting to the v20 system. 76 | 77 | ### v20 Configuration File Format 78 | 79 | The v20 configuration is stored in a YAML file that resembles the following: 80 | 81 | ```yaml 82 | hostname: api-fxpractice.oanda.com 83 | streaming_hostname: stream-fxpractice.oanda.com 84 | port: 443 85 | ssl: true 86 | token: e6ab562b039325f12a026c6fdb7b71bb-b3d8721445817159410f01514acd19hbc 87 | username: user 88 | accounts: 89 | - 101-001-100000-001 90 | - 101-001-100000-002 91 | active_account: 101-001-100000-001 92 | ``` 93 | 94 | ### Generating v20 Configuration files 95 | 96 | v20 configuration files may be generated manually, however a script is provided that 97 | will generate one interactively located at `src/configure.py`. 98 | 99 | To run it and generate a v20 configuration file, simply run: 100 | 101 | ```bash 102 | (env)user@host: ~/v20-python-samples$ v20-configure 103 | ``` 104 | 105 | and follow the instructions. 106 | 107 | ### Using v20 Configuration files 108 | 109 | There are several ways to load a v20 configuration file in each of v20 sample scripts: 110 | 111 | #### 1. Run the script with the `--config` option 112 | 113 | The `--config` options allows you to specify the location of a valid v20 configuration file v20. Example: 114 | 115 | ```bash 116 | (env)user@host: ~/v20-python-samples$ v20-account-details --config /home/user/v20.conf 117 | ``` 118 | 119 | #### 2. Use the default v20 configuration file location 120 | 121 | The default location for the v20 configuration file is `~/.v20.conf`. If a v20 122 | configuration file exists in the default location, no `--config` option needs 123 | to be used. Example: 124 | 125 | ```bash 126 | # Looks for config file at ~/.v20.conf 127 | (env)user@host: ~/v20-python-samples$ v20-account-details 128 | ``` 129 | 130 | #### 3. Set the location of the `V20_CONF` environment variable 131 | 132 | This `V20_CONF` environment variable changes what the default location of the 133 | v20 configuration file is. If a configuration file exists in this location, no 134 | `--config` option needs to be used. Example: 135 | 136 | ```bash 137 | (env)user@host: ~/v20-python-samples$ export V20_CONF=/home/user/v20.conf 138 | (env)user@host: ~/v20-python-samples$ v20-account-details 139 | ``` 140 | 141 | 142 | ## Sample Code 143 | 144 | Following is a listing of the sample code provided. More details can be found 145 | in the READMEs provided in each src directory. 146 | 147 | | Source File | Entry Point | Description | 148 | | ----------- | ----------- | ----------- | 149 | | `src/configure.py` | v20-configure | Create/update a v20.conf file | 150 | | `src/market_order_full_example.py` | v20-market-order-full-example | Limited Market Order example that does not use the v20.conf file | 151 | | `src/account/details.py` | v20-account-details | Get the details of the current active Account | 152 | | `src/account/summary.py` | v20-account-summary | Get the summary of the current active Account | 153 | | `src/account/instruments.py` | v20-account-instruments | Get the list of tradeable instruments for the current active Account | 154 | | `src/account/changes.py` | v20-account-changes | Follow changes to the current active Account | 155 | | `src/account/configure.py` | v20-account-configure | Set configuration in the current active Account | 156 | | `src/instrument/candles.py` | v20-instrument-candles | Fetch candles for an instrument | 157 | | `src/instrument/candles_poll.py` | v20-instrument-candles-poll | Fetch and poll for candle updates for an instrument | 158 | | `src/order/get.py` | v20-order-get | Get the details of an order in the current active Account | 159 | | `src/order/list_pending.py` | v20-order-list-pending | List all pending Orders for the current active Account | 160 | | `src/order/cancel.py` | v20-order-cancel | Cancel a pending Order in the current active Account | 161 | | `src/order/set_client_extensions.py` | v20-order-set-client-extensions | Set the client extensions for a pending Order in the current active Account | 162 | | `src/order/market.py` | v20-order-market | Create a Market Order in the current active Account | 163 | | `src/order/entry.py` | v20-order-entry | Create or replace an Entry Order in the current active Account | 164 | | `src/order/limit.py` | v20-order-limit | Create or replace a Limit Order in the current active Account | 165 | | `src/order/stop.py` | v20-order-stop | Create or replace a Stop Order in the current active Account | 166 | | `src/order/take-profit.py` | v20-order-take-profit | Create or replace a Take Profit Order in the current active Account | 167 | | `src/order/stop-loss.py` | v20-order-stop-loss | Create or replace a Stop Loss Order in the current active Account | 168 | | `src/order/trailing-stop-loss.py` | v20-order-trailing-stop-loss | Create or replace a Trailing Stop Loss Order in the current active Account | 169 | | `src/pricing/get.py` | v20-pricing-get | Fetch/poll the current Prices for a list of Instruments | 170 | | `src/pricing/stream.py` | v20-pricing-stream | Stream Prices for a list of Instruments | 171 | | `src/transaction/stream.py` | v20-transaction-stream | Stream Transactions for the current active Account | 172 | | `src/transaction/poll.py` | v20-transaction-poll | Poll Transactions for the current active Account | 173 | | `src/transaction/get.py` | v20-transaction-get | Get details for a Transaction in the current active Account | 174 | | `src/transaction/range.py` | v20-transaction-range | Get a range of Transactions in the current active Account | 175 | | `src/trade/get.py` | v20-trade-get | Get all open Trades or a specific Trade in the current active Account | 176 | | `src/trade/close.py` | v20-trade-close | Close (partially or fully) a Trade in the current active Account | 177 | | `src/trade/set_client_extensions.py` | v20-trade-set-client-extensions | Set the client extensions for an open Trade in the current active Account | 178 | | `src/position/close.py` | v20-position-close | Close a position for an instrument in the current active Account | 179 | -------------------------------------------------------------------------------- /src/account/account.py: -------------------------------------------------------------------------------- 1 | import common.view 2 | from position.view import print_positions_map 3 | from order.view import print_orders_map 4 | from trade.view import print_trades_map 5 | 6 | def update_attribute(dest, name, value): 7 | """ 8 | Set dest[name] to value if it exists and is not None 9 | """ 10 | 11 | if hasattr(dest, name) and \ 12 | getattr(dest, name) is not None: 13 | setattr(dest, name, value) 14 | 15 | class Account(object): 16 | """ 17 | An Account object is a wrapper for the Account entities fetched from the 18 | v20 REST API. It is used for caching and updating Account state. 19 | """ 20 | def __init__(self, account, transaction_cache_depth=100): 21 | """ 22 | Create a new Account wrapper 23 | 24 | Args: 25 | account: a v20.account.Account fetched from the server 26 | """ 27 | 28 | # 29 | # The collection of Trades open in the Account 30 | # 31 | self.trades = {} 32 | 33 | for trade in getattr(account, "trades", []): 34 | self.trades[trade.id] = trade 35 | 36 | setattr(account, "trades", None) 37 | 38 | # 39 | # The collection of Orders pending in the Account 40 | # 41 | self.orders = {} 42 | 43 | for order in getattr(account, "orders", []): 44 | self.orders[order.id] = order 45 | 46 | setattr(account, "orders", None) 47 | 48 | # 49 | # Map from OrderID -> OrderState. Order State is tracked for 50 | # TrailingStopLoss orders, and includes the trailingStopValue 51 | # and triggerDistance 52 | # 53 | self.order_states = {} 54 | 55 | # 56 | # Teh collection of Positions open in the Account 57 | # 58 | self.positions = {} 59 | 60 | for position in getattr(account, "positions", []): 61 | self.positions[position.instrument] = position 62 | 63 | setattr(account, "positions", None) 64 | 65 | # 66 | # Keep a cache of the last self.transaction_cache_depth Transactions 67 | # 68 | self.transaction_cache_depth = transaction_cache_depth 69 | self.transactions = [] 70 | 71 | # 72 | # The Account details 73 | # 74 | self.details = account 75 | 76 | 77 | def dump(self): 78 | """ 79 | Print out the whole Account state 80 | """ 81 | 82 | common.view.print_entity( 83 | self.details, 84 | title=self.details.title() 85 | ) 86 | 87 | print("") 88 | 89 | print_positions_map(self.positions) 90 | 91 | print_orders_map(self.orders) 92 | 93 | print_trades_map(self.trades) 94 | 95 | 96 | def trade_get(self, id): 97 | """ 98 | Fetch an open Trade 99 | 100 | Args: 101 | id: The ID of the Trade to fetch 102 | 103 | Returns: 104 | The Trade with the matching ID if it exists, None otherwise 105 | """ 106 | 107 | return self.trades.get(id, None) 108 | 109 | def order_get(self, id): 110 | """ 111 | Fetch a pending Order 112 | 113 | Args: 114 | id: The ID of the Order to fetch 115 | 116 | Returns: 117 | The Order with the matching ID if it exists, None otherwise 118 | """ 119 | 120 | return self.orders.get(id, None) 121 | 122 | 123 | def position_get(self, instrument): 124 | """ 125 | Fetch an open Position 126 | 127 | Args: 128 | instrument: The instrument of the Position to fetch 129 | 130 | Returns: 131 | The Position with the matching instrument if it exists, None 132 | otherwise 133 | """ 134 | 135 | return self.positions.get(instrument, None) 136 | 137 | 138 | def apply_changes(self, changes): 139 | """ 140 | Update the Account state with a set of changes provided by the server. 141 | 142 | Args: 143 | changes: a v20.account.AccountChanges object representing the 144 | changes that have been made to the Account 145 | """ 146 | 147 | for order in changes.ordersCreated: 148 | print("[Order Created] {}".format(order.title())) 149 | self.orders[order.id] = order 150 | 151 | for order in changes.ordersCancelled: 152 | print("[Order Cancelled] {}".format(order.title())) 153 | self.orders.pop(order.id, None) 154 | 155 | for order in changes.ordersFilled: 156 | print("[Order Filled] {}".format(order.title())) 157 | self.orders.pop(order.id, None) 158 | 159 | for order in changes.ordersTriggered: 160 | print("[Order Triggered] {}".format(order.title())) 161 | self.orders.pop(order.id, None) 162 | 163 | for trade in changes.tradesOpened: 164 | print("[Trade Opened] {}".format(trade.title())) 165 | self.trades[trade.id] = trade 166 | 167 | for trade in changes.tradesReduced: 168 | print("[Trade Reduced] {}".format(trade.title())) 169 | self.trades[trade.id] = trade 170 | 171 | for trade in changes.tradesClosed: 172 | print("[Trade Closed] {}".format(trade.title())) 173 | self.trades.pop(trade.id, None) 174 | 175 | for position in changes.positions: 176 | print("[Position Changed] {}".format(position.instrument)) 177 | self.positions[position.instrument] = position 178 | 179 | for transaction in changes.transactions: 180 | print("[Transaction] {}".format(transaction.title())) 181 | 182 | self.transactions.append(transaction) 183 | 184 | if len(self.transactions) > self.transaction_cache_depth: 185 | self.transactions.pop(0) 186 | 187 | 188 | def apply_trade_states(self, trade_states): 189 | """ 190 | Update state for open Trades 191 | 192 | Args: 193 | trade_states: A list of v20.trade.CalculatedTradeState objects 194 | representing changes to the state of open Trades 195 | 196 | """ 197 | for trade_state in trade_states: 198 | trade = self.trade_get(trade_state.id) 199 | 200 | if trade is None: 201 | continue 202 | 203 | for field in trade_state.fields(): 204 | setattr(trade, field.name, field.value) 205 | 206 | 207 | def apply_position_states(self, position_states): 208 | """ 209 | Update state for all Positions 210 | 211 | Args: 212 | position_states: A list of v20.trade.CalculatedPositionState objects 213 | representing changes to the state of open Position 214 | 215 | """ 216 | 217 | for position_state in position_states: 218 | position = self.position_get(position_state.instrument) 219 | 220 | if position is None: 221 | continue 222 | 223 | position.unrealizedPL = position_state.netUnrealizedPL 224 | position.long.unrealizedPL = position_state.longUnrealizedPL 225 | position.short.unrealizedPL = position_state.shortUnrealizedPL 226 | 227 | 228 | def apply_order_states(self, order_states): 229 | """ 230 | Update state for all Orders 231 | 232 | Args: 233 | order_states: A list of v20.order.DynamicOrderState objects 234 | representing changes to the state of pending Orders 235 | """ 236 | 237 | for order_state in order_states: 238 | order = self.order_get(order_state.id) 239 | 240 | if order is None: 241 | continue 242 | 243 | order.trailingStopValue = order_state.trailingStopValue 244 | 245 | self.order_states[order.id] = order_state 246 | 247 | 248 | def apply_state(self, state): 249 | """ 250 | Update the state of an Account 251 | 252 | Args: 253 | state: A v20.account.AccountState object representing changes to 254 | the Account's trades, positions, orders and state. 255 | """ 256 | 257 | # 258 | # Update Account details from the state 259 | # 260 | for field in state.fields(): 261 | update_attribute(self.details, field.name, field.value) 262 | 263 | self.apply_trade_states(state.trades) 264 | 265 | self.apply_position_states(state.positions) 266 | 267 | self.apply_order_states(state.orders) 268 | -------------------------------------------------------------------------------- /src/common/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import yaml 3 | import os 4 | import sys 5 | import v20 6 | 7 | from common import input 8 | 9 | 10 | # 11 | # The default environment variable that points to the location of the v20 12 | # configuration file 13 | # 14 | DEFAULT_ENV = "V20_CONF" 15 | 16 | # 17 | # The default path for the v20 configuration file 18 | # 19 | DEFAULT_PATH = "~/.v20.conf" 20 | 21 | 22 | class ConfigPathError(Exception): 23 | """ 24 | Exception that indicates that the path specifed for a v20 config file 25 | location doesn't exist 26 | """ 27 | 28 | def __init__(self, path): 29 | self.path = path 30 | 31 | def __str__(self): 32 | return "Config file '{}' could not be loaded.".format(self.path) 33 | 34 | 35 | class ConfigValueError(Exception): 36 | """ 37 | Exception that indicates that the v20 configuration file is missing 38 | a required value 39 | """ 40 | 41 | def __init__(self, value): 42 | self.value = value 43 | 44 | def __str__(self): 45 | return "Config is missing value for '{}'.".format(self.value) 46 | 47 | 48 | class Config(object): 49 | """ 50 | The Config object encapsulates all of the configuration required to create 51 | a v20 API context and configure it to work with a specific Account. 52 | 53 | Using the Config object enables the scripts to exist without many command 54 | line arguments (host, token, accountID, etc) 55 | """ 56 | def __init__(self): 57 | """ 58 | Initialize an empty Config object 59 | """ 60 | self.hostname = None 61 | self.streaming_hostname = None 62 | self.port = 443 63 | self.ssl = True 64 | self.token = None 65 | self.username = None 66 | self.accounts = [] 67 | self.active_account = None 68 | self.path = None 69 | self.datetime_format = "RFC3339" 70 | 71 | def __str__(self): 72 | """ 73 | Create the string (YAML) representaion of the Config instance 74 | """ 75 | 76 | s = "" 77 | s += "hostname: {}\n".format(self.hostname) 78 | s += "streaming_hostname: {}\n".format(self.streaming_hostname) 79 | s += "port: {}\n".format(self.port) 80 | s += "ssl: {}\n".format(str(self.ssl).lower()) 81 | s += "token: {}\n".format(self.token) 82 | s += "username: {}\n".format(self.username) 83 | s += "datetime_format: {}\n".format(self.datetime_format) 84 | s += "accounts:\n" 85 | for a in self.accounts: 86 | s += "- {}\n".format(a) 87 | s += "active_account: {}".format(self.active_account) 88 | 89 | return s 90 | 91 | def dump(self, path): 92 | """ 93 | Dump the YAML representation of the Config instance to a file. 94 | 95 | Args: 96 | path: The location to write the config YAML 97 | """ 98 | 99 | path = os.path.expanduser(path) 100 | 101 | with open(path, "w") as f: 102 | print(str(self), file=f) 103 | 104 | def load(self, path): 105 | """ 106 | Load the YAML config representation from a file into the Config instance 107 | 108 | Args: 109 | path: The location to read the config YAML from 110 | """ 111 | 112 | self.path = path 113 | 114 | try: 115 | with open(os.path.expanduser(path)) as f: 116 | y = yaml.load(f) 117 | self.hostname = y.get("hostname", self.hostname) 118 | self.streaming_hostname = y.get( 119 | "streaming_hostname", self.streaming_hostname 120 | ) 121 | self.port = y.get("port", self.port) 122 | self.ssl = y.get("ssl", self.ssl) 123 | self.username = y.get("username", self.username) 124 | self.token = y.get("token", self.token) 125 | self.accounts = y.get("accounts", self.accounts) 126 | self.active_account = y.get( 127 | "active_account", self.active_account 128 | ) 129 | self.datetime_format = y.get("datetime_format", self.datetime_format) 130 | except: 131 | raise ConfigPathError(path) 132 | 133 | def validate(self): 134 | """ 135 | Ensure that the Config instance is valid 136 | """ 137 | 138 | if self.hostname is None: 139 | raise ConfigValueError("hostname") 140 | if self.streaming_hostname is None: 141 | raise ConfigValueError("hostname") 142 | if self.port is None: 143 | raise ConfigValueError("port") 144 | if self.ssl is None: 145 | raise ConfigValueError("ssl") 146 | if self.username is None: 147 | raise ConfigValueError("username") 148 | if self.token is None: 149 | raise ConfigValueError("token") 150 | if self.accounts is None: 151 | raise ConfigValueError("account") 152 | if self.active_account is None: 153 | raise ConfigValueError("account") 154 | if self.datetime_format is None: 155 | raise ConfigValueError("datetime_format") 156 | 157 | def update_from_input(self): 158 | """ 159 | Populate the configuration instance by interacting with the user using 160 | prompts 161 | """ 162 | 163 | environments = [ 164 | "fxtrade", 165 | "fxpractice" 166 | ] 167 | 168 | hostnames = [ 169 | "api-fxtrade.oanda.com", 170 | "api-fxpractice.oanda.com" 171 | ] 172 | 173 | streaming_hostnames = [ 174 | "stream-fxtrade.oanda.com", 175 | "stream-fxpractice.oanda.com" 176 | ] 177 | 178 | index = 0 179 | 180 | try: 181 | index = hostnames.index(self.hostname) 182 | except: 183 | pass 184 | 185 | environment = input.get_from_list( 186 | environments, 187 | "Available environments:", 188 | "Select environment", 189 | index 190 | ) 191 | 192 | index = environments.index(environment) 193 | 194 | self.hostname = hostnames[index] 195 | self.streaming_hostname = streaming_hostnames[index] 196 | 197 | print("> API host selected is: {}".format(self.hostname)) 198 | print("> Streaming host selected is: {}".format(self.streaming_hostname)) 199 | print("") 200 | 201 | self.username = input.get_string("Enter username", self.username) 202 | 203 | print("> username is: {}".format(self.username)) 204 | print("") 205 | 206 | self.token = input.get_string("Enter personal access token", self.token) 207 | 208 | print("> Using personal access token: {}".format(self.token)) 209 | 210 | ctx = v20.Context( 211 | self.hostname, 212 | self.port, 213 | self.ssl 214 | ) 215 | 216 | ctx.set_token(self.token) 217 | 218 | ctx_streaming = v20.Context( 219 | self.streaming_hostname, 220 | self.port, 221 | self.ssl 222 | ) 223 | 224 | ctx_streaming.set_token(self.token) 225 | 226 | response = ctx.account.list() 227 | 228 | if response.status != 200: 229 | print(response) 230 | sys.exit() 231 | 232 | self.accounts = [ 233 | account.id for account in response.body.get("accounts") 234 | ] 235 | 236 | self.accounts.sort() 237 | 238 | if len(self.accounts) == 0: 239 | print("No Accounts available") 240 | sys.exit() 241 | 242 | index = 0 243 | 244 | try: 245 | index = self.accounts.index(self.active_account) 246 | except: 247 | pass 248 | 249 | print("") 250 | 251 | self.active_account = input.get_from_list( 252 | self.accounts, 253 | "Available Accounts:", 254 | "Select Active Account", 255 | index 256 | ) 257 | 258 | print("> Active Account is: {}".format(self.active_account)) 259 | print("") 260 | 261 | time_formats = ["RFC3339", "UNIX"] 262 | 263 | index = 0 264 | 265 | try: 266 | index = time_formats.index(self.datetime_format) 267 | except: 268 | pass 269 | 270 | self.datetime_format = input.get_from_list( 271 | time_formats, 272 | "Available Time Formats:", 273 | "Select Time Format", 274 | index 275 | ) 276 | 277 | def create_context(self): 278 | """ 279 | Initialize an API context based on the Config instance 280 | """ 281 | ctx = v20.Context( 282 | self.hostname, 283 | self.port, 284 | self.ssl, 285 | application="sample_code", 286 | token=self.token, 287 | datetime_format=self.datetime_format 288 | ) 289 | 290 | return ctx 291 | 292 | def create_streaming_context(self): 293 | """ 294 | Initialize a streaming API context based on the Config instance 295 | """ 296 | ctx = v20.Context( 297 | self.streaming_hostname, 298 | self.port, 299 | self.ssl, 300 | application="sample_code", 301 | token=self.token, 302 | datetime_format=self.datetime_format 303 | ) 304 | 305 | return ctx 306 | 307 | 308 | def make_config_instance(path): 309 | """ 310 | Create a Config instance, load its state from the provided path and 311 | ensure that it is valid. 312 | 313 | Args: 314 | path: The location of the configuration file 315 | """ 316 | 317 | config = Config() 318 | 319 | config.load(path) 320 | 321 | config.validate() 322 | 323 | return config 324 | 325 | 326 | def default_config_path(): 327 | """ 328 | Calculate the default configuration file path. 329 | 330 | The default is first selected to be the contents of the V20_CONF 331 | environment variable, followed by the default path ~/.v20.conf 332 | """ 333 | 334 | global DEFAULT_ENV 335 | global DEFAULT_PATH 336 | 337 | return os.environ.get(DEFAULT_ENV, DEFAULT_PATH) 338 | 339 | 340 | def add_argument(parser): 341 | """ 342 | Add the --config argument to an ArgumentParser that enables the creation of 343 | a Config instance. The user is required to provide the path to load the 344 | configuration from, else the parser falls back to the location specified in 345 | the V20_CONF environment variable followed by the default config file 346 | location of ~/.v20.conf 347 | 348 | Args: 349 | parser: The ArgumentParser to add the config option to 350 | """ 351 | global DEFAULT_ENV 352 | global DEFAULT_PATH 353 | 354 | parser.add_argument( 355 | "--config", 356 | type=make_config_instance, 357 | default=default_config_path(), 358 | help="The location of the v20 config file to load. " 359 | "This defaults to the file set in the ${} " 360 | "environment variable, followed by file {}".format( 361 | DEFAULT_ENV, 362 | DEFAULT_PATH 363 | ) 364 | ) 365 | -------------------------------------------------------------------------------- /src/order/args.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import argparse 3 | import common.args 4 | import v20.transaction 5 | 6 | def add_replace_order_id_argument(parser): 7 | """ 8 | Add an argument to the parser for replacing an existing Order 9 | """ 10 | parser.add_argument( 11 | "--replace-order-id", "-r", 12 | help=( 13 | "The ID of the Order to replace, only provided if the intent is to " 14 | "replace an existing pending Order. If prepended " 15 | "with an '@', this will be interpreted as a client Order ID" 16 | ) 17 | ) 18 | 19 | 20 | class OrderArguments(object): 21 | """ 22 | OrderArguments is a wrapper that manages adding command line parameters 23 | used to configure Order creation 24 | """ 25 | 26 | def __init__(self, parser): 27 | # Store the argument parser to add arguments to 28 | self.parser = parser 29 | 30 | # The parsed_args contains all of the parameters parsed for order 31 | # creation 32 | self.parsed_args = {} 33 | 34 | # The list of param_parsers are used to automatically interpret 35 | # order arguments that have been added 36 | self.param_parsers = [] 37 | 38 | # The default formatter for arguments that are parsed as datetimes 39 | self.datetime_formatter = lambda dt: dt.strftime("%Y-%m-%dT%H:%M:%S.000000000Z") 40 | 41 | 42 | def set_datetime_formatter(self, datetime_formatter): 43 | """ 44 | Provide an alternate implementation of a datetime formatter 45 | """ 46 | self.datetime_formatter = datetime_formatter 47 | 48 | def parse_arguments(self, args): 49 | """ 50 | Call each param parser with the parsed arguments to extract the value 51 | into the parsed_args 52 | """ 53 | for parser in self.param_parsers: 54 | parser(args) 55 | 56 | 57 | def add_trade_id(self): 58 | self.parser.add_argument( 59 | "tradeid", 60 | help=( 61 | "The ID of the Trade to create an Order for. If prepended " 62 | "with an '@', this will be interpreted as a client Trade ID" 63 | ) 64 | ) 65 | 66 | self.param_parsers.append( 67 | lambda args: self.parse_trade_id(args) 68 | ) 69 | 70 | 71 | def parse_trade_id(self, args): 72 | if args.tradeid is None: 73 | return 74 | 75 | if args.tradeid[0] == '@': 76 | self.parsed_args["clientTradeID"] = args.tradeid[1:] 77 | else: 78 | self.parsed_args["tradeID"] = args.tradeid 79 | 80 | 81 | def add_instrument(self): 82 | self.parser.add_argument( 83 | "instrument", 84 | type=common.args.instrument, 85 | help="The instrument to place the Order for" 86 | ) 87 | 88 | self.param_parsers.append( 89 | lambda args: self.parse_instrument(args) 90 | ) 91 | 92 | 93 | def parse_instrument(self, args): 94 | if args.instrument is None: 95 | return 96 | 97 | self.parsed_args["instrument"] = args.instrument 98 | 99 | 100 | def add_units(self): 101 | self.parser.add_argument( 102 | "units", 103 | help=( 104 | "The number of units for the Order. " 105 | "Negative values indicate sell, Positive values indicate buy" 106 | ) 107 | ) 108 | 109 | self.param_parsers.append( 110 | lambda args: self.parse_units(args) 111 | ) 112 | 113 | 114 | def parse_units(self, args): 115 | if args.units is None: 116 | return 117 | 118 | self.parsed_args["units"] = args.units 119 | 120 | 121 | def add_price(self): 122 | self.parser.add_argument( 123 | "price", 124 | help="The price threshold for the Order" 125 | ) 126 | 127 | self.param_parsers.append( 128 | lambda args: self.parse_price(args) 129 | ) 130 | 131 | 132 | def parse_price(self, args): 133 | if args.price is None: 134 | return 135 | 136 | self.parsed_args["price"] = args.price 137 | 138 | 139 | def add_distance(self): 140 | self.parser.add_argument( 141 | "distance", 142 | help="The price distance for the Order" 143 | ) 144 | 145 | self.param_parsers.append( 146 | lambda args: self.parse_distance(args) 147 | ) 148 | 149 | 150 | def parse_distance(self, args): 151 | if args.distance is None: 152 | return 153 | 154 | self.parsed_args["distance"] = args.distance 155 | 156 | 157 | def add_time_in_force(self, choices=["FOK", "IOC", "GTC", "GFD", "GTD"]): 158 | self.parser.add_argument( 159 | "--time-in-force", "--tif", 160 | choices=choices, 161 | help="The time-in-force to use for the Order" 162 | ) 163 | 164 | if "GTD" in choices: 165 | self.parser.add_argument( 166 | "--gtd-time", 167 | type=common.args.date_time(), 168 | help=( 169 | "The date to use when the time-in-force is GTD. " 170 | "Format is 'YYYY-MM-DD HH:MM:SS" 171 | ) 172 | ) 173 | 174 | self.param_parsers.append( 175 | lambda args: self.parse_time_in_force(args) 176 | ) 177 | 178 | 179 | def parse_time_in_force(self, args): 180 | if args.time_in_force is None: 181 | return 182 | 183 | self.parsed_args["timeInForce"] = args.time_in_force 184 | 185 | if args.time_in_force != "GTD": 186 | return 187 | 188 | if args.gtd_time is None: 189 | self.parser.error( 190 | "must set --gtd-time \"YYYY-MM-DD HH:MM:SS\" when " 191 | "--time-in-force=GTD" 192 | ) 193 | return 194 | 195 | self.parsed_args["gtdTime"] = self.datetime_formatter(args.gtd_time) 196 | 197 | 198 | def add_price_bound(self): 199 | self.parser.add_argument( 200 | "--price-bound", "-b", 201 | help="The worst price bound allowed for the Order" 202 | ) 203 | 204 | self.param_parsers.append( 205 | lambda args: self.parse_price_bound(args) 206 | ) 207 | 208 | 209 | def parse_price_bound(self, args): 210 | if args.price_bound is None: 211 | return 212 | 213 | self.parsed_args["priceBound"] = args.price_bound 214 | 215 | 216 | def add_position_fill(self): 217 | self.parser.add_argument( 218 | "--position-fill", 219 | choices=["DEFAULT", "OPEN_ONLY", "REDUCE_FIRST", "REDUCE_ONLY"], 220 | required=False, 221 | help="Specification of how the Order may affect open positions." 222 | ) 223 | 224 | self.param_parsers.append( 225 | lambda args: self.parse_position_fill(args) 226 | ) 227 | 228 | 229 | def parse_position_fill(self, args): 230 | if args.position_fill is None: 231 | return 232 | 233 | self.parsed_args["positionFill"] = args.position_fill 234 | 235 | 236 | def add_client_order_extensions(self): 237 | self.parser.add_argument( 238 | "--client-order-id", "--coi", 239 | help="The client-provided ID to assign to the Order" 240 | ) 241 | 242 | self.parser.add_argument( 243 | "--client-order-tag", "--cot", 244 | help="The client-provided tag to assign to the Order" 245 | ) 246 | 247 | self.parser.add_argument( 248 | "--client-order-comment", "--coc", 249 | help="The client-provided comment to assign to the Order" 250 | ) 251 | 252 | self.param_parsers.append( 253 | lambda args: self.parse_client_order_extensions(args) 254 | ) 255 | 256 | 257 | def parse_client_order_extensions(self, args): 258 | if (args.client_order_id is None and 259 | args.client_order_tag is None and 260 | args.client_order_comment is None): 261 | return 262 | 263 | kwargs = {} 264 | 265 | if args.client_order_id is not None: 266 | kwargs["id"] = args.client_order_id 267 | 268 | if args.client_order_tag is not None: 269 | kwargs["tag"] = args.client_order_tag 270 | 271 | if args.client_order_comment is not None: 272 | kwargs["comment"] = args.client_order_comment 273 | 274 | self.parsed_args["clientExtensions"] = \ 275 | v20.transaction.ClientExtensions( 276 | **kwargs 277 | ) 278 | 279 | 280 | def add_client_trade_extensions(self): 281 | self.parser.add_argument( 282 | "--client-trade-id", "--cti", 283 | help="The client-provided ID to assign a Trade opened by the Order" 284 | ) 285 | 286 | self.parser.add_argument( 287 | "--client-trade-tag", "--ctt", 288 | help=( 289 | "The client-provided tag to assign to a Trade opened by the " 290 | "Order" 291 | ) 292 | ) 293 | 294 | self.parser.add_argument( 295 | "--client-trade-comment", "--ctc", 296 | help=( 297 | "The client-provided comment to assign to a Trade opened by " 298 | "the Order" 299 | ) 300 | ) 301 | 302 | self.param_parsers.append( 303 | lambda args: self.parse_client_trade_extensions(args) 304 | ) 305 | 306 | 307 | def parse_client_trade_extensions(self, args): 308 | if (args.client_trade_id is None and 309 | args.client_trade_tag is None and 310 | args.client_trade_comment is None): 311 | return None 312 | 313 | kwargs = {} 314 | 315 | if args.client_trade_id is not None: 316 | kwargs["id"] = args.client_trade_id 317 | 318 | if args.client_trade_tag is not None: 319 | kwargs["tag"] = args.client_trade_tag 320 | 321 | if args.client_trade_comment is not None: 322 | kwargs["comment"] = args.client_trade_comment 323 | 324 | self.parsed_args["tradeClientExtensions"] = \ 325 | v20.transaction.ClientExtensions( 326 | **kwargs 327 | ) 328 | 329 | 330 | def add_take_profit_on_fill(self): 331 | self.parser.add_argument( 332 | "--take-profit-price", "--tp", 333 | help=( 334 | "The price of the Take Profit to add to a Trade opened by this " 335 | "Order" 336 | ) 337 | ) 338 | 339 | self.param_parsers.append( 340 | lambda args: self.parse_take_profit_on_fill(args) 341 | ) 342 | 343 | 344 | def parse_take_profit_on_fill(self, args): 345 | if args.take_profit_price is None: 346 | return 347 | 348 | kwargs = {} 349 | 350 | kwargs["price"] = args.take_profit_price 351 | 352 | self.parsed_args["takeProfitOnFill"] = \ 353 | v20.transaction.TakeProfitDetails(**kwargs) 354 | 355 | 356 | def add_stop_loss_on_fill(self): 357 | self.parser.add_argument( 358 | "--stop-loss-price", "--sl", 359 | help=( 360 | "The price of the Stop Loss to add to a Trade opened by this " 361 | "Order" 362 | ) 363 | ) 364 | 365 | self.param_parsers.append( 366 | lambda args: self.parse_stop_loss_on_fill(args) 367 | ) 368 | 369 | 370 | def parse_stop_loss_on_fill(self, args): 371 | if args.stop_loss_price is None: 372 | return 373 | 374 | kwargs = {} 375 | 376 | kwargs["price"] = args.stop_loss_price 377 | 378 | self.parsed_args["stopLossOnFill"] = \ 379 | v20.transaction.StopLossDetails(**kwargs) 380 | 381 | 382 | def add_trailing_stop_loss_on_fill(self): 383 | self.parser.add_argument( 384 | "--trailing-stop-loss-distance", "--tsl", 385 | help=( 386 | "The price distance for the Trailing Stop Loss to add to a " 387 | "Trade opened by this Order" 388 | ) 389 | ) 390 | 391 | self.param_parsers.append( 392 | lambda args: self.parse_trailing_stop_loss_on_fill(args) 393 | ) 394 | 395 | 396 | def parse_trailing_stop_loss_on_fill(self, args): 397 | if args.trailing_stop_loss_distance is None: 398 | return 399 | 400 | kwargs = {} 401 | 402 | kwargs["distance"] = args.stop_loss_distance 403 | 404 | self.parsed_args["trailingStopLossOnFill"] = \ 405 | v20.transaction.TrailingStopLossDetails( 406 | **kwargs 407 | ) 408 | --------------------------------------------------------------------------------