├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DESCRIPTION.rst ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst ├── install.rst ├── license.rst ├── make.bat ├── oanda.rst ├── pyoanda.rst ├── pyoanda.tests.rst └── usage.rst ├── examples ├── create_stop_order.py ├── fetch.ipynb ├── grab_data.py └── multi_currency_prices.py ├── pyoanda ├── __init__.py ├── client.py ├── exceptions.py ├── order.py └── tests │ ├── __init__.py │ ├── _test_integration │ ├── __init__.py │ ├── integration_test_case.py │ ├── test_account.py │ ├── test_client_fundation.py │ ├── test_instruments.py │ ├── test_orders.py │ ├── test_position.py │ ├── test_trade.py │ └── test_transaction.py │ ├── test_client │ ├── __init__.py │ ├── test_account.py │ ├── test_fundation.py │ ├── test_instruments.py │ ├── test_orders.py │ ├── test_position.py │ ├── test_trade.py │ └── test_transaction.py │ └── test_order.py ├── requirements.txt ├── requirements2.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | .eggs 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | - "pypy" 8 | # dev versions 9 | - "3.5.0b3" 10 | - "3.5-dev" 11 | - "3.5" 12 | install: 13 | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then travis_retry pip install -r requirements2.txt; fi 14 | - travis_retry pip install -r requirements.txt 15 | - travis_retry python setup.py install 16 | script: 17 | - coverage run --source=pyoanda setup.py test 18 | after_success: 19 | - coveralls 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tolopalmer@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 5 | 6 | 7 | 8 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 9 | 10 | Please note we have a code of conduct, please follow it in all your interactions with the project. 11 | 12 | 13 | ## Pull Request Process 14 | 15 | * Ensure any install or build dependencies are removed before the end of the layer when doing a build. 16 | * Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 17 | * Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. 18 | -------------------------------------------------------------------------------- /DESCRIPTION.rst: -------------------------------------------------------------------------------- 1 | Oanda’s API python wrapper. Robust and Fast API wrapper for your Forex bot Python library that wraps Oanda API. Built on top of requests, it’s easy to use and makes sense. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Tolo Palmer 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PYOANDA 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/pyoanda/badge/?version=latest)](https://readthedocs.org/projects/pyoanda/?badge=latest) 4 | [![Build Status](https://travis-ci.org/toloco/pyoanda.svg?branch=master)](https://travis-ci.org/toloco/pyoanda) 5 | [![Coverage Status](https://coveralls.io/repos/toloco/pyoanda/badge.svg)](https://coveralls.io/r/toloco/pyoanda) 6 | [![PyPi version](https://img.shields.io/pypi/v/pyoanda.svg)](https://pypi.python.org/pypi/pyoanda) 7 | [![PyPi downloads](https://img.shields.io/pypi/dm/pyoanda.svg)](https://pypi.python.org/pypi/pyoanda) 8 | [![Code Health](https://landscape.io/github/toloco/pyoanda/master/landscape.svg?style=flat)](https://landscape.io/github/toloco/pyoanda/master) 9 | 10 | Oanda’s API python wrapper. Robust and Fast API wrapper for your Forex bot 11 | Python library that wraps [Oanda](http://oanda.com) API. Built on top of requests, it’s easy to use and makes sense. 12 | 13 | Pyoanda is released under the [MIT license](https://raw.githubusercontent.com/toloco/pyoanda/master/LICENSE). The source code is on [GitHub](https://github.com/toloco/pyoanda/) and [issues are also tracked on GitHub](https://github.com/toloco/pyoanda/issues). Works well with python __2.7, 3, 3.1, 3.2, 3.3, 3.4 and pypy__. 14 | 15 | ### Install 16 | #### Pypi 17 | ```bash 18 | pip install pyoanda 19 | ``` 20 | 21 | #### Manual 22 | ```bash 23 | git clone git@github.com:toloco/pyoanda.git 24 | cd pyoanda 25 | python setup.py install 26 | ``` 27 | 28 | ##### Run the tests 29 | ```bash 30 | python setup.py test 31 | ``` 32 | 33 | 34 | ### Code example 35 | 36 | ```python 37 | from pyoanda import Client, PRACTICE 38 | 39 | client = Client( 40 | environment=PRACTICE, 41 | account_id="Your Oanda account ID", 42 | access_token="Your Oanda access token" 43 | ) 44 | 45 | client.get_instrument_history( 46 | instrument="EUR_GBP", 47 | candle_format="midpoint", 48 | granularity="S30" 49 | ) 50 | ``` 51 | 52 | Note that if you are indenting to use the sandbox environment, you should first use the API to create an account then use the account_id to run the example above. 53 | 54 | ```python 55 | from pyoanda import Client, SANDBOX 56 | 57 | client = Client(environment=SANDBOX) 58 | 59 | # Create an account 60 | user = client.create_account() 61 | 62 | # Retrieve the username and accountId values for future use 63 | print "username: %s\naccount_id: %d" % (user['username'], user['accountId']) 64 | ``` 65 | 66 | ### Examples 67 | 68 | Check out the [examples gallery](examples) (Working progress) 69 | 70 | Please feel free to send or post new examples! everybody will love to see them. 71 | 72 | 73 | 74 | See [Pypi](https://pypi.python.org/pypi/pyoanda) project page. 75 | 76 | See [Docs](http://pyoanda.readthedocs.org/) project page. 77 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | api: 52 | sphinx-apidoc -f -o . ../pyoanda/ ../pyoanda/tests/ 53 | 54 | clean: 55 | rm -rf $(BUILDDIR)/* 56 | 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | dirhtml: 63 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 64 | @echo 65 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 66 | 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | pickle: 73 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 74 | @echo 75 | @echo "Build finished; now you can process the pickle files." 76 | 77 | json: 78 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 79 | @echo 80 | @echo "Build finished; now you can process the JSON files." 81 | 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | qthelp: 89 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 90 | @echo 91 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 92 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 93 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyoanda.qhcp" 94 | @echo "To view the help file:" 95 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyoanda.qhc" 96 | 97 | applehelp: 98 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 99 | @echo 100 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 101 | @echo "N.B. You won't be able to view it unless you put it in" \ 102 | "~/Library/Documentation/Help or install it in your application" \ 103 | "bundle." 104 | 105 | devhelp: 106 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 107 | @echo 108 | @echo "Build finished." 109 | @echo "To view the help file:" 110 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyoanda" 111 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyoanda" 112 | @echo "# devhelp" 113 | 114 | epub: 115 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 116 | @echo 117 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 118 | 119 | latex: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo 122 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 123 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 124 | "(use \`make latexpdf' here to do that automatically)." 125 | 126 | latexpdf: 127 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 128 | @echo "Running LaTeX files through pdflatex..." 129 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 130 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 131 | 132 | latexpdfja: 133 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 134 | @echo "Running LaTeX files through platex and dvipdfmx..." 135 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 136 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 137 | 138 | text: 139 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 140 | @echo 141 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 142 | 143 | man: 144 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 145 | @echo 146 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 147 | 148 | texinfo: 149 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 150 | @echo 151 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 152 | @echo "Run \`make' in that directory to run these through makeinfo" \ 153 | "(use \`make info' here to do that automatically)." 154 | 155 | info: 156 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 157 | @echo "Running Texinfo files through makeinfo..." 158 | make -C $(BUILDDIR)/texinfo info 159 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 160 | 161 | gettext: 162 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 163 | @echo 164 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 165 | 166 | changes: 167 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 168 | @echo 169 | @echo "The overview file is in $(BUILDDIR)/changes." 170 | 171 | linkcheck: 172 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 173 | @echo 174 | @echo "Link check complete; look for any errors in the above output " \ 175 | "or in $(BUILDDIR)/linkcheck/output.txt." 176 | 177 | doctest: 178 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 179 | @echo "Testing of doctests in the sources finished, look at the " \ 180 | "results in $(BUILDDIR)/doctest/output.txt." 181 | 182 | coverage: 183 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 184 | @echo "Testing of coverage in the sources finished, look at the " \ 185 | "results in $(BUILDDIR)/coverage/python.txt." 186 | 187 | xml: 188 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 189 | @echo 190 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 191 | 192 | pseudoxml: 193 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 194 | @echo 195 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 196 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyoanda documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jul 30 20:34:06 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'pyoanda' 55 | copyright = u'2015, Author' 56 | author = u'Author' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = 'en' 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = ['_build', '../env'] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = True 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = 'default' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | #html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'pyoandadoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'pyoanda.tex', u'pyoanda Documentation', 231 | u'Author', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'pyoanda', u'pyoanda Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'pyoanda', u'pyoanda Documentation', 275 | author, 'pyoanda', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | 291 | 292 | # -- Options for Epub output ---------------------------------------------- 293 | 294 | # Bibliographic Dublin Core info. 295 | epub_title = project 296 | epub_author = author 297 | epub_publisher = author 298 | epub_copyright = copyright 299 | 300 | # The basename for the epub file. It defaults to the project name. 301 | #epub_basename = project 302 | 303 | # The HTML theme for the epub output. Since the default themes are not optimized 304 | # for small screen space, using the same theme for HTML and epub output is 305 | # usually not wise. This defaults to 'epub', a theme designed to save visual 306 | # space. 307 | #epub_theme = 'epub' 308 | 309 | # The language of the text. It defaults to the language option 310 | # or 'en' if the language is not set. 311 | #epub_language = '' 312 | 313 | # The scheme of the identifier. Typical schemes are ISBN or URL. 314 | #epub_scheme = '' 315 | 316 | # The unique identifier of the text. This can be a ISBN number 317 | # or the project homepage. 318 | #epub_identifier = '' 319 | 320 | # A unique identification for the text. 321 | #epub_uid = '' 322 | 323 | # A tuple containing the cover image and cover page html template filenames. 324 | #epub_cover = () 325 | 326 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 327 | #epub_guide = () 328 | 329 | # HTML files that should be inserted before the pages created by sphinx. 330 | # The format is a list of tuples containing the path and title. 331 | #epub_pre_files = [] 332 | 333 | # HTML files shat should be inserted after the pages created by sphinx. 334 | # The format is a list of tuples containing the path and title. 335 | #epub_post_files = [] 336 | 337 | # A list of files that should not be packed into the epub file. 338 | epub_exclude_files = ['search.html'] 339 | 340 | # The depth of the table of contents in toc.ncx. 341 | #epub_tocdepth = 3 342 | 343 | # Allow duplicate toc entries. 344 | #epub_tocdup = True 345 | 346 | # Choose between 'default' and 'includehidden'. 347 | #epub_tocscope = 'default' 348 | 349 | # Fix unsupported image types using the Pillow. 350 | #epub_fix_images = False 351 | 352 | # Scale large images. 353 | #epub_max_image_width = 0 354 | 355 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 356 | #epub_show_urls = 'inline' 357 | 358 | # If false, no index is generated. 359 | #epub_use_index = True 360 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pyoanda's documentation! 2 | =================================== 3 | 4 | .. image:: https://travis-ci.org/toloco/pyoanda.svg?branch=master 5 | :target: https://travis-ci.org/toloco/pyoanda 6 | .. image:: https://coveralls.io/repos/toloco/pyoanda/badge.svg?branch=master&service=github :target: https://coveralls.io/github/toloco/pyoanda?branch=master 7 | 8 | Oanda’s API python wrapper. Robust and Fast API wrapper for your Forex bot. 9 | 10 | Python library that wraps `oanda `_ API. Built on top of requests, it’s easy to use and makes sense. 11 | 12 | Pyoanda is released under the MIT license. The source code is on `GitHub `_ and `issues are also tracked on GitHub `_. Works well with python 2.7, 3, 3.1, 3.2, 3.3, 3.4 and pypy. 13 | 14 | Contents: 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | license 20 | install 21 | usage 22 | 23 | 24 | .. toctree:: 25 | :maxdepth: 4 26 | 27 | pyoanda 28 | 29 | 30 | 31 | Indices and tables 32 | ================== 33 | 34 | * :ref:`genindex` 35 | * :ref:`modindex` 36 | * :ref:`search` 37 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | ================== 3 | Installing pyoanda 4 | ================== 5 | 6 | Pyoanda is on the Python Package Index (PyPI), so it can be installed standard Python tools like ``pip`` or ``easy_install``, and as well you can install from sources 7 | 8 | 9 | Pypi 10 | ---- 11 | 12 | For an easy and always standard setup: 13 | 14 | .. code-block:: bash 15 | 16 | $ pip install pyoanda 17 | 18 | 19 | 20 | Manual 21 | ------ 22 | For a custom or developer installation: 23 | 24 | .. code-block:: bash 25 | 26 | $ git clone git@github.com:toloco/pyoanda.git 27 | $ cd pyoanda 28 | $ python setup.py install 29 | # Make sure it works 30 | $ python setup.py test 31 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | The MIT License (MIT) 3 | ===================== 4 | 5 | 6 | Copyright (c) 2015 Tolo Palmer 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyoanda.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyoanda.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/oanda.rst: -------------------------------------------------------------------------------- 1 | Oanda 2 | ===== 3 | -------------------------------------------------------------------------------- /docs/pyoanda.rst: -------------------------------------------------------------------------------- 1 | pyoanda package 2 | =============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pyoanda.client module 8 | --------------------- 9 | 10 | .. automodule:: pyoanda.client 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pyoanda.exceptions module 16 | ------------------------- 17 | 18 | .. automodule:: pyoanda.exceptions 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | pyoanda.order module 24 | -------------------- 25 | 26 | .. automodule:: pyoanda.order 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: pyoanda 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/pyoanda.tests.rst: -------------------------------------------------------------------------------- 1 | pyoanda.tests package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pyoanda.tests.test_client module 8 | -------------------------------- 9 | 10 | .. automodule:: pyoanda.tests.test_client 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pyoanda.tests.test_order module 16 | ------------------------------- 17 | 18 | .. automodule:: pyoanda.tests.test_order 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: pyoanda.tests 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | ===== 3 | Usage 4 | ===== 5 | 6 | .. code-block:: python 7 | 8 | from pyoanda import Client, PRACTICE 9 | 10 | c = Client( 11 | environment=PRACTICE, 12 | account_id="Your Oanda account ID", 13 | access_token="Your Oanda access token" 14 | ) 15 | 16 | c.get_instrument_history( 17 | instrument="EUR_GBP", 18 | candle_format="midpoint", 19 | granularity="S30" 20 | ) 21 | 22 | .. Note:: that if you are indenting to use the sandbox environment, you should first use the API to create an account then use the account_id to run the example above. 23 | 24 | .. code-block:: python 25 | 26 | from pyoanda import Client, SANDBOX 27 | 28 | c = Client(environment=SANDBOX) 29 | 30 | # Create an account 31 | user = c.create_account() 32 | 33 | # Retrieve the username and accountId values for future use 34 | print "username: %s\naccount_id: %d" % (user['username'], user['accountId']) 35 | -------------------------------------------------------------------------------- /examples/create_stop_order.py: -------------------------------------------------------------------------------- 1 | """How to create a stop order without pulling out all your hair :-) 2 | """ 3 | import datetime 4 | 5 | from pyoanda import Client, SANDBOX, Order 6 | 7 | client = Client(environment=SANDBOX) 8 | 9 | test_order = Order( 10 | instrument="EUR_JPY", 11 | units=10, 12 | side="buy", 13 | type="stop", 14 | stopLoss=80.95, 15 | takeProfit=170.56, 16 | price=10.0, 17 | expiry=datetime.datetime.now() + datetime.timedelta(days=1) 18 | ) 19 | 20 | stop_order = client.create_order(order=test_order) 21 | -------------------------------------------------------------------------------- /examples/grab_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import csv 4 | import sys 5 | import yaml 6 | from pyoanda import Client, TRADE 7 | from pyoanda.exceptions import BadRequest 8 | from datetime import datetime, timedelta 9 | 10 | # Get my credentials 11 | # 12 | # YAML format, please see: 13 | # http://ess.khhq.net/wiki/YAML_Tutorial 14 | # 15 | # Config file for connecting to Oanda 16 | # 17 | # ACCOUNT_NUM: Integer number 18 | # ACCOUNT_KEY: String of your account Key 19 | 20 | 21 | with open("Config.yaml") as f: 22 | config = yaml.load(f.read()) 23 | 24 | client = Client( 25 | TRADE, 26 | account_id=config['ACCOUNT_NUM'], 27 | access_token=config['ACCOUNT_KEY'] 28 | ) 29 | 30 | DAYS = 50 31 | GRAN = 'M15' 32 | INST = 'EUR_JPY' 33 | start = datetime.now() - timedelta(days=DAYS) 34 | 35 | with open('data/data-set-{}-days.csv'.format(DAYS), 'w') as f: 36 | 37 | # We will map fields of the returned data to a more human readable format. 38 | mapFields = {} 39 | 40 | # Remove ""Mids", remove "Complete" 41 | mapFields['time'] = 'time' 42 | mapFields['lowMid'] = 'low' 43 | mapFields['complete'] = None 44 | mapFields['highMid'] = 'high' 45 | mapFields['openMid'] = 'open' 46 | mapFields['volume'] = 'volume' 47 | mapFields['closeMid'] = 'close' 48 | 49 | # Create the writer which will output to file 50 | writer = csv.DictWriter(f, fieldnames=mapFields.values()) 51 | writer.writeheader() 52 | 53 | print('Fetching data from server...') 54 | print('-'*100) 55 | 56 | # Save the day to display progression output 57 | lastPct = 0. 58 | 59 | # Loop through every day and save the data found 60 | for day in range(DAYS): 61 | 62 | # Flush previously written output to file (Force write) 63 | f.flush() 64 | 65 | for _ in range(2): # 24/12 hours 66 | 67 | # Find the start time from which to begin retrieving data 68 | end = start + timedelta(hours=12) 69 | 70 | # Create the dictionary which will be sent to PyOanda 71 | kwargs = dict( 72 | count=None, 73 | instrument=INST, 74 | granularity=GRAN, 75 | candle_format="midpoint", 76 | end=end.strftime("%Y-%m-%dT%H:%M:%S.%f%z"), 77 | start=start.strftime("%Y-%m-%dT%H:%M:%S.%f%z") 78 | ) 79 | 80 | # Try to retrieve the data from Oanda 81 | try: 82 | 83 | # Fetch the data from Oanda 84 | candles = client.get_instrument_history(**kwargs)["candles"] 85 | 86 | # Remove 'Mid' from the key names 87 | for candle in candles: 88 | for key, newKey in mapFields.items(): 89 | 90 | # Does the key exist in the candle? 91 | if key in candle: 92 | 93 | # Does the new key exist? (Delete it if not) 94 | if newKey is None: 95 | del candle[key] 96 | 97 | # Change the key name 98 | else: 99 | candle[newKey] = candle.pop(key) 100 | 101 | # Try to fetch and write the data to the file 102 | writer.writerows(candles) 103 | 104 | # Print progress to the terminal 105 | pct = int(float(day) / DAYS * 100.0) 106 | # print pct 107 | if pct != lastPct: 108 | strToWrite = '#' 109 | if pct - lastPct > 1: 110 | strToWrite *= int(pct-lastPct) 111 | 112 | sys.stdout.write(strToWrite) 113 | sys.stdout.flush() 114 | lastPct = pct 115 | 116 | # Catch any bad requests to Oanda and print the error 117 | except BadRequest as br: 118 | 119 | # print 'PyOanda suffered a bad request exception:', br 120 | pass 121 | 122 | # Catch generic exceptions 123 | except Exception as e: 124 | print('GrabData suffered an exception:', e) 125 | print('GrabData will now terminate!') 126 | print('-'*100) 127 | exit() 128 | 129 | # Reset the end time to the last candle retrieved 130 | start = end 131 | print() 132 | print('-'*100) 133 | -------------------------------------------------------------------------------- /examples/multi_currency_prices.py: -------------------------------------------------------------------------------- 1 | 2 | from pyoanda import Client, SANDBOX 3 | 4 | client = Client(environment=SANDBOX) 5 | 6 | # Get prices for a list of instruments 7 | 8 | pair_list = ('AUD_CAD', 'AUD_CHF') 9 | 10 | dataset = client.get_prices( 11 | instruments=','.join(pair_list), 12 | stream=False 13 | ) 14 | 15 | """ 16 | Response sample 17 | { 18 | "prices": [ 19 | { 20 | "ask": 81.551, 21 | "bid": 81.53, 22 | "instrument": "AUD_JPY", 23 | "time": "2016-01-26T07:39:56.525788Z" 24 | }, 25 | { 26 | "ask": 127.975, 27 | "bid": 127.957, 28 | "instrument": "EUR_JPY", 29 | "time": "2016-01-26T07:39:55.712253Z" 30 | }, 31 | { 32 | "ask": 167.269, 33 | "bid": 167.239, 34 | "instrument": "GBP_JPY", 35 | "time": "2016-01-26T07:39:58.333404Z" 36 | }, 37 | { 38 | "ask": 0.69277, 39 | "bid": 0.6926, 40 | "instrument": "AUD_USD", 41 | "time": "2016-01-26T07:39:50.358020Z" 42 | } 43 | ] 44 | } 45 | """ 46 | 47 | # simplistic way of extracting data from the json response:: 48 | aud_jpy = [d for d in dataset['prices'] if d['instrument'] == 'AUD_JPY'] 49 | bid = [d['bid'] for d in aud_jpy][-1] 50 | ask = [d['ask'] for d in aud_jpy][-1] 51 | time = [d['time'] for d in aud_jpy][-1] 52 | -------------------------------------------------------------------------------- /pyoanda/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.021" 2 | 3 | 4 | try: 5 | from .client import Client 6 | from .order import Order 7 | except ImportError: 8 | pass 9 | 10 | 11 | # OANDA API URLS 12 | SANDBOX = ( 13 | "http://api-sandbox.oanda.com", 14 | "http://stream-sandbox.oanda.com" 15 | ) 16 | PRACTICE = ( 17 | "https://api-fxpractice.oanda.com", 18 | "https://stream-fxpractice.oanda.com" 19 | ) 20 | TRADE = ( 21 | "https://api-fxtrade.oanda.com", 22 | "https://stream-fxtrade.oanda.com" 23 | ) 24 | -------------------------------------------------------------------------------- /pyoanda/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import requests 4 | 5 | from io import BytesIO 6 | from time import sleep, time 7 | from zipfile import ZipFile, BadZipfile 8 | from requests.exceptions import RequestException 9 | try: 10 | from types import NoneType 11 | except ImportError: 12 | NoneType = type(None) 13 | 14 | from .exceptions import BadCredentials, BadRequest 15 | 16 | logging.basicConfig(level='CRITICAL') 17 | log = logging.getLogger(__name__) 18 | 19 | class Client(object): 20 | API_VERSION = "v1" 21 | 22 | def __init__( 23 | self, 24 | environment, 25 | account_id=None, 26 | access_token=None, 27 | json_options=None 28 | ): 29 | self.domain, self.domain_stream = environment 30 | self.access_token = access_token 31 | self.account_id = account_id 32 | self.json_options = json_options or {} 33 | if account_id and not self.get_credentials(): 34 | raise BadCredentials() 35 | 36 | def get_credentials(self): 37 | """ 38 | See more: http://developer.oanda.com/rest-live/accounts/ 39 | """ 40 | url = "{0}/{1}/accounts/{2}".format( 41 | self.domain, 42 | self.API_VERSION, 43 | self.account_id 44 | ) 45 | try: 46 | response = self._Client__call(uri=url) 47 | assert len(response) > 0 48 | return response 49 | except RequestException: 50 | return False 51 | except AssertionError: 52 | return False 53 | 54 | def __get_response(self, uri, params=None, method="get", stream=False): 55 | """Creates a response object with the given params and option 56 | 57 | Parameters 58 | ---------- 59 | url : string 60 | The full URL to request. 61 | params: dict 62 | A list of parameters to send with the request. This 63 | will be sent as data for methods that accept a request 64 | body and will otherwise be sent as query parameters. 65 | method : str 66 | The HTTP method to use. 67 | stream : bool 68 | Whether to stream the response. 69 | 70 | Returns a requests.Response object. 71 | """ 72 | if not hasattr(self, "session") or not self.session: 73 | self.session = requests.Session() 74 | if self.access_token: 75 | self.session.headers.update( 76 | {'Authorization': 'Bearer {}'.format(self.access_token)} 77 | ) 78 | 79 | # Remove empty params 80 | if params: 81 | params = {k: v for k, v in params.items() if v is not None} 82 | 83 | kwargs = { 84 | "url": uri, 85 | "verify": True, 86 | "stream": stream 87 | } 88 | 89 | kwargs["params" if method == "get" else "data"] = params 90 | 91 | return getattr(self.session, method)(**kwargs) 92 | 93 | def __call(self, uri, params=None, method="get"): 94 | """Only returns the response, nor the status_code 95 | """ 96 | try: 97 | resp = self.__get_response(uri, params, method, False) 98 | rjson = resp.json(**self.json_options) 99 | assert resp.ok 100 | except AssertionError: 101 | msg = "OCode-{}: {}".format(resp.status_code, rjson["message"]) 102 | raise BadRequest(msg) 103 | except Exception as e: 104 | msg = "Bad response: {}".format(e) 105 | log.error(msg, exc_info=True) 106 | raise BadRequest(msg) 107 | else: 108 | return rjson 109 | 110 | def __call_stream(self, uri, params=None, method="get"): 111 | """Returns an stream response 112 | """ 113 | try: 114 | resp = self.__get_response(uri, params, method, True) 115 | assert resp.ok 116 | except AssertionError: 117 | raise BadRequest(resp.status_code) 118 | except Exception as e: 119 | log.error("Bad response: {}".format(e), exc_info=True) 120 | else: 121 | return resp 122 | 123 | def get_instruments(self): 124 | """ 125 | See more: 126 | http://developer.oanda.com/rest-live/rates/#getInstrumentList 127 | """ 128 | url = "{0}/{1}/instruments".format(self.domain, self.API_VERSION) 129 | params = {"accountId": self.account_id} 130 | try: 131 | response = self._Client__call(uri=url, params=params) 132 | assert len(response) > 0 133 | return response 134 | except RequestException: 135 | return False 136 | except AssertionError: 137 | return False 138 | 139 | def get_prices(self, instruments, stream=True): 140 | """ 141 | See more: 142 | http://developer.oanda.com/rest-live/rates/#getCurrentPrices 143 | """ 144 | url = "{0}/{1}/prices".format( 145 | self.domain_stream if stream else self.domain, 146 | self.API_VERSION 147 | ) 148 | params = {"accountId": self.account_id, "instruments": instruments} 149 | 150 | call = {"uri": url, "params": params, "method": "get"} 151 | 152 | try: 153 | if stream: 154 | return self._Client__call_stream(**call) 155 | else: 156 | return self._Client__call(**call) 157 | except RequestException: 158 | return False 159 | except AssertionError: 160 | return False 161 | 162 | def get_instrument_history(self, instrument, candle_format="bidask", 163 | granularity='S5', count=500, 164 | daily_alignment=None, alignment_timezone=None, 165 | weekly_alignment="Monday", start=None, 166 | end=None): 167 | """ 168 | See more: 169 | http://developer.oanda.com/rest-live/rates/#retrieveInstrumentHistory 170 | """ 171 | url = "{0}/{1}/candles".format(self.domain, self.API_VERSION) 172 | params = { 173 | "accountId": self.account_id, 174 | "instrument": instrument, 175 | "candleFormat": candle_format, 176 | "granularity": granularity, 177 | "count": count, 178 | "dailyAlignment": daily_alignment, 179 | "alignmentTimezone": alignment_timezone, 180 | "weeklyAlignment": weekly_alignment, 181 | "start": start, 182 | "end": end, 183 | } 184 | try: 185 | return self._Client__call(uri=url, params=params, method="get") 186 | except RequestException: 187 | return False 188 | except AssertionError: 189 | return False 190 | 191 | def get_orders(self, instrument=None, count=50): 192 | """ 193 | See more: 194 | http://developer.oanda.com/rest-live/orders/#getOrdersForAnAccount 195 | """ 196 | url = "{0}/{1}/accounts/{2}/orders".format( 197 | self.domain, 198 | self.API_VERSION, 199 | self.account_id 200 | ) 201 | params = {"instrument": instrument, "count": count} 202 | try: 203 | return self._Client__call(uri=url, params=params, method="get") 204 | except RequestException: 205 | return False 206 | except AssertionError: 207 | return False 208 | 209 | def get_order(self, order_id): 210 | """ 211 | See more: 212 | http://developer.oanda.com/rest-live/orders/#getInformationForAnOrder 213 | """ 214 | url = "{0}/{1}/accounts/{2}/orders/{3}".format( 215 | self.domain, 216 | self.API_VERSION, 217 | self.account_id, 218 | order_id 219 | ) 220 | try: 221 | return self._Client__call(uri=url, method="get") 222 | except RequestException: 223 | return False 224 | except AssertionError: 225 | return False 226 | 227 | def create_order(self, order): 228 | """ 229 | See more: 230 | http://developer.oanda.com/rest-live/orders/#createNewOrder 231 | """ 232 | url = "{0}/{1}/accounts/{2}/orders".format( 233 | self.domain, 234 | self.API_VERSION, 235 | self.account_id 236 | ) 237 | try: 238 | return self._Client__call( 239 | uri=url, 240 | params=order.__dict__, 241 | method="post" 242 | ) 243 | except RequestException: 244 | return False 245 | except AssertionError: 246 | return False 247 | 248 | def update_order(self, order_id, order): 249 | """ 250 | See more: 251 | http://developer.oanda.com/rest-live/orders/#modifyExistingOrder 252 | """ 253 | url = "{0}/{1}/accounts/{2}/orders/{3}".format( 254 | self.domain, 255 | self.API_VERSION, 256 | self.account_id, 257 | order_id 258 | ) 259 | try: 260 | return self._Client__call( 261 | uri=url, 262 | params=order.__dict__, 263 | method="patch" 264 | ) 265 | except RequestException: 266 | return False 267 | except AssertionError: 268 | return False 269 | 270 | def close_order(self, order_id): 271 | """ 272 | See more: 273 | http://developer.oanda.com/rest-live/orders/#closeOrder 274 | """ 275 | url = "{0}/{1}/accounts/{2}/orders/{3}".format( 276 | self.domain, 277 | self.API_VERSION, 278 | self.account_id, 279 | order_id 280 | ) 281 | try: 282 | return self._Client__call(uri=url, method="delete") 283 | except RequestException: 284 | return False 285 | except AssertionError: 286 | return False 287 | 288 | def get_trades(self, max_id=None, count=None, instrument=None, ids=None): 289 | """ Get a list of open trades 290 | 291 | Parameters 292 | ---------- 293 | max_id : int 294 | The server will return trades with id less than or equal 295 | to this, in descending order (for pagination) 296 | count : int 297 | Maximum number of open trades to return. Default: 50 Max 298 | value: 500 299 | instrument : str 300 | Retrieve open trades for a specific instrument only 301 | Default: all 302 | ids : list 303 | A list of trades to retrieve. Maximum number of ids: 50. 304 | No other parameter may be specified with the ids 305 | parameter. 306 | 307 | See more: 308 | http://developer.oanda.com/rest-live/trades/#getListOpenTrades 309 | """ 310 | url = "{0}/{1}/accounts/{2}/trades".format( 311 | self.domain, 312 | self.API_VERSION, 313 | self.account_id 314 | ) 315 | params = { 316 | "maxId": int(max_id) if max_id and max_id > 0 else None, 317 | "count": int(count) if count and count > 0 else None, 318 | "instrument": instrument, 319 | "ids": ','.join(ids) if ids else None 320 | } 321 | 322 | try: 323 | return self._Client__call(uri=url, params=params, method="get") 324 | except RequestException: 325 | return False 326 | except AssertionError: 327 | return False 328 | 329 | def get_trade(self, trade_id): 330 | """ Get information on a specific trade. 331 | 332 | Parameters 333 | ---------- 334 | trade_id : int 335 | The id of the trade to get information on. 336 | 337 | See more: 338 | http://developer.oanda.com/rest-live/trades/#getInformationSpecificTrade 339 | """ 340 | url = "{0}/{1}/accounts/{2}/trades/{3}".format( 341 | self.domain, 342 | self.API_VERSION, 343 | self.account_id, 344 | trade_id 345 | ) 346 | try: 347 | return self._Client__call(uri=url, method="get") 348 | except RequestException: 349 | return False 350 | except AssertionError: 351 | return False 352 | 353 | def update_trade( 354 | self, 355 | trade_id, 356 | stop_loss=None, 357 | take_profit=None, 358 | trailing_stop=None 359 | ): 360 | """ Modify an existing trade. 361 | 362 | Note: Only the specified parameters will be modified. All 363 | other parameters will remain unchanged. To remove an 364 | optional parameter, set its value to 0. 365 | 366 | Parameters 367 | ---------- 368 | trade_id : int 369 | The id of the trade to modify. 370 | stop_loss : number 371 | Stop Loss value. 372 | take_profit : number 373 | Take Profit value. 374 | trailing_stop : number 375 | Trailing Stop distance in pips, up to one decimal place 376 | 377 | See more: 378 | http://developer.oanda.com/rest-live/trades/#modifyExistingTrade 379 | """ 380 | url = "{0}/{1}/accounts/{2}/trades/{3}".format( 381 | self.domain, 382 | self.API_VERSION, 383 | self.account_id, 384 | trade_id 385 | ) 386 | params = { 387 | "stopLoss": stop_loss, 388 | "takeProfit": take_profit, 389 | "trailingStop": trailing_stop 390 | } 391 | try: 392 | return self._Client__call(uri=url, params=params, method="patch") 393 | except RequestException: 394 | return False 395 | except AssertionError: 396 | return False 397 | raise NotImplementedError() 398 | 399 | def close_trade(self, trade_id): 400 | """ Close an open trade. 401 | 402 | Parameters 403 | ---------- 404 | trade_id : int 405 | The id of the trade to close. 406 | 407 | See more: 408 | http://developer.oanda.com/rest-live/trades/#closeOpenTrade 409 | """ 410 | url = "{0}/{1}/accounts/{2}/trades/{3}".format( 411 | self.domain, 412 | self.API_VERSION, 413 | self.account_id, 414 | trade_id 415 | ) 416 | try: 417 | return self._Client__call(uri=url, method="delete") 418 | except RequestException: 419 | return False 420 | except AssertionError: 421 | return False 422 | 423 | def get_positions(self): 424 | """ Get a list of all open positions. 425 | 426 | See more: 427 | http://developer.oanda.com/rest-live/positions/#getListAllOpenPositions 428 | """ 429 | url = "{0}/{1}/accounts/{2}/positions".format( 430 | self.domain, 431 | self.API_VERSION, 432 | self.account_id 433 | ) 434 | try: 435 | return self._Client__call(uri=url, method="get") 436 | except RequestException: 437 | return False 438 | except AssertionError: 439 | return False 440 | 441 | def get_position(self, instrument): 442 | """ Get the position for an instrument. 443 | 444 | Parameters 445 | ---------- 446 | instrument : string 447 | The instrument to get the open position for. 448 | 449 | See more: 450 | http://developer.oanda.com/rest-live/positions/#getPositionForInstrument 451 | """ 452 | url = "{0}/{1}/accounts/{2}/positions/{3}".format( 453 | self.domain, 454 | self.API_VERSION, 455 | self.account_id, 456 | instrument 457 | ) 458 | try: 459 | return self._Client__call(uri=url, method="get") 460 | except RequestException: 461 | return False 462 | except AssertionError: 463 | return False 464 | 465 | def close_position(self, instrument): 466 | """ Close an existing position 467 | 468 | Parameters 469 | ---------- 470 | instrument : string 471 | The instrument to close the position for. 472 | 473 | See more: 474 | http://developer.oanda.com/rest-live/positions/#closeExistingPosition 475 | """ 476 | url = "{0}/{1}/accounts/{2}/positions/{3}".format( 477 | self.domain, 478 | self.API_VERSION, 479 | self.account_id, 480 | instrument 481 | ) 482 | try: 483 | return self._Client__call(uri=url, method="delete") 484 | except RequestException: 485 | return False 486 | except AssertionError: 487 | return False 488 | 489 | def get_transactions( 490 | self, 491 | max_id=None, 492 | count=None, 493 | instrument="all", 494 | ids=None 495 | ): 496 | """ Get a list of transactions. 497 | 498 | Parameters 499 | ---------- 500 | max_id : int 501 | The server will return transactions with id less than or 502 | equal to this, in descending order (for pagination). 503 | count : int 504 | Maximum number of open transactions to return. Default: 505 | 50. Max value: 500. 506 | instrument : str 507 | Retrieve open transactions for a specific instrument 508 | only. Default: all. 509 | ids : list 510 | A list of transactions to retrieve. Maximum number of 511 | ids: 50. No other parameter may be specified with the 512 | ids parameter. 513 | 514 | See more: 515 | http://developer.oanda.com/rest-live/transaction-history/#getTransactionHistory 516 | http://developer.oanda.com/rest-live/transaction-history/#transactionTypes 517 | """ 518 | url = "{0}/{1}/accounts/{2}/transactions".format( 519 | self.domain, 520 | self.API_VERSION, 521 | self.account_id 522 | ) 523 | params = { 524 | "maxId": int(max_id) if max_id and max_id > 0 else None, 525 | "count": int(count) if count and count > 0 else None, 526 | "instrument": instrument, 527 | "ids": ','.join(ids) if ids else None 528 | } 529 | 530 | try: 531 | return self._Client__call(uri=url, params=params, method="get") 532 | except RequestException: 533 | return False 534 | except AssertionError: 535 | return False 536 | 537 | def get_transaction(self, transaction_id): 538 | """ Get information on a specific transaction. 539 | 540 | Parameters 541 | ---------- 542 | transaction_id : int 543 | The id of the transaction to get information on. 544 | 545 | See more: 546 | http://developer.oanda.com/rest-live/transaction-history/#getInformationForTransaction 547 | http://developer.oanda.com/rest-live/transaction-history/#transactionTypes 548 | """ 549 | url = "{0}/{1}/accounts/{2}/transactions/{3}".format( 550 | self.domain, 551 | self.API_VERSION, 552 | self.account_id, 553 | transaction_id 554 | ) 555 | try: 556 | return self._Client__call(uri=url, method="get") 557 | except RequestException: 558 | return False 559 | except AssertionError: 560 | return False 561 | 562 | def request_transaction_history(self): 563 | """ Request full account history. 564 | 565 | Submit a request for a full transaction history. A 566 | successfully accepted submission results in a response 567 | containing a URL in the Location header to a file that will 568 | be available once the request is served. Response for the 569 | URL will be HTTP 404 until the file is ready. Once served 570 | the URL will be valid for a certain amount of time. 571 | 572 | See more: 573 | http://developer.oanda.com/rest-live/transaction-history/#getFullAccountHistory 574 | http://developer.oanda.com/rest-live/transaction-history/#transactionTypes 575 | """ 576 | url = "{0}/{1}/accounts/{2}/alltransactions".format( 577 | self.domain, 578 | self.API_VERSION, 579 | self.account_id 580 | ) 581 | try: 582 | resp = self.__get_response(url) 583 | return resp.headers['location'] 584 | except RequestException: 585 | return False 586 | except AssertionError: 587 | return False 588 | 589 | def get_transaction_history(self, max_wait=5.0): 590 | """ Download full account history. 591 | 592 | Uses request_transaction_history to get the transaction 593 | history URL, then polls the given URL until it's ready (or 594 | the max_wait time is reached) and provides the decoded 595 | response. 596 | 597 | Parameters 598 | ---------- 599 | max_wait : float 600 | The total maximum time to spend waiting for the file to 601 | be ready; if this is exceeded a failed response will be 602 | returned. This is not guaranteed to be strictly 603 | followed, as one last attempt will be made to check the 604 | file before giving up. 605 | 606 | See more: 607 | http://developer.oanda.com/rest-live/transaction-history/#getFullAccountHistory 608 | http://developer.oanda.com/rest-live/transaction-history/#transactionTypes 609 | """ 610 | url = self.request_transaction_history() 611 | if not url: 612 | return False 613 | 614 | ready = False 615 | start = time() 616 | delay = 0.1 617 | while not ready and delay: 618 | response = requests.head(url) 619 | ready = response.ok 620 | if not ready: 621 | sleep(delay) 622 | time_remaining = max_wait - time() + start 623 | max_delay = max(0., time_remaining - .1) 624 | delay = min(delay * 2, max_delay) 625 | 626 | if not ready: 627 | return False 628 | 629 | response = requests.get(url) 630 | try: 631 | with ZipFile(BytesIO(response.content)) as container: 632 | files = container.namelist() 633 | if not files: 634 | log.error('Transaction ZIP has no files.') 635 | return False 636 | history = container.open(files[0]) 637 | raw = history.read().decode('ascii') 638 | except BadZipfile: 639 | log.error('Response is not a valid ZIP file', exc_info=True) 640 | return False 641 | 642 | return json.loads(raw, **self.json_options) 643 | 644 | def create_account(self, currency=None): 645 | """ Create a new account. 646 | 647 | This call is only available on the sandbox system. Please 648 | create accounts on fxtrade.oanda.com on our production 649 | system. 650 | 651 | See more: 652 | http://developer.oanda.com/rest-sandbox/accounts/#-a-name-createtestaccount-a-create-a-test-account 653 | """ 654 | url = "{0}/{1}/accounts".format(self.domain, self.API_VERSION) 655 | params = {"currency": currency} 656 | try: 657 | return self._Client__call(uri=url, params=params, method="post") 658 | except RequestException: 659 | return False 660 | except AssertionError: 661 | return False 662 | 663 | def get_accounts(self, username=None): 664 | """ Get a list of accounts owned by the user. 665 | 666 | Parameters 667 | ---------- 668 | username : string 669 | The name of the user. Note: This is only required on the 670 | sandbox, on production systems your access token will 671 | identify you. 672 | 673 | See more: 674 | http://developer.oanda.com/rest-sandbox/accounts/#-a-name-getaccountsforuser-a-get-accounts-for-a-user 675 | """ 676 | url = "{0}/{1}/accounts".format(self.domain, self.API_VERSION) 677 | params = {"username": username} 678 | try: 679 | return self._Client__call(uri=url, params=params, method="get") 680 | except RequestException: 681 | return False 682 | except AssertionError: 683 | return False 684 | -------------------------------------------------------------------------------- /pyoanda/exceptions.py: -------------------------------------------------------------------------------- 1 | class BadCredentials(Exception): 2 | pass 3 | 4 | 5 | class BadRequest(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /pyoanda/order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from datetime import datetime 3 | from decimal import Decimal, InvalidOperation 4 | try: 5 | from types import NoneType 6 | except ImportError: 7 | NoneType = type(None) 8 | 9 | 10 | class Order(object): 11 | __allowed = ("instrument", "units", "side", "type", "expiry", "price", 12 | "lowerBound", "upperBound", "stopLoss", "takeProfit", 13 | "trailingStop") 14 | __requiered = ("instrument", "units", "side", "type") 15 | __side = ("sell", "buy") 16 | __type = ("limit", "stop", "marketIfTouched", "market") 17 | 18 | def __init__(self, **kwargs): 19 | self.__dict__.update(kwargs) 20 | 21 | def check(self): 22 | """ 23 | Logic extracted from: 24 | http://developer.oanda.com/rest-live/orders/#createNewOrder 25 | """ 26 | for k in iter(self.__dict__.keys()): 27 | if k not in self.__allowed: 28 | raise TypeError("Parameter not allowed {}".format(k)) 29 | 30 | for k in self.__requiered: 31 | if k not in self.__dict__: 32 | raise TypeError("Requiered parameter not found {}".format(k)) 33 | 34 | if not isinstance(self.units, (int, float)): 35 | msg = "Unit must be either int or float, '{}'' found".format( 36 | type(self.units)) 37 | raise TypeError(msg) 38 | 39 | if self.side not in self.__side: 40 | msg = "Side must be in {1}, '{0}' found".format( 41 | self.side, self.__side) 42 | raise TypeError(msg) 43 | 44 | if self.type not in self.__type: 45 | msg = "Type must be in {1}, '{0}' found".format( 46 | self.type, self.__type) 47 | raise TypeError(msg) 48 | 49 | if not self.type == "market" and ( 50 | not hasattr(self, "expiry") or not hasattr(self, "price")): 51 | msg = "As type is {}, expiry and price must be provided".format( 52 | self.type) 53 | raise TypeError(msg) 54 | if hasattr(self, "expiry") and not isinstance(self.expiry, datetime): 55 | msg = "Expiry must be {1}, '{0}' found".format( 56 | type(self.expiry), datetime) 57 | raise TypeError(msg) 58 | 59 | if hasattr(self, "price"): 60 | try: 61 | Decimal(self.price) 62 | except InvalidOperation: 63 | msg = "Expiry must be int or float, '{0}' found".format( 64 | type(self.price)) 65 | raise TypeError(msg) 66 | 67 | return True 68 | -------------------------------------------------------------------------------- /pyoanda/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toloco/pyoanda/26b3f28a89d07c5c20d2a645884505387f1daae8/pyoanda/tests/__init__.py -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toloco/pyoanda/26b3f28a89d07c5c20d2a645884505387f1daae8/pyoanda/tests/_test_integration/__init__.py -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/integration_test_case.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | try: 3 | import unittest2 as unittest 4 | except ImportError: 5 | import unittest 6 | 7 | from pyoanda import SANDBOX 8 | from pyoanda.client import Client 9 | from pyoanda.order import Order 10 | 11 | 12 | class IntegrationTestCase(unittest.TestCase): 13 | 14 | # Keep this as it will be share between all tests cases, prevent to over 15 | # use as this literaly creates new users (I expext the use to be wipeout) 16 | client = Client(SANDBOX) 17 | user = client.create_account(currency="GBP") 18 | client.account_id = user['accountId'] 19 | 20 | def build_order(self, immediate=False): 21 | """ Build an order to be used with create_order. 22 | 23 | Building an order is commonly required in the integration 24 | tests, so this makes it easy. 25 | 26 | Parameters 27 | ---------- 28 | immediate: bool 29 | Whether to place an order that will be met immediately 30 | or not; this is achieved by examining current prices and 31 | bidding well below for non-immediate or by placing a 32 | market order for immediate. 33 | 34 | Returns an Order 35 | """ 36 | if immediate: 37 | return Order( 38 | instrument="GBP_USD", 39 | units=1, 40 | side="buy", 41 | type="market" 42 | ) 43 | 44 | expiry = datetime.utcnow() + timedelta(minutes=1) 45 | prices = self.client.get_prices("GBP_USD", False) 46 | price = prices['prices'][0] 47 | at = round(price['bid'] * 0.9, 5) 48 | 49 | # order must not be met straight away, otherwise we can't get it back 50 | return Order( 51 | instrument="GBP_USD", 52 | units=1, 53 | side="buy", 54 | type="limit", 55 | price=at, 56 | expiry=expiry.isoformat() 57 | ) 58 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_account.py: -------------------------------------------------------------------------------- 1 | from .integration_test_case import IntegrationTestCase 2 | 3 | 4 | class TestAccountAPI(IntegrationTestCase): 5 | def test_create_account(self): 6 | assert self.client.create_account() 7 | 8 | def test_create_account_with_currency(self): 9 | assert self.client.create_account('GBP') 10 | assert self.client.create_account(currency='GBP') 11 | 12 | def test_get_accounts(self): 13 | assert self.client.get_accounts(username=self.user['username']) 14 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_client_fundation.py: -------------------------------------------------------------------------------- 1 | from pyoanda import SANDBOX 2 | from pyoanda.client import Client 3 | from pyoanda.exceptions import BadRequest 4 | 5 | 6 | from .integration_test_case import IntegrationTestCase 7 | 8 | 9 | class TestClientFoundation(IntegrationTestCase): 10 | def test_connect_pass(self): 11 | assert self.client.get_credentials() 12 | 13 | def test_connect_fail(self): 14 | with self.assertRaises(BadRequest): 15 | Client(SANDBOX, 999999999) 16 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_instruments.py: -------------------------------------------------------------------------------- 1 | from .integration_test_case import IntegrationTestCase 2 | 3 | 4 | class TestInstrumentsAPI(IntegrationTestCase): 5 | def test_get_instruments_pass(self): 6 | assert self.client.get_instruments() 7 | 8 | def test_get_prices_unstreamed(self): 9 | assert self.client.get_prices(instruments="EUR_GBP", stream=False) 10 | 11 | def test_get_prices_streamed(self): 12 | resp = self.client.get_prices(instruments="EUR_GBP", stream=True) 13 | prices = resp.iter_lines() 14 | assert next(prices) 15 | 16 | def test_get_instrument_history(self): 17 | assert self.client.get_instrument_history('EUR_GBP') 18 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_orders.py: -------------------------------------------------------------------------------- 1 | from pyoanda.exceptions import BadRequest 2 | 3 | from .integration_test_case import IntegrationTestCase 4 | 5 | 6 | class TestOrderAPI(IntegrationTestCase): 7 | def test_get_orders(self): 8 | assert self.client.get_orders() 9 | 10 | def test_get_order(self): 11 | order = self.build_order() 12 | result = self.client.create_order(order) 13 | assert self.client.get_order(result['orderOpened']['id']) 14 | assert self.client.get_order(order_id=result['orderOpened']['id']) 15 | 16 | def test_create_order(self): 17 | order = self.build_order() 18 | assert self.client.create_order(order) 19 | 20 | def test_update_order(self): 21 | order = self.build_order() 22 | result = self.client.create_order(order) 23 | assert self.client.update_order(result['orderOpened']['id'], order) 24 | 25 | def test_close_order(self): 26 | order = self.build_order() 27 | result = self.client.create_order(order) 28 | assert self.client.close_order(result['orderOpened']['id']) 29 | 30 | with self.assertRaises(BadRequest): 31 | # Cannot close twice 32 | self.client.close_order(result['orderOpened']['id']) 33 | 34 | with self.assertRaises(BadRequest): 35 | # No longer in orders once closed 36 | self.client.get_order(result['orderOpened']['id']) 37 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_position.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | from pyoanda.exceptions import BadRequest 7 | 8 | from .integration_test_case import IntegrationTestCase 9 | 10 | 11 | class TestPositionAPI(IntegrationTestCase): 12 | def test_get_positions(self): 13 | self.client.get_positions() 14 | 15 | def test_get_position(self): 16 | order = self.build_order(immediate=True) 17 | self.client.create_order(order) 18 | assert self.client.get_position('GBP_USD') 19 | assert self.client.get_position(instrument='GBP_USD') 20 | 21 | @unittest.skip("Failing due to Oanda bug, HTTP 500 on close") 22 | def test_close_position(self): 23 | order = self.build_order(immediate=True) 24 | self.client.create_order(order) 25 | assert self.client.close_position('GBP_USD') 26 | 27 | # cannot close twice 28 | with self.assertRaises(BadRequest): 29 | assert self.client.close_position('GBP_USD') 30 | 31 | # cannot get position if closed 32 | with self.assertRaises(BadRequest): 33 | assert self.client.get_position('GBP_USD') 34 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_trade.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | from pyoanda.exceptions import BadRequest 7 | 8 | from .integration_test_case import IntegrationTestCase 9 | 10 | 11 | class TestTradeAPI(IntegrationTestCase): 12 | def test_get_trades(self): 13 | assert self.client.get_trades() 14 | 15 | def test_get_trade(self): 16 | order = self.build_order(immediate=True) 17 | result = self.client.create_order(order) 18 | assert self.client.get_trade(result['tradeOpened']['id']) 19 | assert self.client.get_trade(trade_id=result['tradeOpened']['id']) 20 | 21 | def test_update_trade(self): 22 | order = self.client.create_order(self.build_order(immediate=True)) 23 | trade = self.client.get_trade(order['tradeOpened']['id']) 24 | 25 | for stop_loss in [round(trade['price'] * 0.5, 5), 0]: 26 | result = self.client.update_trade(trade['id'], stop_loss=stop_loss) 27 | self.assertEqual(stop_loss, result['stopLoss']) 28 | 29 | for take_profit in [round(trade['price'] * 1.5, 5), 0]: 30 | result = self.client.update_trade( 31 | trade['id'], 32 | take_profit=take_profit 33 | ) 34 | self.assertEqual(take_profit, result['takeProfit']) 35 | 36 | for trailing_stop in [100, 0]: 37 | result = self.client.update_trade( 38 | trade['id'], 39 | trailing_stop=trailing_stop 40 | ) 41 | self.assertEqual(trailing_stop, result['trailingStop']) 42 | 43 | @unittest.skip("Failing due to Oanda bug, HTTP 500 on close") 44 | def test_close_trade(self): 45 | order = self.client.create_order(self.build_order(immediate=True)) 46 | trade = self.client.get_trade(order['tradeOpened']['id']) 47 | assert self.client.close_trade(trade['id']) 48 | 49 | with self.assertRaises(BadRequest): 50 | # Cannot close twice 51 | self.client.close_trade(trade['id']) 52 | 53 | with self.assertRaises(BadRequest): 54 | # No longer in trades once closed 55 | self.client.get_trade(trade['id']) 56 | -------------------------------------------------------------------------------- /pyoanda/tests/_test_integration/test_transaction.py: -------------------------------------------------------------------------------- 1 | from .integration_test_case import IntegrationTestCase 2 | 3 | 4 | class TestTransactionAPI(IntegrationTestCase): 5 | def test_get_transactions(self): 6 | assert self.client.get_transactions() 7 | 8 | def test_get_transaction(self): 9 | order = self.build_order(immediate=True) 10 | self.client.create_order(order) 11 | transactions = self.client.get_transactions() 12 | transaction = transactions['transactions'][0] 13 | assert self.client.get_transaction(transaction['id']) 14 | 15 | def test_request_transaction_history(self): 16 | assert self.client.request_transaction_history() 17 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toloco/pyoanda/26b3f28a89d07c5c20d2a645884505387f1daae8/pyoanda/tests/test_client/__init__.py -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_account.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from pyoanda.client import Client 11 | 12 | 13 | class TestAccountAPI(unittest.TestCase): 14 | def setUp(self): 15 | with mock.patch.object(Client, 'get_credentials', return_value=True): 16 | self.client = Client( 17 | ("http://mydomain.com", "http://mystreamingdomain.com"), 18 | "my_account", 19 | "my_token" 20 | ) 21 | 22 | def test_create_account(self): 23 | with mock.patch.object(Client, '_Client__call', return_value=True): 24 | assert self.client.create_account() 25 | 26 | def test_create_account_with_currency(self): 27 | with mock.patch.object(Client, '_Client__call', return_value=True): 28 | assert self.client.create_account('AUD') 29 | assert self.client.create_account(currency='AUD') 30 | 31 | def test_get_accounts(self): 32 | with mock.patch.object(Client, '_Client__call', return_value=True): 33 | assert self.client.get_accounts() 34 | 35 | def test_get_accounts_with_username(self): 36 | with mock.patch.object(Client, '_Client__call', return_value=True): 37 | assert self.client.get_accounts('bob') 38 | assert self.client.get_accounts(username='bob') 39 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_fundation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import requests_mock 4 | 5 | try: 6 | import unittest2 as unittest 7 | except ImportError: 8 | import unittest 9 | try: 10 | from unittest import mock 11 | except ImportError: 12 | import mock 13 | from decimal import Decimal 14 | 15 | from pyoanda.client import Client 16 | from pyoanda.exceptions import BadCredentials, BadRequest 17 | 18 | 19 | class TestClientFundation(unittest.TestCase): 20 | def test_connect_pass(self): 21 | with mock.patch.object(Client, 'get_credentials', return_value=True): 22 | Client( 23 | ("http://mydomain.com", "http://mystreamingdomain.com"), 24 | "my_account", 25 | "my_token" 26 | ) 27 | 28 | def test_connect_fail(self): 29 | with mock.patch.object(Client, 'get_credentials', return_value=False): 30 | with self.assertRaises(BadCredentials): 31 | Client( 32 | ("http://mydomain.com", "http://mystreamingdomain.com"), 33 | "my_account", 34 | "my_token" 35 | ) 36 | 37 | @requests_mock.Mocker() 38 | def test_call_pass(self, m): 39 | """Ensure that successful HTTP response codes pass.""" 40 | with mock.patch.object(Client, 'get_credentials', return_value=True): 41 | c = Client( 42 | ("http://mydomain.com", "http://mystreamingdomain.com"), 43 | "my_account", 44 | "my_token" 45 | ) 46 | c.session = requests.Session() 47 | 48 | for status_code in [200, 201, 202, 301, 302]: 49 | m.get(requests_mock.ANY, text='{}', status_code=status_code) 50 | c._Client__call( 51 | uri="http://example.com", 52 | params={"test": "test"}, 53 | method="get" 54 | ) 55 | 56 | m.post(requests_mock.ANY, text='{}', status_code=status_code) 57 | c._Client__call( 58 | uri="http://example.com", 59 | params={"test": "test"}, 60 | method="post" 61 | ) 62 | 63 | @requests_mock.Mocker() 64 | def test_call_fail(self, m): 65 | """Ensure that failure HTTP response codes fail.""" 66 | with mock.patch.object(Client, 'get_credentials', return_value=True): 67 | c = Client( 68 | ("http://mydomain.com", "http://mystreamingdomain.com"), 69 | "my_account", 70 | "my_token" 71 | ) 72 | c.session = requests.Session() 73 | 74 | status_codes = [400, 401, 403, 404, 500] 75 | caught = 0 76 | for status_code in status_codes: 77 | m.get( 78 | requests_mock.ANY, 79 | text=json.dumps({'message': 'test'}), 80 | status_code=400 81 | ) 82 | try: 83 | c._Client__call( 84 | uri="http://example.com", 85 | params=None, 86 | method="get" 87 | ) 88 | except BadRequest: 89 | caught += 1 90 | self.assertEqual(len(status_codes), caught) 91 | 92 | @requests_mock.Mocker() 93 | def test_call_stream_pass(self, m): 94 | """Ensure that successful HTTP streaming response codes pass.""" 95 | with mock.patch.object(Client, 'get_credentials', return_value=True): 96 | c = Client( 97 | ("http://mydomain.com", "http://mystreamingdomain.com"), 98 | "my_account", 99 | "my_token" 100 | ) 101 | c.session = requests.Session() 102 | 103 | for status_code in [200, 201, 202, 301, 302]: 104 | m.get(requests_mock.ANY, status_code=status_code) 105 | c._Client__call_stream( 106 | uri="http://example.com", 107 | params={"test": "test"}, 108 | method="get" 109 | ) 110 | 111 | m.post(requests_mock.ANY, status_code=status_code) 112 | c._Client__call_stream( 113 | uri="http://example.com", 114 | params={"test": "test"}, 115 | method="post" 116 | ) 117 | 118 | @requests_mock.Mocker() 119 | def test_call_stream_fail(self, m): 120 | """Ensure that failure HTTP streaming response codes fail.""" 121 | with mock.patch.object(Client, 'get_credentials', return_value=True): 122 | c = Client( 123 | ("http://mydomain.com", "http://mystreamingdomain.com"), 124 | "my_account", 125 | "my_token" 126 | ) 127 | c.session = requests.Session() 128 | 129 | status_codes = [400, 401, 403, 404, 500] 130 | caught = 0 131 | for status_code in status_codes: 132 | m.get( 133 | requests_mock.ANY, 134 | text=json.dumps({'message': 'test'}), 135 | status_code=400 136 | ) 137 | try: 138 | c._Client__call_stream( 139 | uri="http://example.com", 140 | params={"test": "test"}, 141 | method="get" 142 | ) 143 | except BadRequest: 144 | caught += 1 145 | self.assertEqual(len(status_codes), caught) 146 | 147 | @requests_mock.Mocker() 148 | def test_custom_json_options(self, m): 149 | with mock.patch.object(Client, 'get_credentials', return_value=True): 150 | c = Client( 151 | ("http://mydomain.com", "http://mystreamingdomain.com"), 152 | "my_account", 153 | "my_token" 154 | ) 155 | c.json_options['parse_float'] = Decimal 156 | m.get(requests_mock.ANY, text=json.dumps({'float': 1.01})) 157 | r = c._Client__call('http://www.example.com/') 158 | assert isinstance(r['float'], Decimal) 159 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_instruments.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from pyoanda.client import Client 11 | 12 | 13 | class TestInstrumentsAPI(unittest.TestCase): 14 | def setUp(self): 15 | with mock.patch.object(Client, 'get_credentials', return_value=True): 16 | self.client = Client( 17 | ("http://mydomain.com", "http://mystreamingdomain.com"), 18 | "my_account", 19 | "my_token" 20 | ) 21 | 22 | def test_get_instruments_pass(self): 23 | with mock.patch.object( 24 | Client, '_Client__call', 25 | return_value={"message": "good one"} 26 | ): 27 | assert self.client.get_instruments() 28 | 29 | def test_get_prices(self): 30 | with mock.patch.object( 31 | Client, '_Client__call', 32 | return_value={"message": "good one"} 33 | ): 34 | assert self.client.get_prices(instruments="EUR_GBP", stream=False) 35 | 36 | def test_get_instrument_history(self): 37 | with mock.patch.object( 38 | Client, '_Client__call', 39 | return_value=[{}] 40 | ): 41 | assert self.client.get_instrument_history('EUR_GBP') 42 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_orders.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | from requests.exceptions import RequestException 10 | 11 | from pyoanda.client import Client 12 | from pyoanda.order import Order 13 | 14 | 15 | class TestOrdersAPI(unittest.TestCase): 16 | def setUp(self): 17 | with mock.patch.object(Client, 'get_credentials', return_value=True): 18 | self.client = Client( 19 | ("http://mydomain.com", "http://mystreamingdomain.com"), 20 | "my_account", 21 | "my_token" 22 | ) 23 | 24 | def test_credentials_pass(self): 25 | with mock.patch.object( 26 | Client, '_Client__call', 27 | return_value={"message": "good one"} 28 | ): 29 | assert self.client.get_credentials() 30 | 31 | def test_credentials_fail(self): 32 | with mock.patch.object(Client, '_Client__call', return_value=()): 33 | assert not self.client.get_credentials() 34 | 35 | e = RequestException("I fail") 36 | with mock.patch.object(Client, '_Client__call', side_effect=e): 37 | assert not self.client.get_credentials() 38 | 39 | def test_get_orders(self): 40 | with mock.patch.object(Client, '_Client__call', return_value=True): 41 | assert self.client.get_orders() 42 | 43 | def test_get_order(self): 44 | with mock.patch.object(Client, '_Client__call', return_value=True): 45 | assert self.client.get_order(1) 46 | 47 | def test_create_order(self): 48 | order = Order(instrument="GBP_EUR", units=1, side="buy", type="market") 49 | with mock.patch.object(Client, '_Client__call', return_value=True): 50 | assert self.client.create_order(order) 51 | 52 | def test_update_order(self): 53 | order = Order(instrument="GBP_EUR", units=1, side="buy", type="market") 54 | with mock.patch.object(Client, '_Client__call', return_value=True): 55 | assert self.client.update_order(1, order) 56 | 57 | def test_close_order(self): 58 | with mock.patch.object(Client, '_Client__call', return_value=True): 59 | assert self.client.close_order(1) 60 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_position.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from pyoanda.client import Client 11 | 12 | 13 | class TestPositionAPI(unittest.TestCase): 14 | def setUp(self): 15 | with mock.patch.object(Client, 'get_credentials', return_value=True): 16 | self.client = Client( 17 | ("http://mydomain.com", "http://mystreamingdomain.com"), 18 | "my_account", 19 | "my_token" 20 | ) 21 | 22 | def test_get_positions(self): 23 | with mock.patch.object(Client, '_Client__call', return_value=True): 24 | assert self.client.get_positions() 25 | 26 | def test_get_position(self): 27 | with mock.patch.object(Client, '_Client__call', return_value=True): 28 | assert self.client.get_position('AUD_USD') 29 | assert self.client.get_position(instrument='AUD_USD') 30 | 31 | def test_close_position(self): 32 | with mock.patch.object(Client, '_Client__call', return_value=True): 33 | assert self.client.close_position('AUD_USD') 34 | assert self.client.close_position(instrument='AUD_USD') 35 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_trade.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from pyoanda.client import Client 11 | 12 | 13 | class TestTradeAPI(unittest.TestCase): 14 | def setUp(self): 15 | with mock.patch.object(Client, 'get_credentials', return_value=True): 16 | self.client = Client( 17 | ("http://mydomain.com", "http://mystreamingdomain.com"), 18 | "my_account", 19 | "my_token" 20 | ) 21 | 22 | def test_get_trades(self): 23 | with mock.patch.object(Client, '_Client__call', return_value=True): 24 | assert self.client.get_trades() 25 | 26 | def test_get_trade(self): 27 | with mock.patch.object(Client, '_Client__call', return_value=True): 28 | assert self.client.get_trade(1) 29 | assert self.client.get_trade(trade_id=1) 30 | 31 | def test_update_trade(self): 32 | with mock.patch.object(Client, '_Client__call', return_value=True): 33 | assert self.client.update_trade(1) 34 | assert self.client.update_trade(1, 0) 35 | assert self.client.update_trade(1, 1) 36 | assert self.client.update_trade(1, None, 0) 37 | assert self.client.update_trade(1, None, 1) 38 | assert self.client.update_trade(1, None, None, 0) 39 | assert self.client.update_trade(1, None, None, 1) 40 | 41 | def test_close_trade(self): 42 | with mock.patch.object(Client, '_Client__call', return_value=True): 43 | assert self.client.close_trade(1) 44 | assert self.client.close_trade(trade_id=1) 45 | -------------------------------------------------------------------------------- /pyoanda/tests/test_client/test_transaction.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests_mock 3 | 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | try: 9 | from unittest import mock 10 | except ImportError: 11 | import mock 12 | from zipfile import ZipFile 13 | from io import BytesIO 14 | 15 | from pyoanda.client import Client 16 | 17 | 18 | class TestTransactionAPI(unittest.TestCase): 19 | def setUp(self): 20 | with mock.patch.object(Client, 'get_credentials', return_value=True): 21 | self.client = Client( 22 | ("http://mydomain.com", "http://mystreamingdomain.com"), 23 | "my_account", 24 | "my_token" 25 | ) 26 | 27 | def test_get_transactions(self): 28 | with mock.patch.object(Client, '_Client__call', return_value=True): 29 | assert self.client.get_transactions() 30 | 31 | def test_get_transaction(self): 32 | with mock.patch.object(Client, '_Client__call', return_value=True): 33 | assert self.client.get_transaction(1) 34 | 35 | @requests_mock.Mocker() 36 | def test_request_transaction_history(self, m): 37 | location = 'http://example.com/transactions.json.gz' 38 | m.get( 39 | requests_mock.ANY, 40 | headers={'Location': location}, status_code=202 41 | ) 42 | self.assertEqual(location, self.client.request_transaction_history()) 43 | 44 | @requests_mock.Mocker() 45 | def test_get_transaction_history(self, m): 46 | # Mock zip file content 47 | content = BytesIO() 48 | with ZipFile(content, 'w') as zip: 49 | zip.writestr('transactions.json', json.dumps({'ok': True})) 50 | content.seek(0) 51 | 52 | # Mock requests, one HEAD to check if it's there, one GET once it is 53 | location = 'http://example.com/transactions.json.gz' 54 | m.head(location, status_code=200) 55 | m.get(location, body=content, status_code=200) 56 | 57 | method = 'request_transaction_history' 58 | with mock.patch.object(Client, method, return_value=location): 59 | transactions = self.client.get_transaction_history() 60 | assert transactions['ok'] 61 | assert m.call_count == 2 62 | 63 | @requests_mock.Mocker() 64 | def test_get_transaction_history_slow(self, m): 65 | """Ensures that get_transaction_history retries requests.""" 66 | # Mock zip file content 67 | content = BytesIO() 68 | with ZipFile(content, 'w') as zip: 69 | zip.writestr('transactions.json', json.dumps({'ok': True})) 70 | content.seek(0) 71 | 72 | # Mock requests, one HEAD to check if it's there, one GET once it is 73 | location = 'http://example.com/transactions.json.gz' 74 | m.head(location, [{'status_code': 404}, {'status_code': 200}]) 75 | m.get(location, body=content, status_code=200) 76 | 77 | method = 'request_transaction_history' 78 | with mock.patch.object(Client, method, return_value=location): 79 | transactions = self.client.get_transaction_history() 80 | assert transactions['ok'] 81 | assert m.call_count == 3 82 | 83 | @requests_mock.Mocker() 84 | def test_get_transaction_history_gives_up(self, m): 85 | """Ensures that get_transaction_history eventually gives up.""" 86 | # Mock requests, one HEAD to check if it's there, one GET once it is 87 | location = 'http://example.com/transactions.json.gz' 88 | m.head(location, [{'status_code': 404}, {'status_code': 404}]) 89 | 90 | method = 'request_transaction_history' 91 | with mock.patch.object(Client, method, return_value=location): 92 | transactions = self.client.get_transaction_history(.3) 93 | assert not transactions 94 | # Possible timing issue, may be one or the other 95 | assert m.call_count == 2 or m.call_count == 3 96 | 97 | @requests_mock.Mocker() 98 | def test_get_transaction_history_handles_bad_files(self, m): 99 | """Ensures that get_transaction_history gracefully handles bad files. 100 | """ 101 | # Mock requests, one HEAD to check if it's there, one GET once it is 102 | location = 'http://example.com/transactions.json.gz' 103 | m.head(location, status_code=200) 104 | m.get(location, text='invalid', status_code=200) 105 | 106 | method = 'request_transaction_history' 107 | with mock.patch.object(Client, method, return_value=location): 108 | transactions = self.client.get_transaction_history() 109 | assert not transactions 110 | -------------------------------------------------------------------------------- /pyoanda/tests/test_order.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | try: 3 | import unittest2 as unittest 4 | except ImportError: 5 | import unittest 6 | 7 | from ..order import Order 8 | 9 | 10 | class OrderClassTest(unittest.TestCase): 11 | 12 | def fail(self, order): 13 | with self.assertRaises(TypeError): 14 | assert order.check() 15 | 16 | def test_creation(self): 17 | order = Order( 18 | instrument="GBP_EUR", 19 | units=11, 20 | side="buy", 21 | type="market" 22 | ) 23 | assert order.check() 24 | 25 | def test_creation_bad_param(self): 26 | order = Order( 27 | instrument="GBP_EUR", 28 | units=11, 29 | side="buy", 30 | type="market", 31 | bad="param" 32 | ) 33 | self.fail(order) 34 | 35 | def test_creation_bad_units(self): 36 | order = Order( 37 | instrument="GBP_EUR", 38 | units="bad", 39 | side="buy", 40 | type="market" 41 | ) 42 | self.fail(order) 43 | 44 | def test_creation_bad_side(self): 45 | order = Order( 46 | instrument="GBP_EUR", 47 | units=1, 48 | side="bad", 49 | type="market" 50 | ) 51 | self.fail(order) 52 | 53 | def test_creation_bad_type(self): 54 | order = Order( 55 | instrument="GBP_EUR", 56 | units=1, 57 | side="sell", 58 | type="bad" 59 | ) 60 | self.fail(order) 61 | 62 | def test_creation_with_type(self): 63 | order = Order( 64 | instrument="GBP_EUR", 65 | units=1, 66 | side="sell", 67 | type="limit", 68 | price=10.0, 69 | expiry=datetime.now() 70 | ) 71 | assert order.check() 72 | 73 | def test_creation_with_type_error(self): 74 | order = Order( 75 | instrument="GBP_EUR", 76 | units=1, 77 | side="sell", 78 | type="limit", 79 | price=10.0, 80 | ) 81 | self.fail(order) 82 | 83 | def test_creation_bad_expiry(self): 84 | order = Order( 85 | instrument="GBP_EUR", 86 | units=1, 87 | side="sell", 88 | type="limit", 89 | price=10.0, 90 | expiry="datetime.now()" 91 | ) 92 | self.fail(order) 93 | 94 | def test_creation_bad_price(self): 95 | order = Order( 96 | instrument="GBP_EUR", 97 | units=1, 98 | side="sell", 99 | type="limit", 100 | price="10.0", 101 | expiry=datetime.now() 102 | ) 103 | assert order.check() 104 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | setuptools 3 | twine 4 | wheel 5 | nose 6 | coveralls 7 | requests-mock 8 | sphinx 9 | -------------------------------------------------------------------------------- /requirements2.txt: -------------------------------------------------------------------------------- 1 | unittest2 2 | mock 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | 7 | long_description = "Oanda's API python wrapper. Robust and Fast API wrapper for your Forex bot Python library that wraps Oanda API. Built on top of requests, it's easy to use and makes sense." 8 | 9 | 10 | setup( 11 | name='PyOanda', 12 | 13 | # Versions should comply with PEP440. For a discussion on single-sourcing 14 | # the version across setup.py and the project code, see 15 | # https://packaging.python.org/en/latest/single_source_version.html 16 | version="1.022", 17 | description=long_description, 18 | long_description=long_description, 19 | url='https://github.com/toloco/pyoanda', 20 | author='Tolo Palmer', 21 | author_email='tolopalmer@gmail.com', 22 | license='MIT', 23 | 24 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 25 | classifiers=[ 26 | # How mature is this project? Common values are 27 | # '3 - Alpha', 28 | # '4 - Beta', 29 | # '5 - Production/Stable', 30 | # 'Development Status :: 3 - Alpha', 31 | 32 | # Indicate who your project is intended for 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Office/Business :: Financial :: Investment', 35 | 36 | # Pick your license as you wish (should match "license" above) 37 | 'License :: OSI Approved :: MIT License', 38 | 39 | # Specify the Python versions you support here. In particular, ensure 40 | # that you indicate whether you support Python 2, Python 3 or both. 41 | 'Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 2.6', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.1', 46 | 'Programming Language :: Python :: 3.2', 47 | 'Programming Language :: Python :: 3.3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Programming Language :: Python :: 3.5', 50 | ], 51 | 52 | # What does your project relate to? 53 | keywords='oanda, wrapper', 54 | 55 | # You can just specify the packages manually here if your project is 56 | # simple. Or you can use find_packages(). 57 | packages=find_packages(exclude=['contrib', 'docs', 'tests*']), 58 | 59 | # List run-time dependencies here. These will be installed by pip when 60 | # your project is installed. For an analysis of "install_requires" vs pip's 61 | # requirements files see: 62 | # https://packaging.python.org/en/latest/requirements.html 63 | install_requires=['requests'], 64 | tests_require=['nose', 'coveralls', 'requests-mock'], 65 | test_suite='nose.collector', 66 | 67 | ) 68 | --------------------------------------------------------------------------------