diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..024ca6a9 --- /dev/null +++ b/404.html @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/advanced/index.html b/advanced/index.html new file mode 100644 index 00000000..7ed94251 --- /dev/null +++ b/advanced/index.html @@ -0,0 +1,1133 @@ + + + + + + + + + + + + + + + + + + + + + + + + Advanced Usage - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + + +
+
+ + + + +

Advanced

+

Flumine

+

Functions:

+ +

The Flumine class can be adapted by overriding the following functions:

+
    +
  • _process_market_books() called on MarketBook event
  • +
  • _process_sports_data() called on SportsData event
  • +
  • _process_market_orders() called when market has pending orders
  • +
  • _process_order_package() called on new OrderPackage
  • +
  • _add_market() called when new Market received through streams
  • +
  • _remove_market() called when Market removed from framework
  • +
  • _process_raw_data() called on RawData event
  • +
  • _process_market_catalogues called on MarketCatalogue event
  • +
  • _process_current_orders called on currentOrders event
  • +
  • _process_custom_event called on CustomEvent event see here
  • +
  • _process_close_market called on Market closure
  • +
  • _process_cleared_orders() called on ClearedOrders event
  • +
  • _process_cleared_markets() called on ClearedMarkets event
  • +
  • _process_end_flumine() called on Flumine termination
  • +
+

Streams

+

Market Stream

+

Flumine handles market streams by taking the parameters provided in the strategies, a strategy will then subscribe to the stream. This means strategies can share streams reducing load or create new if they require different markets or data filter.

+

Data Stream

+

Similar to Market Streams but the raw streaming data is passed back, this reduces ram/CPU and allows recording of the data for future playback, see the example marketrecorder.py

+

Historical Stream

+

This is created on a per market basis when simulating.

+

Order Stream

+

Subscribes to all orders per running instance using the config.customer_strategy_ref

+

Custom Streams

+

Custom streams (aka threads) can be added as per:

+
from flumine.streams.basestream import BaseStream
+from flumine.events.events import CustomEvent
+
+
+class CustomStream(BaseStream):
+    def run(self) -> None:
+        # connect to stream / make API requests etc.
+        response = api_call()
+
+        # callback func
+        def callback(framework, event):
+            for strategy in framework.strategies:
+                strategy.process_my_event(event)
+
+        # push results through using custom event
+        event = CustomEvent(response, callback)
+
+        # put in main queue
+        self.flumine.handler_queue.put(event)
+
+
+custom_stream = CustomStream(framework, custom=True)
+framework.streams.add_custom_stream(custom_stream)
+
+ +

Error Handling

+

Flumine will catch all errors that occur in strategy.check_market and strategy.process_market_book, and log either error or critical errors.

+
+

Tip

+

You can remove this error handling by setting config.raise_errors = True

+
+

Logging

+

jsonlogger is used to log extra detail, see below for a typical setup:

+
import time
+import logging
+from pythonjsonlogger import jsonlogger
+
+logger = logging.getLogger()
+
+custom_format = "%(asctime) %(levelname) %(message)"
+log_handler = logging.StreamHandler()
+formatter = jsonlogger.JsonFormatter(custom_format)
+formatter.converter = time.gmtime
+log_handler.setFormatter(formatter)
+logger.addHandler(log_handler)
+logger.setLevel(logging.INFO)
+
+ +

Config

+

simulated

+

Updated to True when simulating or paper trading

+

simulated_strategy_isolation

+

Defaults to True to match orders per strategy, when False prevents double counting of passive liquidity on all orders regardless of strategy.

+

simulation_available_prices

+

When True will simulate matches against available prices after initial execution, note this will double count liquidity.

+

instance_id

+

Store server id or similar (e.g. AWS ec2 instanceId)

+

customer_strategy_ref

+

Used as customerStrategyRefs so that only orders created by the running instance are returned.

+

process_id

+

OS process id of running application.

+

current_time

+

Used for simulation

+

raise_errors

+

Raises errors on strategy functions, see Error Handling

+

max_execution_workers

+

Max number of workers in execution thread pool

+

async_place_orders

+

Place orders sent with place orders flag, prevents waiting for bet delay

+

place_latency

+

Place latency used for simulation / simulation execution

+

cancel_latency

+

Cancel latency used for simulation / simulation execution

+

update_latency

+

Update latency used for simulation / simulation execution

+

replace_latency

+

Replace latency used for simulation / simulation execution

+

order_sep

+

customer_order_ref separator

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/architecture/index.html b/architecture/index.html new file mode 100644 index 00000000..5b61a695 --- /dev/null +++ b/architecture/index.html @@ -0,0 +1,878 @@ + + + + + + + + + + + + + + + + + + + + + + Architecture - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Design

+

Main loop

+

Typical to most trading frameworks flumine uses an event driven design with the main thread handling these events through a FIFO queue.

+
    +
  • handles all events in order one by one
  • +
  • runs in main
  • +
+

UML Diagrams

+

Packages

+

Placeholder

+

Classes

+

Placeholder

+

Events:

+
    +
  • MARKET_CATALOGUE Betfair MarketCatalogue object
  • +
  • MARKET_BOOK Betfair MarketBook object
  • +
  • RAW_DATA Raw streaming data
  • +
  • CURRENT_ORDERS Betfair CurrentOrders object
  • +
  • CLEARED_MARKETS Betfair ClearedMarkets object
  • +
  • CLEARED_ORDERS Betfair ClearedOrders object
  • +
+
+
    +
  • CLOSE_MARKET flumine Close Market update
  • +
  • STRATEGY_RESET flumine Strategy Reset update
  • +
  • CUSTOM_EVENT flumine Custom event update
  • +
  • TERMINATOR flumine End instance update
  • +
+

The above events are handled in the flumine class

+

MarketBook Cycle

+

Simulation

+

Simulation is achieved by monkeypatching the datetime function utcnow(), this allows strategies to be simulated as if they were being executed in real time. Functions such as market.seconds_to_start and fillKill.seconds work as per a live execution.

+

Streams

+
    +
  • Single stream (market)
  • +
  • As above but 'data' (flumine listener)
  • +
  • Order stream
  • +
  • Future work:
      +
    • Custom stream
    • +
    +
  • +
+

Strategy

+
    +
  • Class based
  • +
  • Subscribe to streams
  • +
  • Single strategy subscribes to a single market stream
  • +
+

Handles

+
    +
  • Stream reconnect
  • +
  • Trading client login/logout
  • +
  • Trading client keep alive
  • +
  • Future work:
      +
    • Execution
        +
      • place/cancel/replace/update
      • +
      • controls
      • +
      • fillKill
      • +
      +
    • +
    • Market Catalogue
    • +
    • Polling (scores/raceCard etc)
    • +
    • CurrentOrders / ClearedOrders
    • +
    • database connection/logging
    • +
    +
  • +
+

notes

+
    +
  • market middleware (analytics/logging)
  • +
  • order middleware (controls)
  • +
  • paper trading
  • +
  • simulation
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 00000000..1cf13b9f Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.220ee61c.min.js b/assets/javascripts/bundle.220ee61c.min.js new file mode 100644 index 00000000..116072a1 --- /dev/null +++ b/assets/javascripts/bundle.220ee61c.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Y())}function Y(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Y(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Y=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Y=!1,B.search=h.toString(),Y=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Y&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Y=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Y(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Y(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var M=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Y=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Y(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Y();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Y(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Y(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?M:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function _(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=_("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():M))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>M),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=_("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return G([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=G([n,r]).pipe(l(()=>Xe(e)));return G([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Clients

+

Flumine is capable of using multiple clients, these can be of the same ExchangeType, a variation depending on use case or your own custom client/wrapper. The default workers handle login/keep-alive/logout and market closure for all clients added to the framework automatically.

+

ExchangeTypes

+
    +
  • BETFAIR: BetfairClient
  • +
  • SIMULATED: SimulatedClient
  • +
  • BETCONNECT: BetconnectClient
  • +
+

Strategy use

+

To add a client use the add_client this will allow use via framework.clients or strategy.clients

+
from flumine import Flumine, clients
+
+framework = Flumine()
+
+client = clients.BetfairClient(trading)
+framework.add_client(client)
+
+ +

or when simulating:

+
from flumine import FlumineSimulation, clients
+
+framework = FlumineSimulation()
+
+client = clients.SimulatedClient(username="123")
+framework.add_client(client)
+
+ +

To access clients within a strategy use the helper functions:

+
betfair_client = self.clients.get_betfair_default()
+
+client = self.clients.get_client(ExchangeType.SIMULATED, username="123")
+
+ +
+

Tip

+

get_default and get_betfair_default will use the first client added via add_client (ordered list)

+
+

By default a transaction will use clients.get_default() however you can use a particular client:

+
client = self.clients.get_client(ExchangeType.SIMULATED, username="123")
+
+market.place_order(order, client=client)
+
+ +

or using a transaction directly:

+
client = self.clients.get_client(ExchangeType.SIMULATED, username="123")
+
+with market.transaction(client=client) as t:
+    t.place_order(order)
+
+ +

Future Development

+
    +
  • BetConnect client #566
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/controls/index.html b/controls/index.html new file mode 100644 index 00000000..e45b3bba --- /dev/null +++ b/controls/index.html @@ -0,0 +1,754 @@ + + + + + + + + + + + + + + + + + + + + + + + + Controls - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Controls

+

Trading Controls

+

Before placing an order flumine will check the client and trading controls, this allows validation to occur before execution. If an order does not meet any of these validations it is not executed and status is updated to Violation.

+

Client Controls

+
    +
  • MaxTransactionCount: Checks transaction count is not over betfair transaction limit (5000 per hour)
  • +
+

Trading Controls

+
    +
  • OrderValidation: Checks order is valid (size/odds)
  • +
  • StrategyExposure: Checks order does not invalidate strategy.validate_order, strategy.max_order_exposure or strategy.max_selection_exposure
  • +
+

Skipping Controls

+

Sometimes it is desirable to skip the controls, for example when canceling an open order even if the transaction count has already reached the betfair transaction limit. This can be done by passing force=True when placing or changing an order:

+
market.place_order(order, force=True)
+transaction.place_order(order, force=True)
+
+ +

This works for markets and transactions and is supported by the operations place_order, cancel_order, update_order, and replace_order.

+

Logging Controls

+

Custom logging is available using the LoggingControl class, the base class creates debug logs and can be used as follows:

+
from flumine.controls.loggingcontrols import LoggingControl
+
+control = LoggingControl()
+
+framework.add_logging_control(control)
+
+ +
+

Tip

+

More than one control can be added, for example a csv logger and db logger.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/help/index.html b/help/index.html new file mode 100644 index 00000000..9b39cab4 --- /dev/null +++ b/help/index.html @@ -0,0 +1,615 @@ + + + + + + + + + + + + + + + + + + + + + + + + Help - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/images/jupyterloggingcontrol-screenshot.png b/images/jupyterloggingcontrol-screenshot.png new file mode 100644 index 00000000..29f3960c Binary files /dev/null and b/images/jupyterloggingcontrol-screenshot.png differ diff --git a/images/logo-full.png b/images/logo-full.png new file mode 100644 index 00000000..9dae2e30 Binary files /dev/null and b/images/logo-full.png differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 00000000..a390f736 Binary files /dev/null and b/images/logo.png differ diff --git a/images/uml_classes.png b/images/uml_classes.png new file mode 100644 index 00000000..4e8e7713 Binary files /dev/null and b/images/uml_classes.png differ diff --git a/images/uml_packages.png b/images/uml_packages.png new file mode 100644 index 00000000..26d9059f Binary files /dev/null and b/images/uml_packages.png differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..86a67612 --- /dev/null +++ b/index.html @@ -0,0 +1,816 @@ + + + + + + + + + + + + + + + + + + + + + + flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

+flūmine +

+ +
+
+

+ + Build Status + + + Coverage + + + Package version + +

+
+ +

Betfair trading framework with a focus on:

+
    +
  • simplicity
  • +
  • modular
  • +
  • pythonic
  • +
  • rock-solid
  • +
  • safe
  • +
+

Support for market, order and custom streaming data.

+

join slack group

+

Tested on Python 3.7, 3.8, 3.9, 3.10 and 3.11.

+

installation

+
$ pip install flumine
+
+ +

flumine requires Python 3.7+

+

setup

+

Get started...

+
import betfairlightweight
+from flumine import Flumine, clients
+
+trading = betfairlightweight.APIClient("username")
+client = clients.BetfairClient(trading)
+
+framework = Flumine(
+    client=client,
+)
+
+ +

Example strategy:

+
from flumine import BaseStrategy
+from flumine.order.trade import Trade
+from flumine.order.order import LimitOrder, OrderStatus
+from flumine.markets.market import Market
+from betfairlightweight.filters import streaming_market_filter
+from betfairlightweight.resources import MarketBook
+
+
+class ExampleStrategy(BaseStrategy):
+    def start(self) -> None:
+        print("starting strategy 'ExampleStrategy'")
+
+    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:
+        # process_market_book only executed if this returns True
+        if market_book.status != "CLOSED":
+            return True
+
+    def process_market_book(self, market: Market, market_book: MarketBook) -> None:
+        # process marketBook object
+        for runner in market_book.runners:
+            if runner.status == "ACTIVE" and runner.last_price_traded < 1.5:
+                trade = Trade(
+                    market_id=market_book.market_id,
+                    selection_id=runner.selection_id,
+                    handicap=runner.handicap,
+                    strategy=self,
+                )
+                order = trade.create_order(
+                    side="LAY", order_type=LimitOrder(price=1.01, size=2.00)
+                )
+                market.place_order(order)
+
+    def process_orders(self, market: Market, orders: list) -> None:
+        for order in orders:
+            if order.status == OrderStatus.EXECUTABLE:
+                if order.size_remaining == 2.00:
+                    market.cancel_order(order, 0.02)  # reduce size to 1.98
+                if order.order_type.persistence_type == "LAPSE":
+                    market.update_order(order, "PERSIST")
+                if order.size_remaining > 0:
+                    market.replace_order(order, 1.02)  # move
+
+
+strategy = ExampleStrategy(
+    market_filter=streaming_market_filter(
+        event_type_ids=["7"],
+        country_codes=["GB"],
+        market_types=["WIN"],
+    )
+)
+
+framework.add_strategy(strategy)
+
+ +

Run framework:

+
framework.run()
+
+ +
+

Danger

+

By default flumine will try to prevent coding errors which result in flash crashes and burnt fingers but use at your own risk as per the MIT license.

+

Recommendation is not to remove the trading controls and carry out extensive testing before executing on live markets, even then only use new strategies on an account with a small balance (transfer balance to games wallet).

+
+

Features

+
    +
  • Streaming
  • +
  • Multiple strategies
  • +
  • Order execution
  • +
  • Paper trading
  • +
  • Simulation
  • +
  • Event simulation (multi market)
  • +
  • Middleware and background workers to enable Scores / RaceCard / InPlayService
  • +
+

Dependencies

+

flumine relies on these libraries:

+
    +
  • betfairlightweight - Betfair API support
  • +
  • tenacity - Used for connection retrying (streaming)
  • +
  • python-json-logger - JSON logging
  • +
  • requests - HTTP support
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/markets/index.html b/markets/index.html new file mode 100644 index 00000000..4f5ffa74 --- /dev/null +++ b/markets/index.html @@ -0,0 +1,874 @@ + + + + + + + + + + + + + + + + + + + + + + + + Markets - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Markets

+

Market

+

Within markets you have market objects which contains current up to date market data.

+

Class variables

+
    +
  • flumine Framework
  • +
  • market_id MarketBook id
  • +
  • closed Closed bool
  • +
  • date_time_closed Closed datetime
  • +
  • market_book Latest MarketBook object
  • +
  • market_catalogue Latest MarketCatalogue object
  • +
  • context Market context, store market specific context e.g. simulated data store
  • +
  • blotter Holds all order data and position
  • +
+

Functions

+
    +
  • place_order(order) Place new order from order object
  • +
  • cancel_order(order, size_reduction) Cancel order
  • +
  • update_order(order, new_persistance_type) Update order
  • +
  • replace_order(order, new_price) Replace order
  • +
+

Properties

+
    +
  • event Dictionary containing all event related markets (assumes markets have been subscribed)
  • +
  • event_type_id Betfair event type id (horse racing: 7)
  • +
  • event_id Market event id (12345)
  • +
  • market_type Market type ('WIN')
  • +
  • seconds_to_start Seconds to scheduled market start time (123.45)
  • +
  • elapsed_seconds_closed Seconds since market was closed (543.21)
  • +
  • market_start_datetime Market scheduled start time
  • +
+

Transaction

+

The transaction class is used by default when orders are executed, however it is possible to control the execution behaviour using the transaction class like so:

+
with market.transaction() as t:
+    market.place_order(order)  # executed immediately in separate transaction
+    t.place_order(order)  # executed on transaction __exit__
+
+with market.transaction() as t:
+    t.place_order(order)
+    ..
+    t.execute()  # above order executed
+    ..
+    t.cancel_order(order)
+    t.place_order(order)  # both executed on transaction __exit__
+
+ +

Blotter

+

The blotter is a simple and fast class to hold all orders for a particular market.

+

Functions

+
    +
  • strategy_orders(strategy) Returns all orders related to a strategy
  • +
  • strategy_selection_orders(strategy, selection_id, handicap) Returns all orders related to a strategy selection
  • +
  • selection_exposure(strategy, lookup) Returns strategy/selection exposure
  • +
  • market_exposure(strategy, market_book) Returns strategy/market exposure
  • +
+

Properties

+
    +
  • live_orders List of live orders
  • +
  • has_live_orders Bool on live orders
  • +
+

Middleware

+

It is common that you want to carry about analysis on a market before passing through to strategies, similar to Django's middleware design flumine allows middleware to be executed.

+

For example simulation uses simulated middleware in order to calculate order matching.

+
+

Note

+

Middleware will be executed in the order it is added and before the strategies are processed.

+
+

Please see below for the example middleware class if you wish to create your own:

+
from flumine.markets.middleware import Middleware
+
+class CustomMiddleware(Middleware):
+    def __call__(self, market) -> None:
+        pass  # called on each MarketBook update
+
+    def add_market(self, market) -> None:
+        print("market {0} added".format(market.market_id))
+
+    def remove_market(self, market) -> None:
+        print("market {0} removed".format(market.market_id))
+
+ +

The above middleware can then be added to the framework:

+
framework.add_logging_control(CustomMiddleware())
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/performance/index.html b/performance/index.html new file mode 100644 index 00000000..1194656b --- /dev/null +++ b/performance/index.html @@ -0,0 +1,955 @@ + + + + + + + + + + + + + + + + + + + + + + + + Performance - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Performance

+

Flumine is heavily optimised out of the box to be as quick as possible however there are various ways to improve the performance further with minimal effort.

+

Simulation

+

Listener Kwargs

+

This is one of the most powerful options available as the variables are passed down to betfairlightweight limiting the number of updates to process, examples:

+

600s before scheduled start and no inplay

+
strategy = ExampleStrategy(
+    market_filter={
+        "markets": ["/tmp/marketdata/1.170212754"],
+        "listener_kwargs": {"seconds_to_start": 600, "inplay": False},
+    }
+)
+
+ +

inplay only

+
strategy = ExampleStrategy(
+    market_filter={
+        "markets": ["/tmp/marketdata/1.170212754"],
+        "listener_kwargs": {"inplay": True},
+    }
+)
+
+ +

Logging

+

Logging in python can add a lot of function calls, it is therefore recommended to switch it off once you are comfortable with the outputs from a strategy:

+
logger.setLevel(logging.CRITICAL)
+
+ +

File location

+

This might sound obvious but having the market files stored locally on your machine will allow much quicker processing. A common pattern is to use s3 to store all market files but a local cache for common markets processed.

+

smart_open is a commonly used package for processing gz/s3 files:

+
with patch("builtins.open", smart_open.open):
+    framework.add_strategy(strategy)
+    framework.run()
+
+ +
+

Tip

+

Note that add_strategy needs to be in the patch as well.

+
+

Betfair Historical Data

+

Sometimes a download from the betfair site will include market and event files in the same directory resulting in duplicate processing, flumine will log a warning on this but it is worth checking if you are seeing slow processing times.

+

Multiprocessing

+

Simulation is CPU bound so can therefore be improved through the use of multiprocessing, threading offers no improvement due to the limitations of the GIL.

+

The multiprocessing example code below will:

+
    +
  • run a process per core
  • +
  • each run_process will process 8 markets at a time (prevents memory leaks)
  • +
  • will wait for all results before completing
  • +
+
import os
+import math
+import smart_open
+from concurrent import futures
+from unittest.mock import patch as mock_patch
+from flumine import FlumineSimulation, clients, utils
+from strategies.lowestlayer import LowestLayer
+
+
+def run_process(markets):
+    client = clients.SimulatedClient()
+    framework = FlumineSimulation(client=client)
+    strategy = LowestLayer(
+        market_filter={"markets": markets},
+        context={"stake": 2},
+    )
+    with mock_patch("builtins.open", smart_open.open):
+        framework.add_strategy(strategy)
+        framework.run()
+
+
+if __name__ == "__main__":
+    all_markets = [...]
+    processes = os.cpu_count()
+    markets_per_process = 8  # optimal
+
+    _process_jobs = []
+    with futures.ProcessPoolExecutor(max_workers=processes) as p:
+        chunk = min(
+            markets_per_process, math.ceil(len(all_markets) / processes)
+        )
+        for m in (utils.chunks(all_markets, chunk)):
+            _process_jobs.append(
+                p.submit(
+                    run_process,
+                    markets=m,
+                )
+            )
+        for job in futures.as_completed(_process_jobs):
+            job.result()  # wait for result
+
+ +
+

Tip

+

If the code above is failing add logging to the run_process function to find the error or run the strategy in a single process with logging

+
+

Strategy

+

The heaviest load on CPU comes from reading the files and processing into py objects before processing through flumine, after this the bottleneck becomes the number of orders that need to be processed. Therefore anything that can be done to limit the number of redundant or control blocked orders will see an improvement.

+

cprofile

+

Profiling code is always the best option for finding improvements, cprofilev is a commonly used python library for this:

+
python -m cprofilev examples/simulate.py
+
+ +

Middleware

+

If you don't need the simulation middleware remove it from framework._market_middleware, this is useful when processing markets for data collection. This can dramatically improve processing time due to the heavy functions contained in the simulation logic.

+

Libraries

+

Installing betfairlightweight[speed] will have a big impact on processing speed due to the inclusion of C and Rust libraries for datetime and json decoding.

+

Live

+

For improving live trading 'Strategy' and 'cprofile' tips above will help although CPU load tends to be considerably lower compared to simulating.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/quickstart/index.html b/quickstart/index.html new file mode 100644 index 00000000..fdeeb9e8 --- /dev/null +++ b/quickstart/index.html @@ -0,0 +1,1004 @@ + + + + + + + + + + + + + + + + + + + + + + + + QuickStart - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

QuickStart

+

Live

+
+

Tip

+

flumine uses betfairlightweight for communicating with the Betfair API, please see docs for how to use/setup before proceeding.

+
+

First, start by importing flumine/bflw and creating a trading and framework client:

+
import betfairlightweight
+from flumine import Flumine, clients
+
+trading = betfairlightweight.APIClient("username")
+client = clients.BetfairClient(trading)
+
+framework = Flumine(client=client)
+
+ +
+

Note

+

flumine will handle login, logout and keep alive whilst the framework is running using the keep_alive worker.

+
+

A strategy can now be created by using the BaseStrategy class:

+
from flumine import BaseStrategy
+
+
+class ExampleStrategy(BaseStrategy):
+    def start(self):
+        # subscribe to streams
+        print("starting strategy 'ExampleStrategy'")
+
+    def check_market_book(self, market, market_book):
+        # process_market_book only executed if this returns True
+        if market_book.status != "CLOSED":
+            return True
+
+    def process_market_book(self, market, market_book):
+        # process marketBook object
+        print(market_book.status)
+
+ +

This strategy can now be initiated with the market and data filter before being added to the framework:

+
from betfairlightweight.filters import (
+    streaming_market_filter, 
+    streaming_market_data_filter,
+)
+
+strategy = ExampleStrategy(
+    market_filter=streaming_market_filter(
+        event_type_ids=["7"],
+        country_codes=["GB"],
+        market_types=["WIN"],
+    ),
+    market_data_filter=streaming_market_data_filter(fields=["EX_ALL_OFFERS"])
+)
+
+framework.add_strategy(strategy)
+
+ +

The framework can now be started:

+
framework.run()
+
+ +

Order placement

+

Orders can be placed as followed:

+
from flumine.order.trade import Trade
+from flumine.order.order import LimitOrder
+
+
+class ExampleStrategy(BaseStrategy):
+    def process_market_book(self, market, market_book):
+        for runner in market_book.runners:
+            if runner.selection_id == 123:
+                trade = Trade(
+                    market_id=market_book.market_id, 
+                    selection_id=runner.selection_id,
+                    handicap=runner.handicap,
+                    strategy=self
+                )
+                order = trade.create_order(
+                    side="LAY", 
+                    order_type=LimitOrder(price=1.01, size=2.00)
+                )
+                market.place_order(order)
+
+ +

This order will be validated through controls, stored in the blotter and sent straight to the execution thread pool for execution. It is also possible to batch orders into transactions as follows:

+
with market.transaction() as t:
+    market.place_order(order)  # executed immediately in separate transaction
+    t.place_order(order)  # executed on transaction __exit__
+
+with market.transaction() as t:
+    t.place_order(order)
+
+    t.execute()  # above order executed
+
+    t.cancel_order(order)
+    t.place_order(order)  # both executed on transaction __exit__
+
+ +

Stream class

+

By default the stream class will be a MarketStream which provides a MarketBook python object, if collecting data this can be changed to a DataStream class however process_raw_data will be called and not process_market_book:

+
from flumine import BaseStrategy
+from flumine.streams.datastream import DataStream
+
+
+class ExampleDataStrategy(BaseStrategy):
+    def process_raw_data(self, publish_time, data):
+        print(publish_time, data)
+
+strategy = ExampleDataStrategy(
+    market_filter=streaming_market_filter(
+        event_type_ids=["7"],
+        country_codes=["GB"],
+        market_types=["WIN"],
+    ),
+    stream_class=DataStream
+)
+
+flumine.add_strategy(strategy)
+
+ +

The OrderDataStream class can be used to record order data as per market:

+
from flumine.streams.datastream import OrderDataStream
+
+strategy = ExampleDataStrategy(
+    market_filter=None,
+    stream_class=OrderDataStream
+)
+
+flumine.add_strategy(strategy)
+
+ +

Paper Trading

+

Flumine can be used to paper trade strategies live using the following code:

+
from flumine import clients
+
+client = clients.BetfairClient(trading, paper_trade=True)
+
+ +

Market data will be recieved as per live but any orders will use Simulated execution and Simulated order polling to replicate live trading.

+
+

Tip

+

This can be handy when testing strategies as the betfair website can be used to validate the market.

+
+

Simulation

+

Flumine can be used to simulate strategies using the following code:

+
from flumine import FlumineSimulation, clients
+
+client = clients.SimulatedClient()
+framework = FlumineSimulation(client=client)
+
+strategy = ExampleStrategy(
+    market_filter={"markets": ["/tmp/marketdata/1.170212754"]}
+)
+framework.add_strategy(strategy)
+
+framework.run()
+
+ +

Note the use of market filter to pass the file directories.

+

Listener kwargs

+

Sometimes a subset of the market lifetime is required, this can be optimised by limiting the number of updates to process resulting in faster simulation:

+
strategy = ExampleStrategy(
+    market_filter={
+        "markets": ["/tmp/marketdata/1.170212754"],
+        "listener_kwargs": {"inplay": False, "seconds_to_start": 600},
+    }
+)
+
+ +
    +
  • inplay: Filter inplay flag
  • +
  • seconds_to_start: Filter market seconds to start
  • +
  • calculate_market_tv: As per bflw listener arg
  • +
  • cumulative_runner_tv: As per bflw listener arg
  • +
+

The extra kwargs above will limit processing to preplay in the final 10 minutes.

+
+

Tip

+

Multiple strategies and markets can be passed, flumine will pass the MarketBooks to the correct strategy via its subscription.

+
+

Event Processing

+

It is also possible to process events with multiple markets such as win/place in racing or all football markets as per live by adding the following flag:

+
strategy = ExampleStrategy(
+    market_filter={"markets": [..], "event_processing": True}
+)
+
+ +

The Market object contains a helper method for accessing other event linked markets:

+
place_market = market.event["PLACE"]
+
+ +

Market Filter

+

When simulating you can filter markets to be processed by using the market_type and country_code filter as per live:

+
strategy = ExampleStrategy(
+    market_filter={"markets": [..], "market_types": ["MATCH_ODDS"], "country_codes": ["GB"]}
+)
+
+ +

Simulation

+

Simulation uses the SimulatedExecution execution class and tries to accurately simulate matching with the following:

+
    +
  • Place/Cancel/Replace latency delay added
  • +
  • BetDelay added based on market state
  • +
  • Queue positioning based on liquidity available
  • +
  • Order lapse on market version change
  • +
  • Order lapse and reduction on runner removal
  • +
  • BSP
  • +
+

Limitations #192:

+
    +
  • Queue cancellations
  • +
  • Double counting of liquidity (active)
  • +
  • Currency fluctuations
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 00000000..62ef1060 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Introduction","text":"fl\u016bmine

Betfair trading framework with a focus on:

  • simplicity
  • modular
  • pythonic
  • rock-solid
  • safe

Support for market, order and custom streaming data.

join slack group

Tested on Python 3.7, 3.8, 3.9, 3.10 and 3.11.

"},{"location":"#installation","title":"installation","text":"
$ pip install flumine\n

flumine requires Python 3.7+

"},{"location":"#setup","title":"setup","text":"

Get started...

import betfairlightweight\nfrom flumine import Flumine, clients\n\ntrading = betfairlightweight.APIClient(\"username\")\nclient = clients.BetfairClient(trading)\n\nframework = Flumine(\n    client=client,\n)\n

Example strategy:

from flumine import BaseStrategy\nfrom flumine.order.trade import Trade\nfrom flumine.order.order import LimitOrder, OrderStatus\nfrom flumine.markets.market import Market\nfrom betfairlightweight.filters import streaming_market_filter\nfrom betfairlightweight.resources import MarketBook\n\n\nclass ExampleStrategy(BaseStrategy):\n    def start(self) -> None:\n        print(\"starting strategy 'ExampleStrategy'\")\n\n    def check_market_book(self, market: Market, market_book: MarketBook) -> bool:\n        # process_market_book only executed if this returns True\n        if market_book.status != \"CLOSED\":\n            return True\n\n    def process_market_book(self, market: Market, market_book: MarketBook) -> None:\n        # process marketBook object\n        for runner in market_book.runners:\n            if runner.status == \"ACTIVE\" and runner.last_price_traded < 1.5:\n                trade = Trade(\n                    market_id=market_book.market_id,\n                    selection_id=runner.selection_id,\n                    handicap=runner.handicap,\n                    strategy=self,\n                )\n                order = trade.create_order(\n                    side=\"LAY\", order_type=LimitOrder(price=1.01, size=2.00)\n                )\n                market.place_order(order)\n\n    def process_orders(self, market: Market, orders: list) -> None:\n        for order in orders:\n            if order.status == OrderStatus.EXECUTABLE:\n                if order.size_remaining == 2.00:\n                    market.cancel_order(order, 0.02)  # reduce size to 1.98\n                if order.order_type.persistence_type == \"LAPSE\":\n                    market.update_order(order, \"PERSIST\")\n                if order.size_remaining > 0:\n                    market.replace_order(order, 1.02)  # move\n\n\nstrategy = ExampleStrategy(\n    market_filter=streaming_market_filter(\n        event_type_ids=[\"7\"],\n        country_codes=[\"GB\"],\n        market_types=[\"WIN\"],\n    )\n)\n\nframework.add_strategy(strategy)\n

Run framework:

framework.run()\n

Danger

By default flumine will try to prevent coding errors which result in flash crashes and burnt fingers but use at your own risk as per the MIT license.

Recommendation is not to remove the trading controls and carry out extensive testing before executing on live markets, even then only use new strategies on an account with a small balance (transfer balance to games wallet).

"},{"location":"#features","title":"Features","text":"
  • Streaming
  • Multiple strategies
  • Order execution
  • Paper trading
  • Simulation
  • Event simulation (multi market)
  • Middleware and background workers to enable Scores / RaceCard / InPlayService
"},{"location":"#dependencies","title":"Dependencies","text":"

flumine relies on these libraries:

  • betfairlightweight - Betfair API support
  • tenacity - Used for connection retrying (streaming)
  • python-json-logger - JSON logging
  • requests - HTTP support
"},{"location":"advanced/","title":"Advanced","text":""},{"location":"advanced/#flumine","title":"Flumine","text":"

Functions:

  • add_client Adds a client to the framework
  • add_strategy Adds a strategy to the framework
  • add_worker Adds a worker to the framework
  • add_client_control Adds a client control to the framework
  • add_trading_control Adds a trading control to the framework
  • add_market_middleware Adds market middleware to the framework
  • add_logging_control Adds a logging control to the framework

The Flumine class can be adapted by overriding the following functions:

  • _process_market_books() called on MarketBook event
  • _process_sports_data() called on SportsData event
  • _process_market_orders() called when market has pending orders
  • _process_order_package() called on new OrderPackage
  • _add_market() called when new Market received through streams
  • _remove_market() called when Market removed from framework
  • _process_raw_data() called on RawData event
  • _process_market_catalogues called on MarketCatalogue event
  • _process_current_orders called on currentOrders event
  • _process_custom_event called on CustomEvent event see here
  • _process_close_market called on Market closure
  • _process_cleared_orders() called on ClearedOrders event
  • _process_cleared_markets() called on ClearedMarkets event
  • _process_end_flumine() called on Flumine termination
"},{"location":"advanced/#streams","title":"Streams","text":""},{"location":"advanced/#market-stream","title":"Market Stream","text":"

Flumine handles market streams by taking the parameters provided in the strategies, a strategy will then subscribe to the stream. This means strategies can share streams reducing load or create new if they require different markets or data filter.

"},{"location":"advanced/#data-stream","title":"Data Stream","text":"

Similar to Market Streams but the raw streaming data is passed back, this reduces ram/CPU and allows recording of the data for future playback, see the example marketrecorder.py

"},{"location":"advanced/#historical-stream","title":"Historical Stream","text":"

This is created on a per market basis when simulating.

"},{"location":"advanced/#order-stream","title":"Order Stream","text":"

Subscribes to all orders per running instance using the config.customer_strategy_ref

"},{"location":"advanced/#custom-streams","title":"Custom Streams","text":"

Custom streams (aka threads) can be added as per:

from flumine.streams.basestream import BaseStream\nfrom flumine.events.events import CustomEvent\n\n\nclass CustomStream(BaseStream):\n    def run(self) -> None:\n        # connect to stream / make API requests etc.\n        response = api_call()\n\n        # callback func\n        def callback(framework, event):\n            for strategy in framework.strategies:\n                strategy.process_my_event(event)\n\n        # push results through using custom event\n        event = CustomEvent(response, callback)\n\n        # put in main queue\n        self.flumine.handler_queue.put(event)\n\n\ncustom_stream = CustomStream(framework, custom=True)\nframework.streams.add_custom_stream(custom_stream)\n
"},{"location":"advanced/#error-handling","title":"Error Handling","text":"

Flumine will catch all errors that occur in strategy.check_market and strategy.process_market_book, and log either error or critical errors.

Tip

You can remove this error handling by setting config.raise_errors = True

"},{"location":"advanced/#logging","title":"Logging","text":"

jsonlogger is used to log extra detail, see below for a typical setup:

import time\nimport logging\nfrom pythonjsonlogger import jsonlogger\n\nlogger = logging.getLogger()\n\ncustom_format = \"%(asctime) %(levelname) %(message)\"\nlog_handler = logging.StreamHandler()\nformatter = jsonlogger.JsonFormatter(custom_format)\nformatter.converter = time.gmtime\nlog_handler.setFormatter(formatter)\nlogger.addHandler(log_handler)\nlogger.setLevel(logging.INFO)\n
"},{"location":"advanced/#config","title":"Config","text":""},{"location":"advanced/#simulated","title":"simulated","text":"

Updated to True when simulating or paper trading

"},{"location":"advanced/#simulated_strategy_isolation","title":"simulated_strategy_isolation","text":"

Defaults to True to match orders per strategy, when False prevents double counting of passive liquidity on all orders regardless of strategy.

"},{"location":"advanced/#simulation_available_prices","title":"simulation_available_prices","text":"

When True will simulate matches against available prices after initial execution, note this will double count liquidity.

"},{"location":"advanced/#instance_id","title":"instance_id","text":"

Store server id or similar (e.g. AWS ec2 instanceId)

"},{"location":"advanced/#customer_strategy_ref","title":"customer_strategy_ref","text":"

Used as customerStrategyRefs so that only orders created by the running instance are returned.

"},{"location":"advanced/#process_id","title":"process_id","text":"

OS process id of running application.

"},{"location":"advanced/#current_time","title":"current_time","text":"

Used for simulation

"},{"location":"advanced/#raise_errors","title":"raise_errors","text":"

Raises errors on strategy functions, see Error Handling

"},{"location":"advanced/#max_execution_workers","title":"max_execution_workers","text":"

Max number of workers in execution thread pool

"},{"location":"advanced/#async_place_orders","title":"async_place_orders","text":"

Place orders sent with place orders flag, prevents waiting for bet delay

"},{"location":"advanced/#place_latency","title":"place_latency","text":"

Place latency used for simulation / simulation execution

"},{"location":"advanced/#cancel_latency","title":"cancel_latency","text":"

Cancel latency used for simulation / simulation execution

"},{"location":"advanced/#update_latency","title":"update_latency","text":"

Update latency used for simulation / simulation execution

"},{"location":"advanced/#replace_latency","title":"replace_latency","text":"

Replace latency used for simulation / simulation execution

"},{"location":"advanced/#order_sep","title":"order_sep","text":"

customer_order_ref separator

"},{"location":"architecture/","title":"Design","text":""},{"location":"architecture/#main-loop","title":"Main loop","text":"

Typical to most trading frameworks flumine uses an event driven design with the main thread handling these events through a FIFO queue.

  • handles all events in order one by one
  • runs in main
"},{"location":"architecture/#uml-diagrams","title":"UML Diagrams","text":""},{"location":"architecture/#packages","title":"Packages","text":""},{"location":"architecture/#classes","title":"Classes","text":""},{"location":"architecture/#events","title":"Events:","text":"
  • MARKET_CATALOGUE Betfair MarketCatalogue object
  • MARKET_BOOK Betfair MarketBook object
  • RAW_DATA Raw streaming data
  • CURRENT_ORDERS Betfair CurrentOrders object
  • CLEARED_MARKETS Betfair ClearedMarkets object
  • CLEARED_ORDERS Betfair ClearedOrders object
  • CLOSE_MARKET flumine Close Market update
  • STRATEGY_RESET flumine Strategy Reset update
  • CUSTOM_EVENT flumine Custom event update
  • TERMINATOR flumine End instance update

The above events are handled in the flumine class

"},{"location":"architecture/#marketbook-cycle","title":"MarketBook Cycle","text":""},{"location":"architecture/#simulation","title":"Simulation","text":"

Simulation is achieved by monkeypatching the datetime function utcnow(), this allows strategies to be simulated as if they were being executed in real time. Functions such as market.seconds_to_start and fillKill.seconds work as per a live execution.

"},{"location":"architecture/#streams","title":"Streams","text":"
  • Single stream (market)
  • As above but 'data' (flumine listener)
  • Order stream
  • Future work:
    • Custom stream
"},{"location":"architecture/#strategy","title":"Strategy","text":"
  • Class based
  • Subscribe to streams
  • Single strategy subscribes to a single market stream
"},{"location":"architecture/#handles","title":"Handles","text":"
  • Stream reconnect
  • Trading client login/logout
  • Trading client keep alive
  • Future work:
    • Execution
      • place/cancel/replace/update
      • controls
      • fillKill
    • Market Catalogue
    • Polling (scores/raceCard etc)
    • CurrentOrders / ClearedOrders
    • database connection/logging
"},{"location":"architecture/#notes","title":"notes","text":"
  • market middleware (analytics/logging)
  • order middleware (controls)
  • paper trading
  • simulation
"},{"location":"clients/","title":"Clients","text":"

Flumine is capable of using multiple clients, these can be of the same ExchangeType, a variation depending on use case or your own custom client/wrapper. The default workers handle login/keep-alive/logout and market closure for all clients added to the framework automatically.

"},{"location":"clients/#exchangetypes","title":"ExchangeTypes","text":"
  • BETFAIR: BetfairClient
  • SIMULATED: SimulatedClient
  • BETCONNECT: BetconnectClient
"},{"location":"clients/#strategy-use","title":"Strategy use","text":"

To add a client use the add_client this will allow use via framework.clients or strategy.clients

from flumine import Flumine, clients\n\nframework = Flumine()\n\nclient = clients.BetfairClient(trading)\nframework.add_client(client)\n

or when simulating:

from flumine import FlumineSimulation, clients\n\nframework = FlumineSimulation()\n\nclient = clients.SimulatedClient(username=\"123\")\nframework.add_client(client)\n

To access clients within a strategy use the helper functions:

betfair_client = self.clients.get_betfair_default()\n\nclient = self.clients.get_client(ExchangeType.SIMULATED, username=\"123\")\n

Tip

get_default and get_betfair_default will use the first client added via add_client (ordered list)

By default a transaction will use clients.get_default() however you can use a particular client:

client = self.clients.get_client(ExchangeType.SIMULATED, username=\"123\")\n\nmarket.place_order(order, client=client)\n

or using a transaction directly:

client = self.clients.get_client(ExchangeType.SIMULATED, username=\"123\")\n\nwith market.transaction(client=client) as t:\n    t.place_order(order)\n
"},{"location":"clients/#future-development","title":"Future Development","text":"
  • BetConnect client #566
"},{"location":"controls/","title":"Controls","text":""},{"location":"controls/#trading-controls","title":"Trading Controls","text":"

Before placing an order flumine will check the client and trading controls, this allows validation to occur before execution. If an order does not meet any of these validations it is not executed and status is updated to Violation.

"},{"location":"controls/#client-controls","title":"Client Controls","text":"
  • MaxTransactionCount: Checks transaction count is not over betfair transaction limit (5000 per hour)
"},{"location":"controls/#trading-controls_1","title":"Trading Controls","text":"
  • OrderValidation: Checks order is valid (size/odds)
  • StrategyExposure: Checks order does not invalidate strategy.validate_order, strategy.max_order_exposure or strategy.max_selection_exposure
"},{"location":"controls/#skipping-controls","title":"Skipping Controls","text":"

Sometimes it is desirable to skip the controls, for example when canceling an open order even if the transaction count has already reached the betfair transaction limit. This can be done by passing force=True when placing or changing an order:

market.place_order(order, force=True)\ntransaction.place_order(order, force=True)\n

This works for markets and transactions and is supported by the operations place_order, cancel_order, update_order, and replace_order.

"},{"location":"controls/#logging-controls","title":"Logging Controls","text":"

Custom logging is available using the LoggingControl class, the base class creates debug logs and can be used as follows:

from flumine.controls.loggingcontrols import LoggingControl\n\ncontrol = LoggingControl()\n\nframework.add_logging_control(control)\n

Tip

More than one control can be added, for example a csv logger and db logger.

"},{"location":"help/","title":"Help","text":"

Please try the following channels for any support:

  • Betfair Developer Support
  • Slack Group for any help in using the library
  • API Status if things don't seem to be working
"},{"location":"markets/","title":"Markets","text":""},{"location":"markets/#market","title":"Market","text":"

Within markets you have market objects which contains current up to date market data.

"},{"location":"markets/#class-variables","title":"Class variables","text":"
  • flumine Framework
  • market_id MarketBook id
  • closed Closed bool
  • date_time_closed Closed datetime
  • market_book Latest MarketBook object
  • market_catalogue Latest MarketCatalogue object
  • context Market context, store market specific context e.g. simulated data store
  • blotter Holds all order data and position
"},{"location":"markets/#functions","title":"Functions","text":"
  • place_order(order) Place new order from order object
  • cancel_order(order, size_reduction) Cancel order
  • update_order(order, new_persistance_type) Update order
  • replace_order(order, new_price) Replace order
"},{"location":"markets/#properties","title":"Properties","text":"
  • event Dictionary containing all event related markets (assumes markets have been subscribed)
  • event_type_id Betfair event type id (horse racing: 7)
  • event_id Market event id (12345)
  • market_type Market type ('WIN')
  • seconds_to_start Seconds to scheduled market start time (123.45)
  • elapsed_seconds_closed Seconds since market was closed (543.21)
  • market_start_datetime Market scheduled start time
"},{"location":"markets/#transaction","title":"Transaction","text":"

The transaction class is used by default when orders are executed, however it is possible to control the execution behaviour using the transaction class like so:

with market.transaction() as t:\n    market.place_order(order)  # executed immediately in separate transaction\n    t.place_order(order)  # executed on transaction __exit__\n\nwith market.transaction() as t:\n    t.place_order(order)\n    ..\n    t.execute()  # above order executed\n    ..\n    t.cancel_order(order)\n    t.place_order(order)  # both executed on transaction __exit__\n
"},{"location":"markets/#blotter","title":"Blotter","text":"

The blotter is a simple and fast class to hold all orders for a particular market.

"},{"location":"markets/#functions_1","title":"Functions","text":"
  • strategy_orders(strategy) Returns all orders related to a strategy
  • strategy_selection_orders(strategy, selection_id, handicap) Returns all orders related to a strategy selection
  • selection_exposure(strategy, lookup) Returns strategy/selection exposure
  • market_exposure(strategy, market_book) Returns strategy/market exposure
"},{"location":"markets/#properties_1","title":"Properties","text":"
  • live_orders List of live orders
  • has_live_orders Bool on live orders
"},{"location":"markets/#middleware","title":"Middleware","text":"

It is common that you want to carry about analysis on a market before passing through to strategies, similar to Django's middleware design flumine allows middleware to be executed.

For example simulation uses simulated middleware in order to calculate order matching.

Note

Middleware will be executed in the order it is added and before the strategies are processed.

Please see below for the example middleware class if you wish to create your own:

from flumine.markets.middleware import Middleware\n\nclass CustomMiddleware(Middleware):\n    def __call__(self, market) -> None:\n        pass  # called on each MarketBook update\n\n    def add_market(self, market) -> None:\n        print(\"market {0} added\".format(market.market_id))\n\n    def remove_market(self, market) -> None:\n        print(\"market {0} removed\".format(market.market_id))\n

The above middleware can then be added to the framework:

framework.add_logging_control(CustomMiddleware())\n
"},{"location":"performance/","title":"Performance","text":"

Flumine is heavily optimised out of the box to be as quick as possible however there are various ways to improve the performance further with minimal effort.

"},{"location":"performance/#simulation","title":"Simulation","text":""},{"location":"performance/#listener-kwargs","title":"Listener Kwargs","text":"

This is one of the most powerful options available as the variables are passed down to betfairlightweight limiting the number of updates to process, examples:

"},{"location":"performance/#600s-before-scheduled-start-and-no-inplay","title":"600s before scheduled start and no inplay","text":"
strategy = ExampleStrategy(\n    market_filter={\n        \"markets\": [\"/tmp/marketdata/1.170212754\"],\n        \"listener_kwargs\": {\"seconds_to_start\": 600, \"inplay\": False},\n    }\n)\n
"},{"location":"performance/#inplay-only","title":"inplay only","text":"
strategy = ExampleStrategy(\n    market_filter={\n        \"markets\": [\"/tmp/marketdata/1.170212754\"],\n        \"listener_kwargs\": {\"inplay\": True},\n    }\n)\n
"},{"location":"performance/#logging","title":"Logging","text":"

Logging in python can add a lot of function calls, it is therefore recommended to switch it off once you are comfortable with the outputs from a strategy:

logger.setLevel(logging.CRITICAL)\n
"},{"location":"performance/#file-location","title":"File location","text":"

This might sound obvious but having the market files stored locally on your machine will allow much quicker processing. A common pattern is to use s3 to store all market files but a local cache for common markets processed.

smart_open is a commonly used package for processing gz/s3 files:

with patch(\"builtins.open\", smart_open.open):\n    framework.add_strategy(strategy)\n    framework.run()\n

Tip

Note that add_strategy needs to be in the patch as well.

"},{"location":"performance/#betfair-historical-data","title":"Betfair Historical Data","text":"

Sometimes a download from the betfair site will include market and event files in the same directory resulting in duplicate processing, flumine will log a warning on this but it is worth checking if you are seeing slow processing times.

"},{"location":"performance/#multiprocessing","title":"Multiprocessing","text":"

Simulation is CPU bound so can therefore be improved through the use of multiprocessing, threading offers no improvement due to the limitations of the GIL.

The multiprocessing example code below will:

  • run a process per core
  • each run_process will process 8 markets at a time (prevents memory leaks)
  • will wait for all results before completing
import os\nimport math\nimport smart_open\nfrom concurrent import futures\nfrom unittest.mock import patch as mock_patch\nfrom flumine import FlumineSimulation, clients, utils\nfrom strategies.lowestlayer import LowestLayer\n\n\ndef run_process(markets):\n    client = clients.SimulatedClient()\n    framework = FlumineSimulation(client=client)\n    strategy = LowestLayer(\n        market_filter={\"markets\": markets},\n        context={\"stake\": 2},\n    )\n    with mock_patch(\"builtins.open\", smart_open.open):\n        framework.add_strategy(strategy)\n        framework.run()\n\n\nif __name__ == \"__main__\":\n    all_markets = [...]\n    processes = os.cpu_count()\n    markets_per_process = 8  # optimal\n\n    _process_jobs = []\n    with futures.ProcessPoolExecutor(max_workers=processes) as p:\n        chunk = min(\n            markets_per_process, math.ceil(len(all_markets) / processes)\n        )\n        for m in (utils.chunks(all_markets, chunk)):\n            _process_jobs.append(\n                p.submit(\n                    run_process,\n                    markets=m,\n                )\n            )\n        for job in futures.as_completed(_process_jobs):\n            job.result()  # wait for result\n

Tip

If the code above is failing add logging to the run_process function to find the error or run the strategy in a single process with logging

"},{"location":"performance/#strategy","title":"Strategy","text":"

The heaviest load on CPU comes from reading the files and processing into py objects before processing through flumine, after this the bottleneck becomes the number of orders that need to be processed. Therefore anything that can be done to limit the number of redundant or control blocked orders will see an improvement.

"},{"location":"performance/#cprofile","title":"cprofile","text":"

Profiling code is always the best option for finding improvements, cprofilev is a commonly used python library for this:

python -m cprofilev examples/simulate.py\n
"},{"location":"performance/#middleware","title":"Middleware","text":"

If you don't need the simulation middleware remove it from framework._market_middleware, this is useful when processing markets for data collection. This can dramatically improve processing time due to the heavy functions contained in the simulation logic.

"},{"location":"performance/#libraries","title":"Libraries","text":"

Installing betfairlightweight[speed] will have a big impact on processing speed due to the inclusion of C and Rust libraries for datetime and json decoding.

"},{"location":"performance/#live","title":"Live","text":"

For improving live trading 'Strategy' and 'cprofile' tips above will help although CPU load tends to be considerably lower compared to simulating.

"},{"location":"quickstart/","title":"QuickStart","text":""},{"location":"quickstart/#live","title":"Live","text":"

Tip

flumine uses betfairlightweight for communicating with the Betfair API, please see docs for how to use/setup before proceeding.

First, start by importing flumine/bflw and creating a trading and framework client:

import betfairlightweight\nfrom flumine import Flumine, clients\n\ntrading = betfairlightweight.APIClient(\"username\")\nclient = clients.BetfairClient(trading)\n\nframework = Flumine(client=client)\n

Note

flumine will handle login, logout and keep alive whilst the framework is running using the keep_alive worker.

A strategy can now be created by using the BaseStrategy class:

from flumine import BaseStrategy\n\n\nclass ExampleStrategy(BaseStrategy):\n    def start(self):\n        # subscribe to streams\n        print(\"starting strategy 'ExampleStrategy'\")\n\n    def check_market_book(self, market, market_book):\n        # process_market_book only executed if this returns True\n        if market_book.status != \"CLOSED\":\n            return True\n\n    def process_market_book(self, market, market_book):\n        # process marketBook object\n        print(market_book.status)\n

This strategy can now be initiated with the market and data filter before being added to the framework:

from betfairlightweight.filters import (\n    streaming_market_filter, \n    streaming_market_data_filter,\n)\n\nstrategy = ExampleStrategy(\n    market_filter=streaming_market_filter(\n        event_type_ids=[\"7\"],\n        country_codes=[\"GB\"],\n        market_types=[\"WIN\"],\n    ),\n    market_data_filter=streaming_market_data_filter(fields=[\"EX_ALL_OFFERS\"])\n)\n\nframework.add_strategy(strategy)\n

The framework can now be started:

framework.run()\n
"},{"location":"quickstart/#order-placement","title":"Order placement","text":"

Orders can be placed as followed:

from flumine.order.trade import Trade\nfrom flumine.order.order import LimitOrder\n\n\nclass ExampleStrategy(BaseStrategy):\n    def process_market_book(self, market, market_book):\n        for runner in market_book.runners:\n            if runner.selection_id == 123:\n                trade = Trade(\n                    market_id=market_book.market_id, \n                    selection_id=runner.selection_id,\n                    handicap=runner.handicap,\n                    strategy=self\n                )\n                order = trade.create_order(\n                    side=\"LAY\", \n                    order_type=LimitOrder(price=1.01, size=2.00)\n                )\n                market.place_order(order)\n

This order will be validated through controls, stored in the blotter and sent straight to the execution thread pool for execution. It is also possible to batch orders into transactions as follows:

with market.transaction() as t:\n    market.place_order(order)  # executed immediately in separate transaction\n    t.place_order(order)  # executed on transaction __exit__\n\nwith market.transaction() as t:\n    t.place_order(order)\n\n    t.execute()  # above order executed\n\n    t.cancel_order(order)\n    t.place_order(order)  # both executed on transaction __exit__\n
"},{"location":"quickstart/#stream-class","title":"Stream class","text":"

By default the stream class will be a MarketStream which provides a MarketBook python object, if collecting data this can be changed to a DataStream class however process_raw_data will be called and not process_market_book:

from flumine import BaseStrategy\nfrom flumine.streams.datastream import DataStream\n\n\nclass ExampleDataStrategy(BaseStrategy):\n    def process_raw_data(self, publish_time, data):\n        print(publish_time, data)\n\nstrategy = ExampleDataStrategy(\n    market_filter=streaming_market_filter(\n        event_type_ids=[\"7\"],\n        country_codes=[\"GB\"],\n        market_types=[\"WIN\"],\n    ),\n    stream_class=DataStream\n)\n\nflumine.add_strategy(strategy)\n

The OrderDataStream class can be used to record order data as per market:

from flumine.streams.datastream import OrderDataStream\n\nstrategy = ExampleDataStrategy(\n    market_filter=None,\n    stream_class=OrderDataStream\n)\n\nflumine.add_strategy(strategy)\n
"},{"location":"quickstart/#paper-trading","title":"Paper Trading","text":"

Flumine can be used to paper trade strategies live using the following code:

from flumine import clients\n\nclient = clients.BetfairClient(trading, paper_trade=True)\n

Market data will be recieved as per live but any orders will use Simulated execution and Simulated order polling to replicate live trading.

Tip

This can be handy when testing strategies as the betfair website can be used to validate the market.

"},{"location":"quickstart/#simulation","title":"Simulation","text":"

Flumine can be used to simulate strategies using the following code:

from flumine import FlumineSimulation, clients\n\nclient = clients.SimulatedClient()\nframework = FlumineSimulation(client=client)\n\nstrategy = ExampleStrategy(\n    market_filter={\"markets\": [\"/tmp/marketdata/1.170212754\"]}\n)\nframework.add_strategy(strategy)\n\nframework.run()\n

Note the use of market filter to pass the file directories.

"},{"location":"quickstart/#listener-kwargs","title":"Listener kwargs","text":"

Sometimes a subset of the market lifetime is required, this can be optimised by limiting the number of updates to process resulting in faster simulation:

strategy = ExampleStrategy(\n    market_filter={\n        \"markets\": [\"/tmp/marketdata/1.170212754\"],\n        \"listener_kwargs\": {\"inplay\": False, \"seconds_to_start\": 600},\n    }\n)\n
  • inplay: Filter inplay flag
  • seconds_to_start: Filter market seconds to start
  • calculate_market_tv: As per bflw listener arg
  • cumulative_runner_tv: As per bflw listener arg

The extra kwargs above will limit processing to preplay in the final 10 minutes.

Tip

Multiple strategies and markets can be passed, flumine will pass the MarketBooks to the correct strategy via its subscription.

"},{"location":"quickstart/#event-processing","title":"Event Processing","text":"

It is also possible to process events with multiple markets such as win/place in racing or all football markets as per live by adding the following flag:

strategy = ExampleStrategy(\n    market_filter={\"markets\": [..], \"event_processing\": True}\n)\n

The Market object contains a helper method for accessing other event linked markets:

place_market = market.event[\"PLACE\"]\n
"},{"location":"quickstart/#market-filter","title":"Market Filter","text":"

When simulating you can filter markets to be processed by using the market_type and country_code filter as per live:

strategy = ExampleStrategy(\n    market_filter={\"markets\": [..], \"market_types\": [\"MATCH_ODDS\"], \"country_codes\": [\"GB\"]}\n)\n
"},{"location":"quickstart/#simulation_1","title":"Simulation","text":"

Simulation uses the SimulatedExecution execution class and tries to accurately simulate matching with the following:

  • Place/Cancel/Replace latency delay added
  • BetDelay added based on market state
  • Queue positioning based on liquidity available
  • Order lapse on market version change
  • Order lapse and reduction on runner removal
  • BSP

Limitations #192:

  • Queue cancellations
  • Double counting of liquidity (active)
  • Currency fluctuations
"},{"location":"sportsdata/","title":"Sports Data","text":"

Flumine is able to connect to the sports-data-stream provided by Betfair for live data on cricket and races.

Tip

Your appKey must be authorised to access the sports-data stream, contact bdp@betfair.com

"},{"location":"sportsdata/#cricket-subscription","title":"Cricket Subscription","text":"

A cricket subscription can be added via the sports_data_filter on a strategy

strategy = ExampleStrategy(\n    market_filter=streaming_market_filter(\n        event_type_ids=[\"4\"], market_types=[\"MATCH_ODDS\"]\n    ),\n    sports_data_filter=[\"cricketSubscription\"],\n)\n
"},{"location":"sportsdata/#race-subscription","title":"Race Subscription","text":"

A race subscription can be added via the sports_data_filter on a strategy

strategy = ExampleStrategy(\n    market_filter=streaming_market_filter(\n        event_type_ids=[\"7\"], market_types=[\"WIN\"]\n    ),\n    sports_data_filter=[\"raceSubscription\"],\n)\n
"},{"location":"sportsdata/#strategy","title":"Strategy","text":"

Any sports data stream updates will be available in the strategy via the process_sports_data function

class ExampleStrategy(BaseStrategy):\n    def process_sports_data(\n        self, market: Market, sports_data: Union[Race, CricketMatch]\n    ) -> None:\n        # called on each update from sports-data-stream\n        print(market, sports_data)\n
"},{"location":"sportsdata/#data-recorder","title":"Data Recorder","text":"

The example marketrecorder.py can be modified to record race and cricket data by updating the process_raw_data with the matching op and data keys.

  • marketSubscription mcm and mc
  • orderSubscription ocm and oc
  • cricketSubscription ccm and cc
  • raceSubscription rcm and rc

And using the correct stream class:

"},{"location":"sportsdata/#cricket-recorder","title":"Cricket Recorder","text":"
from flumine.streams.datastream import CricketDataStream\n\nstrategy= MarketRecorder(\n    market_filter=None,\n    stream_class=CricketDataStream,\n    context={\n        \"local_dir\": \"/tmp\",\n        \"force_update\": False,\n        \"remove_file\": True,\n        \"remove_gz_file\": False,\n    },\n)\n
"},{"location":"sportsdata/#race-recorder","title":"Race Recorder","text":"
from flumine.streams.datastream import RaceDataStream\n\nstrategy= MarketRecorder(\n    market_filter=None,\n    stream_class=RaceDataStream,\n    context={\n        \"local_dir\": \"/tmp\",\n        \"force_update\": False,\n        \"remove_file\": True,\n        \"remove_gz_file\": False,\n    },\n)\n
"},{"location":"strategies/","title":"Strategies","text":""},{"location":"strategies/#basestrategy","title":"BaseStrategy","text":"

The base strategy class should be used for all strategies and contains the following parameters / functions for order/trade execution.

"},{"location":"strategies/#parameters","title":"Parameters","text":"
  • market_filter Streaming market filter or list of filters required
  • market_data_filter Streaming market data filter required
  • streaming_timeout Streaming timeout, will call snap() on cache every x seconds
  • conflate_ms Streaming conflate
  • stream_class MarketStream or RawDataStream
  • name Strategy name, if None will default to class name
  • context Dictionary object where any extra data can be stored here such as triggers
  • max_selection_exposure Max exposure per selection (including new order), note this does not handle reduction in exposure due to laying another runner
  • max_order_exposure Max exposure per order
  • clients flumine.clients
  • max_trade_count Max total number of trades per runner
  • max_live_trade_count Max live (with executable orders) trades per runner
  • multi_order_trades Allow multiple live orders per trade
"},{"location":"strategies/#functions","title":"Functions","text":"

The following functions can be overridden dependent on the strategy:

  • add() Function called when strategy is added to framework
  • start() Function called when framework starts
  • process_new_market() Process Market when it gets added to the framework for the first time
  • check_market_book() Function called with marketBook, process_market_book is only executed if this returns True
  • process_market_book() Processes market book updates, called on every update that is received
  • process_raw_data() As per process_market_book but handles raw data
  • process_orders() Process list of Order objects for strategy and Market
  • process_closed_market() Process Market after closure
  • finish() Function called when framework ends
"},{"location":"strategies/#runner-context","title":"Runner Context","text":"

Each strategy stores a RunnerContext object which contains the state of a runner based on all and current active trades. This is used by controls to calculate exposure and control the number of live or total trades.

runner_context = self.get_runner_context(\n    market.market_id, runner.selection_id, runner.handicap\n)\n\nrunner_context.live_trade_count\n
"},{"location":"trades/","title":"Trades / Orders","text":""},{"location":"trades/#trade","title":"Trade","text":"

A trade object is used to handle order execution.

from flumine.order.trade import Trade\nfrom flumine.order.ordertype import LimitOrder\n\ntrade = Trade(\n    market_id=\"1.2345678\",\n    selection_id=123456,\n    handicap=1.0,\n    strategy=strategy\n)\ntrade.orders  # []\ntrade.status  # TradeStatus.LIVE\n\norder = trade.create_order(\n    side=\"LAY\",\n    order_type=LimitOrder(price=1.01, size=2.00)\n)\ntrade.orders  # [<BetfairOrder>]\n
"},{"location":"trades/#parameters","title":"Parameters","text":"
  • market_id Market Id
  • selection_id Selection Id
  • handicap Runner handicap
  • strategy Strategy object
  • notes Trade notes, used to store market / trigger info for later analysis
  • place_reset_seconds Seconds to wait since runner_context.reset before allowing another order
  • reset_seconds Seconds to wait since runner_context.place before allowing another order
"},{"location":"trades/#custom","title":"custom","text":"

You can create your own trade classes and then handle the logic within the strategy.process_orders function.

"},{"location":"trades/#order","title":"Order","text":"

Order objects store all order data locally allowing trade logic to be applied.

from flumine.order.order import BetfairOrder, LimitOrder\n\norder = BetfairOrder(\n    trade=trade,\n    side=\"LAY\",\n    order_type=LimitOrder(price=1.01, size=2.00)\n)\n\norder.status  # OrderStatus.PENDING\norder.executable()\norder.execution_complete()\n
"},{"location":"workers/","title":"Workers","text":""},{"location":"workers/#background-workers","title":"Background Workers","text":"

Background workers run in their own thread allowing cleanup / cron like workers to run in the background, by default flumine adds the following workers:

  • keep_alive: runs every 1200s (or session_timeout/2) to make sure clients are logged and kept alive
  • poll_account_balance: runs every 120s to poll account balance endpoint
  • poll_market_catalogue: runs every 60s to poll listMarketCatalogue endpoint
  • poll_market_closure: checks for closed markets to get cleared orders at order and market level
"},{"location":"workers/#variables","title":"Variables","text":"
  • flumine: Framework
  • function: Function to be called
  • interval: Interval in seconds, set to None for single call
  • func_args: Function args
  • func_kwargs: Function kwargs
  • start_delay: Start delay in seconds
  • context: Worker context
  • name: Worker name
"},{"location":"workers/#custom-workers","title":"Custom Workers","text":"

Further workers can be added as per:

from flumine.worker import BackgroundWorker\n\ndef func(context: dict, flumine, name=\"\"):\n    print(name)\n\n\nworker = BackgroundWorker(\n    framework, interval=10, function=func, func_args=(\"hello\",)\n)\n\nframework.add_worker(\n    worker\n)\n
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..0f8724ef --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 00000000..614c0fbb Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/sportsdata/index.html b/sportsdata/index.html new file mode 100644 index 00000000..3e347d6b --- /dev/null +++ b/sportsdata/index.html @@ -0,0 +1,810 @@ + + + + + + + + + + + + + + + + + + + + + + + + Sports Data - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Sports Data

+

Flumine is able to connect to the sports-data-stream provided by Betfair for live data on cricket and races.

+
+

Tip

+

Your appKey must be authorised to access the sports-data stream, contact bdp@betfair.com

+
+

Cricket Subscription

+

A cricket subscription can be added via the sports_data_filter on a strategy

+
strategy = ExampleStrategy(
+    market_filter=streaming_market_filter(
+        event_type_ids=["4"], market_types=["MATCH_ODDS"]
+    ),
+    sports_data_filter=["cricketSubscription"],
+)
+
+ +

Race Subscription

+

A race subscription can be added via the sports_data_filter on a strategy

+
strategy = ExampleStrategy(
+    market_filter=streaming_market_filter(
+        event_type_ids=["7"], market_types=["WIN"]
+    ),
+    sports_data_filter=["raceSubscription"],
+)
+
+ +

Strategy

+

Any sports data stream updates will be available in the strategy via the process_sports_data function

+
class ExampleStrategy(BaseStrategy):
+    def process_sports_data(
+        self, market: Market, sports_data: Union[Race, CricketMatch]
+    ) -> None:
+        # called on each update from sports-data-stream
+        print(market, sports_data)
+
+ +

Data Recorder

+

The example marketrecorder.py can be modified to record race and cricket data by updating the process_raw_data with the matching op and data keys.

+
    +
  • marketSubscription mcm and mc
  • +
  • orderSubscription ocm and oc
  • +
  • cricketSubscription ccm and cc
  • +
  • raceSubscription rcm and rc
  • +
+

And using the correct stream class:

+

Cricket Recorder

+
from flumine.streams.datastream import CricketDataStream
+
+strategy= MarketRecorder(
+    market_filter=None,
+    stream_class=CricketDataStream,
+    context={
+        "local_dir": "/tmp",
+        "force_update": False,
+        "remove_file": True,
+        "remove_gz_file": False,
+    },
+)
+
+ +

Race Recorder

+
from flumine.streams.datastream import RaceDataStream
+
+strategy= MarketRecorder(
+    market_filter=None,
+    stream_class=RaceDataStream,
+    context={
+        "local_dir": "/tmp",
+        "force_update": False,
+        "remove_file": True,
+        "remove_gz_file": False,
+    },
+)
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/strategies/index.html b/strategies/index.html new file mode 100644 index 00000000..2a3edf1a --- /dev/null +++ b/strategies/index.html @@ -0,0 +1,748 @@ + + + + + + + + + + + + + + + + + + + + + + + + Strategies - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Strategies

+

BaseStrategy

+

The base strategy class should be used for all strategies and contains the following parameters / functions for order/trade execution.

+

Parameters

+
    +
  • market_filter Streaming market filter or list of filters required
  • +
  • market_data_filter Streaming market data filter required
  • +
  • streaming_timeout Streaming timeout, will call snap() on cache every x seconds
  • +
  • conflate_ms Streaming conflate
  • +
  • stream_class MarketStream or RawDataStream
  • +
  • name Strategy name, if None will default to class name
  • +
  • context Dictionary object where any extra data can be stored here such as triggers
  • +
  • max_selection_exposure Max exposure per selection (including new order), note this does not handle reduction in exposure due to laying another runner
  • +
  • max_order_exposure Max exposure per order
  • +
  • clients flumine.clients
  • +
  • max_trade_count Max total number of trades per runner
  • +
  • max_live_trade_count Max live (with executable orders) trades per runner
  • +
  • multi_order_trades Allow multiple live orders per trade
  • +
+

Functions

+

The following functions can be overridden dependent on the strategy:

+
    +
  • add() Function called when strategy is added to framework
  • +
  • start() Function called when framework starts
  • +
  • process_new_market() Process Market when it gets added to the framework for the first time
  • +
  • check_market_book() Function called with marketBook, process_market_book is only executed if this returns True
  • +
  • process_market_book() Processes market book updates, called on every update that is received
  • +
  • process_raw_data() As per process_market_book but handles raw data
  • +
  • process_orders() Process list of Order objects for strategy and Market
  • +
  • process_closed_market() Process Market after closure
  • +
  • finish() Function called when framework ends
  • +
+

Runner Context

+

Each strategy stores a RunnerContext object which contains the state of a runner based on all and current active trades. This is used by controls to calculate exposure and control the number of live or total trades.

+
runner_context = self.get_runner_context(
+    market.market_id, runner.selection_id, runner.handicap
+)
+
+runner_context.live_trade_count
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/trades/index.html b/trades/index.html new file mode 100644 index 00000000..5bb6dd3b --- /dev/null +++ b/trades/index.html @@ -0,0 +1,756 @@ + + + + + + + + + + + + + + + + + + + + + + + + Trades / Orders - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Trades / Orders

+

Trade

+

A trade object is used to handle order execution.

+
from flumine.order.trade import Trade
+from flumine.order.ordertype import LimitOrder
+
+trade = Trade(
+    market_id="1.2345678",
+    selection_id=123456,
+    handicap=1.0,
+    strategy=strategy
+)
+trade.orders  # []
+trade.status  # TradeStatus.LIVE
+
+order = trade.create_order(
+    side="LAY",
+    order_type=LimitOrder(price=1.01, size=2.00)
+)
+trade.orders  # [<BetfairOrder>]
+
+ +

Parameters

+
    +
  • market_id Market Id
  • +
  • selection_id Selection Id
  • +
  • handicap Runner handicap
  • +
  • strategy Strategy object
  • +
  • notes Trade notes, used to store market / trigger info for later analysis
  • +
  • place_reset_seconds Seconds to wait since runner_context.reset before allowing another order
  • +
  • reset_seconds Seconds to wait since runner_context.place before allowing another order
  • +
+

custom

+

You can create your own trade classes and then handle the logic within the strategy.process_orders function.

+

Order

+

Order objects store all order data locally allowing trade logic to be applied.

+
from flumine.order.order import BetfairOrder, LimitOrder
+
+order = BetfairOrder(
+    trade=trade,
+    side="LAY",
+    order_type=LimitOrder(price=1.01, size=2.00)
+)
+
+order.status  # OrderStatus.PENDING
+order.executable()
+order.execution_complete()
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/workers/index.html b/workers/index.html new file mode 100644 index 00000000..513b6d03 --- /dev/null +++ b/workers/index.html @@ -0,0 +1,718 @@ + + + + + + + + + + + + + + + + + + + + + + + + Workers - flumine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Workers

+

Background Workers

+

Background workers run in their own thread allowing cleanup / cron like workers to run in the background, by default flumine adds the following workers:

+
    +
  • keep_alive: runs every 1200s (or session_timeout/2) to make sure clients are logged and kept alive
  • +
  • poll_account_balance: runs every 120s to poll account balance endpoint
  • +
  • poll_market_catalogue: runs every 60s to poll listMarketCatalogue endpoint
  • +
  • poll_market_closure: checks for closed markets to get cleared orders at order and market level
  • +
+

Variables

+
    +
  • flumine: Framework
  • +
  • function: Function to be called
  • +
  • interval: Interval in seconds, set to None for single call
  • +
  • func_args: Function args
  • +
  • func_kwargs: Function kwargs
  • +
  • start_delay: Start delay in seconds
  • +
  • context: Worker context
  • +
  • name: Worker name
  • +
+

Custom Workers

+

Further workers can be added as per:

+
from flumine.worker import BackgroundWorker
+
+def func(context: dict, flumine, name=""):
+    print(name)
+
+
+worker = BackgroundWorker(
+    framework, interval=10, function=func, func_args=("hello",)
+)
+
+framework.add_worker(
+    worker
+)
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file