BitShares Forum
Main => General Discussion => Topic started by: xeroc on January 11, 2016, 02:08:31 pm
-
I present to you a *release candidate* for a python module that can be
used to easily build a trading bot.
As an orientation, I used the polonex trading interface. However there
are some differences that you should be aware of:
* market pairs are denoted as 'quote'_'base', e.g. `USD_BTS`
* Prices/Rates are denoted in 'quote', i.e. the USD_BTS market
is priced in USD and buying 1 USD costs `rate` BTS
* All markets could be considered reversed as well ('BTS_USD')
Usage
... is quite simple:
dex = GrapheneExchange(config)
dex.returnTradeHistory("USD_BTS")
dex.returnTicker()
dex.return24Volume()
dex.returnOrderBook("USD_BTS")
dex.returnBalances()
dex.returnOpenOrders("all")
dex.buy("USD_BTS", 0.001, 10)
dex.sell("USD_BTS", 0.001, 10)
Documentation:
http://python-graphenelib.readthedocs.org/en/latest/exchange.html
Example Usage:
Script: https://github.com/xeroc/python-graphenelib/blob/master/scripts/exchange-simpleticker-stats/main.py
Output:
CNY_BTS
=======
- Trade Premium: 8.109%
- Bid Order Premium: 1.171%
- Ask Order Premium: 8.109%
- Spread: 6.630%
- CER premium: 5.22%
GOLD_BTS
========
- Trade Premium: 2.264%
- Bid Order Premium: 19.559%
- Ask Order Premium: 2.264%
- Spread: 19.413%
- CER premium: 3.53%
USD_BTS
=======
- Trade Premium: 0.180%
- Bid Order Premium: 2.137%
- Ask Order Premium: 0.439%
- Spread: 1.721%
- CER premium: 4.97%
SILVER_BTS
==========
- Trade Premium: 8.595%
- Bid Order Premium: 8.098%
- Ask Order Premium: 1.031%
- Spread: 9.464%
- CER premium: 4.16%
So,
To all the Traders and Bot developers:
* Please give this library a try and improve liquidity in the markets
* Note that the library is not well tested yet, so please a) use only
what you can affort to lose and b) review the code :)
cass edit: title spelling mistake GrapehenExchange --> GrapheneExchange
-
+5% well done!
-
+5% +5%
-
I can verify that the exchange-simpleticker-stats example works on a clean 14.04 install :D.
Great work Xeroc!
-
Great work
(as always)
-
Good move.
-
thanks, great work
-
I just fixed some bugs in the "sell" and "buy" methods .. It is important for the bot developer to understand that prices should always be denoted in 'quote'.
For example: in the USD_BTS market .. a price of 0.5 means ... every 1 BTS costs 0.5 USD.
Further .. I implemented a POC for a bot that places orders around the peg in markets that are supposed to trade near 1 (BTC:OPENBTC, etc...)
The script can be found here:
https://github.com/xeroc/python-graphenelib/blob/master/scripts/exchange-bridge-market-maker
Documentation is here:
http://python-graphenelib.readthedocs.org/en/latest/scripts-exchange-bridge-market-maker.html
I hope to see alot tighter pegs in some of these markets ..
-
hey this is awesome, i dabble in python myself but this blows me away.
nice job.
-
+5%
-
I would like to run this bot but I'm stuck on
No module named 'grapheneexchange'
python3 ~/tmp/git/python-graphenelib/scripts/exchange-simpleticker-stats/main.py
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-simpleticker-stats/main.py", line 1, in <module>
from grapheneexchange import GrapheneExchange
ImportError: No module named 'grapheneexchange'
python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py", line 1, in <module>
from grapheneexchange import GrapheneExchange
ImportError: No module named 'grapheneexchange'
Do I need to set parameters in python-graphenelib/grapheneexchange/grapheneexchange.py ?
-
You need to upgrade the python-graphenelib library .. read the README.md
-
+5%
-
You need to upgrade the python-graphenelib library .. read the README.md
pip install --user --upgrade -r requirements.txt graphenelib ran without errors, also tried a fresh git clone.
Could it be a Archlinux specific issue as feeds are working fine. Will try on an Ubuntu server tomorrow.
-
I am running archlinux as well ..
Try
make install-user
In the library root dir ..
-
saved
-
Well done xeroc, this is great!
-
Do I need to open another RPC wallet port to get this bad boy running?
feed port ArchlinuxARM:
206136ms th_a main.cpp:169 main ] wdata.ws_server: ws://localhost:8090
206165ms th_a main.cpp:174 main ] wdata.ws_user: wdata.ws_password:
206256ms th_a main.cpp:240 main ] Listening for incoming HTTP RPC requests on 127.0.0.1:8093
installed and feeds working without issues:
[jason@i4p python-graphenelib]$ make install-user
python setup.py install --user
/usr/lib/python3.5/site-packages/setuptools/dist.py:285: UserWarning: Normalizing '0.2-rc5' to '0.2rc5'
normalized_version,
running install
running bdist_egg
running egg_info
writing top-level names to graphenelib.egg-info/top_level.txt
writing dependency_links to graphenelib.egg-info/dependency_links.txt
writing graphenelib.egg-info/PKG-INFO
reading manifest file 'graphenelib.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'graphenelib.egg-info/SOURCES.txt'
installing library code to build/bdist.linux-armv7l/egg
running install_lib
running build_py
creating build/bdist.linux-armv7l/egg
creating build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/base58.py -> build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/memo.py -> build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/transactions.py -> build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/account.py -> build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/dictionary.py -> build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/bip38.py -> build/bdist.linux-armv7l/egg/graphenebase
copying build/lib/graphenebase/__init__.py -> build/bdist.linux-armv7l/egg/graphenebase
creating build/bdist.linux-armv7l/egg/grapheneextra
copying build/lib/grapheneextra/proposal.py -> build/bdist.linux-armv7l/egg/grapheneextra
copying build/lib/grapheneextra/__init__.py -> build/bdist.linux-armv7l/egg/grapheneextra
creating build/bdist.linux-armv7l/egg/grapheneapi
copying build/lib/grapheneapi/graphenewsprotocol.py -> build/bdist.linux-armv7l/egg/grapheneapi
copying build/lib/grapheneapi/graphenewsrpc.py -> build/bdist.linux-armv7l/egg/grapheneapi
copying build/lib/grapheneapi/grapheneclient.py -> build/bdist.linux-armv7l/egg/grapheneapi
copying build/lib/grapheneapi/grapheneapi.py -> build/bdist.linux-armv7l/egg/grapheneapi
copying build/lib/grapheneapi/graphenews.py -> build/bdist.linux-armv7l/egg/grapheneapi
copying build/lib/grapheneapi/__init__.py -> build/bdist.linux-armv7l/egg/grapheneapi
creating build/bdist.linux-armv7l/egg/grapheneexchange
copying build/lib/grapheneexchange/exchange.py -> build/bdist.linux-armv7l/egg/grapheneexchange
copying build/lib/grapheneexchange/grapheneexchange.py -> build/bdist.linux-armv7l/egg/grapheneexchange
copying build/lib/grapheneexchange/__init__.py -> build/bdist.linux-armv7l/egg/grapheneexchange
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/base58.py to base58.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/memo.py to memo.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/transactions.py to transactions.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/account.py to account.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/dictionary.py to dictionary.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/bip38.py to bip38.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/graphenebase/__init__.py to __init__.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneextra/proposal.py to proposal.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneextra/__init__.py to __init__.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneapi/graphenewsprotocol.py to graphenewsprotocol.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneapi/graphenewsrpc.py to graphenewsrpc.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneapi/grapheneclient.py to grapheneclient.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneapi/grapheneapi.py to grapheneapi.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneapi/graphenews.py to graphenews.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneapi/__init__.py to __init__.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneexchange/exchange.py to exchange.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneexchange/grapheneexchange.py to grapheneexchange.cpython-35.pyc
byte-compiling build/bdist.linux-armv7l/egg/grapheneexchange/__init__.py to __init__.cpython-35.pyc
creating build/bdist.linux-armv7l/egg/EGG-INFO
copying graphenelib.egg-info/PKG-INFO -> build/bdist.linux-armv7l/egg/EGG-INFO
copying graphenelib.egg-info/SOURCES.txt -> build/bdist.linux-armv7l/egg/EGG-INFO
copying graphenelib.egg-info/dependency_links.txt -> build/bdist.linux-armv7l/egg/EGG-INFO
copying graphenelib.egg-info/top_level.txt -> build/bdist.linux-armv7l/egg/EGG-INFO
zip_safe flag not set; analyzing archive contents...
creating 'dist/graphenelib-0.2rc5-py3.5.egg' and adding 'build/bdist.linux-armv7l/egg' to it
removing 'build/bdist.linux-armv7l/egg' (and everything under it)
Processing graphenelib-0.2rc5-py3.5.egg
Removing /home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg
Copying graphenelib-0.2rc5-py3.5.egg to /home/jason/.local/lib/python3.5/site-packages
graphenelib 0.2rc5 is already the active version in easy-install.pth
Installed /home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg
Processing dependencies for graphenelib==0.2rc5
Finished processing dependencies for graphenelib==0.2rc5
[jason@i4p python-graphenelib]$ pip install --user --upgrade -r requirements.txt graphenelib
Requirement already up-to-date: graphenelib in /home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg
Requirement already up-to-date: autobahn in /home/jason/.local/lib/python3.5/site-packages (from -r requirements.txt (line 1))
Requirement already up-to-date: requests in /home/jason/.local/lib/python3.5/site-packages (from -r requirements.txt (line 2))
Requirement already up-to-date: pycrypto in /usr/lib/python3.5/site-packages (from -r requirements.txt (line 3))
Requirement already up-to-date: scrypt in /home/jason/.local/lib/python3.5/site-packages (from -r requirements.txt (line 4))
Requirement already up-to-date: ecdsa in /home/jason/.local/lib/python3.5/site-packages (from -r requirements.txt (line 5))
Requirement already up-to-date: websocket-client in /home/jason/.local/lib/python3.5/site-packages (from -r requirements.txt (line 6))
Requirement already up-to-date: six>=1.9.0 in /usr/lib/python3.5/site-packages (from autobahn->-r requirements.txt (line 1))
Requirement already up-to-date: txaio>=2.2.0 in /home/jason/.local/lib/python3.5/site-packages (from autobahn->-r requirements.txt (line 1))
error on Archlinux and ArchlinuxARM:
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-simpleticker-stats/main.py
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-simpleticker-stats/main.py", line 1, in <module>
from grapheneexchange import GrapheneExchange
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneexchange/__init__.py", line 2, in <module>
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneexchange/exchange.py", line 1, in <module>
ImportError: cannot import name 'GrapheneClient'
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py", line 1, in <module>
from grapheneexchange import GrapheneExchange
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneexchange/__init__.py", line 2, in <module>
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneexchange/exchange.py", line 1, in <module>
ImportError: cannot import name 'GrapheneClient'
feeds port Ubuntu
2812934ms th_a main.cpp:169 main ] wdata.ws_server: ws://localhost:8090
2813021ms th_a main.cpp:174 main ] wdata.ws_user: wdata.ws_password:
2813141ms th_a main.cpp:240 main ] Listening for incoming HTTP RPC requests on 127.0.0.1:8092
error on Ubuntu:
Successfully installed graphenelib requests six
Cleaning up...
:~/tmp/python-graphenelib$ python3 ~/tmp/python-graphenelib/scripts/exchange-simpleticker-stats/main.py
Traceback (most recent call last):
File "tmp/python-graphenelib/scripts/exchange-simpleticker-stats/main.py", line 1, in <module>
from grapheneexchange import GrapheneExchange
ImportError: No module named 'grapheneexchange'
-
Could you re-pull from github then do
make install-user
and try again?
-
Good job, xeroc! You are all charged up in a new year. :)
-
Thank you for your help and patience Xeroc
# delete all - start fresh
rm ~/.local/lib/python*
rm python-graphenelib
# install
git clone https://github.com/xeroc/python-graphenelib.git
cd python-graphenlib
make install-user
# Finished processing dependencies for graphenelib==0.2rc5
pip install --user -r requirements.txt graphenelib
# Successfully installed autobahn-0.11.0 ecdsa-0.13 requests-2.9.1 scrypt-0.7.1 txaio-2.2.0 websocket-client-0.35.0
# feed script error
# ImportError: No module named 'numpy'
pip3 install numpy --user
# Successfully installed numpy-1.10.4
# ImportError: No module named 'prettytable'
pip3 install prettytable --user
feed scripts working :)
will now try the simple ticker
-
+5% +5% +5%
-
one step closer 8)
[jason@i4p python-graphenelib]$ date ; python3 ~/tmp/git/python-graphenelib/scripts/exchange-simpleticker-stats/main.py
Wed 13 Jan 22:47:53 CET 2016
GOLD_BTS
========
- Trade Premium: 24.085%
- Bid Order Premium: 24.081%
- Ask Order Premium: 8.121%
- Spread: 19.024%
- CER premium: 5.26%
BTC_BTS
=======
- Trade Premium: 15.561%
- Bid Order Premium: 20.528%
- Ask Order Premium: 13.149%
- Spread: 8.874%
- CER premium: 5.22%
EUR_BTS
=======
- Trade Premium: 5.071%
- Bid Order Premium: 21.758%
- Ask Order Premium: 5.071%
- Spread: 19.272%
- CER premium: 5.26%
SILVER_BTS
==========
- Trade Premium: 11.765%
- Bid Order Premium: 11.286%
- Ask Order Premium: 3.340%
- Spread: 8.573%
- CER premium: 5.27%
CNY_BTS
=======
- Trade Premium: 4.270%
- Bid Order Premium: 3.073%
- Ask Order Premium: 4.270%
- Spread: 7.299%
- CER premium: 5.26%
-
+5% +5% +5% +5%
-
testing config using MUSE
#: Markets that are of interest for us
watch_markets = ["OPENMUSE : TRADE.MUSE",
]
but got this error - (I do have open orders on TRADE.MUSE : OPENMUSE)
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py", line 11, in <module>
for o in orders[m]:
TypeError: list indices must be integers or slices, not dict
the bot should close my open orders and create new orders at 5% spread?
#: place orders at this spread (in percent)
bridge_spread_percent = 5
-
testing config using MUSE
#: Markets that are of interest for us
watch_markets = ["OPENMUSE : TRADE.MUSE",
]
but got this error - (I do have open orders on TRADE.MUSE : OPENMUSE)
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py", line 11, in <module>
for o in orders[m]:
TypeError: list indices must be integers or slices, not dict
the bot should close my open orders and create new orders at 5% spread?
#: place orders at this spread (in percent)
bridge_spread_percent = 5
Yes .. i noticed it too ..
thing is I coded the api too close to what poloniex is doing .. and they change syntax if you have only one market (compared to several markets)
I will definietly change this behavior ..
in the meantime .. just add another market for which you don't have funds ..
//edit: just pushed a new master that should change the behavior so that the bot works on one market as well
-
error:
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
- 1.7.19973
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py", line 13, in <module>
dex.cancel(o["orderNumber"])
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneexchange/exchange.py", line 908, in cancel
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneapi/grapheneapi.py", line 160, in method
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneapi/grapheneapi.py", line 146, in rpcexec
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneapi/grapheneapi.py", line 138, in rpcexec
grapheneapi.grapheneapi.RPCError: 10 assert_exception: Assert Exception
!is_locked():
{}
th_a wallet.cpp:3508 cancel_order
works with unlocked wallet
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
- 1.7.19524
- 1.7.19527
- 1.7.19530
Placing Orders:
- Selling 89541.300000 OPENMUSE for TRADE.MUSE @1.034500
- Buying 68965.500000 TRADE.MUSE with OPENMUSE @0.965500
-
Exchange bridge market maker running :) with a little BTC - OPENBTC - TRADE.BTC - TRADE.MUSE - OPENMUSE
Thank you again your help, patience and BOT Xeroc
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
- 1.7.19973
- 1.7.19974
Placing Orders:
- Selling 0.034526 BTC for TRADE.BTC @1.034500
- Buying 0.034452 TRADE.BTC with BTC @0.965500
- Selling 0.034526 BTC for OPENBTC @1.034500
- Buying 0.034476 OPENBTC with BTC @0.965500
- Selling 0.034476 OPENBTC for TRADE.BTC @1.034500
- Buying 0.034452 TRADE.BTC with OPENBTC @0.965500
- Selling 89541.300000 OPENMUSE for TRADE.MUSE @1.034500
- Buying 68965.500000 TRADE.MUSE with OPENMUSE @0.965500
Markets:
TRADE.MUSE : OPENMUSE (https://bitshares.openledger.info/?r=by24seven#/market/TRADE.MUSE_OPENMUSE)
TRADE.BTC : BTC (https://bitshares.openledger.info/?r=by24seven#/market/TRADE.BTC_BTC)
OPENBTC : BTC (https://bitshares.openledger.info/?r=by24seven#/market/OPENBTC_BTC)
OPENBTC : TRADE.BTC (https://bitshares.openledger.info/?r=by24seven#/market/OPENBTC_TRADE.BTC)
-
error:
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
- 1.7.19973
Traceback (most recent call last):
File "/home/jason/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py", line 13, in <module>
dex.cancel(o["orderNumber"])
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneexchange/exchange.py", line 908, in cancel
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneapi/grapheneapi.py", line 160, in method
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneapi/grapheneapi.py", line 146, in rpcexec
File "/home/jason/.local/lib/python3.5/site-packages/graphenelib-0.2rc5-py3.5.egg/grapheneapi/grapheneapi.py", line 138, in rpcexec
grapheneapi.grapheneapi.RPCError: 10 assert_exception: Assert Exception
!is_locked():
{}
th_a wallet.cpp:3508 cancel_order
works with unlocked wallet
[jason@i4p python-graphenelib]$ python3 ~/tmp/git/python-graphenelib/scripts/exchange-bridge-market-maker/main.py
Closing Orders:
- 1.7.19524
- 1.7.19527
- 1.7.19530
Placing Orders:
- Selling 89541.300000 OPENMUSE for TRADE.MUSE @1.034500
- Buying 68965.500000 TRADE.MUSE with OPENMUSE @0.965500
I should probably do a check for unlocked wallet somewhere ..
Lots of other checks can be added for misconfiguration and similar stuff .. still in release candidate :)
Good to see it working for you now
-
Will add METAEX.BTC too.
Just need some more BTC and BTS.
Plus BTS to rise in price so I can borrow more BTC ;) +5%
-
I know it is only semi related...
In graphenewsrpc.py
where can I find some explanation/examples etc.
payload in:
def rpcexec(self, payload):
is bothering me right this moment, but general info will be appreciated as well.
-
The whole thing is documented at
http://python-graphenelib.readthedocs.org/en/latest/
which is a page that is build from the content of docs/* and the comments in the code.
The graphenewsrpc.py file is to talk to the witness node via rpc.
the payload is constructed to follow the JSON syntax for calls properly:
https://github.com/xeroc/python-graphenelib/blob/master/grapheneapi/graphenewsrpc.py#L82-L86
-
For those using this module: Please note that prices are now denoted in 'base'/'quote'. I highly recommend to read the latest release notes and the documentation:
http://python-graphenelib.readthedocs.org/en/latest/
Note that this release is not available via pip