Skip to content

Instantly share code, notes, and snippets.

@dogwood008
Last active March 5, 2021 13:52
Show Gist options
  • Save dogwood008/d26627648b5a670a46f979557a6d1e9d to your computer and use it in GitHub Desktop.
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/
Display the source blob
Display the rendered blob
Raw
{
"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
}
#!/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