Last active
March 5, 2021 13:52
-
-
Save dogwood008/d26627648b5a670a46f979557a6d1e9d to your computer and use it in GitHub Desktop.
kabu STATION API を backtraderから使う https://how-to-make-stock-trading-system.dogwood008.com/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"metadata": { | |
"code_folding": [] | |
}, | |
"source": [ | |
"## Utilities" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"metadata": { | |
"scrolled": true | |
}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"True\n" | |
] | |
} | |
], | |
"source": [ | |
"def is_in_jupyter() -> bool:\n", | |
" '''\n", | |
" Determine wheather is the environment Jupyter Notebook\n", | |
" https://blog.amedama.jp/entry/detect-jupyter-env\n", | |
" '''\n", | |
" if 'get_ipython' not in globals():\n", | |
" # Python shell\n", | |
" return False\n", | |
" env_name = get_ipython().__class__.__name__\n", | |
" if env_name == 'TerminalInteractiveShell':\n", | |
" # IPython shell\n", | |
" return False\n", | |
" # Jupyter Notebook\n", | |
" return True\n", | |
"print(is_in_jupyter())" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/html": [ | |
"<style type=\"text/css\">:root {\n", | |
" --background-color: #1e1e1e;\n", | |
" --cell-background-color: rgb(7, 7, 7);\n", | |
" --focus-background-color: rgba(180, 180, 180, 0.14);\n", | |
" --focus-color: #6e6c6c;\n", | |
" --font-color: #f8f8f0;\n", | |
" --dark-font-color: #d4d4d4;\n", | |
" --disabled-font-color: #4b4b4b;\n", | |
" --divider: rgba(180, 180, 180, 0.3);\n", | |
"\n", | |
" --input-prompt-color: #75715e;\n", | |
" --output-prompt-color: #bf0000;\n", | |
" --gutter: #49483e;\n", | |
"}\n", | |
"\n", | |
"body,\n", | |
"div.body {\n", | |
" font-family: sans-serif;\n", | |
" font-size: 10pt;\n", | |
" color: var(--font-color);\n", | |
" background-color: var(--background-color);\n", | |
" background: var(--background-color);\n", | |
" -webkit-font-smoothing: antialiased;\n", | |
"}\n", | |
"body.notebook_app {\n", | |
" padding: 0;\n", | |
" background-color: var(--background-color);\n", | |
" background: var(--background-color);\n", | |
" padding-right: 0px;\n", | |
" overflow-y: hidden;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* notebook */\n", | |
"div#notebook {\n", | |
" font-family: sans-serif;\n", | |
" font-size: 8pt;\n", | |
" padding-top: 4px;\n", | |
" -webkit-font-smoothing: antialiased;\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
"div#notebook-container {\n", | |
" padding: 13px 2px;\n", | |
" color: var(--dark-font-color);\n", | |
" background-color: var(--background-color);\n", | |
" min-height: 0px;\n", | |
" box-shadow: none;\n", | |
" width: 99%;\n", | |
" margin-right: 20px;\n", | |
" margin-left: 0px;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* header */\n", | |
"body > #header #header-container {\n", | |
" padding-bottom: 0px;\n", | |
" padding-top: 4px;\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
" box-sizing: border-box;\n", | |
" -moz-box-sizing: border-box;\n", | |
" -webkit-box-sizing: border-box;\n", | |
" margin-bottom: 0;\n", | |
" border: 0px;\n", | |
"}\n", | |
"body > #header {\n", | |
" font-size: 10pt;\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
" position: relative;\n", | |
" border: none;\n", | |
" z-index: 100;\n", | |
"}\n", | |
"\n", | |
".container {\n", | |
" width: 100% !important;\n", | |
"}\n", | |
"\n", | |
"#menubar-container,\n", | |
"#menubar,\n", | |
"div#menubar {\n", | |
" padding-top: 0px;\n", | |
" margin-top: 0;\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"#menubar .navbar,\n", | |
".navbar-default {\n", | |
" background-color: var(--background-color);\n", | |
" margin-bottom: 0px;\n", | |
" margin-top: 0px;\n", | |
"}\n", | |
"\n", | |
".navbar {\n", | |
" border: none;\n", | |
" color: var(--focus-color);\n", | |
"}\n", | |
"\n", | |
"div.navbar-text,\n", | |
".navbar-text,\n", | |
".navbar-text.indicator_area,\n", | |
"p.navbar-text.indicator_area {\n", | |
" margin-top: 8px;\n", | |
" margin-bottom: 0px;\n", | |
" color: var(--focus-color);\n", | |
"}\n", | |
"\n", | |
".navbar-default .navbar-nav > li > a:hover,\n", | |
".navbar-default .navbar-nav > li > a:focus {\n", | |
" color: var(--focus-color);\n", | |
" background-color: var(--focus-background-color);\n", | |
" border-color: var(--dark-font-color);\n", | |
" transition: 80ms ease;\n", | |
"}\n", | |
"\n", | |
".dropdown-menu {\n", | |
" box-shadow: none;\n", | |
" padding: 0px;\n", | |
" text-align: left;\n", | |
" border: none;\n", | |
" color: var(--dark-font-color);\n", | |
" background-color: var(--background-color);\n", | |
" background: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".dropdown-menu:hover {\n", | |
" box-shadow: none;\n", | |
" padding: 0px;\n", | |
" text-align: left;\n", | |
" border: none;\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".dropdown-menu > li > a {\n", | |
" font-family: sans-serif;\n", | |
" font-size: 8pt;\n", | |
" display: block;\n", | |
" padding: 10px 20px 9px 10px;\n", | |
" color: var(--focus-color);\n", | |
" background-color: #111;\n", | |
" background: #111;\n", | |
"}\n", | |
"\n", | |
".dropdown-menu > li > a:hover,\n", | |
".dropdown-menu > li > a:focus {\n", | |
" color: var(--focus-color);\n", | |
" background-color: var(--focus-background-color);\n", | |
" border-color: var(--dark-font-color);\n", | |
" transition: 200ms ease;\n", | |
"}\n", | |
"\n", | |
".dropdown-submenu > .dropdown-menu {\n", | |
" top: 2px;\n", | |
" left: 100%;\n", | |
" margin-top: -2px;\n", | |
" margin-left: 0px;\n", | |
" padding-top: 0px;\n", | |
" transition: 200ms ease;\n", | |
"}\n", | |
"\n", | |
".dropdown-submenu > a:after {\n", | |
" color: var(--focus-color);\n", | |
" margin-right: -16px;\n", | |
" margin-top: 0px;\n", | |
" display: inline-block;\n", | |
"}\n", | |
"\n", | |
".dropdown-submenu:hover > a:after,\n", | |
".dropdown-submenu:active > a:after,\n", | |
".dropdown-submenu:focus > a:after,\n", | |
".dropdown-submenu:visited > a:after {\n", | |
" color: var(--background-color);\n", | |
" margin-right: -16px;\n", | |
" display: inline-block;\n", | |
"}\n", | |
"\n", | |
".dropdown-menu .divider {\n", | |
" height: 1px;\n", | |
" margin: 0px 0px;\n", | |
" overflow: hidden;\n", | |
" background-color: var(--divider);\n", | |
"}\n", | |
"\n", | |
".dropdown-menu > .disabled > a,\n", | |
".dropdown-menu > .disabled > a:hover,\n", | |
".dropdown-menu > .disabled > a:focus {\n", | |
" font-family: sans-serif;\n", | |
" font-size: 8pt;\n", | |
" font-weight: normal;\n", | |
" color: var(--disabled-font-color);\n", | |
" display: block;\n", | |
" clear: both;\n", | |
" white-space: nowrap;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* menubar */\n", | |
".btn,\n", | |
".btn-default,\n", | |
"#logout,\n", | |
"#shutdown {\n", | |
" color: var(--focus-color);\n", | |
" background-color: transparent;\n", | |
" border: none;\n", | |
" box-shadow: none;\n", | |
" text-shadow: none;\n", | |
"}\n", | |
".btn:hover,\n", | |
".btn:active:hover,\n", | |
".btn.active:hover,\n", | |
".btn-default:hover,\n", | |
"#logout:hover,\n", | |
"#shutdown:hover {\n", | |
" color: var(--background-color);\n", | |
" border: 2px solid var(--focus-color);\n", | |
" background-color: var(--focus-color);\n", | |
" background: var(--focus-color);\n", | |
" background-image: none;\n", | |
" box-shadow: none;\n", | |
"}\n", | |
"\n", | |
".btn:active,\n", | |
".btn.active,\n", | |
".btn:active:focus,\n", | |
".btn.active:focus,\n", | |
".btn:active.focus,\n", | |
".btn.active.focus,\n", | |
".btn-default:focus,\n", | |
".btn-default.focus,\n", | |
".btn-default:active,\n", | |
".btn-default.active,\n", | |
".btn-default:active:hover,\n", | |
".btn-default.active:hover,\n", | |
".btn-default:active:focus,\n", | |
".btn-default.active:focus,\n", | |
".btn-default:active.focus,\n", | |
".btn-default.active.focus {\n", | |
" color: var(--background-color);\n", | |
" border: 2px solid var(--focus-color);\n", | |
" background-color: var(--focus-color);\n", | |
" box-shadow: none;\n", | |
"}\n", | |
"\n", | |
".btn:focus,\n", | |
".btn.focus,\n", | |
".btn:active:focus,\n", | |
".btn.active:focus,\n", | |
".btn:active,\n", | |
".btn.active,\n", | |
".btn:active.focus,\n", | |
".btn.active.focus {\n", | |
" color: var(--background-color);\n", | |
" outline: none;\n", | |
" outline-width: 0px;\n", | |
" background: var(--focus-color);\n", | |
" background-color: var(--focus-color);\n", | |
" border-color: var(--focus-color);\n", | |
" transition: 200ms ease;\n", | |
"}\n", | |
"\n", | |
".btn-sm.navbar-btn {\n", | |
" position: relative;\n", | |
" color: var(--focus-color);\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".toolbar select,\n", | |
".toolbar label {\n", | |
" vertical-align: middle;\n", | |
" display: inline;\n", | |
" flex-wrap: wrap;\n", | |
" font-size: 9;\n", | |
" color: var(--focus-color);\n", | |
" background: var(--background-color) !important;\n", | |
" background-color: var(--background-color);\n", | |
" border: 2px solid var(--background-color);\n", | |
" border-radius: 2%;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* cell */\n", | |
"div.cell {\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* cell prompt */\n", | |
".prompt {\n", | |
" font-family: monospace, monospace;\n", | |
" font-size: 9pt;\n", | |
" font-weight: normal;\n", | |
" padding-top: 4px;\n", | |
" padding-right: 5px;\n", | |
" text-align: right;\n", | |
" min-width: 11.5ex;\n", | |
" width: 11.5ex;\n", | |
"}\n", | |
"div.prompt.input_prompt {\n", | |
" color: var(--input-prompt-color);\n", | |
"}\n", | |
"div.prompt.output_prompt {\n", | |
" color: var(--output-prompt-color);\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* cell input */\n", | |
"div.input_area {\n", | |
" background-color: var(--cell-background-color);\n", | |
" background: var(--cell-background-color);\n", | |
" padding-right: 1.2em;\n", | |
" border: 0px;\n", | |
" border-radius: 0px;\n", | |
" border-top-right-radius: 4px;\n", | |
" border-bottom-right-radius: 4px;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* code setting */\n", | |
"div.CodeMirror,\n", | |
"div.CodeMirror pre {\n", | |
" font-family: Fira Code, 源真ゴシック等幅, monospace;\n", | |
" font-size: 10pt;\n", | |
" line-height: 150%;\n", | |
" color: var(--font-color);\n", | |
" /* background: var(--cell-background-color) */\n", | |
"}\n", | |
"div.CodeMirror-gutters,\n", | |
".CodeMirror-sizer {\n", | |
" border: none;\n", | |
" border-right: 2px solid var(--gutter);\n", | |
" background-color: var(--cell-background-color);\n", | |
" background: var(--cell-background-color);\n", | |
" border-radius: 0px;\n", | |
" white-space: nowrap;\n", | |
"}\n", | |
"#texteditor-backdrop,\n", | |
"#texteditor-backdrop #texteditor-container .CodeMirror-gutter,\n", | |
"#texteditor-backdrop #texteditor-container .CodeMirror-gutters {\n", | |
" background: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".CodeMirror-linenumber,\n", | |
"div.CodeMirror-linenumber,\n", | |
".CodeMirror-gutter.CodeMirror-linenumberdiv.CodeMirror-gutter.CodeMirror-linenumber {\n", | |
" padding-right: 1px;\n", | |
" margin-left: 0px;\n", | |
" margin: 0px;\n", | |
" width: 26px;\n", | |
" padding: 0px;\n", | |
" text-align: right;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* syntax highlight */\n", | |
"/* code */\n", | |
".CodeMirror-linenumber {\n", | |
" color: #75715e;\n", | |
"}\n", | |
".cm-s-ipython .CodeMirror-cursor {\n", | |
" border-left: 2px solid #0095ff;\n", | |
"}\n", | |
".cm-s-ipython span.cm-comment {\n", | |
" color: #75715e;\n", | |
" font-style: italic;\n", | |
"}\n", | |
".cm-s-ipython span.cm-atom {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-ipython span.cm-number {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-ipython span.cm-property {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".cm-s-ipython span.cm-attribute {\n", | |
" color: var(--font-color);\n", | |
"}\n", | |
".cm-s-ipython span.cm-keyword {\n", | |
" color: #f92672;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
".cm-s-ipython span.cm-string {\n", | |
" color: #e6db74;\n", | |
"}\n", | |
".cm-s-ipython span.cm-meta {\n", | |
" color: #fd971f;\n", | |
"}\n", | |
".cm-s-ipython span.cm-operator {\n", | |
" color: #f92672;\n", | |
"}\n", | |
".cm-s-ipython span.cm-builtin {\n", | |
" color: rgb(102, 217, 239);\n", | |
"}\n", | |
".cm-s-ipython span.cm-variable {\n", | |
" color: var(--font-color);\n", | |
"}\n", | |
".cm-s-ipython span.cm-variable-2 {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".cm-s-ipython span.cm-variable-3 {\n", | |
" color: #fd971f;\n", | |
"}\n", | |
".cm-s-ipython span.cm-def {\n", | |
" color: #a6e22e;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
".cm-s-ipython span.cm-error {\n", | |
" background: rgba(249, 38, 114, 0.4);\n", | |
"}\n", | |
".cm-s-ipython span.cm-tag {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-ipython span.cm-link {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".cm-s-ipython span.cm-storage {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-ipython span.cm-entity {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".cm-s-ipython span.cm-quote {\n", | |
" color: #e6db74;\n", | |
"}\n", | |
"\n", | |
"/* markdown */\n", | |
"div.CodeMirror span.CodeMirror-matchingbracket {\n", | |
" color: #f8f8f2;\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"div.CodeMirror span.CodeMirror-nonmatchingbracket {\n", | |
" color: #f8f8f2;\n", | |
" background: rgba(249, 38, 114, 0.4);\n", | |
"}\n", | |
"\n", | |
".cm-s-default .cm-hr {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
"div.cell.text_cell .cm-s-default .cm-header {\n", | |
" font-family: sans-serif;\n", | |
" font-weight: normal;\n", | |
" color: #a6e22e;\n", | |
" margin-top: 0.3em;\n", | |
" margin-bottom: 0.3em;\n", | |
"}\n", | |
"div.cell.text_cell .cm-s-default span.cm-variable-2 {\n", | |
" color: var(--font-color);\n", | |
"}\n", | |
"div.cell.text_cell .cm-s-default span.cm-variable-3 {\n", | |
" color: #fd971f;\n", | |
"}\n", | |
".cm-s-default span.cm-comment {\n", | |
" color: #75715e;\n", | |
"}\n", | |
".cm-s-default .cm-tag {\n", | |
" color: #529b2f;\n", | |
"}\n", | |
".cm-s-default .cm-builtin {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".cm-s-default .cm-string {\n", | |
" color: #e6db74;\n", | |
"}\n", | |
".cm-s-default .cm-keyword {\n", | |
" color: #f92672;\n", | |
"}\n", | |
".cm-s-default .cm-number {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-default .cm-error {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-default .cm-link {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".cm-s-default .cm-atom {\n", | |
" color: #ae81ff;\n", | |
"}\n", | |
".cm-s-default .cm-def {\n", | |
" color: #a6e22e;\n", | |
"}\n", | |
".CodeMirror-cursor {\n", | |
" border-left: 2px solid #0095ff;\n", | |
" border-right: none;\n", | |
" width: 0;\n", | |
"}\n", | |
".cm-s-default div.CodeMirror-selected {\n", | |
" background: #4f4f4f;\n", | |
"}\n", | |
".cm-s-default .cm-selected {\n", | |
" background: #4f4f4f;\n", | |
"}\n", | |
"\n", | |
".MathJax_Display,\n", | |
".MathJax {\n", | |
" border: 0;\n", | |
" font-size: 100%;\n", | |
" text-align: center;\n", | |
" margin: 0em;\n", | |
" line-height: 2.25;\n", | |
"}\n", | |
".MathJax:focus,\n", | |
"body :focus .MathJax {\n", | |
" display: inline-block;\n", | |
"}\n", | |
".MathJax:focus,\n", | |
"body :focus .MathJax {\n", | |
" display: inline-block;\n", | |
"}\n", | |
".completions {\n", | |
" position: absolute;\n", | |
" z-index: 110;\n", | |
" overflow: hidden;\n", | |
" border: medium solid var(--gutter);\n", | |
" box-shadow: none;\n", | |
" line-height: 1;\n", | |
"}\n", | |
".completions select {\n", | |
" background: #282828;\n", | |
" background-color: #282828;\n", | |
" outline: none;\n", | |
" border: none;\n", | |
" padding: 0px;\n", | |
" margin: 0px;\n", | |
" margin-left: 2px;\n", | |
" overflow: auto;\n", | |
" font-family: monospace, monospace;\n", | |
" font-size: 11pt;\n", | |
" color: var(--font-color);\n", | |
" width: auto;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* output */\n", | |
"div.output.output_scroll {\n", | |
" box-shadow: none;\n", | |
"}\n", | |
"::-webkit-scrollbar {\n", | |
" width: 11px;\n", | |
" max-height: 9px;\n", | |
" background-color: #2d2d2d;\n", | |
" border-radius: 3px;\n", | |
" border: none;\n", | |
"}\n", | |
"::-webkit-scrollbar-track {\n", | |
" background: #2d2d2d;\n", | |
" border: none;\n", | |
" width: 11px;\n", | |
" max-height: 9px;\n", | |
"}\n", | |
"::-webkit-scrollbar-thumb {\n", | |
" border-radius: 2px;\n", | |
" border: none;\n", | |
" background: var(--gutter);\n", | |
" background-clip: content-box;\n", | |
" width: 11px;\n", | |
"}\n", | |
"\n", | |
"div.output_subarea {\n", | |
" margin-left: 3%;\n", | |
"}\n", | |
"\n", | |
"div.output_subarea.output_text.output_stream.output_stdout,\n", | |
"div.output_subarea.output_text {\n", | |
" font-size: 10pt;\n", | |
" line-height: 150%;\n", | |
" margin-left: 1%;\n", | |
" background-color: var(--background-color);\n", | |
" color: var(--dark-font-color);\n", | |
"}\n", | |
"\n", | |
"div.output_area pre {\n", | |
" font-size: 10pt;\n", | |
" line-height: 150%;\n", | |
" color: var(--dark-font-color);\n", | |
"}\n", | |
"\n", | |
"div.output_html {\n", | |
" font-size: 10pt;\n", | |
" color: var(--dark-font-color);\n", | |
"}\n", | |
"/* markdown */\n", | |
"div.text_cell,\n", | |
"div.text_cell_render pre,\n", | |
"div.text_cell_render {\n", | |
" font-size: 11pt;\n", | |
" line-height: 100%;\n", | |
" color: var(--font-color);\n", | |
" border-radius: 0px;\n", | |
"}\n", | |
"div.text_cell_render {\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"div.cell.text_cell.unrendered div.input_area,\n", | |
"div.cell.text_cell.rendered div.input_area {\n", | |
" border: 0px;\n", | |
" border-radius: 2px;\n", | |
"}\n", | |
"div.cell.text_cell .prompt {\n", | |
" font-size: 9.5pt;\n", | |
" color: var(--gutter);\n", | |
" text-align: right;\n", | |
" min-width: 14.5ex;\n", | |
" width: 14.5ex;\n", | |
" background-color: transparent;\n", | |
" border-right: 2px solid var(--gutter);\n", | |
"}\n", | |
"\n", | |
"div.text_cell,\n", | |
"div.text_cell_render pre,\n", | |
"div.text_cell_render {\n", | |
" font-family: sans-serif;\n", | |
" font-size: 11pt;\n", | |
" line-height: 130%;\n", | |
" color: var(--font-color);\n", | |
" border-radius: 0px;\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
"div.rendered_html code {\n", | |
" font-size: 10pt;\n", | |
" padding-top: 3px;\n", | |
" padding-left: 2px;\n", | |
" color: var(--font-color);\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".rendered_html table {\n", | |
" margin-left: 8px;\n", | |
" margin-right: auto;\n", | |
" border: none;\n", | |
" border-collapse: collapse;\n", | |
" border-spacing: 0;\n", | |
" table-layout: fixed;\n", | |
"}\n", | |
"\n", | |
".rendered_html thead {\n", | |
" background: var(--background-color);\n", | |
" color: var(--dark-font-color);\n", | |
"}\n", | |
"\n", | |
".rendered_html tbody tr:nth-child(odd) {\n", | |
" background: #e4e4e4;\n", | |
"}\n", | |
".rendered_html tbody tr {\n", | |
" background: #d3d3d3;\n", | |
"}\n", | |
".rendered_html tbody tr:hover {\n", | |
" background: #878787;\n", | |
"}\n", | |
"\n", | |
".rendered_html h1,\n", | |
".text_cell_render h1 {\n", | |
" color: #1de9b6;\n", | |
" font-size: 170%;\n", | |
" text-align: left;\n", | |
" font-style: normal;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
".rendered_html h2,\n", | |
".text_cell_render h2 {\n", | |
" color: #a7ffeb;\n", | |
" font-size: 150%;\n", | |
" font-style: normal;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
".rendered_html h3,\n", | |
".text_cell_render h3 {\n", | |
" color: #1de9b6;\n", | |
" font-size: 140%;\n", | |
" font-style: normal;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
".rendered_html h4,\n", | |
".text_cell_render h4 {\n", | |
" color: #a7ffeb;\n", | |
" font-size: 110%;\n", | |
" font-style: normal;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
".rendered_html h5,\n", | |
".text_cell_render h5 {\n", | |
" color: #1de9b6;\n", | |
" font-size: 100%;\n", | |
" font-style: normal;\n", | |
" font-weight: normal;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* toc */\n", | |
"div#toc-wrapper {\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
"#toc a,\n", | |
".toc-item-num {\n", | |
" color: var(--font-color) !important;\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* modal */\n", | |
".modal-content {\n", | |
" color: var(--font-color);\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
"div.form-control,\n", | |
".form-control {\n", | |
" font-family: sans-serif;\n", | |
" font-size: initial;\n", | |
" color: var(--font-color);\n", | |
" background-color: var(--background-color);\n", | |
" border: 1px solid var(--divider);\n", | |
"}\n", | |
"\n", | |
"/* */\n", | |
"/* */\n", | |
"/* toppage panel, list */\n", | |
"#running .panel-group .panel .panel-heading,\n", | |
"#notebook_list_header.row.list_header {\n", | |
" color: var(--font-color);\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".nav-tabs,\n", | |
".panel-default,\n", | |
".list_container > div {\n", | |
" border: 0;\n", | |
" border-bottom: 1px solid var(--divider);\n", | |
"}\n", | |
"\n", | |
".nav-tabs > li > a:hover,\n", | |
".nav-tabs > li.active > a,\n", | |
".nav-tabs > li.active > a:hover,\n", | |
".nav-tabs > li.active > a:focus,\n", | |
".nav-tabs > li.active > a:focus .list_header {\n", | |
" color: var(--focus-color);\n", | |
" background-color: var(--focus-background-color);\n", | |
" border: 0px;\n", | |
" border-bottom: 2px solid var(--focus-background-color);\n", | |
"}\n", | |
"\n", | |
".list_item,\n", | |
".list_container {\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
" border: 1px solid var(--divider);\n", | |
"}\n", | |
"\n", | |
".list_item:hover {\n", | |
" background: var(--background-color);\n", | |
" background-color: var(--background-color);\n", | |
"}\n", | |
"\n", | |
".form-group.list-group-item {\n", | |
" color: var(--font-color);\n", | |
" background-color: var(--background-color);\n", | |
" border-color: var(--divider);\n", | |
" margin-bottom: 0px;\n", | |
"}\n", | |
"\n", | |
".celltoolbar {\n", | |
" background: var(--background-color);\n", | |
" border: var(--gutter);\n", | |
"}</style>" | |
], | |
"text/plain": [ | |
"<IPython.core.display.HTML object>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"# https://recruit-tech.co.jp/blog/2018/10/16/jupyter_notebook_tips/\n", | |
"if is_in_jupyter():\n", | |
" def set_stylesheet():\n", | |
" from IPython.display import display, HTML\n", | |
" css = !wget https://raw.githubusercontent.com/lapis-zero09/jupyter_notebook_tips/master/css/jupyter_notebook/monokai.css -q -O -\n", | |
" css = \"\\n\".join(css)\n", | |
" display(HTML('<style type=\"text/css\">%s</style>'%css))\n", | |
" set_stylesheet()" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"metadata": {}, | |
"source": [ | |
"## Main" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"#!/usr/bin/env python\n", | |
"# -*- coding: utf-8; py-indent-offset:4 -*-\n", | |
"###############################################################################\n", | |
"#\n", | |
"# Copyright (C) 2015-2020 Daniel Rodriguez\n", | |
"# Copyright (C) 2021 dogwood008 (modified)\n", | |
"#\n", | |
"# This program is free software: you can redistribute it and/or modify\n", | |
"# it under the terms of the GNU General Public License as published by\n", | |
"# the Free Software Foundation, either version 3 of the License, or\n", | |
"# (at your option) any later version.\n", | |
"#\n", | |
"# This program is distributed in the hope that it will be useful,\n", | |
"# but WITHOUT ANY WARRANTY; without even the implied warranty of\n", | |
"# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n", | |
"# GNU General Public License for more details.\n", | |
"#\n", | |
"# You should have received a copy of the GNU General Public License\n", | |
"# along with this program. If not, see <http://www.gnu.org/licenses/>.\n", | |
"#\n", | |
"###############################################################################\n", | |
"from __future__ import (absolute_import, division, print_function,\n", | |
" unicode_literals)\n", | |
"\n", | |
"import collections\n", | |
"from datetime import datetime, timedelta\n", | |
"import time as _time\n", | |
"import json\n", | |
"import threading\n", | |
"\n", | |
"# import oandapy\n", | |
"# import requests # oandapy depdendency\n", | |
"\n", | |
"import backtrader as bt\n", | |
"from backtrader.metabase import MetaParams\n", | |
"from backtrader.utils.py3 import queue, with_metaclass\n", | |
"from backtrader.utils import AutoDict\n", | |
"\n", | |
"import kabusapi" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"code_folding": [ | |
0 | |
] | |
}, | |
"outputs": [], | |
"source": [ | |
"# Extend the exceptions to support extra cases\n", | |
"# '''\n", | |
"# class OandaRequestError(oandapy.OandaError):\n", | |
"# def __init__(self):\n", | |
"# er = dict(code=599, message='Request Error', description='')\n", | |
"# super(self.__class__, self).__init__(er)\n", | |
"# \n", | |
"# \n", | |
"# class OandaStreamError(oandapy.OandaError):\n", | |
"# def __init__(self, content=''):\n", | |
"# er = dict(code=598, message='Failed Streaming', description=content)\n", | |
"# super(self.__class__, self).__init__(er)\n", | |
"# \n", | |
"# \n", | |
"# class OandaTimeFrameError(oandapy.OandaError):\n", | |
"# def __init__(self, content):\n", | |
"# er = dict(code=597, message='Not supported TimeFrame', description='')\n", | |
"# super(self.__class__, self).__init__(er)\n", | |
"# \n", | |
"# \n", | |
"# class OandaNetworkError(oandapy.OandaError):\n", | |
"# def __init__(self):\n", | |
"# er = dict(code=596, message='Network Error', description='')\n", | |
"# super(self.__class__, self).__init__(er)\n", | |
"# '''\n", | |
"\n", | |
"# class API(oandapy.API):\n", | |
"# def request(self, endpoint, method='GET', params=None):\n", | |
"# # Overriden to make something sensible out of a\n", | |
"# # request.RequestException rather than simply issuing a print(str(e))\n", | |
"# url = '%s/%s' % (self.api_url, endpoint)\n", | |
"# \n", | |
"# method = method.lower()\n", | |
"# params = params or {}\n", | |
"# \n", | |
"# func = getattr(self.client, method)\n", | |
"# \n", | |
"# request_args = {}\n", | |
"# if method == 'get':\n", | |
"# request_args['params'] = params\n", | |
"# else:\n", | |
"# request_args['data'] = params\n", | |
"# \n", | |
"# # Added the try block\n", | |
"# try:\n", | |
"# response = func(url, **request_args)\n", | |
"# except requests.RequestException as e:\n", | |
"# return OandaRequestError().error_response\n", | |
"# \n", | |
"# content = response.content.decode('utf-8')\n", | |
"# content = json.loads(content)\n", | |
"# \n", | |
"# # error message\n", | |
"# if response.status_code >= 400:\n", | |
"# # changed from raise to return\n", | |
"# return oandapy.OandaError(content).error_response\n", | |
"# \n", | |
"# return content" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": { | |
"code_folding": [ | |
0 | |
] | |
}, | |
"outputs": [], | |
"source": [ | |
"#FIXME\n", | |
"# class Streamer(oandapy.Streamer):\n", | |
"# def __init__(self, q, headers=None, *args, **kwargs):\n", | |
"# # Override to provide headers, which is in the standard API interface\n", | |
"# super(Streamer, self).__init__(*args, **kwargs)\n", | |
"# \n", | |
"# if headers:\n", | |
"# self.client.headers.update(headers)\n", | |
"# \n", | |
"# self.q = q\n", | |
"# \n", | |
"# def run(self, endpoint, params=None):\n", | |
"# # Override to better manage exceptions.\n", | |
"# # Kept as much as possible close to the original\n", | |
"# self.connected = True\n", | |
"# \n", | |
"# params = params or {}\n", | |
"# \n", | |
"# ignore_heartbeat = None\n", | |
"# if 'ignore_heartbeat' in params:\n", | |
"# ignore_heartbeat = params['ignore_heartbeat']\n", | |
"# \n", | |
"# request_args = {}\n", | |
"# request_args['params'] = params\n", | |
"# \n", | |
"# url = '%s/%s' % (self.api_url, endpoint)\n", | |
"# \n", | |
"# while self.connected:\n", | |
"# # Added exception control here\n", | |
"# try:\n", | |
"# response = self.client.get(url, **request_args)\n", | |
"# except requests.RequestException as e:\n", | |
"# self.q.put(OandaRequestError().error_response)\n", | |
"# break\n", | |
"# \n", | |
"# if response.status_code != 200:\n", | |
"# self.on_error(response.content)\n", | |
"# break # added break here\n", | |
"# \n", | |
"# # Changed chunk_size 90 -> None\n", | |
"# try:\n", | |
"# for line in response.iter_lines(chunk_size=None):\n", | |
"# if not self.connected:\n", | |
"# break\n", | |
"# \n", | |
"# if line:\n", | |
"# data = json.loads(line.decode('utf-8'))\n", | |
"# if not (ignore_heartbeat and 'heartbeat' in data):\n", | |
"# self.on_success(data)\n", | |
"# \n", | |
"# except: # socket.error has been seen\n", | |
"# self.q.put(OandaStreamError().error_response)\n", | |
"# break\n", | |
"# \n", | |
"# def on_success(self, data):\n", | |
"# if 'tick' in data:\n", | |
"# self.q.put(data['tick'])\n", | |
"# elif 'transaction' in data:\n", | |
"# self.q.put(data['transaction'])\n", | |
"# \n", | |
"# def on_error(self, data):\n", | |
"# self.disconnect()\n", | |
"# self.q.put(OandaStreamError(data).error_response)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"from enum import Enum\n", | |
"class KabusAPIEnv(Enum):\n", | |
" DEV = 'dev'\n", | |
" PROD = 'prod'" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class MetaSingleton(MetaParams):\n", | |
" '''Metaclass to make a metaclassed class a singleton'''\n", | |
" def __init__(cls, name, bases, dct):\n", | |
" super(MetaSingleton, cls).__init__(name, bases, dct)\n", | |
" cls._singleton = None\n", | |
"\n", | |
" def __call__(cls, *args, **kwargs):\n", | |
" if cls._singleton is None:\n", | |
" cls._singleton = (\n", | |
" super(MetaSingleton, cls).__call__(*args, **kwargs))\n", | |
"\n", | |
" return cls._singleton\n", | |
"\n", | |
"\n", | |
"class KabuSAPIStore(with_metaclass(MetaSingleton, object)):\n", | |
" '''Singleton class wrapping to control the connections to Kabu STATION API.\n", | |
"\n", | |
" Params:\n", | |
"\n", | |
" - ``token`` (default:``None``): API access token\n", | |
"\n", | |
" - ``account`` (default: ``None``): account id\n", | |
"\n", | |
" - ``practice`` (default: ``False``): use the test environment\n", | |
"\n", | |
" - ``account_tmout`` (default: ``10.0``): refresh period for account\n", | |
" value/cash refresh\n", | |
" '''\n", | |
"\n", | |
" BrokerCls = None # broker class will autoregister\n", | |
" DataCls = None # data class will auto register\n", | |
"\n", | |
" params = (\n", | |
" ('url', 'localhost'),\n", | |
" ('env', KabuSAPIEnv.DEV),\n", | |
" ('port', None),\n", | |
" ('password', None),\n", | |
" )\n", | |
"\n", | |
" # _DTEPOCH = datetime(1970, 1, 1)\n", | |
" # _ENVPRACTICE = 'practice'\n", | |
" # _ENVLIVE = 'live'\n", | |
"\n", | |
" @classmethod\n", | |
" def getdata(cls, *args, **kwargs):\n", | |
" '''Returns ``DataCls`` with args, kwargs'''\n", | |
" return cls.DataCls(*args, **kwargs)\n", | |
"\n", | |
" @classmethod\n", | |
" def getbroker(cls, *args, **kwargs):\n", | |
" '''Returns broker with *args, **kwargs from registered ``BrokerCls``'''\n", | |
" return cls.BrokerCls(*args, **kwargs)\n", | |
"\n", | |
" def __init__(self):\n", | |
" def _getport() -> int:\n", | |
" if self.p.port:\n", | |
" return port\n", | |
" return 18081 if self.p.env == KabuSAPIEnv.DEV else 18080\n", | |
"\n", | |
" def _init_kabusapi_client(self) -> kabusapiapi.Context:\n", | |
" url = self.p.url\n", | |
" port = self.p.get('port', _getport())\n", | |
" password = self.p.password\n", | |
" token = kabusapi.Context(url, port, password).token\n", | |
" self.kapi = kabusapi.Context(url, port, token=token)\n", | |
" \n", | |
" super(KabuSAPIStore, self).__init__()\n", | |
"\n", | |
" self.notifs = collections.deque() # store notifications for cerebro\n", | |
"\n", | |
" self._env = None # reference to cerebro for general notifications\n", | |
" self.broker = None # broker instance\n", | |
" self.datas = list() # datas that have registered over start\n", | |
"\n", | |
" self._orders = collections.OrderedDict() # map order.ref to oid\n", | |
" self._ordersrev = collections.OrderedDict() # map oid to order.ref\n", | |
" self._transpend = collections.defaultdict(collections.deque)\n", | |
"\n", | |
" _init_kabusapi_client()\n", | |
" \n", | |
" self._cash = 0.0\n", | |
" self._value = 0.0\n", | |
" self._evt_acct = threading.Event()\n", | |
" \n", | |
"\n", | |
" def start(self, data=None, broker=None):\n", | |
" # Datas require some processing to kickstart data reception\n", | |
" if data is None and broker is None:\n", | |
" self.cash = None\n", | |
" return\n", | |
"\n", | |
" if data is not None:\n", | |
" self._env = data._env\n", | |
" # For datas simulate a queue with None to kickstart co\n", | |
" self.datas.append(data)\n", | |
"\n", | |
" if self.broker is not None:\n", | |
" self.broker.data_started(data)\n", | |
"\n", | |
" elif broker is not None:\n", | |
" self.broker = broker\n", | |
" self.streaming_events()\n", | |
" self.broker_threads()\n", | |
"\n", | |
" def stop(self):\n", | |
" # signal end of thread\n", | |
" if self.broker is not None:\n", | |
" self.q_ordercreate.put(None)\n", | |
" self.q_orderclose.put(None)\n", | |
" self.q_account.put(None)\n", | |
"\n", | |
" def put_notification(self, msg, *args, **kwargs):\n", | |
" self.notifs.append((msg, args, kwargs))\n", | |
"\n", | |
" def get_notifications(self):\n", | |
" '''Return the pending \"store\" notifications'''\n", | |
" self.notifs.append(None) # put a mark / threads could still append\n", | |
" return [x for x in iter(self.notifs.popleft, None)]\n", | |
"\n", | |
" # Oanda supported granularities\n", | |
" # _GRANULARITIES = {\n", | |
" # (bt.TimeFrame.Seconds, 5): 'S5',\n", | |
" # (bt.TimeFrame.Seconds, 10): 'S10',\n", | |
" # (bt.TimeFrame.Seconds, 15): 'S15',\n", | |
" # (bt.TimeFrame.Seconds, 30): 'S30',\n", | |
" # (bt.TimeFrame.Minutes, 1): 'M1',\n", | |
" # (bt.TimeFrame.Minutes, 2): 'M3',\n", | |
" # (bt.TimeFrame.Minutes, 3): 'M3',\n", | |
" # (bt.TimeFrame.Minutes, 4): 'M4',\n", | |
" # (bt.TimeFrame.Minutes, 5): 'M5',\n", | |
" # (bt.TimeFrame.Minutes, 10): 'M5',\n", | |
" # (bt.TimeFrame.Minutes, 15): 'M5',\n", | |
" # (bt.TimeFrame.Minutes, 30): 'M5',\n", | |
" # (bt.TimeFrame.Minutes, 60): 'H1',\n", | |
" # (bt.TimeFrame.Minutes, 120): 'H2',\n", | |
" # (bt.TimeFrame.Minutes, 180): 'H3',\n", | |
" # (bt.TimeFrame.Minutes, 240): 'H4',\n", | |
" # (bt.TimeFrame.Minutes, 360): 'H6',\n", | |
" # (bt.TimeFrame.Minutes, 480): 'H8',\n", | |
" # (bt.TimeFrame.Days, 1): 'D',\n", | |
" # (bt.TimeFrame.Weeks, 1): 'W',\n", | |
" # (bt.TimeFrame.Months, 1): 'M',\n", | |
" # }\n", | |
"\n", | |
" def get_positions(self):\n", | |
" try:\n", | |
" positions = self.oapi.get_positions(self.p.account)\n", | |
" except (oandapy.OandaError, OandaRequestError,):\n", | |
" return None\n", | |
"\n", | |
" poslist = positions.get('positions', [])\n", | |
" return poslist\n", | |
"\n", | |
" #def get_granularity(self, timeframe, compression):\n", | |
" # return self._GRANULARITIES.get((timeframe, compression), None)\n", | |
"\n", | |
" def get_instrument(self, dataname):\n", | |
" try:\n", | |
" insts = self.oapi.get_instruments(self.p.account,\n", | |
" instruments=dataname)\n", | |
" except (oandapy.OandaError, OandaRequestError,):\n", | |
" return None\n", | |
"\n", | |
" i = insts.get('instruments', [{}])\n", | |
" return i[0] or None\n", | |
"\n", | |
" def streaming_events(self, tmout=None):\n", | |
" q = queue.Queue()\n", | |
" kwargs = {'q': q, 'tmout': tmout}\n", | |
"\n", | |
" t = threading.Thread(target=self._t_streaming_listener, kwargs=kwargs)\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
"\n", | |
" t = threading.Thread(target=self._t_streaming_events, kwargs=kwargs) # FIXME: _t_streaming_events\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
" return q\n", | |
"\n", | |
" def _t_streaming_listener(self, q, tmout=None):\n", | |
" while True:\n", | |
" trans = q.get()\n", | |
" self._transaction(trans)\n", | |
"\n", | |
" # FIXME: Streamer\n", | |
" def _t_streaming_events(self, q, tmout=None):\n", | |
" if tmout is not None:\n", | |
" _time.sleep(tmout)\n", | |
"\n", | |
" # FIXME: oandapy.Streamer\n", | |
" # streamer = Streamer(q,\n", | |
" # environment=self._oenv,\n", | |
" # access_token=self.p.token,\n", | |
" # headers={'X-Accept-Datetime-Format': 'UNIX'})\n", | |
"# \n", | |
" # streamer.events(ignore_heartbeat=False)\n", | |
"\n", | |
" def candles(self, dataname, dtbegin, dtend, timeframe, compression,\n", | |
" candleFormat, includeFirst):\n", | |
"\n", | |
" kwargs = locals().copy()\n", | |
" kwargs.pop('self')\n", | |
" kwargs['q'] = q = queue.Queue()\n", | |
" t = threading.Thread(target=self._t_candles, kwargs=kwargs) # FIXME: _t_candles\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
" return q\n", | |
"\n", | |
" def _t_candles(self, dataname, dtbegin, dtend, timeframe, compression,\n", | |
" candleFormat, includeFirst, q):\n", | |
"\n", | |
" # FIXME: granularity = self.get_granularity(timeframe, compression)\n", | |
" if granularity is None:\n", | |
" e = OandaTimeFrameError()\n", | |
" q.put(e.error_response)\n", | |
" return\n", | |
"\n", | |
" dtkwargs = {}\n", | |
" if dtbegin is not None:\n", | |
" dtkwargs['start'] = int((dtbegin - self._DTEPOCH).total_seconds()) # FIXME: _DTEPOCH\n", | |
"\n", | |
" if dtend is not None:\n", | |
" dtkwargs['end'] = int((dtend - self._DTEPOCH).total_seconds()) # FIXME: _DTEPOCH\n", | |
"\n", | |
" try:\n", | |
" # FIXME: granularity\n", | |
" # response = self.oapi.get_history(instrument=dataname,\n", | |
" # granularity=granularity,\n", | |
" # candleFormat=candleFormat,\n", | |
" # **dtkwargs)\n", | |
"\n", | |
" except oandapy.OandaError as e:\n", | |
" q.put(e.error_response)\n", | |
" q.put(None)\n", | |
" return\n", | |
"\n", | |
" # FIXME\n", | |
" for candle in response.get('candles', []):\n", | |
" q.put(candle)\n", | |
"\n", | |
" q.put({}) # end of transmission\n", | |
"\n", | |
" def streaming_prices(self, dataname, tmout=None):\n", | |
" q = queue.Queue()\n", | |
" kwargs = {'q': q, 'dataname': dataname, 'tmout': tmout}\n", | |
" t = threading.Thread(target=self._t_streaming_prices, kwargs=kwargs) # FIXME: _t_streaming_prices\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
" return q\n", | |
"\n", | |
" # FIXME: Streamer\n", | |
" def _t_streaming_prices(self, dataname, q, tmout):\n", | |
" if tmout is not None:\n", | |
" _time.sleep(tmout)\n", | |
"\n", | |
" # FIXME: Streamer\n", | |
" # FIXME streamer = Streamer(q, environment=self._oenv,\n", | |
" # FIXME access_token=self.p.token,\n", | |
" # FIXME headers={'X-Accept-Datetime-Format': 'UNIX'})\n", | |
"\n", | |
" # FIXME streamer.rates(self.p.account, instruments=dataname)\n", | |
"\n", | |
" def get_cash(self):\n", | |
" return self._cash\n", | |
"\n", | |
" def get_value(self):\n", | |
" return self._value\n", | |
"\n", | |
" _ORDEREXECS = {\n", | |
" bt.Order.Market: 'market',\n", | |
" bt.Order.Limit: 'limit',\n", | |
" bt.Order.Stop: 'stop',\n", | |
" bt.Order.StopLimit: 'stop',\n", | |
" }\n", | |
"\n", | |
" def broker_threads(self):\n", | |
" self.q_account = queue.Queue()\n", | |
" self.q_account.put(True) # force an immediate update\n", | |
" t = threading.Thread(target=self._t_account)\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
"\n", | |
" self.q_ordercreate = queue.Queue()\n", | |
" t = threading.Thread(target=self._t_order_create)\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
"\n", | |
" self.q_orderclose = queue.Queue()\n", | |
" t = threading.Thread(target=self._t_order_cancel)\n", | |
" t.daemon = True\n", | |
" t.start()\n", | |
"\n", | |
" # Wait once for the values to be set\n", | |
" self._evt_acct.wait(self.p.account_tmout)\n", | |
"\n", | |
" def _t_account(self):\n", | |
" while True:\n", | |
" try:\n", | |
" msg = self.q_account.get(timeout=self.p.account_tmout)\n", | |
" if msg is None:\n", | |
" break # end of thread\n", | |
" except queue.Empty: # tmout -> time to refresh\n", | |
" pass\n", | |
"\n", | |
" try:\n", | |
" accinfo = self.oapi.get_account(self.p.account)\n", | |
" except Exception as e:\n", | |
" self.put_notification(e)\n", | |
" continue\n", | |
"\n", | |
" try:\n", | |
" self._cash = accinfo['marginAvail']\n", | |
" self._value = accinfo['balance']\n", | |
" except KeyError:\n", | |
" pass\n", | |
"\n", | |
" self._evt_acct.set()\n", | |
"\n", | |
" def order_create(self, order, stopside=None, takeside=None, **kwargs):\n", | |
" okwargs = dict()\n", | |
" okwargs['instrument'] = order.data._dataname\n", | |
" okwargs['units'] = abs(order.created.size)\n", | |
" okwargs['side'] = 'buy' if order.isbuy() else 'sell'\n", | |
" okwargs['type'] = self._ORDEREXECS[order.exectype]\n", | |
" if order.exectype != bt.Order.Market:\n", | |
" okwargs['price'] = order.created.price\n", | |
" if order.valid is None:\n", | |
" # 1 year and datetime.max fail ... 1 month works\n", | |
" valid = datetime.utcnow() + timedelta(days=30)\n", | |
" else:\n", | |
" valid = order.data.num2date(order.valid)\n", | |
" # To timestamp with seconds precision\n", | |
" okwargs['expiry'] = int((valid - self._DTEPOCH).total_seconds()) # FIXME: _DTEPOCH\n", | |
"\n", | |
" if order.exectype == bt.Order.StopLimit:\n", | |
" okwargs['lowerBound'] = order.created.pricelimit\n", | |
" okwargs['upperBound'] = order.created.pricelimit\n", | |
"\n", | |
" if order.exectype == bt.Order.StopTrail:\n", | |
" okwargs['trailingStop'] = order.trailamount\n", | |
"\n", | |
" if stopside is not None:\n", | |
" okwargs['stopLoss'] = stopside.price\n", | |
"\n", | |
" if takeside is not None:\n", | |
" okwargs['takeProfit'] = takeside.price\n", | |
"\n", | |
" okwargs.update(**kwargs) # anything from the user\n", | |
"\n", | |
" self.q_ordercreate.put((order.ref, okwargs,))\n", | |
" return order\n", | |
"\n", | |
" _OIDSINGLE = ['orderOpened', 'tradeOpened', 'tradeReduced']\n", | |
" _OIDMULTIPLE = ['tradesClosed']\n", | |
"\n", | |
" def _t_order_create(self):\n", | |
" while True:\n", | |
" msg = self.q_ordercreate.get()\n", | |
" if msg is None:\n", | |
" break\n", | |
"\n", | |
" oref, okwargs = msg\n", | |
" try:\n", | |
" o = self.oapi.create_order(self.p.account, **okwargs)\n", | |
" except Exception as e:\n", | |
" self.put_notification(e)\n", | |
" self.broker._reject(oref)\n", | |
" return\n", | |
"\n", | |
" # Ids are delivered in different fields and all must be fetched to\n", | |
" # match them (as executions) to the order generated here\n", | |
" oids = list()\n", | |
" for oidfield in self._OIDSINGLE:\n", | |
" if oidfield in o and 'id' in o[oidfield]:\n", | |
" oids.append(o[oidfield]['id'])\n", | |
"\n", | |
" for oidfield in self._OIDMULTIPLE:\n", | |
" if oidfield in o:\n", | |
" for suboidfield in o[oidfield]:\n", | |
" oids.append(suboidfield['id'])\n", | |
"\n", | |
" if not oids:\n", | |
" self.broker._reject(oref)\n", | |
" return\n", | |
"\n", | |
" self._orders[oref] = oids[0]\n", | |
" self.broker._submit(oref)\n", | |
" if okwargs['type'] == 'market':\n", | |
" self.broker._accept(oref) # taken immediately\n", | |
"\n", | |
" for oid in oids:\n", | |
" self._ordersrev[oid] = oref # maps ids to backtrader order\n", | |
"\n", | |
" # An transaction may have happened and was stored\n", | |
" tpending = self._transpend[oid]\n", | |
" tpending.append(None) # eom marker\n", | |
" while True:\n", | |
" trans = tpending.popleft()\n", | |
" if trans is None:\n", | |
" break\n", | |
" self._process_transaction(oid, trans)\n", | |
"\n", | |
" def order_cancel(self, order):\n", | |
" self.q_orderclose.put(order.ref)\n", | |
" return order\n", | |
"\n", | |
" def _t_order_cancel(self):\n", | |
" while True:\n", | |
" oref = self.q_orderclose.get()\n", | |
" if oref is None:\n", | |
" break\n", | |
"\n", | |
" oid = self._orders.get(oref, None)\n", | |
" if oid is None:\n", | |
" continue # the order is no longer there\n", | |
" try:\n", | |
" o = self.oapi.close_order(self.p.account, oid)\n", | |
" except Exception as e:\n", | |
" continue # not cancelled - FIXME: notify\n", | |
"\n", | |
" self.broker._cancel(oref)\n", | |
"\n", | |
" _X_ORDER_CREATE = ('STOP_ORDER_CREATE',\n", | |
" 'LIMIT_ORDER_CREATE', 'MARKET_IF_TOUCHED_ORDER_CREATE',)\n", | |
"\n", | |
" def _transaction(self, trans):\n", | |
" # Invoked from Streaming Events. May actually receive an event for an\n", | |
" # oid which has not yet been returned after creating an order. Hence\n", | |
" # store if not yet seen, else forward to processer\n", | |
" ttype = trans['type']\n", | |
" if ttype == 'MARKET_ORDER_CREATE':\n", | |
" try:\n", | |
" oid = trans['tradeReduced']['id']\n", | |
" except KeyError:\n", | |
" try:\n", | |
" oid = trans['tradeOpened']['id']\n", | |
" except KeyError:\n", | |
" return # cannot do anything else\n", | |
"\n", | |
" elif ttype in self._X_ORDER_CREATE:\n", | |
" oid = trans['id']\n", | |
" elif ttype == 'ORDER_FILLED':\n", | |
" oid = trans['orderId']\n", | |
"\n", | |
" elif ttype == 'ORDER_CANCEL':\n", | |
" oid = trans['orderId']\n", | |
"\n", | |
" elif ttype == 'TRADE_CLOSE':\n", | |
" oid = trans['id']\n", | |
" pid = trans['tradeId']\n", | |
" if pid in self._orders and False: # Know nothing about trade\n", | |
" return # can do nothing\n", | |
"\n", | |
" # Skip above - at the moment do nothing\n", | |
" # Received directly from an event in the WebGUI for example which\n", | |
" # closes an existing position related to order with id -> pid\n", | |
" # COULD BE DONE: Generate a fake counter order to gracefully\n", | |
" # close the existing position\n", | |
" msg = ('Received TRADE_CLOSE for unknown order, possibly generated'\n", | |
" ' over a different client or GUI')\n", | |
" self.put_notification(msg, trans)\n", | |
" return\n", | |
"\n", | |
" else: # Go aways gracefully\n", | |
" try:\n", | |
" oid = trans['id']\n", | |
" except KeyError:\n", | |
" oid = 'None'\n", | |
"\n", | |
" msg = 'Received {} with oid {}. Unknown situation'\n", | |
" msg = msg.format(ttype, oid)\n", | |
" self.put_notification(msg, trans)\n", | |
" return\n", | |
"\n", | |
" try:\n", | |
" oref = self._ordersrev[oid]\n", | |
" self._process_transaction(oid, trans)\n", | |
" except KeyError: # not yet seen, keep as pending\n", | |
" self._transpend[oid].append(trans)\n", | |
"\n", | |
" _X_ORDER_FILLED = ('MARKET_ORDER_CREATE',\n", | |
" 'ORDER_FILLED', 'TAKE_PROFIT_FILLED',\n", | |
" 'STOP_LOSS_FILLED', 'TRAILING_STOP_FILLED',)\n", | |
"\n", | |
" def _process_transaction(self, oid, trans):\n", | |
" try:\n", | |
" oref = self._ordersrev.pop(oid)\n", | |
" except KeyError:\n", | |
" return\n", | |
"\n", | |
" ttype = trans['type']\n", | |
"\n", | |
" if ttype in self._X_ORDER_FILLED:\n", | |
" size = trans['units']\n", | |
" if trans['side'] == 'sell':\n", | |
" size = -size\n", | |
" price = trans['price']\n", | |
" self.broker._fill(oref, size, price, ttype=ttype)\n", | |
"\n", | |
" elif ttype in self._X_ORDER_CREATE:\n", | |
" self.broker._accept(oref)\n", | |
" self._ordersrev[oid] = oref\n", | |
"\n", | |
" elif ttype in 'ORDER_CANCEL':\n", | |
" reason = trans['reason']\n", | |
" if reason == 'ORDER_FILLED':\n", | |
" pass # individual execs have done the job\n", | |
" elif reason == 'TIME_IN_FORCE_EXPIRED':\n", | |
" self.broker._expire(oref)\n", | |
" elif reason == 'CLIENT_REQUEST':\n", | |
" self.broker._cancel(oref)\n", | |
" else: # default action ... if nothing else\n", | |
" self.broker._reject(oref)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": null, | |
"metadata": {}, | |
"outputs": [], | |
"source": [] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.8.6" | |
}, | |
"varInspector": { | |
"cols": { | |
"lenName": 16, | |
"lenType": 16, | |
"lenVar": 40 | |
}, | |
"kernels_config": { | |
"python": { | |
"delete_cmd_postfix": "", | |
"delete_cmd_prefix": "del ", | |
"library": "var_list.py", | |
"varRefreshCmd": "print(var_dic_list())" | |
}, | |
"r": { | |
"delete_cmd_postfix": ") ", | |
"delete_cmd_prefix": "rm(", | |
"library": "var_list.r", | |
"varRefreshCmd": "cat(var_dic_list()) " | |
} | |
}, | |
"types_to_exclude": [ | |
"module", | |
"function", | |
"builtin_function_or_method", | |
"instance", | |
"_Feature" | |
], | |
"window_display": false | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 4 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# coding: utf-8 | |
# ## Utilities | |
# In[1]: | |
def is_in_jupyter() -> bool: | |
''' | |
Determine wheather is the environment Jupyter Notebook | |
https://blog.amedama.jp/entry/detect-jupyter-env | |
''' | |
if 'get_ipython' not in globals(): | |
# Python shell | |
return False | |
env_name = get_ipython().__class__.__name__ | |
if env_name == 'TerminalInteractiveShell': | |
# IPython shell | |
return False | |
# Jupyter Notebook | |
return True | |
print(is_in_jupyter()) | |
# In[3]: | |
# https://recruit-tech.co.jp/blog/2018/10/16/jupyter_notebook_tips/ | |
if is_in_jupyter(): | |
def set_stylesheet(): | |
from IPython.display import display, HTML | |
css = get_ipython().getoutput('wget https://raw.githubusercontent.com/lapis-zero09/jupyter_notebook_tips/master/css/jupyter_notebook/monokai.css -q -O -') | |
css = "\n".join(css) | |
display(HTML('<style type="text/css">%s</style>'%css)) | |
set_stylesheet() | |
# ## Main | |
# In[ ]: | |
#!/usr/bin/env python | |
# -*- coding: utf-8; py-indent-offset:4 -*- | |
############################################################################### | |
# | |
# Copyright (C) 2015-2020 Daniel Rodriguez | |
# Copyright (C) 2021 dogwood008 (modified) | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
# | |
############################################################################### | |
from __future__ import (absolute_import, division, print_function, | |
unicode_literals) | |
import collections | |
from datetime import datetime, timedelta | |
import time as _time | |
import json | |
import threading | |
# import oandapy | |
# import requests # oandapy depdendency | |
import backtrader as bt | |
from backtrader.metabase import MetaParams | |
from backtrader.utils.py3 import queue, with_metaclass | |
from backtrader.utils import AutoDict | |
import kabusapi | |
# In[ ]: | |
# Extend the exceptions to support extra cases | |
# ''' | |
# class OandaRequestError(oandapy.OandaError): | |
# def __init__(self): | |
# er = dict(code=599, message='Request Error', description='') | |
# super(self.__class__, self).__init__(er) | |
# | |
# | |
# class OandaStreamError(oandapy.OandaError): | |
# def __init__(self, content=''): | |
# er = dict(code=598, message='Failed Streaming', description=content) | |
# super(self.__class__, self).__init__(er) | |
# | |
# | |
# class OandaTimeFrameError(oandapy.OandaError): | |
# def __init__(self, content): | |
# er = dict(code=597, message='Not supported TimeFrame', description='') | |
# super(self.__class__, self).__init__(er) | |
# | |
# | |
# class OandaNetworkError(oandapy.OandaError): | |
# def __init__(self): | |
# er = dict(code=596, message='Network Error', description='') | |
# super(self.__class__, self).__init__(er) | |
# ''' | |
# class API(oandapy.API): | |
# def request(self, endpoint, method='GET', params=None): | |
# # Overriden to make something sensible out of a | |
# # request.RequestException rather than simply issuing a print(str(e)) | |
# url = '%s/%s' % (self.api_url, endpoint) | |
# | |
# method = method.lower() | |
# params = params or {} | |
# | |
# func = getattr(self.client, method) | |
# | |
# request_args = {} | |
# if method == 'get': | |
# request_args['params'] = params | |
# else: | |
# request_args['data'] = params | |
# | |
# # Added the try block | |
# try: | |
# response = func(url, **request_args) | |
# except requests.RequestException as e: | |
# return OandaRequestError().error_response | |
# | |
# content = response.content.decode('utf-8') | |
# content = json.loads(content) | |
# | |
# # error message | |
# if response.status_code >= 400: | |
# # changed from raise to return | |
# return oandapy.OandaError(content).error_response | |
# | |
# return content | |
# In[ ]: | |
#FIXME | |
# class Streamer(oandapy.Streamer): | |
# def __init__(self, q, headers=None, *args, **kwargs): | |
# # Override to provide headers, which is in the standard API interface | |
# super(Streamer, self).__init__(*args, **kwargs) | |
# | |
# if headers: | |
# self.client.headers.update(headers) | |
# | |
# self.q = q | |
# | |
# def run(self, endpoint, params=None): | |
# # Override to better manage exceptions. | |
# # Kept as much as possible close to the original | |
# self.connected = True | |
# | |
# params = params or {} | |
# | |
# ignore_heartbeat = None | |
# if 'ignore_heartbeat' in params: | |
# ignore_heartbeat = params['ignore_heartbeat'] | |
# | |
# request_args = {} | |
# request_args['params'] = params | |
# | |
# url = '%s/%s' % (self.api_url, endpoint) | |
# | |
# while self.connected: | |
# # Added exception control here | |
# try: | |
# response = self.client.get(url, **request_args) | |
# except requests.RequestException as e: | |
# self.q.put(OandaRequestError().error_response) | |
# break | |
# | |
# if response.status_code != 200: | |
# self.on_error(response.content) | |
# break # added break here | |
# | |
# # Changed chunk_size 90 -> None | |
# try: | |
# for line in response.iter_lines(chunk_size=None): | |
# if not self.connected: | |
# break | |
# | |
# if line: | |
# data = json.loads(line.decode('utf-8')) | |
# if not (ignore_heartbeat and 'heartbeat' in data): | |
# self.on_success(data) | |
# | |
# except: # socket.error has been seen | |
# self.q.put(OandaStreamError().error_response) | |
# break | |
# | |
# def on_success(self, data): | |
# if 'tick' in data: | |
# self.q.put(data['tick']) | |
# elif 'transaction' in data: | |
# self.q.put(data['transaction']) | |
# | |
# def on_error(self, data): | |
# self.disconnect() | |
# self.q.put(OandaStreamError(data).error_response) | |
# In[ ]: | |
from enum import Enum | |
class KabusAPIEnv(Enum): | |
DEV = 'dev' | |
PROD = 'prod' | |
# In[ ]: | |
class MetaSingleton(MetaParams): | |
'''Metaclass to make a metaclassed class a singleton''' | |
def __init__(cls, name, bases, dct): | |
super(MetaSingleton, cls).__init__(name, bases, dct) | |
cls._singleton = None | |
def __call__(cls, *args, **kwargs): | |
if cls._singleton is None: | |
cls._singleton = ( | |
super(MetaSingleton, cls).__call__(*args, **kwargs)) | |
return cls._singleton | |
class KabuSAPIStore(with_metaclass(MetaSingleton, object)): | |
'''Singleton class wrapping to control the connections to Kabu STATION API. | |
Params: | |
- ``token`` (default:``None``): API access token | |
- ``account`` (default: ``None``): account id | |
- ``practice`` (default: ``False``): use the test environment | |
- ``account_tmout`` (default: ``10.0``): refresh period for account | |
value/cash refresh | |
''' | |
BrokerCls = None # broker class will autoregister | |
DataCls = None # data class will auto register | |
params = ( | |
('url', 'localhost'), | |
('env', KabuSAPIEnv.DEV), | |
('port', None), | |
('password', None), | |
) | |
# _DTEPOCH = datetime(1970, 1, 1) | |
# _ENVPRACTICE = 'practice' | |
# _ENVLIVE = 'live' | |
@classmethod | |
def getdata(cls, *args, **kwargs): | |
'''Returns ``DataCls`` with args, kwargs''' | |
return cls.DataCls(*args, **kwargs) | |
@classmethod | |
def getbroker(cls, *args, **kwargs): | |
'''Returns broker with *args, **kwargs from registered ``BrokerCls``''' | |
return cls.BrokerCls(*args, **kwargs) | |
def __init__(self): | |
def _getport() -> int: | |
if self.p.port: | |
return port | |
return 18081 if self.p.env == KabuSAPIEnv.DEV else 18080 | |
def _init_kabusapi_client(self) -> kabusapiapi.Context: | |
url = self.p.url | |
port = self.p.get('port', _getport()) | |
password = self.p.password | |
token = kabusapi.Context(url, port, password).token | |
self.kapi = kabusapi.Context(url, port, token=token) | |
super(KabuSAPIStore, self).__init__() | |
self.notifs = collections.deque() # store notifications for cerebro | |
self._env = None # reference to cerebro for general notifications | |
self.broker = None # broker instance | |
self.datas = list() # datas that have registered over start | |
self._orders = collections.OrderedDict() # map order.ref to oid | |
self._ordersrev = collections.OrderedDict() # map oid to order.ref | |
self._transpend = collections.defaultdict(collections.deque) | |
_init_kabusapi_client() | |
self._cash = 0.0 | |
self._value = 0.0 | |
self._evt_acct = threading.Event() | |
def start(self, data=None, broker=None): | |
# Datas require some processing to kickstart data reception | |
if data is None and broker is None: | |
self.cash = None | |
return | |
if data is not None: | |
self._env = data._env | |
# For datas simulate a queue with None to kickstart co | |
self.datas.append(data) | |
if self.broker is not None: | |
self.broker.data_started(data) | |
elif broker is not None: | |
self.broker = broker | |
self.streaming_events() | |
self.broker_threads() | |
def stop(self): | |
# signal end of thread | |
if self.broker is not None: | |
self.q_ordercreate.put(None) | |
self.q_orderclose.put(None) | |
self.q_account.put(None) | |
def put_notification(self, msg, *args, **kwargs): | |
self.notifs.append((msg, args, kwargs)) | |
def get_notifications(self): | |
'''Return the pending "store" notifications''' | |
self.notifs.append(None) # put a mark / threads could still append | |
return [x for x in iter(self.notifs.popleft, None)] | |
# Oanda supported granularities | |
# _GRANULARITIES = { | |
# (bt.TimeFrame.Seconds, 5): 'S5', | |
# (bt.TimeFrame.Seconds, 10): 'S10', | |
# (bt.TimeFrame.Seconds, 15): 'S15', | |
# (bt.TimeFrame.Seconds, 30): 'S30', | |
# (bt.TimeFrame.Minutes, 1): 'M1', | |
# (bt.TimeFrame.Minutes, 2): 'M3', | |
# (bt.TimeFrame.Minutes, 3): 'M3', | |
# (bt.TimeFrame.Minutes, 4): 'M4', | |
# (bt.TimeFrame.Minutes, 5): 'M5', | |
# (bt.TimeFrame.Minutes, 10): 'M5', | |
# (bt.TimeFrame.Minutes, 15): 'M5', | |
# (bt.TimeFrame.Minutes, 30): 'M5', | |
# (bt.TimeFrame.Minutes, 60): 'H1', | |
# (bt.TimeFrame.Minutes, 120): 'H2', | |
# (bt.TimeFrame.Minutes, 180): 'H3', | |
# (bt.TimeFrame.Minutes, 240): 'H4', | |
# (bt.TimeFrame.Minutes, 360): 'H6', | |
# (bt.TimeFrame.Minutes, 480): 'H8', | |
# (bt.TimeFrame.Days, 1): 'D', | |
# (bt.TimeFrame.Weeks, 1): 'W', | |
# (bt.TimeFrame.Months, 1): 'M', | |
# } | |
def get_positions(self): | |
try: | |
positions = self.oapi.get_positions(self.p.account) | |
except (oandapy.OandaError, OandaRequestError,): | |
return None | |
poslist = positions.get('positions', []) | |
return poslist | |
#def get_granularity(self, timeframe, compression): | |
# return self._GRANULARITIES.get((timeframe, compression), None) | |
def get_instrument(self, dataname): | |
try: | |
insts = self.oapi.get_instruments(self.p.account, | |
instruments=dataname) | |
except (oandapy.OandaError, OandaRequestError,): | |
return None | |
i = insts.get('instruments', [{}]) | |
return i[0] or None | |
def streaming_events(self, tmout=None): | |
q = queue.Queue() | |
kwargs = {'q': q, 'tmout': tmout} | |
t = threading.Thread(target=self._t_streaming_listener, kwargs=kwargs) | |
t.daemon = True | |
t.start() | |
t = threading.Thread(target=self._t_streaming_events, kwargs=kwargs) # FIXME: _t_streaming_events | |
t.daemon = True | |
t.start() | |
return q | |
def _t_streaming_listener(self, q, tmout=None): | |
while True: | |
trans = q.get() | |
self._transaction(trans) | |
# FIXME: Streamer | |
def _t_streaming_events(self, q, tmout=None): | |
if tmout is not None: | |
_time.sleep(tmout) | |
# FIXME: oandapy.Streamer | |
# streamer = Streamer(q, | |
# environment=self._oenv, | |
# access_token=self.p.token, | |
# headers={'X-Accept-Datetime-Format': 'UNIX'}) | |
# | |
# streamer.events(ignore_heartbeat=False) | |
def candles(self, dataname, dtbegin, dtend, timeframe, compression, | |
candleFormat, includeFirst): | |
kwargs = locals().copy() | |
kwargs.pop('self') | |
kwargs['q'] = q = queue.Queue() | |
t = threading.Thread(target=self._t_candles, kwargs=kwargs) # FIXME: _t_candles | |
t.daemon = True | |
t.start() | |
return q | |
def _t_candles(self, dataname, dtbegin, dtend, timeframe, compression, | |
candleFormat, includeFirst, q): | |
# FIXME: granularity = self.get_granularity(timeframe, compression) | |
if granularity is None: | |
e = OandaTimeFrameError() | |
q.put(e.error_response) | |
return | |
dtkwargs = {} | |
if dtbegin is not None: | |
dtkwargs['start'] = int((dtbegin - self._DTEPOCH).total_seconds()) # FIXME: _DTEPOCH | |
if dtend is not None: | |
dtkwargs['end'] = int((dtend - self._DTEPOCH).total_seconds()) # FIXME: _DTEPOCH | |
try: | |
# FIXME: granularity | |
# response = self.oapi.get_history(instrument=dataname, | |
# granularity=granularity, | |
# candleFormat=candleFormat, | |
# **dtkwargs) | |
except oandapy.OandaError as e: | |
q.put(e.error_response) | |
q.put(None) | |
return | |
# FIXME | |
for candle in response.get('candles', []): | |
q.put(candle) | |
q.put({}) # end of transmission | |
def streaming_prices(self, dataname, tmout=None): | |
q = queue.Queue() | |
kwargs = {'q': q, 'dataname': dataname, 'tmout': tmout} | |
t = threading.Thread(target=self._t_streaming_prices, kwargs=kwargs) # FIXME: _t_streaming_prices | |
t.daemon = True | |
t.start() | |
return q | |
# FIXME: Streamer | |
def _t_streaming_prices(self, dataname, q, tmout): | |
if tmout is not None: | |
_time.sleep(tmout) | |
# FIXME: Streamer | |
# FIXME streamer = Streamer(q, environment=self._oenv, | |
# FIXME access_token=self.p.token, | |
# FIXME headers={'X-Accept-Datetime-Format': 'UNIX'}) | |
# FIXME streamer.rates(self.p.account, instruments=dataname) | |
def get_cash(self): | |
return self._cash | |
def get_value(self): | |
return self._value | |
_ORDEREXECS = { | |
bt.Order.Market: 'market', | |
bt.Order.Limit: 'limit', | |
bt.Order.Stop: 'stop', | |
bt.Order.StopLimit: 'stop', | |
} | |
def broker_threads(self): | |
self.q_account = queue.Queue() | |
self.q_account.put(True) # force an immediate update | |
t = threading.Thread(target=self._t_account) | |
t.daemon = True | |
t.start() | |
self.q_ordercreate = queue.Queue() | |
t = threading.Thread(target=self._t_order_create) | |
t.daemon = True | |
t.start() | |
self.q_orderclose = queue.Queue() | |
t = threading.Thread(target=self._t_order_cancel) | |
t.daemon = True | |
t.start() | |
# Wait once for the values to be set | |
self._evt_acct.wait(self.p.account_tmout) | |
def _t_account(self): | |
while True: | |
try: | |
msg = self.q_account.get(timeout=self.p.account_tmout) | |
if msg is None: | |
break # end of thread | |
except queue.Empty: # tmout -> time to refresh | |
pass | |
try: | |
accinfo = self.oapi.get_account(self.p.account) | |
except Exception as e: | |
self.put_notification(e) | |
continue | |
try: | |
self._cash = accinfo['marginAvail'] | |
self._value = accinfo['balance'] | |
except KeyError: | |
pass | |
self._evt_acct.set() | |
def order_create(self, order, stopside=None, takeside=None, **kwargs): | |
okwargs = dict() | |
okwargs['instrument'] = order.data._dataname | |
okwargs['units'] = abs(order.created.size) | |
okwargs['side'] = 'buy' if order.isbuy() else 'sell' | |
okwargs['type'] = self._ORDEREXECS[order.exectype] | |
if order.exectype != bt.Order.Market: | |
okwargs['price'] = order.created.price | |
if order.valid is None: | |
# 1 year and datetime.max fail ... 1 month works | |
valid = datetime.utcnow() + timedelta(days=30) | |
else: | |
valid = order.data.num2date(order.valid) | |
# To timestamp with seconds precision | |
okwargs['expiry'] = int((valid - self._DTEPOCH).total_seconds()) # FIXME: _DTEPOCH | |
if order.exectype == bt.Order.StopLimit: | |
okwargs['lowerBound'] = order.created.pricelimit | |
okwargs['upperBound'] = order.created.pricelimit | |
if order.exectype == bt.Order.StopTrail: | |
okwargs['trailingStop'] = order.trailamount | |
if stopside is not None: | |
okwargs['stopLoss'] = stopside.price | |
if takeside is not None: | |
okwargs['takeProfit'] = takeside.price | |
okwargs.update(**kwargs) # anything from the user | |
self.q_ordercreate.put((order.ref, okwargs,)) | |
return order | |
_OIDSINGLE = ['orderOpened', 'tradeOpened', 'tradeReduced'] | |
_OIDMULTIPLE = ['tradesClosed'] | |
def _t_order_create(self): | |
while True: | |
msg = self.q_ordercreate.get() | |
if msg is None: | |
break | |
oref, okwargs = msg | |
try: | |
o = self.oapi.create_order(self.p.account, **okwargs) | |
except Exception as e: | |
self.put_notification(e) | |
self.broker._reject(oref) | |
return | |
# Ids are delivered in different fields and all must be fetched to | |
# match them (as executions) to the order generated here | |
oids = list() | |
for oidfield in self._OIDSINGLE: | |
if oidfield in o and 'id' in o[oidfield]: | |
oids.append(o[oidfield]['id']) | |
for oidfield in self._OIDMULTIPLE: | |
if oidfield in o: | |
for suboidfield in o[oidfield]: | |
oids.append(suboidfield['id']) | |
if not oids: | |
self.broker._reject(oref) | |
return | |
self._orders[oref] = oids[0] | |
self.broker._submit(oref) | |
if okwargs['type'] == 'market': | |
self.broker._accept(oref) # taken immediately | |
for oid in oids: | |
self._ordersrev[oid] = oref # maps ids to backtrader order | |
# An transaction may have happened and was stored | |
tpending = self._transpend[oid] | |
tpending.append(None) # eom marker | |
while True: | |
trans = tpending.popleft() | |
if trans is None: | |
break | |
self._process_transaction(oid, trans) | |
def order_cancel(self, order): | |
self.q_orderclose.put(order.ref) | |
return order | |
def _t_order_cancel(self): | |
while True: | |
oref = self.q_orderclose.get() | |
if oref is None: | |
break | |
oid = self._orders.get(oref, None) | |
if oid is None: | |
continue # the order is no longer there | |
try: | |
o = self.oapi.close_order(self.p.account, oid) | |
except Exception as e: | |
continue # not cancelled - FIXME: notify | |
self.broker._cancel(oref) | |
_X_ORDER_CREATE = ('STOP_ORDER_CREATE', | |
'LIMIT_ORDER_CREATE', 'MARKET_IF_TOUCHED_ORDER_CREATE',) | |
def _transaction(self, trans): | |
# Invoked from Streaming Events. May actually receive an event for an | |
# oid which has not yet been returned after creating an order. Hence | |
# store if not yet seen, else forward to processer | |
ttype = trans['type'] | |
if ttype == 'MARKET_ORDER_CREATE': | |
try: | |
oid = trans['tradeReduced']['id'] | |
except KeyError: | |
try: | |
oid = trans['tradeOpened']['id'] | |
except KeyError: | |
return # cannot do anything else | |
elif ttype in self._X_ORDER_CREATE: | |
oid = trans['id'] | |
elif ttype == 'ORDER_FILLED': | |
oid = trans['orderId'] | |
elif ttype == 'ORDER_CANCEL': | |
oid = trans['orderId'] | |
elif ttype == 'TRADE_CLOSE': | |
oid = trans['id'] | |
pid = trans['tradeId'] | |
if pid in self._orders and False: # Know nothing about trade | |
return # can do nothing | |
# Skip above - at the moment do nothing | |
# Received directly from an event in the WebGUI for example which | |
# closes an existing position related to order with id -> pid | |
# COULD BE DONE: Generate a fake counter order to gracefully | |
# close the existing position | |
msg = ('Received TRADE_CLOSE for unknown order, possibly generated' | |
' over a different client or GUI') | |
self.put_notification(msg, trans) | |
return | |
else: # Go aways gracefully | |
try: | |
oid = trans['id'] | |
except KeyError: | |
oid = 'None' | |
msg = 'Received {} with oid {}. Unknown situation' | |
msg = msg.format(ttype, oid) | |
self.put_notification(msg, trans) | |
return | |
try: | |
oref = self._ordersrev[oid] | |
self._process_transaction(oid, trans) | |
except KeyError: # not yet seen, keep as pending | |
self._transpend[oid].append(trans) | |
_X_ORDER_FILLED = ('MARKET_ORDER_CREATE', | |
'ORDER_FILLED', 'TAKE_PROFIT_FILLED', | |
'STOP_LOSS_FILLED', 'TRAILING_STOP_FILLED',) | |
def _process_transaction(self, oid, trans): | |
try: | |
oref = self._ordersrev.pop(oid) | |
except KeyError: | |
return | |
ttype = trans['type'] | |
if ttype in self._X_ORDER_FILLED: | |
size = trans['units'] | |
if trans['side'] == 'sell': | |
size = -size | |
price = trans['price'] | |
self.broker._fill(oref, size, price, ttype=ttype) | |
elif ttype in self._X_ORDER_CREATE: | |
self.broker._accept(oref) | |
self._ordersrev[oid] = oref | |
elif ttype in 'ORDER_CANCEL': | |
reason = trans['reason'] | |
if reason == 'ORDER_FILLED': | |
pass # individual execs have done the job | |
elif reason == 'TIME_IN_FORCE_EXPIRED': | |
self.broker._expire(oref) | |
elif reason == 'CLIENT_REQUEST': | |
self.broker._cancel(oref) | |
else: # default action ... if nothing else | |
self.broker._reject(oref) | |
# In[ ]: | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment