├── tpqoa ├── __init__.py └── tpqoa.py ├── setup.py ├── LICENSE.txt ├── .gitignore ├── test.py ├── README.md └── tpqoa.ipynb /tpqoa/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # tpqoa __init__.py 3 | # 4 | __all__ = ['tpqoa'] 5 | from .tpqoa import tpqoa 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Setup file for 3 | # tpqoa -- Algorithmic Trading with Oanda 4 | # 5 | # (c) Dr. Yves J. Hilpisch 6 | # The Python Quants GmbH 7 | # 8 | from setuptools import setup 9 | 10 | with open('README.md', 'r') as f: 11 | long_description = f.read() 12 | 13 | setup(name='tpqoa', 14 | version='0.0.56', 15 | description='tpqoa Algorithmic Trading with Oanda', 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | author='Yves Hilpisch', 19 | author_email='team@tpq.io', 20 | url='http://home.tpq.io', 21 | packages=['tpqoa'], 22 | install_requires=[ 23 | 'v20==3.0.25.0', 24 | 'pyyaml' 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2021 Dr. Yves J. Hilpisch | The Python Quants GmbH | http://tpq.io 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifics 2 | *.swp 3 | *.cfg 4 | _build/ 5 | .DS_Store 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # 2 | # tpqoa is a wrapper class for the 3 | # Oanda v20 API (RESTful & streaming) 4 | # making use of the v20 Python package 5 | # 6 | # Test Suite 7 | # 8 | # (c) Dr. Yves J. Hilpisch 9 | # The Python Quants GmbH 10 | # 11 | # 12 | # Trading forex/CFDs on margin carries a high level of risk and may 13 | # not be suitable for all investors as you could sustain losses 14 | # in excess of deposits. Leverage can work against you. Due to the certain 15 | # restrictions imposed by the local law and regulation, German resident 16 | # retail client(s) could sustain a total loss of deposited funds but are 17 | # not subject to subsequent payment obligations beyond the deposited funds. 18 | # Be aware and fully understand all risks associated with 19 | # the market and trading. Prior to trading any products, 20 | # carefully consider your financial situation and 21 | # experience level. Any opinions, news, research, analyses, prices, 22 | # or other information is provided as general market commentary, and does not 23 | # constitute investment advice. The Python Quants GmbH will not accept 24 | # liability for any loss or damage, including without limitation to, 25 | # any loss of profit, which may arise directly or indirectly from use 26 | # of or reliance on such information. 27 | # 28 | # The tpqoa package is intended as a technological illustration only. 29 | # It comes with no warranties or representations, 30 | # to the extent permitted by applicable law. 31 | # 32 | 33 | import unittest 34 | from time import sleep 35 | from decimal import Decimal 36 | 37 | from tpqoa import tpqoa 38 | 39 | 40 | class TestTPQOA(unittest.TestCase): 41 | 42 | def setUp(self): 43 | self.tpqoa = tpqoa('oanda.cfg') 44 | 45 | def test_connection(self): 46 | self.assertIsNotNone(self.tpqoa.account_id) 47 | self.assertIsNotNone(self.tpqoa.access_token) 48 | 49 | def test_place_order(self): 50 | oanda_response = self.tpqoa.create_order('EUR_USD', units=10, ret=True) 51 | oanda_response = oanda_response.get('orderFillTransaction').dict() 52 | self.assertEqual( 53 | Decimal(oanda_response['units']), 10, 54 | 'Open Order placed successfully') 55 | self.assertEqual(Decimal( 56 | oanda_response['tradeOpened']['units']), 10, 57 | 'New trade opened check success') 58 | 59 | oanda_response = self.tpqoa.create_order( 60 | 'EUR_USD', units=-10, ret=True) 61 | oanda_response = oanda_response.get('orderFillTransaction').dict() 62 | self.assertEqual( 63 | Decimal(oanda_response['units']), -10, 64 | 'Close Order placed successfully') 65 | self.assertEqual(Decimal( 66 | oanda_response['tradesClosed'][0]['units']), -10, 67 | 'Trade closed check success') 68 | 69 | def test_sl_distance(self): 70 | oanda_response = self.tpqoa.create_order( 71 | 'EUR_USD', units=10, sl_distance=0.005, ret=True) 72 | oanda_response = oanda_response.get('orderCreateTransaction').dict() 73 | self.assertEqual( 74 | Decimal(oanda_response['stopLossOnFill']['distance']), 75 | round(Decimal(0.005), 3), 76 | 'sl created successfully') 77 | 78 | oanda_response = self.tpqoa.create_order( 79 | 'EUR_USD', units=-10, ret=True) 80 | 81 | def test_tsl_tp_order(self): 82 | oanda_response = self.tpqoa.create_order( 83 | 'EUR_USD', units=10, sl_distance=0.0005, ret=True) 84 | oanda_create_response = oanda_response.get( 85 | 'orderCreateTransaction').dict() 86 | self.assertEqual( 87 | Decimal(oanda_create_response['stopLossOnFill']['distance']), 88 | round(Decimal(0.0005), 4), 89 | 'sl created successfully') 90 | trade_response = oanda_response.get('orderFillTransaction').dict() 91 | tp_price = float(trade_response['price']) + 0.001 92 | 93 | sleep(3) 94 | oanda_response = self.tpqoa.create_order( 95 | 'EUR_USD', units=10, tsl_distance=0.0005, 96 | tp_price=tp_price, ret=True) 97 | oanda_create_response = oanda_response.get( 98 | 'orderCreateTransaction').dict() 99 | self.assertEqual( 100 | Decimal( 101 | oanda_create_response['trailingStopLossOnFill']['distance']), 102 | round(Decimal(0.0005), 4), 'TSL created successfully') 103 | self.assertIsNotNone( 104 | round( 105 | Decimal(oanda_create_response['takeProfitOnFill']['price']), 4), 106 | 'TP created successfully') 107 | 108 | sleep(3) 109 | oanda_response = self.tpqoa.create_order( 110 | 'EUR_USD', units=-20, ret=True) 111 | 112 | def test_get_instruments(self): 113 | instruments = self.tpqoa.get_instruments() 114 | self.assertIsNotNone(instruments) 115 | eur_usd = [x for x in instruments if x[0] == 'EUR/USD'] 116 | self.assertEqual(eur_usd, [('EUR/USD', 'EUR_USD')]) 117 | 118 | 119 | if __name__ == '__main__': 120 | unittest.main() 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Python Quants
2 | 3 | # tpqoa 4 | 5 | ## Algorithmic Trading with Oanda 6 | 7 | `tpqoa` is a wrapper class for the Oanda REST API v20 (http://developer.oanda.com/). It makes use of the Python package `v20` from Oanda (https://github.com/oanda/v20-python). The package is authored and maintained by The Python Quants GmbH. © Dr. Yves J. Hilpisch. MIT License. 8 | 9 | ## Disclaimer 10 | 11 | Trading forex/CFDs on margin carries a high level of risk and may not be suitable for all investors as you could sustain losses in excess of deposits. Leverage can work against you. Due to the certain restrictions imposed by the local law and regulation, German resident retail client(s) could sustain a total loss of deposited funds but are not subject to subsequent payment obligations beyond the deposited funds. Be aware and fully understand all risks associated with the market and trading. Prior to trading any products, carefully consider your financial situation and experience level. Any opinions, news, research, analyses, prices, code examples or other information is provided as general market commentary, and does not constitute investment advice. The Python Quants GmbH will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information. 12 | 13 | The `tpqoa` package is intended as a technological illustration only. It comes with no warranties or representations, to the extent permitted by applicable law. 14 | 15 | ## Installation 16 | 17 | Installing from source via `Git` and `Github`: 18 | 19 | git clone https://github.com/yhilpisch/tpqoa 20 | cd tpqoa 21 | python setup.py install 22 | 23 | Using `pip` in combination with `Github`: 24 | 25 | pip install git+git://github.com/yhilpisch/tpqoa 26 | 27 | ## Connection 28 | 29 | In order to connect to the API, you need to have at least a practice account with Oanda (https://oanda.com/). Once logged in to you account, you can create an API token and can copy your account number. These are expected to be stored in a configuration file, with name `oanda.cfg`, for instance, as follows: 30 | 31 | [oanda] 32 | account_id = XYZ-ABC-... 33 | access_token = ZYXCAB... 34 | account_type = practice (default) or live 35 | 36 | You can then set up an API connection by instantiating a connection object. 37 | 38 | 39 | ```python 40 | import tpqoa 41 | ``` 42 | 43 | 44 | ```python 45 | oanda = tpqoa.tpqoa('../../oanda.cfg') # adjust path as necessary 46 | ``` 47 | 48 | ## Available Instruments 49 | 50 | The `get_instruments()` method retrieves all available instruments. 51 | 52 | 53 | ```python 54 | ins = oanda.get_instruments() 55 | ``` 56 | 57 | 58 | ```python 59 | ins[:10] 60 | ``` 61 | 62 | 63 | 64 | 65 | [('AUD/CAD', 'AUD_CAD'), 66 | ('AUD/CHF', 'AUD_CHF'), 67 | ('AUD/HKD', 'AUD_HKD'), 68 | ('AUD/JPY', 'AUD_JPY'), 69 | ('AUD/NZD', 'AUD_NZD'), 70 | ('AUD/SGD', 'AUD_SGD'), 71 | ('AUD/USD', 'AUD_USD'), 72 | ('Australia 200', 'AU200_AUD'), 73 | ('Brent Crude Oil', 'BCO_USD'), 74 | ('Bund', 'DE10YB_EUR')] 75 | 76 | 77 | 78 | ## Historical Data 79 | 80 | The `get_history()` method retrieves historical data. 81 | 82 | 83 | ```python 84 | help(oanda.get_history) 85 | ``` 86 | 87 | Help on method get_history in module tpqoa.tpqoa: 88 | 89 | get_history(instrument, start, end, granularity, price, localize=True) method of tpqoa.tpqoa.tpqoa instance 90 | Retrieves historical data for instrument. 91 | 92 | Parameters 93 | ========== 94 | instrument: string 95 | valid instrument name 96 | start, end: datetime, str 97 | Python datetime or string objects for start and end 98 | granularity: string 99 | a string like 'S5', 'M1' or 'D' 100 | price: string 101 | one of 'A' (ask), 'B' (bid) or 'M' (middle) 102 | 103 | Returns 104 | ======= 105 | data: pd.DataFrame 106 | pandas DataFrame object with data 107 | 108 | 109 | 110 | 111 | ```python 112 | # oanda.get_history?? 113 | ``` 114 | 115 | 116 | ```python 117 | data = oanda.get_history(instrument='EUR_USD', 118 | start='2022-06-15', 119 | end='2023-06-15', 120 | granularity='D', 121 | price='M') 122 | ``` 123 | 124 | 125 | ```python 126 | data.info() 127 | ``` 128 | 129 | 130 | DatetimeIndex: 261 entries, 2022-06-14 21:00:00 to 2023-06-14 21:00:00 131 | Data columns (total 6 columns): 132 | # Column Non-Null Count Dtype 133 | --- ------ -------------- ----- 134 | 0 o 261 non-null float64 135 | 1 h 261 non-null float64 136 | 2 l 261 non-null float64 137 | 3 c 261 non-null float64 138 | 4 volume 261 non-null int64 139 | 5 complete 261 non-null bool 140 | dtypes: bool(1), float64(4), int64(1) 141 | memory usage: 12.5 KB 142 | 143 | 144 | 145 | ```python 146 | print(data.head()) 147 | ``` 148 | 149 | o h l c volume complete 150 | time 151 | 2022-06-14 21:00:00 1.04114 1.05078 1.03593 1.04466 204826 True 152 | 2022-06-15 21:00:00 1.04444 1.06014 1.03809 1.05524 183417 True 153 | 2022-06-16 21:00:00 1.05496 1.05612 1.04445 1.04938 156233 True 154 | 2022-06-19 21:00:00 1.04841 1.05460 1.04746 1.05112 85713 True 155 | 2022-06-20 21:00:00 1.05088 1.05826 1.05086 1.05348 101517 True 156 | 157 | 158 | ## Streaming Data 159 | 160 | The method `stream_data()` allows the streaming of real-time data (bid & ask). 161 | 162 | 163 | ```python 164 | oanda.stream_data('EUR_USD', stop=3) 165 | ``` 166 | 167 | 2023-06-27T06:57:58.204324464Z 1.09299 1.09313 168 | 2023-06-27T06:57:58.400409926Z 1.09301 1.09315 169 | 2023-06-27T06:58:00.348284643Z 1.093 1.09314 170 | 171 | 172 | By redefining the `on_success()` method, you can control what happes with the streaming data. 173 | 174 | 175 | ```python 176 | class myOanda(tpqoa.tpqoa): 177 | def on_success(self, time, bid, ask): 178 | ''' Method called when new data is retrieved. ''' 179 | print('BID: {:.5f} | ASK: {:.5f}'.format(bid, ask)) 180 | ``` 181 | 182 | 183 | ```python 184 | my_oanda = myOanda('../../oanda.cfg') 185 | ``` 186 | 187 | 188 | ```python 189 | my_oanda.stream_data('EUR_USD', stop=5) 190 | ``` 191 | 192 | BID: 1.09297 | ASK: 1.09311 193 | BID: 1.09297 | ASK: 1.09310 194 | BID: 1.09297 | ASK: 1.09311 195 | BID: 1.09297 | ASK: 1.09312 196 | BID: 1.09294 | ASK: 1.09309 197 | 198 | 199 | ## Placing Orders 200 | 201 | 202 | ```python 203 | help(oanda.create_order) 204 | ``` 205 | 206 | Help on method create_order in module tpqoa.tpqoa: 207 | 208 | create_order(instrument, units, price=None, sl_distance=None, tsl_distance=None, tp_price=None, comment=None, touch=False, suppress=False, ret=False) method of tpqoa.tpqoa.tpqoa instance 209 | Places order with Oanda. 210 | 211 | Parameters 212 | ========== 213 | instrument: string 214 | valid instrument name 215 | units: int 216 | number of units of instrument to be bought 217 | (positive int, eg 'units=50') 218 | or to be sold (negative int, eg 'units=-100') 219 | price: float 220 | limit order price, touch order price 221 | sl_distance: float 222 | stop loss distance price, mandatory eg in Germany 223 | tsl_distance: float 224 | trailing stop loss distance 225 | tp_price: float 226 | take profit price to be used for the trade 227 | comment: str 228 | string 229 | touch: boolean 230 | market_if_touched order (requires price to be set) 231 | suppress: boolean 232 | whether to suppress print out 233 | ret: boolean 234 | whether to return the order object 235 | 236 | 237 | 238 | 239 | ```python 240 | # going long 100 units 241 | # sl_distance of 20 pips 242 | oanda.create_order('EUR_USD', units=100, sl_distance=0.002) 243 | ``` 244 | 245 | 246 | 247 | {'id': '3608', 'time': '2023-06-27T06:58:16.307275954Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3607', 'requestID': '61122547747050135', 'type': 'ORDER_FILL', 'orderID': '3607', 'instrument': 'EUR_USD', 'units': '100.0', 'gainQuoteHomeConversionFactor': '0.910326029089', 'lossQuoteHomeConversionFactor': '0.919475034407', 'price': 1.09309, 'fullVWAP': 1.09309, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.09294, 'liquidity': '10000000'}], 'asks': [{'price': 1.09309, 'liquidity': '10000000'}], 'closeoutBid': 1.09294, 'closeoutAsk': 1.09309}, 'reason': 'MARKET_ORDER', 'pl': '0.0', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101295.189', 'tradeOpened': {'tradeID': '3608', 'units': '100.0', 'price': 1.09309, 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.0069', 'initialMarginRequired': '3.33'}, 'halfSpreadCost': '0.0069'} 248 | 249 | 250 | 251 | 252 | ```python 253 | # closing out the position 254 | oanda.create_order('EUR_USD', units=-100) 255 | ``` 256 | 257 | 258 | 259 | {'id': '3611', 'time': '2023-06-27T06:58:16.523945599Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3610', 'requestID': '61122547747050332', 'type': 'ORDER_FILL', 'orderID': '3610', 'instrument': 'EUR_USD', 'units': '-100.0', 'gainQuoteHomeConversionFactor': '0.910326029089', 'lossQuoteHomeConversionFactor': '0.919475034407', 'price': 1.09294, 'fullVWAP': 1.09294, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.09294, 'liquidity': '10000000'}], 'asks': [{'price': 1.09309, 'liquidity': '9999900'}], 'closeoutBid': 1.09294, 'closeoutAsk': 1.09309}, 'reason': 'MARKET_ORDER', 'pl': '-0.0138', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101295.1752', 'tradesClosed': [{'tradeID': '3608', 'units': '-100.0', 'price': 1.09294, 'realizedPL': '-0.0138', 'financing': '0.0', 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.0069'}], 'halfSpreadCost': '0.0069'} 260 | 261 | 262 | 263 | ## Canceling Orders 264 | 265 | 266 | ```python 267 | order = oanda.create_order('EUR_USD', units=10000, sl_distance=0.01, ret=True) 268 | ``` 269 | 270 | 271 | 272 | {'id': '3614', 'time': '2023-06-27T06:58:33.953341530Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3613', 'requestID': '61122547818369950', 'type': 'ORDER_FILL', 'orderID': '3613', 'instrument': 'EUR_USD', 'units': '10000.0', 'gainQuoteHomeConversionFactor': '0.910363508679', 'lossQuoteHomeConversionFactor': '0.919512890676', 'price': 1.09304, 'fullVWAP': 1.09304, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.0929, 'liquidity': '10000000'}], 'asks': [{'price': 1.09304, 'liquidity': '10000000'}], 'closeoutBid': 1.0929, 'closeoutAsk': 1.09304}, 'reason': 'MARKET_ORDER', 'pl': '0.0', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101295.1752', 'tradeOpened': {'tradeID': '3614', 'units': '10000.0', 'price': 1.09304, 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.6405', 'initialMarginRequired': '333.0'}, 'halfSpreadCost': '0.6405'} 273 | 274 | 275 | 276 | 277 | ```python 278 | oanda.get_transaction(tid=int(order['id']) + 1) 279 | ``` 280 | 281 | 282 | 283 | 284 | {'id': '3615', 285 | 'time': '2023-06-27T06:58:33.953341530Z', 286 | 'userID': 13834683, 287 | 'accountID': '101-004-13834683-001', 288 | 'batchID': '3613', 289 | 'requestID': '61122547818369950', 290 | 'type': 'STOP_LOSS_ORDER', 291 | 'tradeID': '3614', 292 | 'price': 1.08304, 293 | 'distance': '0.01', 294 | 'timeInForce': 'GTC', 295 | 'triggerCondition': 'DEFAULT', 296 | 'reason': 'ON_FILL'} 297 | 298 | 299 | 300 | 301 | ```python 302 | oanda.cancel_order(int(order['id']) + 1) 303 | ``` 304 | 305 | 306 | 307 | 308 | {'orderCancelTransaction': {'id': '3616', 309 | 'accountID': '101-004-13834683-001', 310 | 'userID': 13834683, 311 | 'batchID': '3616', 312 | 'requestID': '61122547826759581', 313 | 'time': '2023-06-27T06:58:35.038788414Z', 314 | 'type': 'ORDER_CANCEL', 315 | 'orderID': '3615', 316 | 'reason': 'CLIENT_REQUEST'}, 317 | 'relatedTransactionIDs': ['3616'], 318 | 'lastTransactionID': '3616'} 319 | 320 | 321 | 322 | 323 | ```python 324 | order = oanda.create_order('EUR_USD', units=-10000) 325 | ``` 326 | 327 | 328 | 329 | {'id': '3618', 'time': '2023-06-27T06:58:36.381641548Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3617', 'requestID': '61122547830955203', 'type': 'ORDER_FILL', 'orderID': '3617', 'instrument': 'EUR_USD', 'units': '-10000.0', 'gainQuoteHomeConversionFactor': '0.910359343552', 'lossQuoteHomeConversionFactor': '0.919508683689', 'price': 1.09291, 'fullVWAP': 1.09291, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.09291, 'liquidity': '10000000'}], 'asks': [{'price': 1.09304, 'liquidity': '9990000'}], 'closeoutBid': 1.09291, 'closeoutAsk': 1.09304}, 'reason': 'MARKET_ORDER', 'pl': '-1.1954', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101293.9798', 'tradesClosed': [{'tradeID': '3614', 'units': '-10000.0', 'price': 1.09291, 'realizedPL': '-1.1954', 'financing': '0.0', 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.5947'}], 'halfSpreadCost': '0.5947'} 330 | 331 | 332 | 333 | ## Account-Related Methods 334 | 335 | 336 | ```python 337 | help(oanda.get_account_summary) 338 | ``` 339 | 340 | Help on method get_account_summary in module tpqoa.tpqoa: 341 | 342 | get_account_summary(detailed=False) method of tpqoa.tpqoa.tpqoa instance 343 | Returns summary data for Oanda account. 344 | 345 | 346 | 347 | 348 | ```python 349 | oanda.get_account_summary() 350 | ``` 351 | 352 | 353 | 354 | 355 | {'id': '101-004-13834683-001', 356 | 'alias': 'Primary', 357 | 'currency': 'EUR', 358 | 'balance': '101293.9798', 359 | 'createdByUserID': 13834683, 360 | 'createdTime': '2020-03-19T06:08:14.363139403Z', 361 | 'guaranteedStopLossOrderMode': 'ALLOWED', 362 | 'pl': '1488.9357', 363 | 'resettablePL': '1488.9357', 364 | 'resettablePLTime': '0', 365 | 'financing': '-194.9559', 366 | 'commission': '0.0', 367 | 'guaranteedExecutionFees': '0.0', 368 | 'marginRate': '0.0333', 369 | 'openTradeCount': 0, 370 | 'openPositionCount': 0, 371 | 'pendingOrderCount': 0, 372 | 'hedgingEnabled': False, 373 | 'unrealizedPL': '0.0', 374 | 'NAV': '101293.9798', 375 | 'marginUsed': '0.0', 376 | 'marginAvailable': '101293.9798', 377 | 'positionValue': '0.0', 378 | 'marginCloseoutUnrealizedPL': '0.0', 379 | 'marginCloseoutNAV': '101293.9798', 380 | 'marginCloseoutMarginUsed': '0.0', 381 | 'marginCloseoutPercent': '0.0', 382 | 'marginCloseoutPositionValue': '0.0', 383 | 'withdrawalLimit': '101293.9798', 384 | 'marginCallMarginUsed': '0.0', 385 | 'marginCallPercent': '0.0', 386 | 'lastTransactionID': '3618'} 387 | 388 | 389 | 390 | 391 | ```python 392 | help(oanda.get_transactions) 393 | ``` 394 | 395 | Help on method get_transactions in module tpqoa.tpqoa: 396 | 397 | get_transactions(tid=0) method of tpqoa.tpqoa.tpqoa instance 398 | Retrieves and returns transactions data. 399 | 400 | 401 | 402 | 403 | ```python 404 | help(oanda.print_transactions) 405 | ``` 406 | 407 | Help on method print_transactions in module tpqoa.tpqoa: 408 | 409 | print_transactions(tid=0) method of tpqoa.tpqoa.tpqoa instance 410 | Prints basic transactions data. 411 | 412 | 413 | 414 | 415 | ```python 416 | oanda.print_transactions(tid=3545) 417 | ``` 418 | 419 | 3547 | 2023-06-27T06:07:18.75 | EUR_USD | -10000.0 | 2.0954 420 | 3550 | 2023-06-27T06:12:25.19 | EUR_USD | 10000.0 | 0.0 421 | 3553 | 2023-06-27T06:37:02.60 | EUR_USD | -10000.0 | 3.0046 422 | 3556 | 2023-06-27T06:39:10.34 | EUR_USD | 100.0 | 0.0 423 | 3559 | 2023-06-27T06:39:10.50 | EUR_USD | -100.0 | -0.0129 424 | 3562 | 2023-06-27T06:39:34.89 | EUR_USD | 10000.0 | 0.0 425 | 3568 | 2023-06-27T06:43:59.81 | EUR_USD | -10000.0 | -1.5631 426 | 3570 | 2023-06-27T06:44:08.72 | EUR_USD | 10000.0 | 0.0 427 | 3574 | 2023-06-27T06:52:58.26 | EUR_USD | 100.0 | 0.0 428 | 3577 | 2023-06-27T06:52:58.45 | EUR_USD | -100.0 | -0.0451 429 | 3579 | 2023-06-27T06:53:12.67 | EUR_USD | 10000.0 | 0.0 430 | 3583 | 2023-06-27T06:54:17.23 | EUR_USD | -10000.0 | -4.9358 431 | 3586 | 2023-06-27T06:54:20.61 | EUR_USD | 10000.0 | 0.0 432 | 3590 | 2023-06-27T06:56:29.21 | EUR_USD | 100.0 | 0.0 433 | 3593 | 2023-06-27T06:56:29.42 | EUR_USD | -100.0 | 0.0246 434 | 3595 | 2023-06-27T06:56:29.63 | EUR_USD | 10000.0 | 0.0 435 | 3599 | 2023-06-27T06:56:30.24 | EUR_USD | -10000.0 | 2.0921 436 | 3601 | 2023-06-27T06:57:23.19 | EUR_USD | -10000.0 | 0.091 437 | 3603 | 2023-06-27T06:57:27.47 | EUR_USD | -100.0 | -0.0074 438 | 3606 | 2023-06-27T06:57:33.23 | EUR_USD | -9900.0 | 3.1541 439 | 3608 | 2023-06-27T06:58:16.30 | EUR_USD | 100.0 | 0.0 440 | 3611 | 2023-06-27T06:58:16.52 | EUR_USD | -100.0 | -0.0138 441 | 3614 | 2023-06-27T06:58:33.95 | EUR_USD | 10000.0 | 0.0 442 | 3618 | 2023-06-27T06:58:36.38 | EUR_USD | -10000.0 | -1.1954 443 | 444 | 445 | The Python Quants
446 | 447 | http://tpq.io | @dyjh | training@tpq.io 448 | -------------------------------------------------------------------------------- /tpqoa/tpqoa.py: -------------------------------------------------------------------------------- 1 | # 2 | # tpqoa is a wrapper class for the 3 | # Oanda v20 API (RESTful & streaming) 4 | # making use of the v20 Python package 5 | # 6 | # (c) Dr. Yves J. Hilpisch 7 | # The Python Quants GmbH 8 | # 9 | # 10 | # Trading forex/CFDs on margin carries a high level of risk and may 11 | # not be suitable for all investors as you could sustain losses 12 | # in excess of deposits. Leverage can work against you. Due to the certain 13 | # restrictions imposed by the local law and regulation, German resident 14 | # retail client(s) could sustain a total loss of deposited funds but are 15 | # not subject to subsequent payment obligations beyond the deposited funds. 16 | # Be aware and fully understand all risks associated with 17 | # the market and trading. Prior to trading any products, 18 | # carefully consider your financial situation and 19 | # experience level. Any opinions, news, research, analyses, prices, 20 | # or other information is provided as general market commentary, and does not 21 | # constitute investment advice. The Python Quants GmbH will not accept 22 | # liability for any loss or damage, including without limitation to, 23 | # any loss of profit, which may arise directly or indirectly from use 24 | # of or reliance on such information. 25 | # 26 | # The tpqoa package is intended as a technological illustration only. 27 | # It comes with no warranties or representations, 28 | # to the extent permitted by applicable law. 29 | # 30 | import _thread 31 | import configparser 32 | import json 33 | import signal 34 | import threading 35 | from time import sleep 36 | 37 | import pandas as pd 38 | import v20 39 | from v20.transaction import StopLossDetails, ClientExtensions 40 | from v20.transaction import TrailingStopLossDetails, TakeProfitDetails 41 | 42 | MAX_REQUEST_COUNT = float(5000) 43 | 44 | 45 | class Job(threading.Thread): 46 | def __init__(self, job_callable, args=None): 47 | threading.Thread.__init__(self) 48 | self.callable = job_callable 49 | self.args = args 50 | 51 | # The shutdown_flag is a threading.Event object that 52 | # indicates whether the thread should be terminated. 53 | self.shutdown_flag = threading.Event() 54 | self.job = None 55 | self.exception = None 56 | 57 | def run(self): 58 | print('Thread #%s started' % self.ident) 59 | try: 60 | self.job = self.callable 61 | while not self.shutdown_flag.is_set(): 62 | print("Starting job loop...") 63 | if self.args is None: 64 | self.job() 65 | else: 66 | self.job(self.args) 67 | except Exception as e: 68 | import sys 69 | import traceback 70 | print(traceback.format_exc()) 71 | self.exception = e 72 | _thread.interrupt_main() 73 | 74 | 75 | class ServiceExit(Exception): 76 | """ 77 | Custom exception which is used to trigger the clean exit 78 | of all running threads and the main program. 79 | """ 80 | 81 | def __init__(self, message=None): 82 | self.message = message 83 | 84 | def __repr__(self): 85 | return repr(self.message) 86 | 87 | 88 | def service_shutdown(signum, frame): 89 | print('exiting ...') 90 | raise ServiceExit 91 | 92 | 93 | class tpqoa(object): 94 | ''' tpqoa is a Python wrapper class for the Oanda v20 API. ''' 95 | 96 | def __init__(self, conf_file): 97 | ''' Init function is expecting a configuration file with 98 | the following content: 99 | 100 | [oanda] 101 | account_id = XYZ-ABC-... 102 | access_token = ZYXCAB... 103 | account_type = practice (default) or live 104 | 105 | Parameters 106 | ========== 107 | conf_file: string 108 | path to and filename of the configuration file, 109 | e.g. '/home/me/oanda.cfg' 110 | ''' 111 | self.config = configparser.ConfigParser() 112 | self.config.read(conf_file) 113 | self.access_token = self.config['oanda']['access_token'] 114 | self.account_id = self.config['oanda']['account_id'] 115 | self.account_type = self.config['oanda']['account_type'] 116 | 117 | if self.account_type == 'live': 118 | self.hostname = 'api-fxtrade.oanda.com' 119 | self.stream_hostname = 'stream-fxtrade.oanda.com' 120 | else: 121 | self.hostname = 'api-fxpractice.oanda.com' 122 | self.stream_hostname = 'stream-fxpractice.oanda.com' 123 | 124 | self.ctx = v20.Context( 125 | hostname=self.hostname, 126 | port=443, 127 | token=self.access_token, 128 | poll_timeout=10 129 | ) 130 | self.ctx_stream = v20.Context( 131 | hostname=self.stream_hostname, 132 | port=443, 133 | token=self.access_token, 134 | ) 135 | 136 | self.suffix = '.000000000Z' 137 | self.stop_stream = False 138 | 139 | def get_instruments(self): 140 | ''' Retrieves and returns all instruments for the given account. ''' 141 | resp = self.ctx.account.instruments(self.account_id) 142 | instruments = resp.get('instruments') 143 | instruments = [ins.dict() for ins in instruments] 144 | instruments = [(ins['displayName'], ins['name']) 145 | for ins in instruments] 146 | return sorted(instruments) 147 | 148 | def get_prices(self, instrument): 149 | ''' Returns the current BID/ASK prices for instrument. ''' 150 | r = self.ctx.pricing.get(self.account_id, instruments=instrument) 151 | r = json.loads(r.raw_body) 152 | bid = float(r['prices'][0]['closeoutBid']) 153 | ask = float(r['prices'][0]['closeoutAsk']) 154 | return r['time'], bid, ask 155 | 156 | def transform_datetime(self, dati): 157 | ''' Transforms Python datetime object to string. ''' 158 | if isinstance(dati, str): 159 | dati = pd.Timestamp(dati).to_pydatetime() 160 | return dati.isoformat('T') + self.suffix 161 | 162 | def retrieve_data(self, instrument, start, end, granularity, price): 163 | raw = self.ctx.instrument.candles( 164 | instrument=instrument, 165 | fromTime=start, toTime=end, 166 | granularity=granularity, price=price) 167 | raw = raw.get('candles') 168 | raw = [cs.dict() for cs in raw] 169 | if price == 'A': 170 | for cs in raw: 171 | cs.update(cs['ask']) 172 | del cs['ask'] 173 | elif price == 'B': 174 | for cs in raw: 175 | cs.update(cs['bid']) 176 | del cs['bid'] 177 | elif price == 'M': 178 | for cs in raw: 179 | cs.update(cs['mid']) 180 | del cs['mid'] 181 | else: 182 | raise ValueError("Price must be either 'B', 'A' or 'M'.") 183 | if len(raw) == 0: 184 | return pd.DataFrame() # return empty DataFrame if no data 185 | data = pd.DataFrame(raw) 186 | data['time'] = pd.to_datetime(data['time']) 187 | data = data.set_index('time') 188 | data.index = pd.DatetimeIndex(data.index) 189 | for col in list('ohlc'): 190 | data[col] = data[col].astype(float) 191 | return data 192 | 193 | def get_history(self, instrument, start, end, 194 | granularity, price, localize=True): 195 | ''' Retrieves historical data for instrument. 196 | 197 | Parameters 198 | ========== 199 | instrument: string 200 | valid instrument name 201 | start, end: datetime, str 202 | Python datetime or string objects for start and end 203 | granularity: string 204 | a string like 'S5', 'M1' or 'D' 205 | price: string 206 | one of 'A' (ask), 'B' (bid) or 'M' (middle) 207 | 208 | Returns 209 | ======= 210 | data: pd.DataFrame 211 | pandas DataFrame object with data 212 | ''' 213 | if granularity.startswith('S') or granularity.startswith('M') \ 214 | or granularity.startswith('H'): 215 | multiplier = float("".join(filter(str.isdigit, granularity))) 216 | if granularity.startswith('S'): 217 | # freq = '1h' 218 | freq = f"{int(MAX_REQUEST_COUNT * multiplier / float(3600))}H" 219 | else: 220 | # freq = 'D' 221 | freq = f"{int(MAX_REQUEST_COUNT * multiplier / float(1440))}D" 222 | data = pd.DataFrame() 223 | dr = pd.date_range(start, end, freq=freq) 224 | 225 | for t in range(len(dr)): 226 | batch_start = self.transform_datetime(dr[t]) 227 | if t != len(dr) - 1: 228 | batch_end = self.transform_datetime(dr[t + 1]) 229 | else: 230 | batch_end = self.transform_datetime(end) 231 | 232 | batch = self.retrieve_data(instrument, batch_start, batch_end, 233 | granularity, price) 234 | data = pd.concat([data, batch]) 235 | else: 236 | start = self.transform_datetime(start) 237 | end = self.transform_datetime(end) 238 | data = self.retrieve_data(instrument, start, end, 239 | granularity, price) 240 | if localize: 241 | data.index = data.index.tz_localize(None) 242 | 243 | return data[['o', 'h', 'l', 'c', 'volume', 'complete']] 244 | 245 | def create_order(self, instrument, units, price=None, sl_distance=None, 246 | tsl_distance=None, tp_price=None, comment=None, 247 | touch=False, suppress=False, ret=False): 248 | ''' Places order with Oanda. 249 | 250 | Parameters 251 | ========== 252 | instrument: string 253 | valid instrument name 254 | units: int 255 | number of units of instrument to be bought 256 | (positive int, eg 'units=50') 257 | or to be sold (negative int, eg 'units=-100') 258 | price: float 259 | limit order price, touch order price 260 | sl_distance: float 261 | stop loss distance price, mandatory eg in Germany 262 | tsl_distance: float 263 | trailing stop loss distance 264 | tp_price: float 265 | take profit price to be used for the trade 266 | comment: str 267 | string 268 | touch: boolean 269 | market_if_touched order (requires price to be set) 270 | suppress: boolean 271 | whether to suppress print out 272 | ret: boolean 273 | whether to return the order object 274 | ''' 275 | client_ext = ClientExtensions( 276 | comment=comment) if comment is not None else None 277 | sl_details = (StopLossDetails(distance=sl_distance, 278 | clientExtensions=client_ext) 279 | if sl_distance is not None else None) 280 | tsl_details = (TrailingStopLossDetails(distance=tsl_distance, 281 | clientExtensions=client_ext) 282 | if tsl_distance is not None else None) 283 | tp_details = (TakeProfitDetails( 284 | price=tp_price, clientExtensions=client_ext) 285 | if tp_price is not None else None) 286 | if price is None: 287 | request = self.ctx.order.market( 288 | self.account_id, 289 | instrument=instrument, 290 | units=units, 291 | stopLossOnFill=sl_details, 292 | trailingStopLossOnFill=tsl_details, 293 | takeProfitOnFill=tp_details, 294 | ) 295 | elif touch: 296 | request = self.ctx.order.market_if_touched( 297 | self.account_id, 298 | instrument=instrument, 299 | price=price, 300 | units=units, 301 | stopLossOnFill=sl_details, 302 | trailingStopLossOnFill=tsl_details, 303 | takeProfitOnFill=tp_details 304 | ) 305 | else: 306 | request = self.ctx.order.limit( 307 | self.account_id, 308 | instrument=instrument, 309 | price=price, 310 | units=units, 311 | stopLossOnFill=sl_details, 312 | trailingStopLossOnFill=tsl_details, 313 | takeProfitOnFill=tp_details 314 | ) 315 | 316 | # First checking if the order is rejected 317 | if 'orderRejectTransaction' in request.body: 318 | order = request.get('orderRejectTransaction') 319 | elif 'orderFillTransaction' in request.body: 320 | order = request.get('orderFillTransaction') 321 | elif 'orderCreateTransaction' in request.body: 322 | order = request.get('orderCreateTransaction') 323 | else: 324 | # This case does not happen. But keeping this for completeness. 325 | order = None 326 | 327 | if not suppress and order is not None: 328 | print('\n\n', order.dict(), '\n') 329 | if ret is True: 330 | return order.dict() if order is not None else None 331 | 332 | def stream_data(self, instrument, stop=None, ret=False, callback=None): 333 | ''' Starts a real-time data stream. 334 | 335 | Parameters 336 | ========== 337 | instrument: string 338 | valid instrument name 339 | ''' 340 | self.stream_instrument = instrument 341 | self.ticks = 0 342 | response = self.ctx_stream.pricing.stream( 343 | self.account_id, snapshot=True, 344 | instruments=instrument) 345 | msgs = [] 346 | for msg_type, msg in response.parts(): 347 | msgs.append(msg) 348 | # print(msg_type, msg) 349 | if msg_type == 'pricing.ClientPrice': 350 | self.ticks += 1 351 | self.time = msg.time 352 | if callback is not None: 353 | callback(msg.instrument, msg.time, 354 | float(msg.bids[0].dict()['price']), 355 | float(msg.asks[0].dict()['price'])) 356 | else: 357 | self.on_success(msg.time, 358 | float(msg.bids[0].dict()['price']), 359 | float(msg.asks[0].dict()['price'])) 360 | if stop is not None: 361 | if self.ticks >= stop: 362 | if ret: 363 | return msgs 364 | break 365 | if self.stop_stream: 366 | if ret: 367 | return msgs 368 | break 369 | 370 | def _stream_data_failsafe_thread(self, args): 371 | try: 372 | print("Starting price streaming") 373 | self.stream_data(args[0], callback=args[1]) 374 | except Exception as e: 375 | import sys 376 | import traceback 377 | print(traceback.format_exc()) 378 | sleep(3) 379 | return 380 | 381 | def stream_data_failsafe(self, instrument, callback=None): 382 | signal.signal(signal.SIGTERM, service_shutdown) 383 | signal.signal(signal.SIGINT, service_shutdown) 384 | signal.signal(signal.SIGSEGV, service_shutdown) 385 | try: 386 | price_stream_thread = Job(self._stream_data_failsafe_thread, 387 | [instrument, callback]) 388 | price_stream_thread.start() 389 | return price_stream_thread 390 | except ServiceExit as e: 391 | print('Handling exception') 392 | import sys 393 | import traceback 394 | print(traceback) 395 | price_stream_thread.shutdown_flag.set() 396 | price_stream_thread.join() 397 | 398 | def on_success(self, time, bid, ask): 399 | ''' Method called when new data is retrieved. ''' 400 | print(time, bid, ask) 401 | 402 | def get_account_summary(self, detailed=False): 403 | ''' Returns summary data for Oanda account.''' 404 | if detailed is True: 405 | response = self.ctx.account.get(self.account_id) 406 | else: 407 | response = self.ctx.account.summary(self.account_id) 408 | raw = response.get('account') 409 | return raw.dict() 410 | 411 | def get_transaction(self, tid=0): 412 | ''' Retrieves and returns transaction data. ''' 413 | response = self.ctx.transaction.get(self.account_id, tid) 414 | transaction = response.get('transaction') 415 | return transaction.dict() 416 | 417 | def get_transactions(self, tid=0): 418 | ''' Retrieves and returns transactions data. ''' 419 | response = self.ctx.transaction.since(self.account_id, id=tid) 420 | transactions = response.get('transactions') 421 | transactions = [t.dict() for t in transactions] 422 | return transactions 423 | 424 | def print_transactions(self, tid=0): 425 | ''' Prints basic transactions data. ''' 426 | transactions = self.get_transactions(tid) 427 | for trans in transactions: 428 | try: 429 | templ = '%4s | %s | %7s | %8s | %8s' 430 | print(templ % (trans['id'], 431 | trans['time'][:-8], 432 | trans['instrument'], 433 | trans['units'], 434 | trans['pl'])) 435 | except Exception: 436 | pass 437 | 438 | def get_positions(self): 439 | ''' Retrieves and returns positions data. ''' 440 | response = self.ctx.position.list_open(self.account_id).body 441 | positions = [p.dict() for p in response.get('positions')] 442 | return positions 443 | 444 | def cancel_order(self, order_id): 445 | ''' Cancels an order (e.g. SL order). 446 | 447 | Parameters 448 | ========== 449 | order_id: int 450 | valid order id 451 | ''' 452 | response = self.ctx.order.cancel(self.account_id, order_id) 453 | return json.loads(response.raw_body) 454 | -------------------------------------------------------------------------------- /tpqoa.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "slideshow": { 7 | "slide_type": "slide" 8 | } 9 | }, 10 | "source": [ 11 | "\"The
" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "# tpqoa" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Algorithmic Trading with Oanda" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "`tpqoa` is a wrapper class for the Oanda REST API v20 (http://developer.oanda.com/). It makes use of the Python package `v20` from Oanda (https://github.com/oanda/v20-python). The package is authored and maintained by The Python Quants GmbH. © Dr. Yves J. Hilpisch. MIT License." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "## Disclaimer" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "Trading forex/CFDs on margin carries a high level of risk and may not be suitable for all investors as you could sustain losses in excess of deposits. Leverage can work against you. Due to the certain restrictions imposed by the local law and regulation, German resident retail client(s) could sustain a total loss of deposited funds but are not subject to subsequent payment obligations beyond the deposited funds. Be aware and fully understand all risks associated with the market and trading. Prior to trading any products, carefully consider your financial situation and experience level. Any opinions, news, research, analyses, prices, code examples or other information is provided as general market commentary, and does not constitute investment advice. The Python Quants GmbH will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information.\n", 47 | "\n", 48 | "The `tpqoa` package is intended as a technological illustration only. It comes with no warranties or representations, to the extent permitted by applicable law." 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "## Installation" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "Installing from source via `Git` and `Github`:\n", 63 | "\n", 64 | " git clone https://github.com/yhilpisch/tpqoa\n", 65 | " cd tpqoa\n", 66 | " python setup.py install\n", 67 | " \n", 68 | "Using `pip` in combination with `Github`:\n", 69 | "\n", 70 | " pip install git+git://github.com/yhilpisch/tpqoa" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "## Connection" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "In order to connect to the API, you need to have at least a practice account with Oanda (https://oanda.com/). Once logged in to you account, you can create an API token and can copy your account number. These are expected to be stored in a configuration file, with name `oanda.cfg`, for instance, as follows:\n", 85 | "\n", 86 | " [oanda]\n", 87 | " account_id = XYZ-ABC-...\n", 88 | " access_token = ZYXCAB...\n", 89 | " account_type = practice (default) or live" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "You can then set up an API connection by instantiating a connection object." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 1, 102 | "metadata": { 103 | "tags": [] 104 | }, 105 | "outputs": [], 106 | "source": [ 107 | "import tpqoa" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 2, 113 | "metadata": { 114 | "tags": [] 115 | }, 116 | "outputs": [], 117 | "source": [ 118 | "oanda = tpqoa.tpqoa('../../oanda.cfg') # adjust path as necessary" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "## Available Instruments" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "The `get_instruments()` method retrieves all available instruments." 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 3, 138 | "metadata": { 139 | "tags": [] 140 | }, 141 | "outputs": [], 142 | "source": [ 143 | "ins = oanda.get_instruments()" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 4, 149 | "metadata": { 150 | "tags": [] 151 | }, 152 | "outputs": [ 153 | { 154 | "data": { 155 | "text/plain": [ 156 | "[('AUD/CAD', 'AUD_CAD'),\n", 157 | " ('AUD/CHF', 'AUD_CHF'),\n", 158 | " ('AUD/HKD', 'AUD_HKD'),\n", 159 | " ('AUD/JPY', 'AUD_JPY'),\n", 160 | " ('AUD/NZD', 'AUD_NZD'),\n", 161 | " ('AUD/SGD', 'AUD_SGD'),\n", 162 | " ('AUD/USD', 'AUD_USD'),\n", 163 | " ('Australia 200', 'AU200_AUD'),\n", 164 | " ('Brent Crude Oil', 'BCO_USD'),\n", 165 | " ('Bund', 'DE10YB_EUR')]" 166 | ] 167 | }, 168 | "execution_count": 4, 169 | "metadata": {}, 170 | "output_type": "execute_result" 171 | } 172 | ], 173 | "source": [ 174 | "ins[:10]" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "## Historical Data" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "The `get_history()` method retrieves historical data." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": 5, 194 | "metadata": { 195 | "tags": [] 196 | }, 197 | "outputs": [ 198 | { 199 | "name": "stdout", 200 | "output_type": "stream", 201 | "text": [ 202 | "Help on method get_history in module tpqoa.tpqoa:\n", 203 | "\n", 204 | "get_history(instrument, start, end, granularity, price, localize=True) method of tpqoa.tpqoa.tpqoa instance\n", 205 | " Retrieves historical data for instrument.\n", 206 | " \n", 207 | " Parameters\n", 208 | " ==========\n", 209 | " instrument: string\n", 210 | " valid instrument name\n", 211 | " start, end: datetime, str\n", 212 | " Python datetime or string objects for start and end\n", 213 | " granularity: string\n", 214 | " a string like 'S5', 'M1' or 'D'\n", 215 | " price: string\n", 216 | " one of 'A' (ask), 'B' (bid) or 'M' (middle)\n", 217 | " \n", 218 | " Returns\n", 219 | " =======\n", 220 | " data: pd.DataFrame\n", 221 | " pandas DataFrame object with data\n", 222 | "\n" 223 | ] 224 | } 225 | ], 226 | "source": [ 227 | "help(oanda.get_history)" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 6, 233 | "metadata": { 234 | "tags": [] 235 | }, 236 | "outputs": [], 237 | "source": [ 238 | "# oanda.get_history??" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 7, 244 | "metadata": { 245 | "tags": [] 246 | }, 247 | "outputs": [], 248 | "source": [ 249 | "data = oanda.get_history(instrument='EUR_USD',\n", 250 | " start='2022-06-15',\n", 251 | " end='2023-06-15',\n", 252 | " granularity='D',\n", 253 | " price='M')" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": 8, 259 | "metadata": { 260 | "tags": [] 261 | }, 262 | "outputs": [ 263 | { 264 | "name": "stdout", 265 | "output_type": "stream", 266 | "text": [ 267 | "\n", 268 | "DatetimeIndex: 261 entries, 2022-06-14 21:00:00 to 2023-06-14 21:00:00\n", 269 | "Data columns (total 6 columns):\n", 270 | " # Column Non-Null Count Dtype \n", 271 | "--- ------ -------------- ----- \n", 272 | " 0 o 261 non-null float64\n", 273 | " 1 h 261 non-null float64\n", 274 | " 2 l 261 non-null float64\n", 275 | " 3 c 261 non-null float64\n", 276 | " 4 volume 261 non-null int64 \n", 277 | " 5 complete 261 non-null bool \n", 278 | "dtypes: bool(1), float64(4), int64(1)\n", 279 | "memory usage: 12.5 KB\n" 280 | ] 281 | } 282 | ], 283 | "source": [ 284 | "data.info()" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": 9, 290 | "metadata": { 291 | "tags": [] 292 | }, 293 | "outputs": [ 294 | { 295 | "name": "stdout", 296 | "output_type": "stream", 297 | "text": [ 298 | " o h l c volume complete\n", 299 | "time \n", 300 | "2022-06-14 21:00:00 1.04114 1.05078 1.03593 1.04466 204826 True\n", 301 | "2022-06-15 21:00:00 1.04444 1.06014 1.03809 1.05524 183417 True\n", 302 | "2022-06-16 21:00:00 1.05496 1.05612 1.04445 1.04938 156233 True\n", 303 | "2022-06-19 21:00:00 1.04841 1.05460 1.04746 1.05112 85713 True\n", 304 | "2022-06-20 21:00:00 1.05088 1.05826 1.05086 1.05348 101517 True\n" 305 | ] 306 | } 307 | ], 308 | "source": [ 309 | "print(data.head())" 310 | ] 311 | }, 312 | { 313 | "cell_type": "markdown", 314 | "metadata": {}, 315 | "source": [ 316 | "## Streaming Data" 317 | ] 318 | }, 319 | { 320 | "cell_type": "markdown", 321 | "metadata": {}, 322 | "source": [ 323 | "The method `stream_data()` allows the streaming of real-time data (bid & ask)." 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": 10, 329 | "metadata": { 330 | "tags": [] 331 | }, 332 | "outputs": [ 333 | { 334 | "name": "stdout", 335 | "output_type": "stream", 336 | "text": [ 337 | "2023-06-27T06:57:58.204324464Z 1.09299 1.09313\n", 338 | "2023-06-27T06:57:58.400409926Z 1.09301 1.09315\n", 339 | "2023-06-27T06:58:00.348284643Z 1.093 1.09314\n" 340 | ] 341 | } 342 | ], 343 | "source": [ 344 | "oanda.stream_data('EUR_USD', stop=3)" 345 | ] 346 | }, 347 | { 348 | "cell_type": "markdown", 349 | "metadata": {}, 350 | "source": [ 351 | "By redefining the `on_success()` method, you can control what happes with the streaming data." 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": 11, 357 | "metadata": { 358 | "tags": [] 359 | }, 360 | "outputs": [], 361 | "source": [ 362 | "class myOanda(tpqoa.tpqoa):\n", 363 | " def on_success(self, time, bid, ask):\n", 364 | " ''' Method called when new data is retrieved. '''\n", 365 | " print('BID: {:.5f} | ASK: {:.5f}'.format(bid, ask))" 366 | ] 367 | }, 368 | { 369 | "cell_type": "code", 370 | "execution_count": 12, 371 | "metadata": { 372 | "tags": [] 373 | }, 374 | "outputs": [], 375 | "source": [ 376 | "my_oanda = myOanda('../../oanda.cfg')" 377 | ] 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": 13, 382 | "metadata": { 383 | "tags": [] 384 | }, 385 | "outputs": [ 386 | { 387 | "name": "stdout", 388 | "output_type": "stream", 389 | "text": [ 390 | "BID: 1.09297 | ASK: 1.09311\n", 391 | "BID: 1.09297 | ASK: 1.09310\n", 392 | "BID: 1.09297 | ASK: 1.09311\n", 393 | "BID: 1.09297 | ASK: 1.09312\n", 394 | "BID: 1.09294 | ASK: 1.09309\n" 395 | ] 396 | } 397 | ], 398 | "source": [ 399 | "my_oanda.stream_data('EUR_USD', stop=5)" 400 | ] 401 | }, 402 | { 403 | "cell_type": "markdown", 404 | "metadata": {}, 405 | "source": [ 406 | "## Placing Orders" 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 14, 412 | "metadata": { 413 | "tags": [] 414 | }, 415 | "outputs": [ 416 | { 417 | "name": "stdout", 418 | "output_type": "stream", 419 | "text": [ 420 | "Help on method create_order in module tpqoa.tpqoa:\n", 421 | "\n", 422 | "create_order(instrument, units, price=None, sl_distance=None, tsl_distance=None, tp_price=None, comment=None, touch=False, suppress=False, ret=False) method of tpqoa.tpqoa.tpqoa instance\n", 423 | " Places order with Oanda.\n", 424 | " \n", 425 | " Parameters\n", 426 | " ==========\n", 427 | " instrument: string\n", 428 | " valid instrument name\n", 429 | " units: int\n", 430 | " number of units of instrument to be bought\n", 431 | " (positive int, eg 'units=50')\n", 432 | " or to be sold (negative int, eg 'units=-100')\n", 433 | " price: float\n", 434 | " limit order price, touch order price\n", 435 | " sl_distance: float\n", 436 | " stop loss distance price, mandatory eg in Germany\n", 437 | " tsl_distance: float\n", 438 | " trailing stop loss distance\n", 439 | " tp_price: float\n", 440 | " take profit price to be used for the trade\n", 441 | " comment: str\n", 442 | " string\n", 443 | " touch: boolean\n", 444 | " market_if_touched order (requires price to be set)\n", 445 | " suppress: boolean\n", 446 | " whether to suppress print out\n", 447 | " ret: boolean\n", 448 | " whether to return the order object\n", 449 | "\n" 450 | ] 451 | } 452 | ], 453 | "source": [ 454 | "help(oanda.create_order)" 455 | ] 456 | }, 457 | { 458 | "cell_type": "code", 459 | "execution_count": 15, 460 | "metadata": { 461 | "tags": [] 462 | }, 463 | "outputs": [ 464 | { 465 | "name": "stdout", 466 | "output_type": "stream", 467 | "text": [ 468 | "\n", 469 | "\n", 470 | " {'id': '3608', 'time': '2023-06-27T06:58:16.307275954Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3607', 'requestID': '61122547747050135', 'type': 'ORDER_FILL', 'orderID': '3607', 'instrument': 'EUR_USD', 'units': '100.0', 'gainQuoteHomeConversionFactor': '0.910326029089', 'lossQuoteHomeConversionFactor': '0.919475034407', 'price': 1.09309, 'fullVWAP': 1.09309, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.09294, 'liquidity': '10000000'}], 'asks': [{'price': 1.09309, 'liquidity': '10000000'}], 'closeoutBid': 1.09294, 'closeoutAsk': 1.09309}, 'reason': 'MARKET_ORDER', 'pl': '0.0', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101295.189', 'tradeOpened': {'tradeID': '3608', 'units': '100.0', 'price': 1.09309, 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.0069', 'initialMarginRequired': '3.33'}, 'halfSpreadCost': '0.0069'} \n", 471 | "\n" 472 | ] 473 | } 474 | ], 475 | "source": [ 476 | "# going long 100 units\n", 477 | "# sl_distance of 20 pips\n", 478 | "oanda.create_order('EUR_USD', units=100, sl_distance=0.002)" 479 | ] 480 | }, 481 | { 482 | "cell_type": "code", 483 | "execution_count": 16, 484 | "metadata": { 485 | "tags": [] 486 | }, 487 | "outputs": [ 488 | { 489 | "name": "stdout", 490 | "output_type": "stream", 491 | "text": [ 492 | "\n", 493 | "\n", 494 | " {'id': '3611', 'time': '2023-06-27T06:58:16.523945599Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3610', 'requestID': '61122547747050332', 'type': 'ORDER_FILL', 'orderID': '3610', 'instrument': 'EUR_USD', 'units': '-100.0', 'gainQuoteHomeConversionFactor': '0.910326029089', 'lossQuoteHomeConversionFactor': '0.919475034407', 'price': 1.09294, 'fullVWAP': 1.09294, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.09294, 'liquidity': '10000000'}], 'asks': [{'price': 1.09309, 'liquidity': '9999900'}], 'closeoutBid': 1.09294, 'closeoutAsk': 1.09309}, 'reason': 'MARKET_ORDER', 'pl': '-0.0138', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101295.1752', 'tradesClosed': [{'tradeID': '3608', 'units': '-100.0', 'price': 1.09294, 'realizedPL': '-0.0138', 'financing': '0.0', 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.0069'}], 'halfSpreadCost': '0.0069'} \n", 495 | "\n" 496 | ] 497 | } 498 | ], 499 | "source": [ 500 | "# closing out the position\n", 501 | "oanda.create_order('EUR_USD', units=-100)" 502 | ] 503 | }, 504 | { 505 | "cell_type": "markdown", 506 | "metadata": {}, 507 | "source": [ 508 | "## Canceling Orders " 509 | ] 510 | }, 511 | { 512 | "cell_type": "code", 513 | "execution_count": 17, 514 | "metadata": { 515 | "tags": [] 516 | }, 517 | "outputs": [ 518 | { 519 | "name": "stdout", 520 | "output_type": "stream", 521 | "text": [ 522 | "\n", 523 | "\n", 524 | " {'id': '3614', 'time': '2023-06-27T06:58:33.953341530Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3613', 'requestID': '61122547818369950', 'type': 'ORDER_FILL', 'orderID': '3613', 'instrument': 'EUR_USD', 'units': '10000.0', 'gainQuoteHomeConversionFactor': '0.910363508679', 'lossQuoteHomeConversionFactor': '0.919512890676', 'price': 1.09304, 'fullVWAP': 1.09304, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.0929, 'liquidity': '10000000'}], 'asks': [{'price': 1.09304, 'liquidity': '10000000'}], 'closeoutBid': 1.0929, 'closeoutAsk': 1.09304}, 'reason': 'MARKET_ORDER', 'pl': '0.0', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101295.1752', 'tradeOpened': {'tradeID': '3614', 'units': '10000.0', 'price': 1.09304, 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.6405', 'initialMarginRequired': '333.0'}, 'halfSpreadCost': '0.6405'} \n", 525 | "\n" 526 | ] 527 | } 528 | ], 529 | "source": [ 530 | "order = oanda.create_order('EUR_USD', units=10000, sl_distance=0.01, ret=True)" 531 | ] 532 | }, 533 | { 534 | "cell_type": "code", 535 | "execution_count": 18, 536 | "metadata": { 537 | "tags": [] 538 | }, 539 | "outputs": [ 540 | { 541 | "data": { 542 | "text/plain": [ 543 | "{'id': '3615',\n", 544 | " 'time': '2023-06-27T06:58:33.953341530Z',\n", 545 | " 'userID': 13834683,\n", 546 | " 'accountID': '101-004-13834683-001',\n", 547 | " 'batchID': '3613',\n", 548 | " 'requestID': '61122547818369950',\n", 549 | " 'type': 'STOP_LOSS_ORDER',\n", 550 | " 'tradeID': '3614',\n", 551 | " 'price': 1.08304,\n", 552 | " 'distance': '0.01',\n", 553 | " 'timeInForce': 'GTC',\n", 554 | " 'triggerCondition': 'DEFAULT',\n", 555 | " 'reason': 'ON_FILL'}" 556 | ] 557 | }, 558 | "execution_count": 18, 559 | "metadata": {}, 560 | "output_type": "execute_result" 561 | } 562 | ], 563 | "source": [ 564 | "oanda.get_transaction(tid=int(order['id']) + 1)" 565 | ] 566 | }, 567 | { 568 | "cell_type": "code", 569 | "execution_count": 19, 570 | "metadata": { 571 | "tags": [] 572 | }, 573 | "outputs": [ 574 | { 575 | "data": { 576 | "text/plain": [ 577 | "{'orderCancelTransaction': {'id': '3616',\n", 578 | " 'accountID': '101-004-13834683-001',\n", 579 | " 'userID': 13834683,\n", 580 | " 'batchID': '3616',\n", 581 | " 'requestID': '61122547826759581',\n", 582 | " 'time': '2023-06-27T06:58:35.038788414Z',\n", 583 | " 'type': 'ORDER_CANCEL',\n", 584 | " 'orderID': '3615',\n", 585 | " 'reason': 'CLIENT_REQUEST'},\n", 586 | " 'relatedTransactionIDs': ['3616'],\n", 587 | " 'lastTransactionID': '3616'}" 588 | ] 589 | }, 590 | "execution_count": 19, 591 | "metadata": {}, 592 | "output_type": "execute_result" 593 | } 594 | ], 595 | "source": [ 596 | "oanda.cancel_order(int(order['id']) + 1)" 597 | ] 598 | }, 599 | { 600 | "cell_type": "code", 601 | "execution_count": 20, 602 | "metadata": { 603 | "tags": [] 604 | }, 605 | "outputs": [ 606 | { 607 | "name": "stdout", 608 | "output_type": "stream", 609 | "text": [ 610 | "\n", 611 | "\n", 612 | " {'id': '3618', 'time': '2023-06-27T06:58:36.381641548Z', 'userID': 13834683, 'accountID': '101-004-13834683-001', 'batchID': '3617', 'requestID': '61122547830955203', 'type': 'ORDER_FILL', 'orderID': '3617', 'instrument': 'EUR_USD', 'units': '-10000.0', 'gainQuoteHomeConversionFactor': '0.910359343552', 'lossQuoteHomeConversionFactor': '0.919508683689', 'price': 1.09291, 'fullVWAP': 1.09291, 'fullPrice': {'type': 'PRICE', 'bids': [{'price': 1.09291, 'liquidity': '10000000'}], 'asks': [{'price': 1.09304, 'liquidity': '9990000'}], 'closeoutBid': 1.09291, 'closeoutAsk': 1.09304}, 'reason': 'MARKET_ORDER', 'pl': '-1.1954', 'financing': '0.0', 'commission': '0.0', 'guaranteedExecutionFee': '0.0', 'accountBalance': '101293.9798', 'tradesClosed': [{'tradeID': '3614', 'units': '-10000.0', 'price': 1.09291, 'realizedPL': '-1.1954', 'financing': '0.0', 'guaranteedExecutionFee': '0.0', 'halfSpreadCost': '0.5947'}], 'halfSpreadCost': '0.5947'} \n", 613 | "\n" 614 | ] 615 | } 616 | ], 617 | "source": [ 618 | "order = oanda.create_order('EUR_USD', units=-10000)" 619 | ] 620 | }, 621 | { 622 | "cell_type": "markdown", 623 | "metadata": {}, 624 | "source": [ 625 | "## Account-Related Methods" 626 | ] 627 | }, 628 | { 629 | "cell_type": "code", 630 | "execution_count": 21, 631 | "metadata": { 632 | "tags": [] 633 | }, 634 | "outputs": [ 635 | { 636 | "name": "stdout", 637 | "output_type": "stream", 638 | "text": [ 639 | "Help on method get_account_summary in module tpqoa.tpqoa:\n", 640 | "\n", 641 | "get_account_summary(detailed=False) method of tpqoa.tpqoa.tpqoa instance\n", 642 | " Returns summary data for Oanda account.\n", 643 | "\n" 644 | ] 645 | } 646 | ], 647 | "source": [ 648 | "help(oanda.get_account_summary)" 649 | ] 650 | }, 651 | { 652 | "cell_type": "code", 653 | "execution_count": 22, 654 | "metadata": { 655 | "tags": [] 656 | }, 657 | "outputs": [ 658 | { 659 | "data": { 660 | "text/plain": [ 661 | "{'id': '101-004-13834683-001',\n", 662 | " 'alias': 'Primary',\n", 663 | " 'currency': 'EUR',\n", 664 | " 'balance': '101293.9798',\n", 665 | " 'createdByUserID': 13834683,\n", 666 | " 'createdTime': '2020-03-19T06:08:14.363139403Z',\n", 667 | " 'guaranteedStopLossOrderMode': 'ALLOWED',\n", 668 | " 'pl': '1488.9357',\n", 669 | " 'resettablePL': '1488.9357',\n", 670 | " 'resettablePLTime': '0',\n", 671 | " 'financing': '-194.9559',\n", 672 | " 'commission': '0.0',\n", 673 | " 'guaranteedExecutionFees': '0.0',\n", 674 | " 'marginRate': '0.0333',\n", 675 | " 'openTradeCount': 0,\n", 676 | " 'openPositionCount': 0,\n", 677 | " 'pendingOrderCount': 0,\n", 678 | " 'hedgingEnabled': False,\n", 679 | " 'unrealizedPL': '0.0',\n", 680 | " 'NAV': '101293.9798',\n", 681 | " 'marginUsed': '0.0',\n", 682 | " 'marginAvailable': '101293.9798',\n", 683 | " 'positionValue': '0.0',\n", 684 | " 'marginCloseoutUnrealizedPL': '0.0',\n", 685 | " 'marginCloseoutNAV': '101293.9798',\n", 686 | " 'marginCloseoutMarginUsed': '0.0',\n", 687 | " 'marginCloseoutPercent': '0.0',\n", 688 | " 'marginCloseoutPositionValue': '0.0',\n", 689 | " 'withdrawalLimit': '101293.9798',\n", 690 | " 'marginCallMarginUsed': '0.0',\n", 691 | " 'marginCallPercent': '0.0',\n", 692 | " 'lastTransactionID': '3618'}" 693 | ] 694 | }, 695 | "execution_count": 22, 696 | "metadata": {}, 697 | "output_type": "execute_result" 698 | } 699 | ], 700 | "source": [ 701 | "oanda.get_account_summary()" 702 | ] 703 | }, 704 | { 705 | "cell_type": "code", 706 | "execution_count": 23, 707 | "metadata": { 708 | "tags": [] 709 | }, 710 | "outputs": [ 711 | { 712 | "name": "stdout", 713 | "output_type": "stream", 714 | "text": [ 715 | "Help on method get_transactions in module tpqoa.tpqoa:\n", 716 | "\n", 717 | "get_transactions(tid=0) method of tpqoa.tpqoa.tpqoa instance\n", 718 | " Retrieves and returns transactions data.\n", 719 | "\n" 720 | ] 721 | } 722 | ], 723 | "source": [ 724 | "help(oanda.get_transactions)" 725 | ] 726 | }, 727 | { 728 | "cell_type": "code", 729 | "execution_count": 24, 730 | "metadata": { 731 | "tags": [] 732 | }, 733 | "outputs": [ 734 | { 735 | "name": "stdout", 736 | "output_type": "stream", 737 | "text": [ 738 | "Help on method print_transactions in module tpqoa.tpqoa:\n", 739 | "\n", 740 | "print_transactions(tid=0) method of tpqoa.tpqoa.tpqoa instance\n", 741 | " Prints basic transactions data.\n", 742 | "\n" 743 | ] 744 | } 745 | ], 746 | "source": [ 747 | "help(oanda.print_transactions)" 748 | ] 749 | }, 750 | { 751 | "cell_type": "code", 752 | "execution_count": 25, 753 | "metadata": { 754 | "collapsed": false, 755 | "jupyter": { 756 | "outputs_hidden": false 757 | }, 758 | "pycharm": { 759 | "name": "#%%\n" 760 | }, 761 | "tags": [] 762 | }, 763 | "outputs": [ 764 | { 765 | "name": "stdout", 766 | "output_type": "stream", 767 | "text": [ 768 | "3547 | 2023-06-27T06:07:18.75 | EUR_USD | -10000.0 | 2.0954\n", 769 | "3550 | 2023-06-27T06:12:25.19 | EUR_USD | 10000.0 | 0.0\n", 770 | "3553 | 2023-06-27T06:37:02.60 | EUR_USD | -10000.0 | 3.0046\n", 771 | "3556 | 2023-06-27T06:39:10.34 | EUR_USD | 100.0 | 0.0\n", 772 | "3559 | 2023-06-27T06:39:10.50 | EUR_USD | -100.0 | -0.0129\n", 773 | "3562 | 2023-06-27T06:39:34.89 | EUR_USD | 10000.0 | 0.0\n", 774 | "3568 | 2023-06-27T06:43:59.81 | EUR_USD | -10000.0 | -1.5631\n", 775 | "3570 | 2023-06-27T06:44:08.72 | EUR_USD | 10000.0 | 0.0\n", 776 | "3574 | 2023-06-27T06:52:58.26 | EUR_USD | 100.0 | 0.0\n", 777 | "3577 | 2023-06-27T06:52:58.45 | EUR_USD | -100.0 | -0.0451\n", 778 | "3579 | 2023-06-27T06:53:12.67 | EUR_USD | 10000.0 | 0.0\n", 779 | "3583 | 2023-06-27T06:54:17.23 | EUR_USD | -10000.0 | -4.9358\n", 780 | "3586 | 2023-06-27T06:54:20.61 | EUR_USD | 10000.0 | 0.0\n", 781 | "3590 | 2023-06-27T06:56:29.21 | EUR_USD | 100.0 | 0.0\n", 782 | "3593 | 2023-06-27T06:56:29.42 | EUR_USD | -100.0 | 0.0246\n", 783 | "3595 | 2023-06-27T06:56:29.63 | EUR_USD | 10000.0 | 0.0\n", 784 | "3599 | 2023-06-27T06:56:30.24 | EUR_USD | -10000.0 | 2.0921\n", 785 | "3601 | 2023-06-27T06:57:23.19 | EUR_USD | -10000.0 | 0.091\n", 786 | "3603 | 2023-06-27T06:57:27.47 | EUR_USD | -100.0 | -0.0074\n", 787 | "3606 | 2023-06-27T06:57:33.23 | EUR_USD | -9900.0 | 3.1541\n", 788 | "3608 | 2023-06-27T06:58:16.30 | EUR_USD | 100.0 | 0.0\n", 789 | "3611 | 2023-06-27T06:58:16.52 | EUR_USD | -100.0 | -0.0138\n", 790 | "3614 | 2023-06-27T06:58:33.95 | EUR_USD | 10000.0 | 0.0\n", 791 | "3618 | 2023-06-27T06:58:36.38 | EUR_USD | -10000.0 | -1.1954\n" 792 | ] 793 | } 794 | ], 795 | "source": [ 796 | "oanda.print_transactions(tid=3545)" 797 | ] 798 | }, 799 | { 800 | "cell_type": "markdown", 801 | "metadata": {}, 802 | "source": [ 803 | "\"The
\n", 804 | "\n", 805 | "http://tpq.io | @dyjh | training@tpq.io" 806 | ] 807 | } 808 | ], 809 | "metadata": { 810 | "anaconda-cloud": {}, 811 | "kernelspec": { 812 | "display_name": "Python 3 (ipykernel)", 813 | "language": "python", 814 | "name": "python3" 815 | }, 816 | "language_info": { 817 | "codemirror_mode": { 818 | "name": "ipython", 819 | "version": 3 820 | }, 821 | "file_extension": ".py", 822 | "mimetype": "text/x-python", 823 | "name": "python", 824 | "nbconvert_exporter": "python", 825 | "pygments_lexer": "ipython3", 826 | "version": "3.10.10" 827 | }, 828 | "pycharm": { 829 | "stem_cell": { 830 | "cell_type": "raw", 831 | "metadata": { 832 | "collapsed": false 833 | }, 834 | "source": [] 835 | } 836 | } 837 | }, 838 | "nbformat": 4, 839 | "nbformat_minor": 4 840 | } 841 | --------------------------------------------------------------------------------