├── .dockerignore ├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md └── no-response.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── default.cfg.example ├── docker-compose.yaml ├── docs ├── Documentation.url ├── Makefile ├── README.txt ├── conf.py ├── configuration.rst ├── contributing.rst ├── index.rst ├── installation.rst ├── make.bat └── market_analysis.rst ├── lendingbot.py ├── market_data └── readme.txt ├── modules ├── Bitfinex.py ├── Bitfinex2Poloniex.py ├── Configuration.py ├── ConsoleUtils.py ├── Data.py ├── ExchangeApi.py ├── ExchangeApiFactory.py ├── Lending.py ├── Logger.py ├── MarketAnalysis.py ├── MaxToLend.py ├── Notify.py ├── PluginsManager.py ├── Poloniex.py ├── RingBuffer.py ├── WebServer.py └── __init__.py ├── plugins ├── AccountStats.py ├── Charts.py ├── Plugin.py └── __init__.py ├── requirements.txt ├── setup-container.sh ├── tests ├── README.md ├── test_BitfinexAPI.py ├── test_MarketAnalysis.py └── test_PoloniexAPI.py └── www ├── charts.html ├── images ├── icon.ico ├── icon192.png ├── icon32.png ├── icon384.png ├── icon48.png └── ios_splash.png ├── index.html ├── lendingbot.html ├── lendingbot.js ├── manifest.json └── robots.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | default.cfg 4 | www/botlog.json 5 | nohup.out 6 | error.log 7 | *.idea 8 | *.csv 9 | *.sqlite3 10 | 11 | docs 12 | market_data 13 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to Poloniexlendingbot 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/Mikadily/poloniexlendingbot/issues). 6 | 7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Mikadily/poloniexlendingbot/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 8 | 9 | When making your report, be sure to mention: 10 | 11 | * Title: Short description of what the issue is. (The clearer the better.) 12 | * **Label: Add the Bug label** 13 | * Your configuration.cfg file. Be sure not to share your API key. 14 | * What version of the bot you are using. 15 | * What you think caused this issue. (On your end, you don't need to diagnose our code, tell us what you did that caused it.) 16 | * If you can, suggest a solution. (Again, you don't need to be technical in your description.) 17 | 18 | **Pat yourself on the back for being a productive contributor of an open source project.** 19 | #### **Did you write a patch that fixes a bug, want to make a PR or want to change some of the code?** 20 | 21 | * Please contribute using Github Flow. Create a branch, add commits, and open a pull request. 22 | 23 | * Please use the already made pull request template to layout your pull request. 24 | 25 | * Ensure the PR template is filled in **fully** and required conditions are met. 26 | 27 | * Adhere to the current code style outlined in the Documentation page. 28 | 29 | * If is all is good, wait for approvals by contributors which decide if your PR will be merged. The repo owner can also merge it in early **if there is good reason to do so**. 30 | 31 | *Pull request are made to be discussed, thus collaborators may want you to change things. Don't make a new PR for these changes. Update your existing PR.* 32 | 33 | #### **Do you intend to add a new feature or change an existing one?** 34 | Check if it's been suggested already [here](https://github.com/Mikadily/poloniexlendingbot/issues). 35 | 36 | If it has, or something very similar has: 37 | * Check out the discussion on that suggestion. 38 | * If you feel you have more to add, then comment it there. Don't make a brand new issue for something that has already been suggested. 39 | 40 | If it hasn't been suggested, suggest your change on [Gitter](https://gitter.im/Mikadily/poloniexlendingbot) if you need help or just [create an issue](https://github.com/Mikadily/poloniexlendingbot/issues/new). 41 | 42 | Be sure to include in your report: 43 | 44 | * Title: A clear, short description of what you suggest. You can always go into detail in the description. 45 | * Information about your suggestion. (Does not need to be technical.) 46 | * **Label: Add the Enhancement label.** 47 | * Why you think this would improve the bot if added. 48 | * If you have Python experience, feel free to include snippets of code. (or even refer to a pull request or fork) 49 | 50 | **Pat yourself on the back. If your suggestion is added, you will be a productive contributor to an open source passion-powered project.** 51 | 52 | #### **Do you have questions about the source code?** 53 | 54 | * Ask any question on [Gitter](https://gitter.im/Mikadily/poloniexlendingbot). 55 | 56 | 57 | Poloniex lending bot is a volunteer effort and is a collaborative and fun project. We encourage you to pitch in and [join the team](https://gitter.im/Mikadily/poloniexlendingbot)! 58 | 59 | Thanks, 60 | 61 | Mikadily, Raanan, Evanito and other contributors. 62 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | 8 | ## TESTING STAGE 9 | 10 | 11 | ## Types of changes 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 16 | 17 | ## Checklist: 18 | 19 | 21 | 22 | - [ ] **I have read CONTRIBUTING.md** 23 | - [ ] **I fully understand [Github Flow.](https://guides.github.com/introduction/flow/)** 24 | - [ ] **My code adheres to the [code style of this project.](https://poloniexlendingbot.readthedocs.io/en/latest/contributing.html)** 25 | - [ ] **I have updated the documentation in /docs if I have changed the config, arguments, logic in how the bot works, or anything that understandably needs a documentation change.** 26 | - [ ] **I have updated the config file accordingly if my change requires a new configuration setting or changes an existing one.** 27 | - [ ] **I have tested the bot with no issues for 24 continuous hours. If issues were experienced, they have been patched and tested again.** 28 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: more-information-needed 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please comment here if you have or find the answers we need so 13 | that we can investigate further. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.cfg 4 | *.json 5 | nohup.out 6 | error.log 7 | *.idea 8 | *.csv 9 | *.sqlite3 10 | *.db* 11 | docs/_build/ 12 | 13 | venv/ 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-slim 2 | LABEL "project.home"="https://github.com/BitBotFactory/poloniexlendingbot" 3 | 4 | # 5 | # Build: docker build -t /pololendingbot . 6 | # Run: docker run -d -v /pololendingbot_data:/data -p 8000:8000 /pololendingbot 7 | # 8 | 9 | WORKDIR /usr/src/app 10 | 11 | COPY requirements.txt . 12 | RUN pip install --no-cache-dir -r ./requirements.txt 13 | 14 | COPY . . 15 | 16 | VOLUME /data 17 | 18 | RUN ln -s /data/market_data market_data; \ 19 | ln -s /data/log/botlog.json www/botlog.json 20 | 21 | EXPOSE 8000 22 | 23 | CMD ["python", "lendingbot.py", "-cfg", "/data/conf/default.cfg"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mikadily and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mika Lending Bot 2 | 3 | ## Devs are Out of Office - Community Pull Requests will be considered 4 | 5 | Mika Lending Bot is written in Python and features automatic lending on exchanges Poloniex and Bitfinex. 6 | It will automatically lend all cryptocurrencies found in your lending account. 7 | 8 | It uses an advanced lending strategy which will spread offers across the lend book to take advantage of possible spikes in lending rates. Inspired by [MarginBot](https://github.com/HFenter/MarginBot) and [BitfinexLendingBot](https://github.com/eAndrius/BitfinexLendingBot). 9 | 10 | Join the discussion at: 11 | 12 | [![Join the chat at https://gitter.im/Mikadily/poloniexlendingbot](https://badges.gitter.im/Mikadily/poloniexlendingbot.svg)](https://gitter.im/Mikadily/poloniexlendingbot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 13 | 14 | [](https://t.me/mikalendingbot) 15 | 16 | [](https://poloniexbot.slack.com/shared_invite/MTc5OTU4MDAzNTY4LTE0OTQzMTA2MzYtZDdkYTg1NjBkYg) **- Recommended for communicating with Devs** 17 | 18 | [Subreddit](https://www.reddit.com/r/poloniexlendingbot/) **- Recommended for focused discussion** 19 | 20 | We also have a public [FAQ on the Github Wiki](https://github.com/BitBotFactory/MikaLendingBot/wiki/FAQ-(Troubleshooting)), feel free to add your questions or check there for support! 21 | 22 | Workflow management with Waffle.io 23 | 24 | [![Throughput Graph](https://graphs.waffle.io/Mikadily/poloniexlendingbot/throughput.svg)](https://waffle.io/Mikadily/poloniexlendingbot/) 25 | 26 | ## Documentation 27 | [Click here to read the Documentation, hosted by readthedocs.io](http://poloniexlendingbot.readthedocs.io/en/latest/index.html) 28 | 29 | 30 | ### Features 31 | - Automatically lend your coins on Poloniex and Bitfinex at the highest possible rates, 24 hours a day. 32 | - Configure your own lending strategy! Be aggressive and hold out for a great rate or be conservative and lend often but at a lower rate, your choice! 33 | - The ability to spread your offers out to take advantage of spikes in the lending rate. 34 | - Withhold lending a percentage of your coins until the going rate reaches a certain threshold to maximize your profits. 35 | - Lock in a high daily rate for a longer period of time period of up to sixty days, all configurable! 36 | - Automatically transfer any funds you deposit (configurable on a coin-by-coin basis) to your lending account instantly after deposit. 37 | - View a summary of your bot's activities, status, and reports via an easy-to-set-up webpage that you can access from anywhere! 38 | - Choose any currency to see your profits in, even show how much you are making in USD! 39 | - Select different lending strategies on a coin-by-coin basis. 40 | - Run multiple instances of the bot for multiple accounts easily using multiple config files. 41 | - Configure a date you would like your coins back, and watch the bot make sure all your coins are available to be traded or withdrawn at the beginning of that day. 42 | - Docker support. 43 | - And the best feature of all: It is absolutely free! 44 | -------------------------------------------------------------------------------- /default.cfg.example: -------------------------------------------------------------------------------- 1 | # Need help configuring? View the Documentation: https://poloniexlendingbot.readthedocs.io/en/latest/configuration.html 2 | 3 | [API] 4 | # Where to work? "Poloniex" or "Bitfinex". Default is "Poloniex" 5 | #exchange = Poloniex 6 | apikey = YourAPIKey 7 | secret = YourSecret 8 | 9 | [POLONIEX] 10 | # Full list of supported currencies 11 | all_currencies = STR,BTC,BTS,CLAM,DOGE,DASH,LTC,MAID,XMR,XRP,ETH,FCT,#BTG 12 | 13 | [BITFINEX] 14 | # Full list of supported currencies 15 | all_currencies = USD,BTC,BCH,ETH,XRP,IOT,XMR,LTC,OMG,ETC,EOS,DSH,ZEC,#BTG 16 | 17 | [BOT] 18 | #Custom name of the bot, that will be displayed in html page 19 | label = Lending Bot 20 | 21 | #Sleeps between active iterations, time in seconds (1-3600) 22 | sleeptimeactive = 60 23 | 24 | #Sleeps between inactive iterations, time in seconds (1-3600) 25 | #Set to the same value as sleeptimeactive to disable 26 | sleeptimeinactive = 300 27 | 28 | #Timeout in seconds, the bot shall wait for a response during each request 29 | #timeout = 30 30 | 31 | #Minimum daily lend rate in percent (0.0031-5) 32 | #Setting to 0.0031 is about 1% a year, not worth it. 33 | mindailyrate = 0.005 34 | 35 | #Maximum lending rate. 2% is good choice because it's default at margin trader interface. 36 | #5% is the maximum rate accepted by the exchange (0.003-5) 37 | maxdailyrate = 5 38 | 39 | #The number of offers to split the available balance across the [gaptop, gapbottom] range. (1-20) 40 | spreadlend = 3 41 | 42 | #The depth of lendbook to move through before placing the first (gapbottom) and last (gaptop) offer. 43 | #If gapbottom is set to 0, the first offer will be at the lowest possible rate. 44 | #However some low value is recommended to skip dust offers. 45 | # Gap modes: Raw, RawBTC, Relative 46 | gapMode = RawBTC 47 | gapbottom = 40 48 | gaptop = 200 49 | 50 | #Daily lend rate threshold after which we offer lends for x days as opposed to 2. 51 | #If set to 0 all offers will be placed for a 2 day period (0.003-5) 52 | # Poloniex max lending period: 60 days 53 | # Bitfinex max lending period: 120 days 54 | xdaythreshold = 0.2 55 | xdays = 60 56 | #When xdayspread is set, lending days will be incremented linear from 2 days at (xdaythreshold/xdayspread) lent rate 57 | #to (xdays) days at (xdaythreshold) lent rate. (0-10) 58 | #xdayspread = 2 59 | 60 | #Auto-transfer of funds from exchange to lending balance. 61 | #Enter ALL to transfer all coins, enter ACTIVE to transfer any coins you have in your lending account when the bot starts, these can be mixed. 62 | #The currencies you enter below (comment it out to disable entirely, just leaving empty will result in an error) will be automatically transferred from your exchange balance to your lending balance whenever you deposit them. 63 | #transferableCurrencies = STR,BTC,BTS,CLAM,DOGE,DASH,LTC,MAID,XMR,XRP,ETH,FCT 64 | #transferableCurrencies = USD,BTC,BCH,ETH,XRP,IOT,XMR,LTC,OMG,ETC,EOS,DSH,ZEC 65 | 66 | #Minimum loan size, the minimum size of offers to make, bigger values prevent the bot from loaning small available amounts but reduce loan fragmentation. 67 | minloansize = 0.01 68 | 69 | #Keep Stuck Orders - Sometimes an order gets partially filled. When this happens it may leave the remainder of your coin under the set minloansize. 70 | #If this happens, KeepStuckOrders will keep your order where it is so maybe it can be filled. Otherwise it will be canceled and held until orders expire. 71 | keepstuckorders = True 72 | 73 | #Hide coins - Instead of keeping your coins lent out at minlendrate when it is not met, the bot will hold them and wait for the rate to surpass it. 74 | hideCoins = True 75 | 76 | #End date for lending, bot will try to make sure all your loans are done by this date so you can withdraw or do whatever you need. 77 | #Uncomment to enable. 78 | #Format: YEAR,MONTH,DAY 79 | #endDate = 2016,12,25 80 | 81 | #Raw maximum amount to lend if under maxtolendrate. 82 | #If set to 0 or commented: the bot will check for maxpercenttolend.(0+) 83 | #maxtolend = 0 84 | 85 | #Maximum percent to lend if under maxtolendrate. 86 | #If set to 0 or commented: the bot will lend 100% of your balance regardless of rate. (0-100) 87 | #maxpercenttolend = 0 88 | 89 | #Max to lend conditional rate. 90 | #If set to more than 0: the maxtolend or maxpercenttolend will be used when the rate is less than or equal to the maxtolendrate. 91 | #If set to 0 or commented: the bot will use the maxtolend or maxpercenttolend all the time. (0.0031-5) 92 | #maxtolendrate = 0 93 | 94 | #FRR as min, tells the bot whether or not to use the flash return rate (https://support.bitfinex.com/hc/en-us/articles/213919009-What-is-the-Flash-Return-Rate-) 95 | #instead of mindailyrate. This only works on bitfinex. 96 | #frrasmin = False 97 | 98 | # The Flash Return Rate Delta (frrdelta) is a dynamic funding rate option that allows you to specify an offset from the Flash Return Rate. This only works on bitfinex and ffrasmin = TRUE 99 | frrdelta = 0.0000 100 | 101 | #This option creates a json log file instead of console output which includes the most recent status. 102 | #Uncomment both jsonfile and jsonlogsize to enable. 103 | #Keep this in the default location if you want to use the webserver. 104 | #jsonfile = www/botlog.json 105 | 106 | #Limits the amount of log lines to save. 107 | #jsonlogsize = 200 108 | 109 | #Enables a webserver for the www folder, in order to easily use the lendingbot.html with the .json log. 110 | #startWebServer = true 111 | 112 | #Customize the IP and port that the webserver is hosted on. Defaults respectively to 0.0.0.0 and 8000 113 | #0.0.0.0 will point to your default IP (Local IP on LAN as well as localhost), set to 127.0.0.1 if you want it to be only accessible by host computer. 114 | #Do not set to a reserved port 115 | #Advanced users only. 116 | #customWebServerAddress = 0.0.0.0 117 | #customWebServerPort = 8000 118 | 119 | #Customize or select the desired template for the webserver. Default to 'www'. 120 | #customWebServerTemplate = www 121 | 122 | #The currency that the HTML Overview will present the earnings summary in. 123 | #Options are BTC, USDT (USD on Bitfinex), ETH or anything as long as it has a direct BTC market. The default is BTC. 124 | #outputCurrency = BTC 125 | 126 | #Plugins allow extending Bot functionality with extra features. 127 | #plugins = AccountStats,Charts 128 | 129 | #[ACCOUNTSTATS] 130 | #ReportInterval = 86400 131 | 132 | #[CHARTS] 133 | #DumpInterval = 21600 134 | #HistoryFile = www/history.json 135 | 136 | # Currencies can be configured here. 137 | #[BTC] 138 | #minloansize = 0.01 139 | #mindailyrate = 0.18 140 | #maxactiveamount = 1 141 | #maxtolend = 0 142 | #maxpercenttolend = 0 143 | #maxtolendrate = 0 144 | #gapmode = RawBTC 145 | #gapbottom = 20 146 | #gaptop = 400 147 | #frrasmin = False 148 | #frrdelta = 0.0000 149 | 150 | #[CLAM] 151 | #minloansize = 1 152 | #mindailyrate = 0.6 153 | #maxactiveamount = 1 154 | #maxtolend = 0 155 | #maxpercenttolend = 0 156 | #maxtolendrate = 0 157 | #gapmode = raw 158 | #gapbottom = 10 159 | #gaptop = 20 160 | #frrasmin = False 161 | #frrdelta = 0.0000 162 | 163 | [notifications] 164 | notify_new_loans = False 165 | notify_tx_coins = False 166 | notify_xday_threshold = False 167 | notify_summary_minutes = 0 168 | notify_caught_exception = False 169 | #notify_prefix = [Polo] 170 | 171 | email = False 172 | email_login_address = me@gmail.com 173 | email_login_password = secretPassword 174 | email_smtp_server = smtp.gmail.com 175 | email_smtp_port = 465 176 | email_smtp_starttls = False 177 | email_to_addresses = me@gmail.com,you@gmail.com 178 | 179 | slack = False 180 | slack_token = 1234567890abcdef 181 | slack_channels = #cryptocurrency,@someUser 182 | slack_username = Poloniex Bot 183 | 184 | telegram = False 185 | telegram_bot_id = 1234567890abcdef 186 | telegram_chat_ids = @polopolo,@cryptocurrency 187 | 188 | pushbullet = False 189 | pushbullet_token = 1234567890abcdef 190 | pushbullet_deviceid = 1234567890abcdef 191 | 192 | irc = False 193 | irc_host = irc.freenode.net 194 | irc_port = 6667 195 | irc_nick = LendingBot 196 | irc_ident = ledningbot 197 | irc_realname = Poloniex lending bot 198 | irc_target = #bitbotfactory 199 | 200 | #[MarketAnalysis] 201 | # PLEASE refer to the docs before attempting to use any of this. There are a lot of things here that will not work 202 | # correctly unless you understand what you are doing. 203 | analyseCurrencies = STR,BTC,BTS,CLAM,DOGE,DASH,LTC,MAID,XMR,XRP,ETH,FCT 204 | lendingStyle = 75 205 | MACD_long_win_seconds = 1800 206 | #MACD_short_win_seconds = 150 207 | # 3 days = 60 * 60 * 24 * 3 = 259200 208 | percentile_seconds = 259200 209 | # keep_history_seconds > (greater of (percentile_seconds, MACD_long_win_seconds) * 1.1) 210 | #keep_history_seconds = 285120 211 | #recorded_levels = 10 212 | # 15 % means we need one data point every 9 seconds. You probably don't need to change this. 213 | #data_tolerance = 15 214 | #delete_thread_sleep = 60 215 | #ma_debug_log = False 216 | 217 | [Daily_min] 218 | # This defaults to percentile, MACD is the moving average calc and should give better rates 219 | #method = MACD 220 | multiplier = 1.05 221 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # start: docker-compose up -d 2 | # stop: docker-compose down 3 | # logs: docker-compose logs 4 | # state: docker-compose ps 5 | 6 | # This docker-compose file starts 3 docker containers. One for nginx and one for a bot on each exchange. 7 | # You can start as many bots as you want using the same config style and they will all have their web 8 | # interface accessible via the name you set of VIRTUAL_HOST 9 | 10 | # VIRTUAL_HOST can be changed to poloniex.yourdomain.com if you want it to be accessible via the internet 11 | 12 | # The environment variables included are the minimum required to get it working, you can change a lot of bot 13 | # configurations with them using the same naming convention, i.e. CATEGORY_OPTION. 14 | 15 | # Environment variables take precedence over the config file 16 | 17 | # If you have a more complicated setup, you can create a new config file from default.cfg and then use that in 18 | # the command line for each container. 19 | 20 | # If you want to use this on your local machine, without a domain name, you can add lines like this to /etc/hosts 21 | # 127.0.0.1 poloniex.localhost 22 | # 127.0.0.1 bitfinex.localhost 23 | 24 | version: '2' 25 | services: 26 | nginx: 27 | image: jwilder/nginx-proxy 28 | environment: 29 | - VIRTUAL_PORT=8000 30 | volumes: 31 | - /var/run/docker.sock:/tmp/docker.sock:ro 32 | ports: 33 | - "80:80" 34 | 35 | poloniex: 36 | depends_on: 37 | - nginx 38 | image: amancevice/pandas:0.23.4-python2 39 | restart: always 40 | working_dir: /usr/src/app/ 41 | environment: 42 | - API_exchange=poloniex 43 | - API_apikey= 44 | - API_secret= 45 | - VIRTUAL_HOST=poloniex.localhost 46 | - VIRTUAL_PROTO=http 47 | - BOT_jsonfile=www/botlog.json 48 | - BOT_jsonlogsize=200 49 | - BOT_startWebServer=true 50 | expose: 51 | - "8000" 52 | volumes: 53 | - "./:/usr/git-src/" 54 | command: bash -c "/usr/git-src/setup-container.sh && pip install -r requirements.txt && python lendingbot.py -cfg default.cfg" 55 | 56 | bitfinex: 57 | depends_on: 58 | - nginx 59 | image: amancevice/pandas:0.23.4-python2 60 | restart: always 61 | working_dir: /usr/src/app/ 62 | environment: 63 | - API_exchange=bitfinex 64 | - API_apikey= 65 | - API_secret= 66 | - VIRTUAL_HOST=bitfinex.localhost 67 | - VIRTUAL_PROTO=http 68 | - BOT_jsonfile=www/botlog.json 69 | - BOT_jsonlogsize=200 70 | - BOT_startWebServer=true 71 | expose: 72 | - "8000" 73 | volumes: 74 | - "./:/usr/git-src/" 75 | command: bash -c "/usr/git-src/setup-container.sh && pip install -r requirements.txt && python lendingbot.py -cfg default.cfg" 76 | -------------------------------------------------------------------------------- /docs/Documentation.url: -------------------------------------------------------------------------------- 1 | [{000214A0-0000-0000-C000-000000000046}] 2 | Prop3=19,2 3 | [InternetShortcut] 4 | IDList= 5 | URL=http://poloniexlendingbot.readthedocs.io/en/latest 6 | HotKey=0 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Poloniexlendingbot.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Poloniexlendingbot.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Poloniexlendingbot" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Poloniexlendingbot" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/README.txt: -------------------------------------------------------------------------------- 1 | To build the .html docs for your own viewing: 2 | 3 | 1) Install python sphinx. 4 | 2) Do 'make html' in this directory. 5 | 3) Wait a few seconds. 6 | 4) The made html files will be under /build_/html/ 7 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Poloniexlendingbot documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Oct 11 16:56:46 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | # 27 | # needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.todo', 34 | 'sphinx.ext.mathjax', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | # 48 | # source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'Poloniexlendingbot' 55 | copyright = u'2016, Mikadily and Contributors' 56 | author = u'Mikadily and Contributors' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = u'1' 64 | # The full version, including alpha/beta/rc tags. 65 | release = u'1' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | # 77 | # today = '' 78 | # 79 | # Else, today_fmt is used as the format for a strftime call. 80 | # 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | # This patterns also effect to html_static_path and html_extra_path 86 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 87 | 88 | # The reST default role (used for this markup: `text`) to use for all 89 | # documents. 90 | # 91 | # default_role = None 92 | 93 | # If true, '()' will be appended to :func: etc. cross-reference text. 94 | # 95 | # add_function_parentheses = True 96 | 97 | # If true, the current module name will be prepended to all description 98 | # unit titles (such as .. function::). 99 | # 100 | # add_module_names = True 101 | 102 | # If true, sectionauthor and moduleauthor directives will be shown in the 103 | # output. They are ignored by default. 104 | # 105 | # show_authors = False 106 | 107 | # The name of the Pygments (syntax highlighting) style to use. 108 | pygments_style = 'sphinx' 109 | 110 | # A list of ignored prefixes for module index sorting. 111 | # modindex_common_prefix = [] 112 | 113 | # If true, keep warnings as "system message" paragraphs in the built documents. 114 | # keep_warnings = False 115 | 116 | # If true, `todo` and `todoList` produce output, else they produce nothing. 117 | todo_include_todos = True 118 | 119 | 120 | # -- Options for HTML output ---------------------------------------------- 121 | 122 | # The theme to use for HTML and HTML Help pages. See the documentation for 123 | # a list of builtin themes. 124 | # 125 | html_theme = 'sphinx_rtd_theme' 126 | 127 | # Theme options are theme-specific and customize the look and feel of a theme 128 | # further. For a list of options available for each theme, see the 129 | # documentation. 130 | # 131 | # html_theme_options = {} 132 | 133 | # Add any paths that contain custom themes here, relative to this directory. 134 | # html_theme_path = [] 135 | 136 | # The name for this set of Sphinx documents. 137 | # " v documentation" by default. 138 | # 139 | # html_title = u'Poloniexlendingbot v1' 140 | 141 | # A shorter title for the navigation bar. Default is the same as html_title. 142 | # 143 | # html_short_title = None 144 | 145 | # The name of an image file (relative to this directory) to place at the top 146 | # of the sidebar. 147 | # 148 | # html_logo = None 149 | 150 | # The name of an image file (relative to this directory) to use as a favicon of 151 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 152 | # pixels large. 153 | # 154 | # html_favicon = None 155 | 156 | # Add any paths that contain custom static files (such as style sheets) here, 157 | # relative to this directory. They are copied after the builtin static files, 158 | # so a file named "default.css" will overwrite the builtin "default.css". 159 | html_static_path = ['_static'] 160 | 161 | # Add any extra paths that contain custom files (such as robots.txt or 162 | # .htaccess) here, relative to this directory. These files are copied 163 | # directly to the root of the documentation. 164 | # 165 | # html_extra_path = [] 166 | 167 | # If not None, a 'Last updated on:' timestamp is inserted at every page 168 | # bottom, using the given strftime format. 169 | # The empty string is equivalent to '%b %d, %Y'. 170 | # 171 | # html_last_updated_fmt = None 172 | 173 | # If true, SmartyPants will be used to convert quotes and dashes to 174 | # typographically correct entities. 175 | # 176 | # html_use_smartypants = True 177 | 178 | # Custom sidebar templates, maps document names to template names. 179 | # 180 | # html_sidebars = {} 181 | 182 | # Additional templates that should be rendered to pages, maps page names to 183 | # template names. 184 | # 185 | # html_additional_pages = {} 186 | 187 | # If false, no module index is generated. 188 | # 189 | # html_domain_indices = True 190 | 191 | # If false, no index is generated. 192 | # 193 | # html_use_index = True 194 | 195 | # If true, the index is split into individual pages for each letter. 196 | # 197 | # html_split_index = False 198 | 199 | # If true, links to the reST sources are added to the pages. 200 | # 201 | # html_show_sourcelink = True 202 | 203 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 204 | # 205 | # html_show_sphinx = True 206 | 207 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 208 | # 209 | # html_show_copyright = True 210 | 211 | # If true, an OpenSearch description file will be output, and all pages will 212 | # contain a tag referring to it. The value of this option must be the 213 | # base URL from which the finished HTML is served. 214 | # 215 | # html_use_opensearch = '' 216 | 217 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 218 | # html_file_suffix = None 219 | 220 | # Language to be used for generating the HTML full-text search index. 221 | # Sphinx supports the following languages: 222 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 223 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' 224 | # 225 | # html_search_language = 'en' 226 | 227 | # A dictionary with options for the search language support, empty by default. 228 | # 'ja' uses this config value. 229 | # 'zh' user can custom change `jieba` dictionary path. 230 | # 231 | # html_search_options = {'type': 'default'} 232 | 233 | # The name of a javascript file (relative to the configuration directory) that 234 | # implements a search results scorer. If empty, the default will be used. 235 | # 236 | # html_search_scorer = 'scorer.js' 237 | 238 | # Output file base name for HTML help builder. 239 | htmlhelp_basename = 'Poloniexlendingbotdoc' 240 | 241 | # -- Options for LaTeX output --------------------------------------------- 242 | 243 | latex_elements = { 244 | # The paper size ('letterpaper' or 'a4paper'). 245 | # 246 | # 'papersize': 'letterpaper', 247 | 248 | # The font size ('10pt', '11pt' or '12pt'). 249 | # 250 | # 'pointsize': '10pt', 251 | 252 | # Additional stuff for the LaTeX preamble. 253 | # 254 | # 'preamble': '', 255 | 256 | # Latex figure (float) alignment 257 | # 258 | # 'figure_align': 'htbp', 259 | } 260 | 261 | # Grouping the document tree into LaTeX files. List of tuples 262 | # (source start file, target name, title, 263 | # author, documentclass [howto, manual, or own class]). 264 | latex_documents = [ 265 | (master_doc, 'Poloniexlendingbot.tex', u'Poloniexlendingbot Documentation', 266 | u'Mikadily, Rnevet, Evanito and Contributors', 'manual'), 267 | ] 268 | 269 | # The name of an image file (relative to this directory) to place at the top of 270 | # the title page. 271 | # 272 | # latex_logo = None 273 | 274 | # For "manual" documents, if this is true, then toplevel headings are parts, 275 | # not chapters. 276 | # 277 | # latex_use_parts = False 278 | 279 | # If true, show page references after internal links. 280 | # 281 | # latex_show_pagerefs = False 282 | 283 | # If true, show URL addresses after external links. 284 | # 285 | # latex_show_urls = False 286 | 287 | # Documents to append as an appendix to all manuals. 288 | # 289 | # latex_appendices = [] 290 | 291 | # It false, will not define \strong, \code, itleref, \crossref ... but only 292 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 293 | # packages. 294 | # 295 | # latex_keep_old_macro_names = True 296 | 297 | # If false, no module index is generated. 298 | # 299 | # latex_domain_indices = True 300 | 301 | 302 | # -- Options for manual page output --------------------------------------- 303 | 304 | # One entry per manual page. List of tuples 305 | # (source start file, name, description, authors, manual section). 306 | man_pages = [ 307 | (master_doc, 'poloniexlendingbot', u'Poloniexlendingbot Documentation', 308 | [author], 1) 309 | ] 310 | 311 | # If true, show URL addresses after external links. 312 | # 313 | # man_show_urls = False 314 | 315 | 316 | # -- Options for Texinfo output ------------------------------------------- 317 | 318 | # Grouping the document tree into Texinfo files. List of tuples 319 | # (source start file, target name, title, author, 320 | # dir menu entry, description, category) 321 | texinfo_documents = [ 322 | (master_doc, 'Poloniexlendingbot', u'Poloniexlendingbot Documentation', 323 | author, 'Poloniexlendingbot', 'One line description of project.', 324 | 'Miscellaneous'), 325 | ] 326 | 327 | # Documents to append as an appendix to all manuals. 328 | # 329 | # texinfo_appendices = [] 330 | 331 | # If false, no module index is generated. 332 | # 333 | # texinfo_domain_indices = True 334 | 335 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 336 | # 337 | # texinfo_show_urls = 'footnote' 338 | 339 | # If true, do not generate a @detailmenu in the "Top" node's menu. 340 | # 341 | # texinfo_no_detailmenu = False 342 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ************ 3 | 4 | How to format Python Code 5 | ========================= 6 | 7 | If you want to make a successful pull request, `here are some suggestions. `_ 8 | 9 | Recommended IDE: `PyCharm `_ 10 | 11 | PEP8 12 | ---- 13 | 14 | Poloniex lending bot follows `PEP8 styling guidelines `_ to maximize code readability and maintenance. 15 | 16 | To help out users and automate much of the process, `the Codacy Continuous Integration bot `_ will comment on pull requests to alert you to any changes you need to make. 17 | Codacy has many inspections it does, which may extend past PEP8 conventions. We recommend you follow its suggestions as much as possible. 18 | 19 | To make following PEP8 as painless as possible, we strongly recommend using an Integrated Development Environment that features PEP8 suggestions, such as `PyCharm `_. 20 | 21 | Indent Style 22 | ------------ 23 | 24 | You may have your own preference, it does not matter because *spaces and tabs do not mix.* 25 | 26 | Poloniexlendingbot uses *spaces* to conform with PEP8 standards. Please use an IDE that can help you with this. 27 | 28 | Commenting Code 29 | --------------- 30 | 31 | Many coders learned to code without commenting their logic. 32 | That works if you are the only person working on the project, but quickly becomes a problem when it is your job to decipher what someone else was thinking when coding. 33 | 34 | You will probably be relieved to read that code comments are not mandatory, because `code comments are an apology. `_ 35 | 36 | Only comment your code if you need to leave a note. (We won't judge you for it.) 37 | 38 | Variable or Option Naming 39 | ------------------------- 40 | 41 | Whenever you create a variable or configuration option, follow PEP8 standards, they are as follows: 42 | 43 | Do not make global single-letter variable names, those do not help anybody. Using them within a function for a few lines in context is okay, but calling it much later requires it be given a proper name. 44 | 45 | Functions are named like ``create_new_offer()`` and variables are named similarly, like ``amount_of_lends``. 46 | 47 | Line Length 48 | ----------- 49 | 50 | To make it simple to review code in a diff viewer (and several other reasons) line length is limited to 128 characters in Python code. 51 | 52 | Python allows plenty of features for one line to be split into multiple lines, those are permitted. 53 | 54 | Configuration Options 55 | --------------------- 56 | 57 | New configuration options should be placed near similar options (see categories on the configuration page) and require a short description above the actual setting. 58 | 59 | If a setting is added that changes functionality, it is required that you add handling for having the option commented out. 60 | 61 | How to use the Configuration module: 62 | 63 | - If your change is in a new module, you need to init it to import the Config object. Create a function init() that set the args to global variables within the module. With this, pass Config from the main of the bot. 64 | - Use ``option = Config.get(CATEGORY, OPTION, DEFAULT_VALUE, LOWER_LIMIT, UPPER_LIMIT)`` to get the option from the Config. Only do this in your init() 65 | - CATEGORY: The category of the config file it goes under. Currently there is only 'API' and 'BOT' 66 | - OPTION: Case-sensitive name of the option you are pulling from the bot. 67 | - DEFAULT_VALUE: Default: False. This is the value that .get() will return if no value is set (option is commented). If set to "None": the bot will not allow it to be left blank ever. Optional. 68 | - LOWER_LIMIT: Default: False. The lower float value that the option can be set to. If OPTION's value is lesser than this, the bot will alert them and exit. Optional. Only use for numerical options. 69 | - UPPER_LIMIT: Default: False. The upper float value that the option can be set to. If OPTION's value is greater than this, the bot will alert them and exit. Optional. Only use for numerical options. 70 | 71 | ``Config.has_option(CATEGORY, OPTION)`` will always return a boolean for whether the option exists or not. If the option is commented it will return False. 72 | 73 | 74 | Making Documentation 75 | ==================== 76 | 77 | It is important to keep proper documentation of configuration options, to make it as clear as possible for the user. 78 | 79 | Building Docs 80 | ------------- 81 | 82 | If you want to be able to build the html files of the documentation, you need to have Sphinx installed. You can install this with ``pip install sphinx``. 83 | From there, run ``make html`` in the docs directory. These instructions can also be found in the included README. 84 | 85 | Writing Docs 86 | ------------ 87 | 88 | Just follow the lead of the rest of the docs. 89 | 90 | - Configurations need a default, allowed values, effect, etc. in a format similar to the other options. 91 | - Installation instructions should be similar to a followable list. 92 | 93 | 94 | Javascript 95 | ========== 96 | 97 | Codacy will offer suggestions for fixes to standardize/fix the code. Do not worry about having too many commits in your PR. 98 | 99 | Lendingbot.js is already quite messy, so following Codacy's suggestions is highly encouraged. 100 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Poloniexlendingbot documentation master file, created by 2 | sphinx-quickstart on Tue Oct 11 16:56:46 2016. 3 | 4 | Welcome to Poloniex Lending Bot's documentation! 5 | ================================================ 6 | 7 | Poloniex Lending Bot is an open-source program for automated lending on Poloniex and Bitfinex cryptocurrency exchange. 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :numbered: 13 | :maxdepth: 2 14 | 15 | installation 16 | configuration 17 | market_analysis 18 | contributing 19 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ************ 3 | 4 | Installing on a Computer 5 | ======================== 6 | 7 | Installing the bot on a computer is drag-and-drop and platform independent. 8 | 9 | Prerequisites 10 | ------------- 11 | 12 | You will need: 13 | 14 | - Python 2.7.x (Must be added to PATH) 15 | 16 | Recommended for easier use: 17 | 18 | - git 19 | - pip (to install following required Python modules) 20 | - Numpy (if using Analysis module) 21 | - requests (HTTPS communication) 22 | - pytz (Timezone calculations) 23 | 24 | It is possible to install all required Python modules **after downloading** of the bot running: 25 | 26 | ``pip install -r requirements.txt`` 27 | 28 | or, if you need to run it as root under Linux: 29 | 30 | ``sudo pip install -r requirements.txt`` 31 | 32 | Downloading 33 | ----------- 34 | 35 | To download the bot you can either: 36 | 37 | - (Recommended) Run ``git clone https://github.com/BitBotFactory/poloniexlendingbot`` if you have git installed. Using this method will allow you to do ``git pull`` at any time to grab updates. 38 | - Download the source .zip file from the GitHub repo page or from `this link `_. Extract it into an empty folder you won't accidentally delete. 39 | 40 | (Optional) Automatically Run on Startup 41 | --------------------------------------- 42 | 43 | * Windows using Startup Folder: 44 | 45 | Add a shortcut to ``lendingbot.py`` to the startup folder of the start menu. 46 | Its location may change with OS version, but for Windows 8/10 is ``C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp`` 47 | 48 | * Linux using systemd: 49 | 50 | Create the file ``/lib/systemd/system/lendingbot.service`` which contains the following text 51 | 52 | .. code-block:: text 53 | 54 | [Unit] 55 | Description=LendingBot service 56 | After=network.target 57 | 58 | [Service] 59 | Type=simple 60 | ExecStart=/usr/bin/python /lendingbot.py 61 | WorkingDirectory= 62 | RestartSec=10 63 | Restart=on-failure 64 | 65 | [Install] 66 | WantedBy=multi-user.target 67 | 68 | Credit to GitHub user utdrmac. 69 | 70 | 71 | The permission on the unit file may need to be set to 644 (depending on your OS) : 72 | 73 |    ``` 74 | sudo chmod 644 /lib/systemd/system/lendingbot.service 75 | ``` 76 | 77 | Modify the ExecStart and WorkingDirectory to match your setup. 78 | 79 | First you need to configure systemd (depending on your OS): 80 |    ``` 81 | sudo systemctl daemon-reload 82 | ``` 83 | 84 | Enable the service using the command below: 85 |    ``` 86 | sudo systemctl enable lendingbot.service 87 | ``` 88 | 89 | * OSx: 90 | 91 | Help needed! If you have a solution for OSx and would like to share, you can either share it directly with us or make a PR with the edits. 92 | 93 | Configuring 94 | ----------- 95 | 96 | You have to configure the bot, especially choosing the exchange and api key/secret to use. 97 | 98 | To configure the bot with your settings: 99 | 100 | #. Copy ``default.cfg.example`` to ``default.cfg`` (Running lendingbot.py also does this for you if default.cfg doesn't already exist.) 101 | #. Open ``default.cfg`` and enter your desired settings `(information on settings here) `_. 102 | #. Save ``default.cfg`` 103 | 104 | You are now ready to run the bot. 105 | 106 | Running 107 | ------- 108 | 109 | To run, either: 110 | 111 | - Double-click lendingbot.py (if you have .py associated with the Python executable) 112 | - Run ``python lendingbot.py`` in command prompt or terminal. 113 | 114 | .. note:: You can use arguments to specify a specific config file ``-cfg`` or to do dry runs ``-dry``. To see these args do: ``python lendingbot.py -h`` 115 | 116 | Installing on Pythonanywhere.com 117 | ================================ 118 | 119 | `Pythonanywhere.com `_ is a useful website that will host and run Python code for you. 120 | 121 | WARNING: While you should be able to setup the bot on pythonanywhere, there are limitations on running the bot. 122 | 123 | Prerequisites 124 | ------------- 125 | 126 | You will need: 127 | 128 | - A pythonanywhere.com account (Free version works fine) 129 | 130 | Downloading the bot's files to Pythonanywhere 131 | --------------------------------------------- 132 | 133 | #. Start a new ``bash`` console from the "Consoles" tab. 134 | #. Get the source code from git GitHub by running ``git clone https://github.com/Mikadily/poloniexlendingbot``. 135 | #. You should see some output with counters increasing. 136 | #. Change directory to the source code ``cd poloniexlendingbot`` 137 | #. You should now see ``~/poloniexlendingbot (master)$`` this means you are looking at the master branch and things are ok to continue. 138 | #. Run the command ``python2.7 lendingbot.py`` once to generate the default.cfg 139 | #. Modify the default.cfg with your settings (See `Configuration `_.) You can do this with a tool called nano. 140 | #. Run ``nano default.cfg``, then use the arrow keys and backspace key to change ``YourAPIKey`` and ``YourSecret``. Make sure the layout of the file stays the same as it was. They should both be on separate lines. 141 | #. Press ``Ctr+x`` to exit, then press ``y`` to save the file, then press enter to accept the file name as ``default.cfg``. 142 | #. Now you can start up the bot. Run ``python2.7 lendingbot.py`` 143 | #. If it's working you will see ``Welcome to Poloniex Lending Bot`` displayed in the console. 144 | #. To update the bot just enter its directory, ``cd poloniexlendingbot`` and type, ``git pull``. This will not change the ``default.cfg`` file. 145 | 146 | .. note:: If you are running out of CPU time every day: It is recommended to use a high sleeptimeinactive time for this website, as they meter your CPU usage. 147 | 148 | Creating the Web App (Optional) 149 | ------------------------------- 150 | 151 | #. If you would like to use the Webserver to view your bot's status, navigate to the "Web" tab. 152 | #. Add a new web app. 153 | #. Set the working directory to ``/home//poloniexlendingbot/www/`` 154 | #. Set the static files to URL: ``/static/`` Directory: ``/home//poloniexlendingbot/www`` 155 | #. Reload your website with the button at the top of the page. 156 | #. You will be able to access the webapp at ``http://.pythonanywhere.com/static/lendingbot.html`` once it finishes setting up. 157 | #. To have the webserver communicate with your bot, you need to edit your settings (``default.cfg``) and uncomment (remove the ``#`` in front of) the following settings: ``jsonfile`` and ``jsonlogsize``. Make sure that ``startWebServer`` REMAINS commented. 158 | 159 | 160 | .. warning:: Do not use the built-in Simple Web Server on any host you do not control. 161 | 162 | Running the Bot 163 | --------------- 164 | 165 | To run the bot continuously (Recommended for free accounts): 166 | 167 | #. Navigate to the "Consoles" tab. 168 | #. Add a new "Custom console," name it "Poloniexlendingbot" and set the path to ``python /home//poloniexlendingbot/lendingbot.py`` 169 | #. Click this link whenever you want to start the bot, it will run continuously until the website goes down for maintenance or the bot experiences an unexpected error. 170 | 171 | To have the bot restart itself every 24 hours, you need to have a `premium pythonanywhere account `_. This will make the bot more or less invincible to crashes and resets, but is not necessary. 172 | 173 | #. Navigate to the "Schedule" tab. 174 | #. Create a new task to run daily (time does not matter) set the path to: ``python /home//poloniexlendingbot/lendingbot.py`` 175 | #. The bot will start once the time comes (UTC) and run indefinitely. 176 | 177 | .. note:: If you are a free user, it will allow you to make the scheduled restart, but then it will only run for one hour and stop for 23. 178 | .. note:: Free users are also limited to the number of output currencies they can use as blockchain.info is blocked from their servers. You can always use the pairs listed on poloniex, BTC, USDT. But will not have access to currencies such as EUR, GBP. 179 | 180 | Using Docker Compose 181 | ==================== 182 | 183 | There is a ``docker-compose.yaml`` file in the root of the source that can be used to start the bot via `docker `_. Compose is a tool for defining and running docker applications using a single file to configure the application’s services. 184 | 185 | By default this file will start 3 containers: 186 | 187 | - An nginx reverse proxy 188 | This allows you to have the nginx web server as the main access point for the other bot's web pages. 189 | It uses `jwilder/nginx-proxy `_ 190 | - A python container running the bot on poloniex. 191 | This starts a bot running that connects to poloniex and exposes a web interface. 192 | It uses `python:2.7-slim `_ 193 | - A python container running the bot on bitfinex. 194 | This starts a bot running that connects to bitfinex and exposes a web interface. 195 | It uses `python:2.7-slim `_ 196 | 197 | This allows for simple deployments on a VPS or dedicated server. Each bot will be dynamically assinged a subdomain. 198 | You can also use it to run the bots locally using subdomains. 199 | 200 | To use this file:- 201 | 202 | #. Install and setup `docker `_ for your platform, available on linux, mac and windows. 203 | #. If you are using linux or windows server, you'll need to install docker-compose separately, see `here `_. 204 | #. If you don't already have a ``default.cfg`` created, then copy the example one and change the values as required using the instructions in this document. 205 | #. Edit the ``docker-compose.yaml`` file and add your ``API_apikey`` and ``API_apisecret`` for each exchange. If you wish to use only one exchange, you can comment out all the lines for the one you don't need. 206 | #. If you are running locally, you can leave the ``VIRTUAL_HOST`` variable as it is. If you are running on a web server with your won domain, you can set it to something like ``poloniex.mydomain.com``. 207 | #. If you don't have a domain name, you can use a service such as `duckdns `_ to get one for free. 208 | #. You can now start the service with ``docker-compose up -d``. It may take a minute or two on the first run as it has to download the required image and then some packages for that image when it starts. 209 | #. If all went well you should see something like ``Starting bitbotfactory_bot_1``. 210 | #. When you see that message it just means that the container was started successfully, we still need to check the application is running as expected. In the yaml file the web service in the container is mapped to localhost. So you can open your web browser at this point and see if you can connect to the serivce. It should be runnning on ``_. You should see an nginx welcome page. 211 | #. If you don't see anything when connecting to that you can check the logs of the container with ``docker-compose logs``. You should get some useful information from there. Ask on Slack if you're stuck. 212 | #. If you are running locally you will need to add the subdomains to your hosts file to make sure they are resolved by DNS. You can ignore this step if you're running on a web server. On linux (and recent OSx) you can add these lines to ``/etc/hosts``, on windows you shoud follow this `guide `_ 213 | 214 | .. code-block:: text 215 | 216 | 127.0.0.1 poloniex.localhost 217 | 127.0.0.1 bitfinex.localhost 218 | 219 | #. You should now be able to point your browser at ``_ and ``_ to see the web pages for each bot. 220 | 221 | Extending the file:- 222 | 223 | - Most config values from default.cfg can be overridden in the docker-compose file. You should add them in the enviroment section in the same format as the ones listed. i.e. ``Category_Option`` 224 | - You can add as many extra bots as you want. Each one will need to have a new ``VIRTUAL_HOST`` entry. 225 | - If you prefer to have everything in config files rather than enviroment variables, you can create a new cfg file for each bot and modify the ``command`` line to use that cfg file instead. 226 | 227 | Other info:- 228 | 229 | - Each bot will create a log file in the root of your git checkout. 230 | - If you are using market analysis, you only need one bot per exchange. Extra bots will be able to share the database. 231 | - When you change the config values you need to restart the container, this can be done with ``docker-compose stop`` and then after changing configs, ``docker-compose up -d``. You should notice it's significantly quicker than the first run now. 232 | - The last command to note is ``docker-compose ps`` this will give infomation on all running instances and the ports that are mapped. This can be useful if you plan on running multiple bots, or you just want to know if it's running. 233 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Poloniexlendingbot.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Poloniexlendingbot.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/market_analysis.rst: -------------------------------------------------------------------------------- 1 | .. _market_analysis-section: 2 | 3 | Market Analysis 4 | --------------- 5 | 6 | Overview 7 | `````````` 8 | This feature records a currency's market and allows the bot see trends. With this data, we can compute a recommended minimum lending rate per currency to avoid lending at times when the rate dips. 9 | 10 | When this module is enabled it will start recording the lending rates for the market in an sqlite database. This will be seen in the market_data folder for your bot. This supersedes the previous method of storing it in a file. The files can be removed if you have them from older versions of the bot. 11 | 12 | There will be a DB created for each currency you wish to record. These can be enabled in the `analyseCurrencies`_ configuration option. 13 | 14 | .. warning:: The more currencies you record, the more data stored on disk and CPU processing time will be used. You will also not get as frequent results for the currencies, i.e. You may have trouble getting results for your configured ``analyseUpdateInterval`` This is explained further in the `Recording currencies`_ section. 15 | 16 | A quick list of each config option and what they do 17 | 18 | ========================= ============================================================================================= 19 | `analyseCurrencies`_ A list of each currency you wish to record and analyse 20 | `analyseUpdateInterval`_ The frequency between rates requested and stored in the DB 21 | `lendingStyle`_ The percentage used for the percentile calculation 22 | `percentile_seconds`_ The number of seconds to analyse when working out the percentile 23 | `MACD_long_win_seconds`_ The number of seconds to used for the long moving average 24 | `MACD_short_win_seconds`_ The number of seconds to used for the short moving average 25 | `keep_history_seconds`_ The age (in seconds) of the oldest data you wish to keep in the DB 26 | `recorded_levels`_ The depth of the lending book to record in the DB, i.e. how many unfilled loans 27 | `data_tolerance`_ The percentage of data that can be ignore as missing for the time requested in 28 | ``percentile_seconds`` and ``MACD_long_win_seconds`` 29 | `daily_min_method`_ Which method (MACD or percentile) to use for the daily min calculation 30 | `MACD_multiplier`_ Only valid for MACD method. The figure to scale up the returned rate value from the MACD calculation 31 | `ma_debug_log`_ Print some extra info on what's happening with the rate calculations 32 | ========================= ============================================================================================= 33 | 34 | The module has two main methods to calculate the minimum rate: 35 | 36 | Percentile 37 | `````````` 38 | This method takes all the data for the given time period (`percentile_seconds`_) and works out the Xth percentile (`lendingStyle`_) for that set of data. For example if you are using a ``lendingStyle`` of 85 and you had a list of rates like so 39 | 40 | :Example: 0.04, 0.04, 0.05, 0.05, 0.05, 0.05, 0.06, 0.06, 0.06, 0.07, 0.07, 0.07, 0.08, 0.08, 0.09, 0.09, 0.09, 0.10, 0.10, 0.10 41 | 42 | The 85th percentile would be 0.985 because 85% of rates are below this. The following configuration options should be considered when using the percentile calculation method:- 43 | 44 | 45 | MACD 46 | ```` 47 | Moving Average Convergence Divergence, this method using moving averages to work out if it's a good time to lend or not. Currently this is only implemented to limit the minimum daily rate for a currency. This will be changing in the future. 48 | It by looking at the best rate that is available from the recorded market data for two windows, the long and short window, then taking an average of them both. If the short average is higher than the long average then it considers the market to be in a good place to lend (as the trend for rates is going up) and it will return a `suggested loan rate`_. If the long window is greater than the short window, then we will not lend as trend for rates is below what it should be. 49 | So for example: 50 | 51 | ===== ===== ==== ========= 52 | Time Short Long Suggested 53 | ===== ===== ==== ========= 54 | 12:00 0.08 0.1 0.1 55 | 12:01 0.09 0.1 0.1 56 | 12:02 0.1 0.1 0.105 57 | 12:03 0.11 0.1 0.1155 58 | 12:04 0.12 0.1 0.126 59 | ===== ===== ==== ========= 60 | 61 | In this example, the bot would start to lend at 12:02 and it would suggest a minimum lending rate of 0.1 * `MACD_multiplier`_, which by default is 1.05. Giving a rate of 0.105. This is then passed back to the main lendingbot where it will use your gaptop and gapbottom, along with spreads and all the other smarts to place loan offers. 62 | 63 | Currently using this method gives the best results with well configured gaptop and gapbottom. This allows you to catch spikes in the market as see above. 64 | 65 | The short window and long window are configured by a number of seconds, the data is then taken from the DB requesting `MACD_long_win_seconds`_ * 1.1. This is to get an extra 10% of data as there is usually some lost in the recording from Poloniex. 66 | You can also use the `data_tolerance`_ to help with the amount of data required by the bot for this calculation, that is the number of seconds that can be missing for the data to still be valid. 67 | 68 | This current implementation is basic in it's approach, but will be built upon with time. Results seem to be good though and we would welcome your feedback if you play around with it. 69 | 70 | suggested loan rate 71 | ''''''''''''''''''' 72 | If the average of the short window is greater than the average of the long window we will return the current 73 | 74 | configuring 75 | ''''''''''' 76 | 77 | The number of config options and combinations for this can be quite daunting. As time goes on I hope more people will feed back useful figures for all our different configuration set ups. I have put in sensible defaults into the config for the MACD section. These are options that I have changed that aren't set by default and work better if you're using MACD as the rate calculation method. Change the currency to whatever you want, though best not use more than 3 really, as it slows down the calls to poloniex considerably. If you can use just one, then do it. 78 | 79 | I'm hoping that once more people test and report back results, this can be updated and more information passed to everyone else. 80 | 81 | The most important is probably the hidecoins change to False. This means that it will always place loans so you don't need to have as low a resolution on the sleep timers. You also want to make sure the gaptop and gapbottom are high so you can get a large spread. 82 | 83 | ======================= ========= 84 | Config Value 85 | ======================= ========= 86 | sleeptimeactive 10 87 | sleeptimeinactive 10 88 | spreadlend 3 89 | gapMode RawBTC 90 | gapbottom 40 91 | gaptop 200 92 | hideCoins False 93 | analyseCurrencies ETH,BTC 94 | ======================= ========= 95 | 96 | notes 97 | ''''' 98 | - MACD will default back to the percentile method if it can't function. This will happen at start up for a while when it's collecting data and can also happen if something goes wrong with the Database or other failures. It's basically a failsafe to make sure you're still using some sort of market analsis while MACD is offline. 99 | - You can turn on `ma_debug_log`_ to get some more information if things aren't working. 100 | - When it's start up you will see ``Need more data for analysis, still collecting. I have Y/X records``, so long as it's still increasing then this is fine. If it always prints that message then you should change your `data_tolerance`_ 101 | 102 | 103 | 104 | Recording currencies 105 | ```````````````````` 106 | 107 | All the options in this section deal with how data from poloniex is collected and stored. All the data is stored in an sqlite database, one per currency that you are recording. You can see the database files in the market_data folder of the bot. 108 | There are a number of things to consider before configuring this section. The most important being that you can only make 6 api calls to poloniex every second. This limit includes returning your open loans, placing an loan and returning data for the live market to store in the database. 109 | 110 | .. warning:: If you start to see the error message: ``HTTP Error 429: Too Many Requests`` then you need to review the settings in this file. In theory this shouldn't be a problem as our API limits calls to 6 per second. But it appears that it's not completely thread safe, so it can sometimes make more than 6 per second. 111 | If this happens, stop the bot. Increase your timer or decrease the number of recorded currencies, wait a five minutes, then start the bot again. Repeat as required. 112 | 113 | analyseCurrencies 114 | ''''''''''''''''' 115 | 116 | ``analyseCurrencies`` is the list of currencies to record (and analyse) 117 | 118 | None of the points below need be considered problematic unless you are planning to run with low (single digit seconds) timers on the bot. That is, the ``sleeptimeinactive``, ``sleeptimeactive`` and the ``analyseUpdateInterval``. 119 | 120 | With that said, every currency you add to this will: 121 | 122 | - Increase the number of db files (and therefore disk usage) 123 | - Increase I/O and CPU usage (each currency will be writing to disk and if there's a balance, calculating the best rate) 124 | - Reduce the number of requests you can make the API per second. This means times between stored records in the DB will be further apart and calls to place loans to Poloniex will be slower. 125 | 126 | configuration 127 | ~~~~~~~~~~~~~ 128 | ========== =========================================================================================================== 129 | Format ``CURRENCY_TICKER,STR,BTC,BTS,CLAM,DOGE,DASH,LTC,MAID,XMR,XRP,ETH,FCT,ALL,ACTIVE`` 130 | Disabling Commenting it out will disable the entire feature. 131 | ``ACTIVE`` Entering ``ACTIVE`` analyses any currencies found in your lending account along with any other configured currencies. 132 | ``ALL`` Will analyse all coins on the lending market, whether or not you are using them. 133 | Example ``ACTIVE, BTC, CLAM`` will record and analyse BTC, CLAM, and any coins you are already lending. 134 | Notes Don't worry about duplicates when using ``ACTIVE``, they are handled internally. 135 | ========== =========================================================================================================== 136 | 137 | keep_history_seconds 138 | '''''''''''''''''''' 139 | ``keep_history_seconds`` is the maximum duration to store market data. Any data that is older that this number of seconds will be deleted from the DB. 140 | This delete runs periodically, so it is possible for the there to be data older than the specified age in the database, however it won't be there for long. 141 | 142 | configuration 143 | ~~~~~~~~~~~~~ 144 | ============= ======================================================================================================== 145 | Default value 86400 (1 day) 146 | Allowed range 3600+ 147 | ============= ======================================================================================================== 148 | 149 | analyseUpdateInterval 150 | ''''''''''''''''''''' 151 | 152 | ``analyseUpdateInterval`` is how long the bot will sleep between requests for rate data from Poloniex. Each coin has it's own thread for requests and each thread has it's own sleep. 153 | You are not guaranteed to get data at exactly the update interval. Setting it to 1 second, with several currencies 154 | each one of them will take up one of the 6 API calls that are allowed per second. These calls need to be used to place 155 | loans and other interactions with poloniex. 156 | Also, it can take some time to get data back from poloniex, because there is a single thread making all the requests 157 | per currency, it will block the next request. I did have a multi threaded model for this currency recording, but it 158 | frequently created too many threads when polo was lagging, causing more harm than good. 159 | 160 | configuration 161 | ~~~~~~~~~~~~~ 162 | ============= ======================================================================================================== 163 | Default value 10 164 | Allowed range 1 - 3600 (1 hour) 165 | ============= ======================================================================================================== 166 | 167 | 168 | recorded_levels 169 | ''''''''''''''' 170 | 171 | ``recorded_levels`` is the number of rates found in the current offers on poloniex that will be recorded in the db. 172 | There is currently no reason to set this greater than 1 as we're not using the rest of the levels, this will change in the future though. You can raise it if you're examining the data yourself also. 173 | 174 | configuration 175 | ~~~~~~~~~~~~~ 176 | ============= ======================================================================================================== 177 | Default value 10 178 | Allowed range 1 - 100 179 | ============= ======================================================================================================== 180 | 181 | 182 | 183 | Analysing currencies 184 | ```````````````````` 185 | Everything in this section relates to how the analysis is carried out. So how much data is used and how it is used. 186 | 187 | lendingStyle 188 | '''''''''''' 189 | 190 | ``lendingStyle`` lets you choose the percentile of each currency's market to lend at. 191 | 192 | - Recommendations: Conservative = 50, Moderate = 75, Aggressive = 90, Very Aggressive = 99 193 | - This is a percentile, so choosing 75 would mean that your minimum will be the value that the market is above 25% of the recorded time. 194 | - This will stop the bot from lending during a large dip in rate, but will still allow you to take advantage of any spikes in rate. 195 | 196 | ============= ======================================================================================================== 197 | Default value 75 198 | Allowed range 1-99 199 | ============= ======================================================================================================== 200 | 201 | 202 | percentile_seconds 203 | '''''''''''''''''' 204 | 205 | ``percentile_seconds`` is the number of seconds worth of data to use for the percentile calculation. This value is not used in `MACD`_ methods. 206 | 207 | configuration 208 | ~~~~~~~~~~~~~ 209 | ============= ======================================================================================================== 210 | Default value 86400 211 | Allowed range 300 - ``keep_history_seconds`` 212 | ============= ======================================================================================================== 213 | 214 | 215 | MACD_long_win_seconds 216 | ''''''''''''''''''''' 217 | 218 | ``MACD_long_win_seconds`` is the number of seconds used for the long window average in the `MACD`_ method. 219 | 220 | configuration 221 | ~~~~~~~~~~~~~ 222 | ============= ======================================================================================================== 223 | Default value 1800 (30 minutes) 224 | Allowed range 300 - ``keep_history_seconds`` 225 | ============= ======================================================================================================== 226 | 227 | 228 | MACD_short_win_seconds 229 | '''''''''''''''''''''' 230 | 231 | ``MACD_short_win_seconds`` is the number of seconds used for the short window average in the `MACD`_ method. 232 | 233 | configuration 234 | ~~~~~~~~~~~~~ 235 | ============= ======================================================================================================== 236 | Default value 150 (2.5 minutes) 237 | Allowed range 25 - ``MACD_long_win_seconds`` 238 | ============= ======================================================================================================== 239 | 240 | 241 | data_tolerance 242 | '''''''''''''' 243 | 244 | ``data_tolerance`` is the percentage of data that can be missed from poloniex and still considered that we have enough data to work with. 245 | This was added because there are frequently problems with poloniex sending back data, also it's not always possible to get all the data you want if you are using multiple currencies. We are limited to 6 calls to poloniex every second. 246 | 247 | If you keep seeing messages saying ``Need more data for analysis, still collecting. I have Y/X records``, then you 248 | need to reduce this or reduce the number of currencies you are analysing. 249 | 250 | configuration 251 | ~~~~~~~~~~~~~ 252 | ============= ======================================================================================================== 253 | Default value 15 254 | Allowed range 10 - 90 255 | ============= ======================================================================================================== 256 | 257 | 258 | daily_min_method 259 | '''''''''''''''' 260 | 261 | ``daily_min_method`` is the method in which you wish to calculate the daily_min for each currency. This is how we stop lending when the market rates are below average. 262 | This can be either MACD or percentile. See `MACD`_ and `Percentile`_ sections for more information. 263 | This will not change the `mindailyrate` that you have set for coins in the main config. So you will still never lend below what you have statically configured. 264 | 265 | configuration 266 | ~~~~~~~~~~~~~ 267 | ============== ======================================================================================================== 268 | Default value MACD 269 | Allowed values MACD, percentile 270 | ============== ======================================================================================================== 271 | 272 | 273 | 274 | MACD_multiplier 275 | ''''''''''''''' 276 | 277 | ``MACD_multiplier`` is what to scale up the returned average from the MACD calculation by. See `MACD`_ for more details. 278 | In the future this will probably be removed in favour of sending back spread information that can be used for gaptop and gapbottom. 279 | 280 | configuration 281 | ~~~~~~~~~~~~~ 282 | ============= ======================================================================================================== 283 | Default value 1.05 284 | Allowed range 1 - 2 285 | ============= ======================================================================================================== 286 | 287 | 288 | ma_debug_log 289 | '''''''''''' 290 | 291 | ``ma_debug_log`` when enabled will print to screen some of the internal information around the MACD and percentile calculations 292 | 293 | configuration 294 | ~~~~~~~~~~~~~ 295 | ============= ======================================================================================================== 296 | Default value False 297 | Allowed range True, False 298 | ============= ======================================================================================================== 299 | -------------------------------------------------------------------------------- /lendingbot.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import argparse 3 | import os 4 | import sys 5 | import time 6 | import traceback 7 | import socket 8 | from decimal import Decimal 9 | from httplib import BadStatusLine 10 | from urllib2 import URLError 11 | 12 | import modules.Configuration as Config 13 | import modules.Data as Data 14 | import modules.Lending as Lending 15 | import modules.MaxToLend as MaxToLend 16 | from modules.Logger import Logger 17 | import modules.PluginsManager as PluginsManager 18 | from modules.ExchangeApiFactory import ExchangeApiFactory 19 | from modules.ExchangeApi import ApiError 20 | 21 | 22 | try: 23 | open('lendingbot.py', 'r') 24 | except IOError: 25 | os.chdir(os.path.dirname(sys.argv[0])) # Allow relative paths 26 | 27 | parser = argparse.ArgumentParser() # Start args. 28 | parser.add_argument("-cfg", "--config", help="Location of custom configuration file, overrides settings below") 29 | parser.add_argument("-dry", "--dryrun", help="Make pretend orders", action="store_true") 30 | args = parser.parse_args() # End args. 31 | 32 | # Start handling args. 33 | dry_run = bool(args.dryrun) 34 | if args.config: 35 | config_location = args.config 36 | else: 37 | config_location = 'default.cfg' 38 | # End handling args. 39 | 40 | # Config format: Config.get(category, option, default_value=False, lower_limit=False, upper_limit=False) 41 | # A default_value "None" means that the option is required and the bot will not run without it. 42 | # Do not use lower or upper limit on any config options which are not numbers. 43 | # Define the variable from the option in the module where you use it. 44 | 45 | Config.init(config_location) 46 | 47 | output_currency = Config.get('BOT', 'outputCurrency', 'BTC') 48 | end_date = Config.get('BOT', 'endDate') 49 | exchange = Config.get_exchange() 50 | 51 | json_output_enabled = Config.has_option('BOT', 'jsonfile') and Config.has_option('BOT', 'jsonlogsize') 52 | jsonfile = Config.get('BOT', 'jsonfile', '') 53 | 54 | # Configure web server 55 | web_server_enabled = Config.getboolean('BOT', 'startWebServer') 56 | if web_server_enabled: 57 | if json_output_enabled is False: 58 | # User wants webserver enabled. Must have JSON enabled. Force logging with defaults. 59 | json_output_enabled = True 60 | jsonfile = Config.get('BOT', 'jsonfile', 'www/botlog.json') 61 | 62 | import modules.WebServer as WebServer 63 | WebServer.initialize_web_server(Config) 64 | 65 | # Configure logging 66 | log = Logger(jsonfile, Decimal(Config.get('BOT', 'jsonlogsize', 200)), exchange) 67 | 68 | # initialize the remaining stuff 69 | api = ExchangeApiFactory.createApi(exchange, Config, log) 70 | MaxToLend.init(Config, log) 71 | Data.init(api, log) 72 | Config.init(config_location, Data) 73 | notify_conf = Config.get_notification_config() 74 | if Config.has_option('MarketAnalysis', 'analyseCurrencies'): 75 | from modules.MarketAnalysis import MarketAnalysis 76 | # Analysis.init(Config, api, Data) 77 | analysis = MarketAnalysis(Config, api) 78 | analysis.run() 79 | else: 80 | analysis = None 81 | Lending.init(Config, api, log, Data, MaxToLend, dry_run, analysis, notify_conf) 82 | 83 | # load plugins 84 | PluginsManager.init(Config, api, log, notify_conf) 85 | # Start dns cache managing 86 | prv_getaddrinfo = socket.getaddrinfo 87 | dns_cache = {} # or a weakref.WeakValueDictionary() 88 | 89 | 90 | def new_getaddrinfo(*urlargs): 91 | """Overloads the default socket dns resolution to have a cache, 92 | resets at the beginning of each loop. 93 | https://stackoverflow.com/questions/2236498/tell-urllib2-to-use-custom-dns""" 94 | try: 95 | return dns_cache[urlargs] 96 | except KeyError: 97 | res = prv_getaddrinfo(*urlargs) 98 | dns_cache[urlargs] = res 99 | return res 100 | 101 | 102 | socket.getaddrinfo = new_getaddrinfo 103 | 104 | print 'Welcome to ' + Config.get("BOT", "label", "Lending Bot") + ' on ' + exchange 105 | 106 | try: 107 | while True: 108 | try: 109 | dns_cache = {} # Flush DNS Cache 110 | Data.update_conversion_rates(output_currency, json_output_enabled) 111 | PluginsManager.before_lending() 112 | Lending.transfer_balances() 113 | Lending.cancel_all() 114 | Lending.lend_all() 115 | PluginsManager.after_lending() 116 | log.refreshStatus(Data.stringify_total_lent(*Data.get_total_lent()), 117 | Data.get_max_duration(end_date, "status")) 118 | log.persistStatus() 119 | sys.stdout.flush() 120 | time.sleep(Lending.get_sleep_time()) 121 | except KeyboardInterrupt: 122 | # allow existing the main bot loop 123 | raise 124 | except Exception as ex: 125 | log.log_error(ex.message) 126 | log.persistStatus() 127 | if 'Invalid API key' in ex.message: 128 | print "!!! Troubleshooting !!!" 129 | print "Are your API keys correct? No quotation. Just plain keys." 130 | exit(1) 131 | elif 'Nonce must be greater' in ex.message: 132 | print "!!! Troubleshooting !!!" 133 | print "Are you reusing the API key in multiple applications? Use a unique key for every application." 134 | exit(1) 135 | elif 'Permission denied' in ex.message: 136 | print "!!! Troubleshooting !!!" 137 | print "Are you using IP filter on the key? Maybe your IP changed?" 138 | exit(1) 139 | elif 'timed out' in ex.message: 140 | print "Timed out, will retry in " + str(Lending.get_sleep_time()) + "sec" 141 | elif isinstance(ex, BadStatusLine): 142 | print "Caught BadStatusLine exception from Poloniex, ignoring." 143 | elif 'Error 429' in ex.message: 144 | additional_sleep = max(130.0-Lending.get_sleep_time(), 0) 145 | sum_sleep = additional_sleep + Lending.get_sleep_time() 146 | log.log_error('IP has been banned due to many requests. Sleeping for {} seconds'.format(sum_sleep)) 147 | if Config.has_option('MarketAnalysis', 'analyseCurrencies'): 148 | if api.req_period <= api.default_req_period * 1.5: 149 | api.req_period += 1000 150 | if Config.getboolean('MarketAnalysis', 'ma_debug_log'): 151 | print("Caught ERR_RATE_LIMIT, sleeping capture and increasing request delay. Current" 152 | " {0}ms".format(api.req_period)) 153 | log.log_error('Expect this 130s ban periodically when using MarketAnalysis, it will fix itself') 154 | time.sleep(additional_sleep) 155 | # Ignore all 5xx errors (server error) as we can't do anything about it (https://httpstatuses.com/) 156 | elif isinstance(ex, URLError): 157 | print "Caught {0} from exchange, ignoring.".format(ex.message) 158 | elif isinstance(ex, ApiError): 159 | print "Caught {0} reading from exchange API, ignoring.".format(ex.message) 160 | else: 161 | print traceback.format_exc() 162 | print "v{0} Unhandled error, please open a Github issue so we can fix it!".format(Data.get_bot_version()) 163 | if notify_conf['notify_caught_exception']: 164 | log.notify("{0}\n-------\n{1}".format(ex, traceback.format_exc()), notify_conf) 165 | sys.stdout.flush() 166 | time.sleep(Lending.get_sleep_time()) 167 | 168 | 169 | except KeyboardInterrupt: 170 | if web_server_enabled: 171 | WebServer.stop_web_server() 172 | PluginsManager.on_bot_exit() 173 | log.log('bye') 174 | print 'bye' 175 | os._exit(0) # Ad-hoc solution in place of 'exit(0)' TODO: Find out why non-daemon thread(s) are hanging on exit 176 | -------------------------------------------------------------------------------- /market_data/readme.txt: -------------------------------------------------------------------------------- 1 | This folder holds market data for any coin lending markets you have chosen to record and analyse. 2 | The bot automatically removes lines that are older than the setting for maximum age. 3 | 4 | Try not to delete this data. -------------------------------------------------------------------------------- /modules/Bitfinex.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import hashlib 3 | import hmac 4 | import base64 5 | import json 6 | import requests 7 | import time 8 | import threading 9 | 10 | from modules.ExchangeApi import ExchangeApi 11 | from modules.ExchangeApi import ApiError 12 | from modules.Bitfinex2Poloniex import Bitfinex2Poloniex 13 | from modules.RingBuffer import RingBuffer 14 | 15 | 16 | class Bitfinex(ExchangeApi): 17 | def __init__(self, cfg, log): 18 | super(Bitfinex, self).__init__(cfg, log) 19 | self.cfg = cfg 20 | self.log = log 21 | self.lock = threading.RLock() 22 | self.req_per_period = 1 23 | self.default_req_period = 5000 # milliseconds, 1000 = 60/min 24 | self.req_period = self.default_req_period 25 | self.req_time_log = RingBuffer(self.req_per_period) 26 | self.url = 'https://api.bitfinex.com' 27 | self.key = self.cfg.get("API", "apikey", None) 28 | self.secret = self.cfg.get("API", "secret", None) 29 | self.apiVersion = 'v1' 30 | self.symbols = [] 31 | self.ticker = {} 32 | self.tickerTime = 0 33 | self.baseCurrencies = ['USD', 'BTC', 'ETH'] 34 | self.all_currencies = self.cfg.get_all_currencies() 35 | self.usedCurrencies = [] 36 | self.timeout = int(self.cfg.get("BOT", "timeout", 30, 1, 180)) 37 | self.api_debug_log = self.cfg.getboolean("BOT", "api_debug_log") 38 | # Initialize usedCurrencies 39 | _ = self.return_available_account_balances("lending") 40 | 41 | @property 42 | def _nonce(self): 43 | """ 44 | Returns a nonce 45 | Used in authentication 46 | """ 47 | return str(int(time.time() * 100000)) 48 | 49 | def limit_request_rate(self): 50 | super(Bitfinex, self).limit_request_rate() 51 | 52 | def increase_request_timer(self): 53 | super(Bitfinex, self).increase_request_timer() 54 | 55 | def decrease_request_timer(self): 56 | super(Bitfinex, self).decrease_request_timer() 57 | 58 | def reset_request_timer(self): 59 | super(Bitfinex, self).reset_request_timer() 60 | 61 | def _sign_payload(self, payload): 62 | j = json.dumps(payload) 63 | data = base64.standard_b64encode(j.encode('utf8')) 64 | 65 | h = hmac.new(self.secret.encode('utf8'), data, hashlib.sha384) 66 | signature = h.hexdigest() 67 | return { 68 | "X-BFX-APIKEY": self.key, 69 | "X-BFX-SIGNATURE": signature, 70 | "X-BFX-PAYLOAD": data, 71 | "Connection": "close" 72 | } 73 | 74 | def _request(self, method, request, payload=None, verify=True): 75 | try: 76 | 77 | r = {} 78 | url = '{}{}'.format(self.url, request) 79 | if method == 'get': 80 | r = requests.get(url, timeout=self.timeout, headers={'Connection': 'close'}) 81 | else: 82 | r = requests.post(url, headers=payload, verify=verify, timeout=self.timeout) 83 | 84 | if r.status_code != 200: 85 | if r.status_code == 502 or r.status_code in range(520, 527, 1): 86 | raise ApiError('API Error ' + str(r.status_code) + 87 | ': The web server reported a bad gateway or gateway timeout error.') 88 | elif r.status_code == 429: 89 | self.increase_request_timer() 90 | raise ApiError('API Error ' + str(r.status_code) + ': ' + r.text) 91 | 92 | # Check in case something has gone wrong and the timer is too big 93 | self.reset_request_timer() 94 | return r.json() 95 | 96 | except Exception as ex: 97 | ex.message = ex.message if ex.message else str(ex) 98 | ex.message = "{0} Requesting {1}".format(ex.message, self.url + request) 99 | raise ex 100 | 101 | @ExchangeApi.synchronized 102 | def _post(self, command, payload=None, verify=True): 103 | # keep the request per minute limit 104 | self.limit_request_rate() 105 | 106 | payload = payload or {} 107 | payload['request'] = '/{}/{}'.format(self.apiVersion, command) 108 | payload['nonce'] = self._nonce 109 | signed_payload = self._sign_payload(payload) 110 | return self._request('post', payload['request'], signed_payload, verify) 111 | 112 | @ExchangeApi.synchronized 113 | def _get(self, command, apiVersion=None): 114 | # keep the request per minute limit 115 | self.limit_request_rate() 116 | 117 | if apiVersion is None: 118 | apiVersion = self.apiVersion 119 | request = '/{}/{}'.format(apiVersion, command) 120 | return self._request('get', request) 121 | 122 | def _get_symbols(self): 123 | """ 124 | A list of symbol names. Currently "btcusd", "ltcusd", "ltcbtc", ... 125 | https://bitfinex.readme.io/v1/reference#rest-public-symbols 126 | """ 127 | if len(self.symbols) == 0: 128 | bfx_resp = self._get('symbols') 129 | for symbol in bfx_resp: 130 | base = symbol[3:].upper() 131 | curr = symbol[:3].upper() 132 | if ( base in self.baseCurrencies ) and ( curr in self.all_currencies ): 133 | self.symbols.append(symbol) 134 | 135 | return self.symbols 136 | 137 | def return_open_loan_offers(self): 138 | """ 139 | Returns active loan offers 140 | https://bitfinex.readme.io/v1/reference#rest-auth-offers 141 | """ 142 | bfx_resp = self._post('offers') 143 | resp = Bitfinex2Poloniex.convertOpenLoanOffers(bfx_resp) 144 | 145 | return resp 146 | 147 | def return_loan_orders(self, currency, limit=0): 148 | command = ('lendbook/' + currency + '?limit_asks=' + str(limit) + '&limit_bids=' + str(limit)) 149 | bfx_resp = self._get(command) 150 | resp = Bitfinex2Poloniex.convertLoanOrders(bfx_resp) 151 | 152 | return resp 153 | 154 | def return_active_loans(self): 155 | """ 156 | Returns own active loan offers 157 | https://bitfinex.readme.io/v1/reference#rest-auth-offers 158 | """ 159 | bfx_resp = self._post('credits') 160 | resp = Bitfinex2Poloniex.convertActiveLoans(bfx_resp) 161 | 162 | return resp 163 | 164 | def return_ticker(self): 165 | """ 166 | The ticker is a high level overview of the state of the market 167 | https://bitfinex.readme.io/v1/reference#rest-public-ticker 168 | """ 169 | t = int(time.time()) 170 | if t - self.tickerTime < 60: 171 | return self.ticker 172 | 173 | set_ticker_time = True 174 | 175 | for symbol in self._get_symbols(): 176 | base = symbol[3:].upper() 177 | curr = symbol[:3].upper() 178 | if ( base in self.baseCurrencies ) and (curr == 'BTC' or curr in self.usedCurrencies): 179 | couple = (base + '_' + curr) 180 | couple_reverse = (curr + '_' + base) 181 | 182 | try: 183 | ticker = self._get('pubticker/' + symbol) 184 | 185 | if 'message' in ticker: 186 | raise ApiError("Error: {} ({})".format(ticker['message'], symbol)) 187 | 188 | self.ticker[couple] = { 189 | "last": ticker['last_price'], 190 | "lowestAsk": ticker['ask'], 191 | "highestBid": ticker['bid'], 192 | "percentChange": "", 193 | "baseVolume": str(float(ticker['volume']) * float(ticker['mid'])), 194 | "quoteVolume": ticker['volume'] 195 | } 196 | self.ticker[couple_reverse] = { 197 | "last": 1 / float(self.ticker[couple]['last']), 198 | "lowestAsk": 1 / float(self.ticker[couple]['lowestAsk']), 199 | "highestBid": 1 / float(self.ticker[couple]['highestBid']) 200 | } 201 | 202 | except Exception as ex: 203 | self.log.log_error('Error retrieving ticker for {}: {}. Continue with next currency.' 204 | .format(symbol, ex.message)) 205 | set_ticker_time = False 206 | continue 207 | 208 | if set_ticker_time and len(self.ticker) > 2: # USD_BTC and BTC_USD are always in 209 | self.tickerTime = t 210 | 211 | return self.ticker 212 | 213 | def return_available_account_balances(self, account): 214 | """ 215 | Returns own balances sorted by account 216 | https://bitfinex.readme.io/v1/reference#rest-auth-wallet-balances 217 | """ 218 | bfx_resp = self._post('balances') 219 | balances = Bitfinex2Poloniex.convertAccountBalances(bfx_resp, account) 220 | 221 | if 'lending' in balances: 222 | for curr in balances['lending']: 223 | if curr not in self.usedCurrencies: 224 | self.usedCurrencies.append(curr) 225 | 226 | return balances 227 | 228 | def cancel_loan_offer(self, currency, order_number): 229 | """ 230 | Cancels an offer 231 | https://bitfinex.readme.io/v1/reference#rest-auth-cancel-offer 232 | """ 233 | payload = { 234 | "offer_id": order_number, 235 | } 236 | 237 | bfx_resp = self._post('offer/cancel', payload) 238 | 239 | success = 0 240 | message = '' 241 | try: 242 | if bfx_resp['id'] == order_number: 243 | success = 1 244 | message = "Loan offer canceled ({:.4f} @ {:.4f}%).".format(float(bfx_resp['remaining_amount']), 245 | float(bfx_resp['rate']) / 365) 246 | except Exception as e: 247 | message = "Error canceling offer: ", str(e) 248 | success = 0 249 | 250 | return {"success": success, "message": message} 251 | 252 | def create_loan_offer(self, currency, amount, duration, auto_renew, lending_rate): 253 | """ 254 | Creates a loan offer for a given currency. 255 | https://bitfinex.readme.io/v1/reference#rest-auth-new-offer 256 | """ 257 | 258 | payload = { 259 | "currency": currency, 260 | "amount": str(amount), 261 | "rate": str(round(float(lending_rate), 10) * 36500), 262 | "period": int(duration), 263 | "direction": "lend" 264 | } 265 | 266 | try: 267 | bfx_resp = self._post('offer/new', payload) 268 | plx_resp = {"success": 0, "message": "Error", "orderID": 0} 269 | if bfx_resp['id']: 270 | plx_resp['orderId'] = bfx_resp['id'] 271 | plx_resp['success'] = 1 272 | plx_resp['message'] = "Loan order placed." 273 | return plx_resp 274 | 275 | except Exception as e: 276 | msg = str(e) 277 | # "Invalid offer: incorrect amount, minimum is 50 dollar or equivalent in USD" 278 | if "Invalid offer: incorrect amount, minimum is 50" in msg: 279 | usd_min = 50 280 | cur_min = usd_min 281 | if currency != 'USD': 282 | if currency == 'EUR': 283 | cur_min = usd_min / float(self.return_ticker()['USD_' + 'BTC']['lowestAsk']) * float(self.return_ticker()['EUR_' + 'BTC']['lowestAsk']) 284 | cur_min = usd_min / float(self.return_ticker()['USD_' + currency]['lowestAsk']) 285 | 286 | raise Exception("Error create_loan_offer: Amount must be at least " + str(cur_min) + " " + currency) 287 | else: 288 | raise e 289 | 290 | def return_balances(self): 291 | """ 292 | Returns balances of exchange wallet 293 | https://bitfinex.readme.io/v1/reference#rest-auth-wallet-balances 294 | """ 295 | balances = self.return_available_account_balances('exchange') 296 | return_dict = {cur: u'0.00000000' for cur in self.cfg.get_all_currencies()} 297 | return_dict.update(balances['exchange']) 298 | return return_dict 299 | 300 | def transfer_balance(self, currency, amount, from_account, to_account): 301 | """ 302 | Transfers values from one account/wallet to another 303 | https://bitfinex.readme.io/v1/reference#rest-auth-transfer-between-wallets 304 | """ 305 | account_map = { 306 | 'margin': 'trading', 307 | 'lending': 'deposit', 308 | 'exchange': 'exchange' 309 | } 310 | payload = { 311 | "currency": currency, 312 | "amount": amount, 313 | "walletfrom": account_map[from_account], 314 | "walletto": account_map[to_account] 315 | } 316 | 317 | bfx_resp = self._post('transfer', payload) 318 | plx_resp = { 319 | "status": 1 if bfx_resp[0]['status'] == "success" else 0, 320 | "message": bfx_resp[0]['message'] 321 | } 322 | 323 | return plx_resp 324 | 325 | def return_lending_history(self, start, stop, limit=500): 326 | """ 327 | Retrieves balance ledger entries. Search funding payments in it and returns 328 | it as history. 329 | https://bitfinex.readme.io/v1/reference#rest-auth-balance-history 330 | """ 331 | history = [] 332 | all_currencies = self.cfg.get_all_currencies() 333 | for curr in all_currencies: 334 | payload = { 335 | "currency": curr, 336 | "since": str(start), 337 | "until": str(stop), 338 | "limit": limit, 339 | "wallet": "deposit" 340 | } 341 | bfx_resp = self._post('history', payload) 342 | for entry in bfx_resp: 343 | if 'Margin Funding Payment' in entry['description']: 344 | amount = float(entry['amount']) 345 | history.append({ 346 | "id": int(float(entry['timestamp'])), 347 | "currency": curr, 348 | "rate": "0.0", 349 | "amount": "0.0", 350 | "duration": "0.0", 351 | "interest": str(amount / 0.85), 352 | "fee": str(amount - amount / 0.85), 353 | "earned": str(amount), 354 | "open": Bitfinex2Poloniex.convertTimestamp(entry['timestamp']), 355 | "close": Bitfinex2Poloniex.convertTimestamp(entry['timestamp']) 356 | }) 357 | 358 | return history 359 | 360 | def get_frr(self, currency): 361 | """ 362 | Retrieves the flash return rate for the given currency 363 | https://bitfinex.readme.io/v2/reference#rest-public-platform-status 364 | """ 365 | command = 'tickers?symbols=f' + currency 366 | resp = self._get(command, 'v2') 367 | return float(resp[0][1]) 368 | -------------------------------------------------------------------------------- /modules/Bitfinex2Poloniex.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Converts Bitfinex to Poloniex Api returns 3 | ''' 4 | 5 | import datetime 6 | import pytz 7 | 8 | 9 | class Bitfinex2Poloniex(object): 10 | @staticmethod 11 | def convertTimestamp(timestamp): 12 | ''' 13 | Converts unix timestamp 14 | ''' 15 | dt = datetime.datetime.fromtimestamp(float(timestamp), pytz.utc) 16 | return dt.strftime('%Y-%m-%d %H:%M:%S') 17 | 18 | @staticmethod 19 | def convertOpenLoanOffers(bfxOffers): 20 | ''' 21 | Convert from "offers" to "returnOpenLoanOffers" 22 | ''' 23 | plxOffers = {} 24 | for offer in bfxOffers: 25 | if offer['currency'] not in plxOffers: 26 | plxOffers[offer['currency']] = [] 27 | 28 | if offer['direction'] == 'lend' and float(offer['remaining_amount']) > 0: 29 | plxOffers[offer['currency']].append({ 30 | "id": offer['id'], 31 | "rate": str(float(offer['rate'])/36500), 32 | "amount": offer['remaining_amount'], 33 | "duration": offer['period'], 34 | "autoRenew": 0, 35 | "date": Bitfinex2Poloniex.convertTimestamp(offer['timestamp']) 36 | }) 37 | 38 | return plxOffers 39 | 40 | @staticmethod 41 | def convertActiveLoans(bfxOffers): 42 | ''' 43 | Convert from "credits" to "returnActiveLoans" 44 | ''' 45 | 46 | plxOffers = {} 47 | plxOffers['provided'] = [] 48 | plxOffers['used'] = [] 49 | for offer in bfxOffers: 50 | plxOffers['provided'].append({ 51 | "id": offer['id'], 52 | "currency": offer['currency'], 53 | "rate": str(float(offer['rate']) / 36500), 54 | "amount": offer['amount'], 55 | "duration": offer['period'], 56 | "autoRenew": 0, 57 | "date": Bitfinex2Poloniex.convertTimestamp(offer['timestamp']) 58 | }) 59 | 60 | return plxOffers 61 | 62 | @staticmethod 63 | def convertLoanOrders(bfxLendbook): 64 | ''' 65 | Converts from 'lendbook' to 'returnLoanOrders' 66 | ''' 67 | 68 | plxOrders = {} 69 | plxOrders['offers'] = [] 70 | plxOrders['demands'] = [] 71 | 72 | for bid in bfxLendbook['bids']: 73 | plxOrders['demands'].append({ 74 | 'rate': '{0:0.8f}'.format(float(bid['rate']) / 36500), 75 | 'amount': bid['amount'], 76 | 'rangeMin': '2', 77 | 'rangeMax': bid['period'] 78 | }) 79 | 80 | for ask in bfxLendbook['asks']: 81 | plxOrders['offers'].append({ 82 | 'rate': '{0:0.8f}'.format(float(ask['rate']) / 36500), 83 | 'amount': ask['amount'], 84 | 'rangeMin': '2', 85 | 'rangeMax': ask['period'] 86 | }) 87 | 88 | return plxOrders 89 | 90 | @staticmethod 91 | def convertAccountBalances(bfxBalances, account=''): 92 | ''' 93 | Converts from 'balances' to 'returnAvailableAccountBalances' 94 | ''' 95 | balances = {} 96 | 97 | accountMap = { 98 | 'trading': 'margin', 99 | 'deposit': 'lending', 100 | 'exchange': 'exchange' 101 | } 102 | 103 | if (account == ''): 104 | balances = {'margin': {}, 'lending': {}, 'exchange': {}} 105 | else: 106 | balances[account] = {} 107 | 108 | for balance in bfxBalances: 109 | if balance['type'] == 'conversion': 110 | continue 111 | if (account == '' or account == accountMap[balance['type']]) and float(balance['amount']) > 0: 112 | curr = balance['currency'].upper() 113 | balances[account][curr] = balance['available'] 114 | 115 | return balances 116 | -------------------------------------------------------------------------------- /modules/Configuration.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import json 3 | import os 4 | from ConfigParser import SafeConfigParser 5 | from decimal import Decimal 6 | 7 | config = SafeConfigParser() 8 | Data = None 9 | # This module is the middleman between the bot and a SafeConfigParser object, so that we can add extra functionality 10 | # without clogging up lendingbot.py with all the config logic. For example, added a default value to get(). 11 | 12 | 13 | def init(file_location, data=None): 14 | global Data 15 | Data = data 16 | loaded_files = config.read(file_location) 17 | if len(loaded_files) != 1: 18 | import shutil 19 | # Copy default config file if not found 20 | try: 21 | shutil.copy('default.cfg.example', file_location) 22 | print '\ndefault.cfg.example has been copied to ' + file_location + '\n' \ 23 | 'Edit it with your API key and custom settings.\n' 24 | raw_input("Press Enter to acknowledge and exit...") 25 | exit(1) 26 | except Exception as ex: 27 | ex.message = ex.message if ex.message else str(ex) 28 | print("Failed to automatically copy config. Please do so manually. Error: {0}".format(ex.message)) 29 | exit(1) 30 | if config.has_option("BOT", "coinconfig"): 31 | print('\'coinconfig\' has been removed, please use section coin config instead.\n' 32 | 'See: http://poloniexlendingbot.readthedocs.io/en/latest/configuration.html#config-per-coin') 33 | exit(1) 34 | return config 35 | 36 | 37 | def has_option(category, option): 38 | try: 39 | return True if os.environ["{0}_{1}".format(category, option)] else _ 40 | except (KeyError, NameError): # KeyError for no env var, NameError for _ (empty var) and then to continue 41 | return config.has_option(category, option) 42 | 43 | 44 | def getboolean(category, option, default_value=False): 45 | if has_option(category, option): 46 | try: 47 | return bool(os.environ["{0}_{1}".format(category, option)]) 48 | except KeyError: 49 | return config.getboolean(category, option) 50 | else: 51 | return default_value 52 | 53 | 54 | def get(category, option, default_value=False, lower_limit=False, upper_limit=False): 55 | if has_option(category, option): 56 | try: 57 | value = os.environ["{0}_{1}".format(category, option)] 58 | except KeyError: 59 | value = config.get(category, option) 60 | try: 61 | if lower_limit and float(value) < float(lower_limit): 62 | print "WARN: [%s]-%s's value: '%s' is below the minimum limit: %s, which will be used instead." % \ 63 | (category, option, value, lower_limit) 64 | value = lower_limit 65 | if upper_limit and float(value) > float(upper_limit): 66 | print "WARN: [%s]-%s's value: '%s' is above the maximum limit: %s, which will be used instead." % \ 67 | (category, option, value, upper_limit) 68 | value = upper_limit 69 | return value 70 | except ValueError: 71 | if default_value is None: 72 | print "ERROR: [%s]-%s is not allowed to be left empty. Please check your config." % (category, option) 73 | exit(1) 74 | return default_value 75 | else: 76 | if default_value is None: 77 | print "ERROR: [%s]-%s is not allowed to be left unset. Please check your config." % (category, option) 78 | exit(1) 79 | return default_value 80 | 81 | # Below: functions for returning some config values that require special treatment. 82 | 83 | 84 | def get_exchange(): 85 | ''' 86 | Returns used exchange 87 | ''' 88 | try: 89 | return os.environ['API_exchange'].upper() 90 | except KeyError: 91 | return get('API', 'exchange', 'Poloniex').upper() 92 | 93 | 94 | def get_coin_cfg(): 95 | coin_cfg = {} 96 | for cur in get_all_currencies(): 97 | if config.has_section(cur): 98 | try: 99 | coin_cfg[cur] = {} 100 | coin_cfg[cur]['minrate'] = (Decimal(config.get(cur, 'mindailyrate'))) / 100 101 | coin_cfg[cur]['maxactive'] = Decimal(config.get(cur, 'maxactiveamount')) 102 | coin_cfg[cur]['maxtolend'] = Decimal(config.get(cur, 'maxtolend')) 103 | coin_cfg[cur]['maxpercenttolend'] = (Decimal(config.get(cur, 'maxpercenttolend'))) / 100 104 | coin_cfg[cur]['maxtolendrate'] = (Decimal(config.get(cur, 'maxtolendrate'))) / 100 105 | coin_cfg[cur]['gapmode'] = get_gap_mode(cur, 'gapmode') 106 | coin_cfg[cur]['gapbottom'] = Decimal(get(cur, 'gapbottom', False, 0)) 107 | coin_cfg[cur]['gaptop'] = Decimal(get(cur, 'gaptop', False, coin_cfg[cur]['gapbottom'])) 108 | coin_cfg[cur]['frrasmin'] = getboolean(cur, 'frrasmin', getboolean('BOT', 'frrasmin')) 109 | coin_cfg[cur]['frrdelta'] = Decimal(get(cur, 'frrdelta', 0.0000)) 110 | 111 | except Exception as ex: 112 | ex.message = ex.message if ex.message else str(ex) 113 | print("Coin Config for " + cur + " parsed incorrectly, please refer to the documentation. " 114 | "Error: {0}".format(ex.message)) 115 | # Need to raise this exception otherwise the bot will continue if you configured one cur correctly 116 | raise 117 | return coin_cfg 118 | 119 | 120 | def get_min_loan_sizes(): 121 | min_loan_sizes = {} 122 | for cur in get_all_currencies(): 123 | if config.has_section(cur): 124 | try: 125 | min_loan_sizes[cur] = Decimal(get(cur, 'minloansize', lower_limit=0.005)) 126 | except Exception as ex: 127 | ex.message = ex.message if ex.message else str(ex) 128 | print("minloansize for " + cur + " parsed incorrectly, please refer to the documentation. " 129 | "Error: {0}".format(ex.message)) 130 | # Bomb out if something isn't configured correctly 131 | raise 132 | return min_loan_sizes 133 | 134 | 135 | def get_currencies_list(option, section='BOT'): 136 | if config.has_option(section, option): 137 | full_list = get_all_currencies() 138 | cur_list = [] 139 | raw_cur_list = config.get(section, option).split(",") 140 | for raw_cur in raw_cur_list: 141 | cur = raw_cur.strip(' ').upper() 142 | if cur == 'ALL': 143 | return full_list 144 | elif cur == 'ACTIVE': 145 | cur_list += Data.get_lending_currencies() 146 | else: 147 | if cur in full_list: 148 | cur_list.append(cur) 149 | return list(set(cur_list)) 150 | else: 151 | return [] 152 | 153 | 154 | def get_gap_mode(category, option): 155 | if config.has_option(category, option): 156 | full_list = ['raw', 'rawbtc', 'relative'] 157 | value = get(category, 'gapmode', False).lower().strip(" ") 158 | if value not in full_list: 159 | print "ERROR: Invalid entry '%s' for [%s]-gapMode. Please check your config. Allowed values are: %s" % \ 160 | (value, category, ", ".join(full_list)) 161 | exit(1) 162 | return value.lower() 163 | else: 164 | return False 165 | 166 | 167 | def get_all_currencies(): 168 | ''' 169 | Get list of all supported currencies by exchange 170 | ''' 171 | exchange = get_exchange() 172 | if config.has_option(exchange, 'all_currencies'): 173 | cur_list = [] 174 | raw_cur_list = config.get(exchange, 'all_currencies').split(',') 175 | for raw_cur in raw_cur_list: 176 | cur = raw_cur.strip(' ').upper() 177 | if (cur[0] != "#"): # Blacklisting: E.g. ETH, #BTG, QTUM 178 | cur_list.append(cur) 179 | return cur_list 180 | elif exchange == 'POLONIEX': 181 | # default, compatibility to old 'Poloniex only' config 182 | return ['STR', 'BTC', 'BTS', 'CLAM', 'DOGE', 'DASH', 'LTC', 'MAID', 'XMR', 'XRP', 'ETH', 'FCT'] 183 | else: 184 | raise Exception('ERROR: List of supported currencies must defined in [' + exchange + '] all_currencies.') 185 | 186 | 187 | def get_notification_config(): 188 | notify_conf = {'enable_notifications': config.has_section('notifications')} 189 | 190 | # For boolean parameters 191 | for conf in ['notify_tx_coins', 'notify_xday_threshold', 'notify_new_loans', 'notify_caught_exception', 'email', 'slack', 'telegram', 192 | 'pushbullet', 'irc']: 193 | notify_conf[conf] = getboolean('notifications', conf) 194 | 195 | # For string-based parameters 196 | for conf in ['notify_prefix']: 197 | _val = get('notifications', conf, '').strip() 198 | if len(_val) > 0: 199 | notify_conf[conf] = _val 200 | 201 | # in order not to break current config, parsing for False 202 | notify_summary_minutes = get('notifications', 'notify_summary_minutes') 203 | notify_conf['notify_summary_minutes'] = 0 if notify_summary_minutes == 'False' else int(notify_summary_minutes) 204 | 205 | if notify_conf['email']: 206 | for conf in ['email_login_address', 'email_login_password', 'email_smtp_server', 'email_smtp_port', 207 | 'email_to_addresses', 'email_smtp_starttls']: 208 | notify_conf[conf] = get('notifications', conf) 209 | notify_conf['email_to_addresses'] = notify_conf['email_to_addresses'].split(',') 210 | 211 | if notify_conf['slack']: 212 | for conf in ['slack_token', 'slack_channels', 'slack_username']: 213 | notify_conf[conf] = get('notifications', conf) 214 | notify_conf['slack_channels'] = notify_conf['slack_channels'].split(',') 215 | if not notify_conf['slack_username']: 216 | notify_conf['slack_username'] = 'Slack API Tester' 217 | 218 | if notify_conf['telegram']: 219 | for conf in ['telegram_bot_id', 'telegram_chat_ids']: 220 | notify_conf[conf] = get('notifications', conf) 221 | notify_conf['telegram_chat_ids'] = notify_conf['telegram_chat_ids'].split(',') 222 | 223 | if notify_conf['pushbullet']: 224 | for conf in ['pushbullet_token', 'pushbullet_deviceid']: 225 | notify_conf[conf] = get('notifications', conf) 226 | 227 | if notify_conf['irc']: 228 | for conf in ['irc_host', 'irc_port', 'irc_nick', 'irc_ident', 'irc_realname', 'irc_target']: 229 | notify_conf[conf] = get('notifications', conf) 230 | notify_conf['irc_port'] = int(notify_conf['irc_port']) 231 | notify_conf['irc_debug'] = getboolean('notifications', 'irc_debug') 232 | 233 | return notify_conf 234 | 235 | 236 | def get_plugins_config(): 237 | active_plugins = [] 238 | if config.has_option("BOT", "plugins"): 239 | active_plugins = map(str.strip, config.get("BOT", "plugins").split(',')) 240 | return active_plugins 241 | -------------------------------------------------------------------------------- /modules/ConsoleUtils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import shlex 4 | import struct 5 | import platform 6 | import subprocess 7 | 8 | def get_terminal_size(): 9 | """ getTerminalSize() 10 | - get width and height of console 11 | - works on linux,os x,windows,cygwin(windows) 12 | originally retrieved from: 13 | http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python 14 | """ 15 | current_os = platform.system() 16 | tuple_xy = None 17 | if current_os == 'Windows': 18 | tuple_xy = _get_terminal_size_windows() 19 | if tuple_xy is None: 20 | tuple_xy = _get_terminal_size_tput() 21 | # needed for window's python in cygwin's xterm! 22 | if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'): 23 | tuple_xy = _get_terminal_size_linux() 24 | if tuple_xy is None: 25 | tuple_xy = (80, 25) # default value 26 | return tuple_xy 27 | 28 | 29 | def _get_terminal_size_windows(): 30 | try: 31 | from ctypes import windll, create_string_buffer 32 | # stdin handle is -10 33 | # stdout handle is -11 34 | # stderr handle is -12 35 | h = windll.kernel32.GetStdHandle(-12) 36 | csbi = create_string_buffer(22) 37 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 38 | if res: 39 | (bufx, bufy, curx, cury, wattr, 40 | left, top, right, bottom, 41 | maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) 42 | sizex = right - left + 1 43 | sizey = bottom - top + 1 44 | return sizex, sizey 45 | except: 46 | pass 47 | 48 | def _get_terminal_size_tput(): 49 | # get terminal width 50 | # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window 51 | try: 52 | cols = int(subprocess.check_call(shlex.split('tput cols'))) 53 | rows = int(subprocess.check_call(shlex.split('tput lines'))) 54 | return (cols, rows) 55 | except: 56 | pass 57 | 58 | 59 | def _get_terminal_size_linux(): 60 | def ioctl_GWINSZ(fd): 61 | try: 62 | import fcntl 63 | import termios 64 | cr = struct.unpack('hh', 65 | fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) 66 | return cr 67 | except: 68 | pass 69 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 70 | if not cr: 71 | try: 72 | fd = os.open(os.ctermid(), os.O_RDONLY) 73 | cr = ioctl_GWINSZ(fd) 74 | os.close(fd) 75 | except: 76 | pass 77 | if not cr: 78 | try: 79 | cr = (os.environ['LINES'], os.environ['COLUMNS']) 80 | except: 81 | return None 82 | return int(cr[1]), int(cr[0]) 83 | -------------------------------------------------------------------------------- /modules/Data.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | from urllib import urlopen 4 | import json 5 | 6 | api = None 7 | log = None 8 | 9 | 10 | def init(api1, log1): 11 | global api, log 12 | api = api1 13 | log = log1 14 | 15 | 16 | def get_on_order_balances(): 17 | loan_offers = api.return_open_loan_offers() 18 | on_order_balances = {} 19 | for CUR in loan_offers: 20 | for offer in loan_offers[CUR]: 21 | on_order_balances[CUR] = on_order_balances.get(CUR, 0) + Decimal(offer['amount']) 22 | return on_order_balances 23 | 24 | 25 | def get_max_duration(end_date, context): 26 | if not end_date: 27 | return "" 28 | try: 29 | now_time = datetime.date.today() 30 | config_date = map(int, end_date.split(',')) 31 | end_time = datetime.date(*config_date) # format YEAR,MONTH,DAY all ints, also used splat operator 32 | diff_days = (end_time - now_time).days 33 | if context == "order": 34 | return diff_days # Order needs int 35 | if context == "status": 36 | return " - Days Remaining: " + str(diff_days) # Status needs string 37 | except Exception as ex: 38 | ex.message = ex.message if ex.message else str(ex) 39 | print("ERROR: There is something wrong with your endDate option. Error: {0}".format(ex.message)) 40 | exit(1) 41 | 42 | 43 | def get_total_lent(): 44 | crypto_lent = api.return_active_loans() 45 | total_lent = {} 46 | rate_lent = {} 47 | for item in crypto_lent["provided"]: 48 | item_str = item["amount"].encode("utf-8") 49 | item_float = Decimal(item_str) 50 | item_rate_str = item["rate"].encode("utf-8") 51 | item_rate_float = Decimal(item_rate_str) 52 | if item["currency"] in total_lent: 53 | crypto_lent_sum = total_lent[item["currency"]] + item_float 54 | crypto_lent_rate = rate_lent[item["currency"]] + (item_rate_float * item_float) 55 | total_lent[item["currency"]] = crypto_lent_sum 56 | rate_lent[item["currency"]] = crypto_lent_rate 57 | else: 58 | crypto_lent_sum = item_float 59 | crypto_lent_rate = item_rate_float * item_float 60 | total_lent[item["currency"]] = crypto_lent_sum 61 | rate_lent[item["currency"]] = crypto_lent_rate 62 | return [total_lent, rate_lent] 63 | 64 | 65 | def timestamp(): 66 | ''' 67 | Returns timestamp in UTC 68 | ''' 69 | return datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') 70 | 71 | 72 | def stringify_total_lent(total_lent, rate_lent): 73 | result = 'Lent: ' 74 | for key in sorted(total_lent): 75 | average_lending_rate = Decimal(rate_lent[key] * 100 / total_lent[key]) 76 | result += '[%.4f %s @ %.4f%%] ' % (Decimal(total_lent[key]), key, average_lending_rate) 77 | log.updateStatusValue(key, "lentSum", total_lent[key]) 78 | log.updateStatusValue(key, "averageLendingRate", average_lending_rate) 79 | return result 80 | 81 | 82 | def update_conversion_rates(output_currency, json_output_enabled): 83 | if json_output_enabled: 84 | total_lent = get_total_lent()[0] 85 | ticker_response = api.return_ticker() 86 | output_currency_found = False 87 | # Set this up now in case we get an exception later and don't have a currency to use 88 | log.updateOutputCurrency('highestBid', '1') 89 | log.updateOutputCurrency('currency', 'BTC') 90 | # default output currency is BTC 91 | if output_currency == 'BTC': 92 | output_currency_found = True 93 | 94 | for couple in ticker_response: 95 | currencies = couple.split('_') 96 | ref = currencies[0] 97 | currency = currencies[1] 98 | if ref == 'BTC' and currency in total_lent: 99 | log.updateStatusValue(currency, 'highestBid', ticker_response[couple]['highestBid']) 100 | log.updateStatusValue(currency, 'couple', couple) 101 | if not output_currency_found: # check for output currency 102 | if ref == 'BTC' and currency == output_currency: 103 | output_currency_found = True 104 | log.updateOutputCurrency('highestBid', 1 / float(ticker_response[couple]['highestBid'])) 105 | log.updateOutputCurrency('currency', output_currency) 106 | if ref == output_currency and currency == 'BTC': 107 | output_currency_found = True 108 | log.updateOutputCurrency('highestBid', ticker_response[couple]['highestBid']) 109 | log.updateOutputCurrency('currency', output_currency) 110 | if not output_currency_found: # fetch output currency rate from blockchain.info 111 | url = "https://blockchain.info/tobtc?currency={0}&value=1".format(output_currency) 112 | try: 113 | highest_bid = json.loads(urlopen(url).read()) 114 | log.updateOutputCurrency('highestBid', 1 / float(highest_bid)) 115 | log.updateOutputCurrency('currency', output_currency) 116 | except ValueError: 117 | log.log_error("Failed to find the exchange rate for outputCurrency {0}! Using BTC as output currency" 118 | .format(output_currency)) 119 | log.log_error("Make sure that {0} is either traded on the exchange or supported by blockchain.info: {1}" 120 | .format(output_currency, "https://blockchain.info/api/exchange_rates_api")) 121 | except Exception: 122 | log.log_error("Can't connect to {0} using BTC as the output currency".format(url)) 123 | 124 | 125 | def get_lending_currencies(): 126 | currencies = [] 127 | total_lent = get_total_lent()[0] 128 | for cur in total_lent: 129 | currencies.append(cur) 130 | lending_balances = api.return_available_account_balances("lending")['lending'] 131 | for cur in lending_balances: 132 | currencies.append(cur) 133 | return currencies 134 | 135 | 136 | def truncate(f, n): 137 | """Truncates/pads a float f to n decimal places without rounding""" 138 | # From https://stackoverflow.com/questions/783897/truncating-floats-in-python 139 | s = '{}'.format(f) 140 | if 'e' in s or 'E' in s: 141 | return float('{0:.{1}f}'.format(f, n)) 142 | i, p, d = s.partition('.') 143 | return float('.'.join([i, (d + '0' * n)[:n]])) 144 | 145 | 146 | # gets the git commit count as version for master. 147 | def get_bot_version(): 148 | import subprocess 149 | try: 150 | output = subprocess.check_output(["git", "rev-list","--count", "HEAD"]) 151 | int(output) 152 | return output 153 | except Exception: 154 | return '3.0.0' 155 | -------------------------------------------------------------------------------- /modules/ExchangeApi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exchange API Base class 3 | """ 4 | 5 | import abc 6 | import calendar 7 | import time 8 | 9 | 10 | class ExchangeApi(object): 11 | __metaclass__ = abc.ABCMeta 12 | 13 | def __str__(self): 14 | return self.__class__.__name__.upper() 15 | 16 | def __repr__(self): 17 | return self.__str__() 18 | 19 | @staticmethod 20 | def create_time_stamp(datestr, formatting="%Y-%m-%d %H:%M:%S"): 21 | return calendar.timegm(time.strptime(datestr, formatting)) 22 | 23 | @staticmethod 24 | def synchronized(method): 25 | """ Work with instance method only !!! """ 26 | 27 | def new_method(self, *arg, **kws): 28 | with self.lock: 29 | return method(self, *arg, **kws) 30 | 31 | return new_method 32 | 33 | @abc.abstractmethod 34 | def __init__(self, cfg, log): 35 | """ 36 | Constructor 37 | """ 38 | 39 | @abc.abstractmethod 40 | def limit_request_rate(self): 41 | now = time.time() * 1000 # milliseconds 42 | # Start throttling only when the queue is full 43 | if len(self.req_time_log) == self.req_per_period: 44 | time_since_oldest_req = now - self.req_time_log[0] 45 | if time_since_oldest_req < self.req_period: 46 | sleep = (self.req_period - time_since_oldest_req) / 1000 47 | self.req_time_log.append(now + self.req_period - time_since_oldest_req) 48 | time.sleep(sleep) 49 | return 50 | 51 | self.req_time_log.append(now) 52 | 53 | @abc.abstractmethod 54 | def increase_request_timer(self): 55 | if self.req_period <= self.default_req_period * 3.0: 56 | self.req_period += 500 57 | 58 | @abc.abstractmethod 59 | def decrease_request_timer(self): 60 | if self.req_period > self.default_req_period: 61 | self.req_period -= 1 62 | 63 | @abc.abstractmethod 64 | def reset_request_timer(self): 65 | if self.req_period >= self.default_req_period * 1.5: 66 | self.req_period = self.default_req_period 67 | 68 | @abc.abstractmethod 69 | def return_ticker(self): 70 | """ 71 | Returns the ticker for all markets. 72 | """ 73 | 74 | @abc.abstractmethod 75 | def return_balances(self): 76 | """ 77 | Returns available exchange balances. 78 | Sample output: 79 | {"BTC":"0.59098578","LTC":"3.31117268", ... } 80 | """ 81 | 82 | @abc.abstractmethod 83 | def return_available_account_balances(self, account): 84 | """ 85 | Returns balances sorted by account. You may optionally specify the 86 | "account" POST parameter if you wish to fetch only the balances of one 87 | account. 88 | 89 | Sample output: 90 | {"exchange":{"BTC":"1.19042859","BTM":"386.52379392","CHA":"0.50000000", 91 | "DASH":"120.00000000","STR":"3205.32958001", "VNL":"9673.22570147"}, 92 | "margin":{"BTC":"3.90015637","DASH":"250.00238240", 93 | "XMR":"497.12028113"}, 94 | "lending":{"DASH":"0.01174765","LTC":"11.99936230"}} 95 | """ 96 | 97 | @abc.abstractmethod 98 | def return_lending_history(self, start, stop, limit=500): 99 | """ 100 | Returns lending history within a time range specified by the "start" and 101 | "end" POST parameters as UNIX timestamps. "limit" may also be specified 102 | to limit the number of rows returned. Sample output: 103 | 104 | [{ "id": 175589553, "currency": "BTC", "rate": "0.00057400", "amount": "0.04374404", 105 | "duration": "0.47610000", "interest": "0.00001196", 106 | "fee": "-0.00000179", "earned": "0.00001017", "open": "2016-09-28 06:47:26", 107 | "close": "2016-09-28 18:13:03" }] 108 | """ 109 | 110 | @abc.abstractmethod 111 | def return_loan_orders(self, currency, limit=0): 112 | """ 113 | Returns the list of loan offers and demands for a given currency, 114 | specified by the "currency". Sample output: 115 | 116 | {"offers":[{"rate":"0.00200000","amount":"64.66305732","rangeMin":2,"rangeMax":8}, ... ], 117 | "demands":[{"rate":"0.00170000","amount":"26.54848841","rangeMin":2,"rangeMax":2}, ... ]} 118 | """ 119 | 120 | @abc.abstractmethod 121 | def return_open_loan_offers(self): 122 | """ 123 | Returns own open loan offers for each currency 124 | """ 125 | 126 | @abc.abstractmethod 127 | def return_active_loans(self): 128 | """ 129 | Returns your active loans for each currency. Sample output: 130 | 131 | {"provided":[{"id":75073,"currency":"LTC","rate":"0.00020000","amount":"0.72234880","range":2, 132 | "autoRenew":0,"date":"2015-05-10 23:45:05","fees":"0.00006000"}, 133 | {"id":74961,"currency":"LTC","rate":"0.00002000","amount":"4.43860711","range":2, 134 | "autoRenew":0,"date":"2015-05-10 23:45:05","fees":"0.00006000"}], 135 | "used":[{"id":75238,"currency":"BTC","rate":"0.00020000","amount":"0.04843834","range":2, 136 | "date":"2015-05-10 23:51:12","fees":"-0.00000001"}]} 137 | """ 138 | 139 | @abc.abstractmethod 140 | def cancel_loan_offer(self, currency, order_number): 141 | """ 142 | Cancels a loan offer specified by the "orderNumber" 143 | """ 144 | 145 | @abc.abstractmethod 146 | def create_loan_offer(self, currency, amount, duration, auto_renew, lending_rate): 147 | """ 148 | Creates a loan offer for a given currency. 149 | """ 150 | 151 | @abc.abstractmethod 152 | def transfer_balance(self, currency, amount, from_account, to_account): 153 | """ 154 | Transfers values from one account/wallet to another 155 | """ 156 | 157 | 158 | class ApiError(Exception): 159 | pass 160 | -------------------------------------------------------------------------------- /modules/ExchangeApiFactory.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Factory to instanciate right API class 3 | ''' 4 | 5 | from modules.Poloniex import Poloniex 6 | from modules.Bitfinex import Bitfinex 7 | 8 | EXCHANGE = {'POLONIEX': Poloniex, 'BITFINEX': Bitfinex} 9 | 10 | 11 | class ExchangeApiFactory(object): 12 | @staticmethod 13 | def createApi(exchange, cfg, log): 14 | if exchange not in EXCHANGE: 15 | raise Exception("Invalid exchange: " + exchange) 16 | return EXCHANGE[exchange](cfg, log) 17 | -------------------------------------------------------------------------------- /modules/Logger.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import atexit 3 | import datetime 4 | import io 5 | import json 6 | import sys 7 | import time 8 | 9 | import ConsoleUtils 10 | import modules.Configuration as Config 11 | from RingBuffer import RingBuffer 12 | from Notify import send_notification 13 | 14 | 15 | class ConsoleOutput(object): 16 | def __init__(self): 17 | self._status = '' 18 | atexit.register(self._exit) 19 | 20 | def _exit(self): 21 | self._status += ' ' # In case the shell added a ^C 22 | self.status('') 23 | 24 | def status(self, msg, time='', days_remaining_msg=''): 25 | status = str(msg) 26 | cols = ConsoleUtils.get_terminal_size()[0] 27 | if msg != '' and len(status) > cols: 28 | # truncate status, try preventing console bloating 29 | status = str(msg)[:cols - 4] + '...' 30 | update = '\r' 31 | update += status 32 | update += ' ' * (len(self._status) - len(status)) 33 | update += '\b' * (len(self._status) - len(status)) 34 | sys.stderr.write(update) 35 | self._status = status 36 | 37 | def printline(self, line): 38 | update = '\r' 39 | update += line + ' ' * (len(self._status) - len(line)) + '\n' 40 | update += self._status 41 | sys.stderr.write(update) 42 | 43 | 44 | class JsonOutput(object): 45 | def __init__(self, file, logLimit, exchange=''): 46 | self.jsonOutputFile = file 47 | self.jsonOutput = {} 48 | self.clearStatusValues() 49 | self.jsonOutputLog = RingBuffer(logLimit) 50 | self.jsonOutput['exchange'] = exchange 51 | self.jsonOutput['label'] = Config.get("BOT", "label", "Lending Bot") 52 | 53 | def status(self, status, time, days_remaining_msg): 54 | self.jsonOutput["last_update"] = time + days_remaining_msg 55 | self.jsonOutput["last_status"] = status 56 | 57 | def printline(self, line): 58 | line = line.replace("\n", ' | ') 59 | self.jsonOutputLog.append(line) 60 | 61 | def writeJsonFile(self): 62 | with io.open(self.jsonOutputFile, 'w', encoding='utf-8') as f: 63 | self.jsonOutput["log"] = self.jsonOutputLog.get() 64 | f.write(unicode(json.dumps(self.jsonOutput, ensure_ascii=True, sort_keys=True), errors='replace')) 65 | f.close() 66 | 67 | def addSectionLog(self, section, key, value): 68 | if section not in self.jsonOutput: 69 | self.jsonOutput[section] = {} 70 | if key not in self.jsonOutput[section]: 71 | self.jsonOutput[section][key] = {} 72 | self.jsonOutput[section][key] = value 73 | 74 | def statusValue(self, coin, key, value): 75 | if coin not in self.jsonOutputCoins: 76 | self.jsonOutputCoins[coin] = {} 77 | self.jsonOutputCoins[coin][key] = str(value) 78 | 79 | def clearStatusValues(self): 80 | self.jsonOutputCoins = {} 81 | self.jsonOutput["raw_data"] = self.jsonOutputCoins 82 | self.jsonOutputCurrency = {} 83 | self.jsonOutput["outputCurrency"] = self.jsonOutputCurrency 84 | 85 | def outputCurrency(self, key, value): 86 | self.jsonOutputCurrency[key] = str(value) 87 | 88 | 89 | class Logger(object): 90 | def __init__(self, json_file='', json_log_size=-1, exchange=''): 91 | self._lent = '' 92 | self._daysRemaining = '' 93 | if json_file != '' and json_log_size != -1: 94 | self.output = JsonOutput(json_file, json_log_size, exchange) 95 | else: 96 | self.output = ConsoleOutput() 97 | self.refreshStatus() 98 | 99 | @staticmethod 100 | def timestamp(): 101 | ts = time.time() 102 | return datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') 103 | 104 | def log(self, msg): 105 | log_message = "{0} {1}".format(self.timestamp(), msg) 106 | self.output.printline(log_message) 107 | self.refreshStatus() 108 | 109 | def log_error(self, msg): 110 | log_message = "{0} Error {1}".format(self.timestamp(), msg) 111 | self.output.printline(log_message) 112 | if isinstance(self.output, JsonOutput): 113 | print log_message 114 | self.refreshStatus() 115 | 116 | def offer(self, amt, cur, rate, days, msg): 117 | line = self.timestamp() + ' Placing ' + str(amt) + ' ' + str(cur) + ' at ' + str( 118 | float(rate) * 100) + '% for ' + days + ' days... ' + self.digestApiMsg(msg) 119 | self.output.printline(line) 120 | self.refreshStatus() 121 | 122 | def cancelOrder(self, cur, msg): 123 | line = self.timestamp() + ' Canceling ' + str(cur) + ' order... ' + self.digestApiMsg(msg) 124 | self.output.printline(line) 125 | self.refreshStatus() 126 | 127 | def refreshStatus(self, lent='', days_remaining=''): 128 | if lent != '': 129 | self._lent = lent 130 | if days_remaining != '': 131 | self._daysRemaining = days_remaining 132 | self.output.status(self._lent, self.timestamp(), self._daysRemaining) 133 | 134 | def addSectionLog(self, section, key, value): 135 | if hasattr(self.output, 'addSectionLog'): 136 | self.output.addSectionLog(section, key, value) 137 | 138 | def updateStatusValue(self, coin, key, value): 139 | if hasattr(self.output, 'statusValue'): 140 | self.output.statusValue(coin, key, value) 141 | 142 | def updateOutputCurrency(self, key, value): 143 | if hasattr(self.output, 'outputCurrency'): 144 | self.output.outputCurrency(key, value) 145 | 146 | def persistStatus(self): 147 | if hasattr(self.output, 'writeJsonFile'): 148 | self.output.writeJsonFile() 149 | if hasattr(self.output, 'clearStatusValues'): 150 | self.output.clearStatusValues() 151 | 152 | @staticmethod 153 | def digestApiMsg(msg): 154 | m = "" 155 | try: 156 | m = (msg['message']) 157 | except KeyError: 158 | pass 159 | try: 160 | m = (msg['error']) 161 | except KeyError: 162 | pass 163 | return m 164 | 165 | @staticmethod 166 | def notify(msg, notify_conf): 167 | if notify_conf['enable_notifications']: 168 | send_notification(msg, notify_conf) 169 | -------------------------------------------------------------------------------- /modules/MaxToLend.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from decimal import Decimal 3 | 4 | coin_cfg = [] 5 | max_to_lend_rate = 0 6 | max_to_lend = 0 7 | max_percent_to_lend = 0 8 | min_loan_size = 0.001 9 | log = None 10 | 11 | 12 | def init(config, log1): 13 | global coin_cfg, max_to_lend_rate, max_to_lend, max_percent_to_lend, min_loan_size, log 14 | coin_cfg = coin_cfg = config.get_coin_cfg() 15 | max_to_lend = Decimal(config.get('BOT', 'maxtolend', False, 0)) 16 | max_percent_to_lend = Decimal(config.get('BOT', 'maxpercenttolend', False, 0, 100)) / 100 17 | max_to_lend_rate = Decimal(config.get('BOT', 'maxtolendrate', False, 0.003, 5)) / 100 18 | min_loan_size = Decimal(config.get("BOT", 'minloansize', None, 0.001)) 19 | log = log1 20 | 21 | 22 | def amount_to_lend(active_cur_test_balance, active_cur, lending_balance, low_rate): 23 | restrict_lend = False 24 | active_bal = Decimal(0) 25 | log_data = str("") 26 | cur_max_to_lend_rate = max_to_lend_rate 27 | cur_max_to_lend = max_to_lend 28 | cur_max_percent_to_lend = max_percent_to_lend 29 | if active_cur in coin_cfg: 30 | cur_max_to_lend_rate = coin_cfg[active_cur]['maxtolendrate'] 31 | cur_max_to_lend = coin_cfg[active_cur]['maxtolend'] 32 | cur_max_percent_to_lend = coin_cfg[active_cur]['maxpercenttolend'] 33 | if cur_max_to_lend_rate == 0 and low_rate > 0 or cur_max_to_lend_rate >= low_rate > 0: 34 | log_data = ("The Lower Rate found on " + active_cur + " is " + str( 35 | "%.4f" % (Decimal(low_rate) * 100)) + "% vs conditional rate " + str( 36 | "%.4f" % (Decimal(cur_max_to_lend_rate) * 100)) + "%. ") 37 | restrict_lend = True 38 | if cur_max_to_lend != 0 and restrict_lend: 39 | log.updateStatusValue(active_cur, "maxToLend", cur_max_to_lend) 40 | if lending_balance > (active_cur_test_balance - cur_max_to_lend): 41 | active_bal = (lending_balance - (active_cur_test_balance - cur_max_to_lend)) 42 | if cur_max_to_lend == 0 and cur_max_percent_to_lend != 0 and restrict_lend: 43 | log.updateStatusValue(active_cur, "maxToLend", (cur_max_percent_to_lend * active_cur_test_balance)) 44 | if lending_balance > (active_cur_test_balance - (cur_max_percent_to_lend * active_cur_test_balance)): 45 | active_bal = (lending_balance - (active_cur_test_balance - ( 46 | cur_max_percent_to_lend * active_cur_test_balance))) 47 | if cur_max_to_lend == 0 and cur_max_percent_to_lend == 0: 48 | log.updateStatusValue(active_cur, "maxToLend", active_cur_test_balance) 49 | active_bal = lending_balance 50 | if not restrict_lend: 51 | log.updateStatusValue(active_cur, "maxToLend", active_cur_test_balance) 52 | active_bal = lending_balance 53 | if (lending_balance - active_bal) < min_loan_size: 54 | active_bal = lending_balance 55 | if active_bal < lending_balance: 56 | log.log(log_data + " Lending " + str("%.8f" % Decimal(active_bal)) + " of " + str( 57 | "%.8f" % Decimal(lending_balance)) + " Available") 58 | return active_bal 59 | -------------------------------------------------------------------------------- /modules/Notify.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import urllib 3 | import urllib2 4 | import json 5 | import smtplib 6 | try: 7 | from irc import client 8 | IRC_LOADED = True 9 | except ImportError: 10 | IRC_LOADED = False 11 | 12 | IRC_CLIENT = None 13 | IRC_SERVER = None 14 | 15 | 16 | # Slack post data needs to be encoded in UTF-8 17 | def encoded_dict(in_dict): 18 | out_dict = {} 19 | for k, v in in_dict.iteritems(): 20 | if isinstance(v, unicode): 21 | v = v.encode('utf8') 22 | elif isinstance(v, str): 23 | # Must be encoded in UTF-8 24 | v.decode('utf8') 25 | out_dict[k] = v 26 | return out_dict 27 | 28 | 29 | class NotificationException(Exception): 30 | pass 31 | 32 | 33 | def check_urlib_response(response, platform): 34 | response_str = response.read() 35 | response_obj = json.loads(response_str) 36 | if not response_obj['ok']: 37 | msg = "Error connecting to {0}, got response: {1}".format(platform, response_obj) 38 | raise NotificationException(msg) 39 | 40 | 41 | def post_to_slack(msg, channels, token, username): 42 | for channel in channels: 43 | post_data = {'text': msg, 'channel': channel, 'token': token, 'username': username} 44 | enc_post_data = urllib.urlencode(encoded_dict(post_data)) 45 | url = 'https://{}/api/{}'.format('slack.com', 'chat.postMessage') 46 | response = urllib2.urlopen(url, enc_post_data) 47 | check_urlib_response(response, 'slack') 48 | 49 | 50 | def post_to_telegram(msg, chat_ids, bot_id): 51 | for chat_id in chat_ids: 52 | post_data = {"chat_id": chat_id, "text": msg} 53 | url = "https://api.telegram.org/bot" + bot_id + "/sendMessage" 54 | try: 55 | response = urllib2.urlopen(url, urllib.urlencode(post_data)) 56 | check_urlib_response(response, 'telegram') 57 | except urllib2.HTTPError as e: 58 | msg = "Your bot id is probably configured incorrectly" 59 | raise NotificationException("{0}\n{1}".format(e, msg)) 60 | 61 | 62 | def send_email(msg, email_login_address, email_login_password, email_smtp_server, email_smtp_port, 63 | email_to_addresses, email_smtp_starttls): 64 | subject = 'Lending bot' 65 | 66 | email_text = "\r\n".join(["From: {0}".format(email_login_address), 67 | "To: {0}".format(", ".join(email_to_addresses)), 68 | "Subject: {0}".format(subject), 69 | "", 70 | "{0}".format(msg) 71 | ]) 72 | 73 | try: 74 | if email_smtp_starttls: 75 | server = smtplib.SMTP(email_smtp_server, email_smtp_port) 76 | server.ehlo() 77 | server.starttls() 78 | else: 79 | server = smtplib.SMTP_SSL(email_smtp_server, email_smtp_port) 80 | server.ehlo() 81 | server.login(email_login_address, email_login_password) 82 | server.sendmail(email_login_address, email_to_addresses, email_text) 83 | server.close() 84 | except Exception as e: 85 | print("Could not send email, got error {0}".format(e)) 86 | raise NotificationException(e) 87 | 88 | 89 | def post_to_pushbullet(msg, token, deviceid): 90 | post_data = {'body': msg, 'device_iden': deviceid, 'title': 'Poloniex Bot', 'type': 'note'} 91 | opener = urllib2.build_opener() 92 | req = urllib2.Request('https://api.pushbullet.com/v2/pushes', data=json.dumps(post_data), 93 | headers={'Content-Type': 'application/json', 'Access-Token': token}) 94 | try: 95 | opener.open(req) 96 | except Exception as e: 97 | print("Could not send pushbullet, got error {0}".format(e)) 98 | raise NotificationException(e) 99 | 100 | 101 | def post_to_irc(msg, host, port, nick, ident, realname, target): 102 | """ 103 | Log into an IRC server and send a message to a channel. 104 | """ 105 | global IRC_CLIENT, IRC_SERVER 106 | if IRC_CLIENT is None: 107 | IRC_CLIENT = client.Reactor() 108 | IRC_SERVER = IRC_CLIENT.server() 109 | 110 | IRC_SERVER.connect(host, port, nick) 111 | if client.is_channel(target): 112 | IRC_SERVER.join(target) 113 | for line in msg.splitlines(): 114 | IRC_SERVER.privmsg(target, line) 115 | 116 | 117 | def send_notification(_msg, notify_conf): 118 | nc = notify_conf 119 | msg = _msg if ('notify_prefix' not in nc) else "{} {}".format(nc['notify_prefix'], _msg) 120 | 121 | if nc['email']: 122 | send_email(msg, nc['email_login_address'], nc['email_login_password'], nc['email_smtp_server'], 123 | nc['email_smtp_port'], nc['email_to_addresses'], nc['email_smtp_starttls']) 124 | if nc['slack']: 125 | post_to_slack(msg, nc['slack_channels'], nc['slack_token'], nc['slack_username']) 126 | if nc['telegram']: 127 | post_to_telegram(msg, nc['telegram_chat_ids'], nc['telegram_bot_id']) 128 | if nc['pushbullet']: 129 | post_to_pushbullet(msg, nc['pushbullet_token'], nc['pushbullet_deviceid']) 130 | if nc['irc']: 131 | if IRC_LOADED: 132 | post_to_irc(msg, nc['irc_host'], nc['irc_port'], nc['irc_nick'], nc['irc_ident'], nc['irc_realname'], 133 | nc['irc_target']) 134 | else: 135 | print("IRC module not available, please run 'pip install irc'") 136 | -------------------------------------------------------------------------------- /modules/PluginsManager.py: -------------------------------------------------------------------------------- 1 | from plugins import * 2 | 3 | config = None 4 | api = None 5 | log = None 6 | notify_conf = None 7 | plugins = [] 8 | 9 | 10 | def init_plugin(plugin_name): 11 | """ 12 | :return: instance of requested class 13 | :rtype: Plugin 14 | """ 15 | klass = globals()[plugin_name] # type: Plugin 16 | instance = klass(config, api, log, notify_conf) 17 | instance.on_bot_init() 18 | return instance 19 | 20 | 21 | def init(cfg, api1, log1, notify_conf1): 22 | """ 23 | @type cfg1: modules.Configuration 24 | @type api1: modules.Poloniex.Poloniex 25 | @type log1: modules.Logger.Logger 26 | """ 27 | global config, api, log, notify_conf 28 | config = cfg 29 | api = api1 30 | log = log1 31 | notify_conf = notify_conf1 32 | 33 | plugin_names = config.get_plugins_config() 34 | active_plugins = [] 35 | for plugin_name in plugin_names: 36 | try: 37 | plugins.append(init_plugin(plugin_name)) 38 | active_plugins.append(plugin_name) 39 | except Exception as ex: 40 | log.log_error('Failed to load plugin {}: {}'.format(plugin_name, ex.message)) 41 | 42 | log.addSectionLog("plugins", "enabled", active_plugins) 43 | 44 | 45 | def after_lending(): 46 | for plugin in plugins: 47 | plugin.after_lending() 48 | 49 | 50 | def before_lending(): 51 | for plugin in plugins: 52 | plugin.before_lending() 53 | 54 | 55 | def on_bot_exit(): 56 | for plugin in plugins: 57 | plugin.on_bot_stop() 58 | -------------------------------------------------------------------------------- /modules/Poloniex.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import hashlib 3 | import hmac 4 | import json 5 | import socket 6 | import time 7 | import urllib 8 | import urllib2 9 | import threading 10 | import modules.Configuration as Config 11 | 12 | from modules.RingBuffer import RingBuffer 13 | from modules.ExchangeApi import ExchangeApi 14 | from modules.ExchangeApi import ApiError 15 | 16 | 17 | def post_process(before): 18 | after = before 19 | 20 | # Add timestamps if there isnt one but is a datetime 21 | if 'return' in after: 22 | if isinstance(after['return'], list): 23 | for x in xrange(0, len(after['return'])): 24 | if isinstance(after['return'][x], dict): 25 | if 'datetime' in after['return'][x] and 'timestamp' not in after['return'][x]: 26 | after['return'][x]['timestamp'] = float(ExchangeApi.create_time_stamp(after['return'][x]['datetime'])) 27 | 28 | return after 29 | 30 | 31 | class Poloniex(ExchangeApi): 32 | def __init__(self, cfg, log): 33 | super(Poloniex, self).__init__(cfg, log) 34 | self.cfg = cfg 35 | self.log = log 36 | self.APIKey = self.cfg.get("API", "apikey", None) 37 | self.Secret = self.cfg.get("API", "secret", None) 38 | self.req_per_period = 6 39 | self.default_req_period = 1000 # milliseconds 40 | self.req_period = self.default_req_period 41 | self.req_time_log = RingBuffer(self.req_per_period) 42 | self.lock = threading.RLock() 43 | socket.setdefaulttimeout(int(Config.get("BOT", "timeout", 30, 1, 180))) 44 | self.api_debug_log = self.cfg.getboolean("BOT", "api_debug_log") 45 | 46 | def limit_request_rate(self): 47 | super(Poloniex, self).limit_request_rate() 48 | 49 | def increase_request_timer(self): 50 | super(Poloniex, self).increase_request_timer() 51 | 52 | def decrease_request_timer(self): 53 | super(Poloniex, self).decrease_request_timer() 54 | 55 | def reset_request_timer(self): 56 | super(Poloniex, self).reset_request_timer() 57 | 58 | @ExchangeApi.synchronized 59 | def api_query(self, command, req=None): 60 | # keep the 6 request per sec limit 61 | self.limit_request_rate() 62 | 63 | if req is None: 64 | req = {} 65 | 66 | def _read_response(resp): 67 | resp_data = json.loads(resp.read()) 68 | if 'error' in resp_data: 69 | raise ApiError(resp_data['error']) 70 | return resp_data 71 | 72 | try: 73 | if command == "returnTicker" or command == "return24hVolume": 74 | ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/public?command=' + command)) 75 | return _read_response(ret) 76 | elif command == "returnOrderBook": 77 | ret = urllib2.urlopen(urllib2.Request( 78 | 'https://poloniex.com/public?command=' + command + '¤cyPair=' + str(req['currencyPair']))) 79 | return _read_response(ret) 80 | elif command == "returnMarketTradeHistory": 81 | ret = urllib2.urlopen(urllib2.Request( 82 | 'https://poloniex.com/public?command=' + "returnTradeHistory" + '¤cyPair=' + str( 83 | req['currencyPair']))) 84 | return _read_response(ret) 85 | elif command == "returnLoanOrders": 86 | req_url = ('https://poloniex.com/public?command=' + "returnLoanOrders" 87 | + '¤cy=' + str(req['currency'])) 88 | if req['limit'] > 0: 89 | req_url += ('&limit=' + str(req['limit'])) 90 | ret = urllib2.urlopen(urllib2.Request(req_url)) 91 | return _read_response(ret) 92 | else: 93 | req['command'] = command 94 | req['nonce'] = int(time.time() * 1000) 95 | post_data = urllib.urlencode(req) 96 | 97 | sign = hmac.new(self.Secret, post_data, hashlib.sha512).hexdigest() 98 | headers = { 99 | 'Sign': sign, 100 | 'Key': self.APIKey 101 | } 102 | 103 | ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/tradingApi', post_data, headers)) 104 | json_ret = _read_response(ret) 105 | return post_process(json_ret) 106 | 107 | # Check in case something has gone wrong and the timer is too big 108 | self.reset_request_timer() 109 | 110 | except urllib2.HTTPError as ex: 111 | raw_polo_response = ex.read() 112 | try: 113 | data = json.loads(raw_polo_response) 114 | polo_error_msg = data['error'] 115 | except: 116 | if hasattr(ex, 'code') and (ex.code == 502 or ex.code in range(520, 527, 1)): 117 | # 502 and 520-526 Bad Gateway so response is likely HTML from Cloudflare 118 | polo_error_msg = 'API Error ' + str(ex.code) + \ 119 | ': The web server reported a bad gateway or gateway timeout error.' 120 | elif hasattr(ex, 'code') and (ex.code == 429): 121 | self.increase_request_timer() 122 | else: 123 | polo_error_msg = raw_polo_response 124 | ex.message = ex.message if ex.message else str(ex) 125 | ex.message = "{0} Requesting {1}. Poloniex reports: '{2}'".format(ex.message, command, polo_error_msg) 126 | raise ex 127 | except Exception as ex: 128 | ex.message = ex.message if ex.message else str(ex) 129 | ex.message = "{0} Requesting {1}".format(ex.message, command) 130 | raise 131 | 132 | def return_ticker(self): 133 | return self.api_query("returnTicker") 134 | 135 | def return24h_volume(self): 136 | return self.api_query("return24hVolume") 137 | 138 | def return_order_book(self, currency_pair): 139 | return self.api_query("returnOrderBook", {'currencyPair': currency_pair}) 140 | 141 | def return_market_trade_history(self, currency_pair): 142 | return self.api_query("returnMarketTradeHistory", {'currencyPair': currency_pair}) 143 | 144 | def transfer_balance(self, currency, amount, from_account, to_account): 145 | return self.api_query("transferBalance", {'currency': currency, 'amount': amount, 'fromAccount': from_account, 146 | 'toAccount': to_account}) 147 | 148 | # Returns all of your balances. 149 | # Outputs: 150 | # {"BTC":"0.59098578","LTC":"3.31117268", ... } 151 | def return_balances(self): 152 | return self.api_query('returnBalances') 153 | 154 | def return_available_account_balances(self, account): 155 | balances = self.api_query('returnAvailableAccountBalances', {"account": account}) 156 | if isinstance(balances, list): # silly api wrapper, empty dict returns a list, which breaks the code later. 157 | balances = {} 158 | return balances 159 | 160 | # Returns your open orders for a given market, specified by the "currencyPair" POST parameter, e.g. "BTC_XCP" 161 | # Inputs: 162 | # currencyPair The currency pair e.g. "BTC_XCP" 163 | # Outputs: 164 | # orderNumber The order number 165 | # type sell or buy 166 | # rate Price the order is selling or buying at 167 | # Amount Quantity of order 168 | # total Total value of order (price * quantity) 169 | def return_open_orders(self, currency_pair): 170 | return self.api_query('returnOpenOrders', {"currencyPair": currency_pair}) 171 | 172 | def return_open_loan_offers(self): 173 | loan_offers = self.api_query('returnOpenLoanOffers') 174 | if isinstance(loan_offers, list): # silly api wrapper, empty dict returns a list, which breaks the code later. 175 | loan_offers = {} 176 | return loan_offers 177 | 178 | def return_active_loans(self): 179 | return self.api_query('returnActiveLoans') 180 | 181 | def return_lending_history(self, start, stop, limit=500): 182 | return self.api_query('returnLendingHistory', {'start': start, 'end': stop, 'limit': limit}) 183 | 184 | # Returns your trade history for a given market, specified by the "currencyPair" POST parameter 185 | # Inputs: 186 | # currencyPair The currency pair e.g. "BTC_XCP" 187 | # Outputs: 188 | # date Date in the form: "2014-02-19 03:44:59" 189 | # rate Price the order is selling or buying at 190 | # amount Quantity of order 191 | # total Total value of order (price * quantity) 192 | # type sell or buy 193 | def return_trade_history(self, currency_pair): 194 | return self.api_query('returnTradeHistory', {"currencyPair": currency_pair}) 195 | 196 | # Places a buy order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". 197 | # If successful, the method will return the order number. 198 | # Inputs: 199 | # currencyPair The curreny pair 200 | # rate price the order is buying at 201 | # amount Amount of coins to buy 202 | # Outputs: 203 | # orderNumber The order number 204 | def buy(self, currency_pair, rate, amount): 205 | return self.api_query('buy', {"currencyPair": currency_pair, "rate": rate, "amount": amount}) 206 | 207 | # Places a sell order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". 208 | # If successful, the method will return the order number. 209 | # Inputs: 210 | # currencyPair The curreny pair 211 | # rate price the order is selling at 212 | # amount Amount of coins to sell 213 | # Outputs: 214 | # orderNumber The order number 215 | def sell(self, currency_pair, rate, amount): 216 | return self.api_query('sell', {"currencyPair": currency_pair, "rate": rate, "amount": amount}) 217 | 218 | def create_loan_offer(self, currency, amount, duration, auto_renew, lending_rate): 219 | return self.api_query('createLoanOffer', 220 | {"currency": currency, "amount": amount, "duration": duration, "autoRenew": auto_renew, 221 | "lendingRate": lending_rate, }) 222 | 223 | # Cancels an order you have placed in a given market. Required POST parameters are "currencyPair" and "orderNumber". 224 | # Inputs: 225 | # currencyPair The curreny pair 226 | # orderNumber The order number to cancel 227 | # Outputs: 228 | # succes 1 or 0 229 | def cancel(self, currency_pair, order_number): 230 | return self.api_query('cancelOrder', {"currencyPair": currency_pair, "orderNumber": order_number}) 231 | 232 | def cancel_loan_offer(self, currency, order_number): 233 | return self.api_query('cancelLoanOffer', {"currency": currency, "orderNumber": order_number}) 234 | 235 | # Immediately places a withdrawal for a given currency, with no email confirmation. 236 | # In order to use this method, the withdrawal privilege must be enabled for your API key. 237 | # Required POST parameters are "currency", "amount", and "address". Sample output: {"response":"Withdrew 2398 NXT."} 238 | # Inputs: 239 | # currency The currency to withdraw 240 | # amount The amount of this coin to withdraw 241 | # address The withdrawal address 242 | # Outputs: 243 | # response Text containing message about the withdrawal 244 | def withdraw(self, currency, amount, address): 245 | return self.api_query('withdraw', {"currency": currency, "amount": amount, "address": address}) 246 | 247 | def return_loan_orders(self, currency, limit=0): 248 | return self.api_query('returnLoanOrders', {"currency": currency, "limit": limit}) 249 | 250 | # Toggles the auto renew setting for the specified orderNumber 251 | def toggle_auto_renew(self, order_number): 252 | return self.api_query('toggleAutoRenew', {"orderNumber": order_number}) 253 | -------------------------------------------------------------------------------- /modules/RingBuffer.py: -------------------------------------------------------------------------------- 1 | # the equivalent of a circular size limited list 2 | # also known as ring buffer, pops the oldest data item 3 | # to make room for newest data item when max size is reached 4 | # uses the double ended queue available in Python24 5 | 6 | from collections import deque 7 | 8 | class RingBuffer(deque): 9 | """ 10 | inherits deque, pops the oldest data to make room 11 | for the newest data when size is reached 12 | """ 13 | def __init__(self, size): 14 | deque.__init__(self) 15 | self.size = size 16 | 17 | def full_append(self, item): 18 | deque.append(self, item) 19 | # full, pop the oldest item, left most item 20 | self.popleft() 21 | 22 | def append(self, item): 23 | deque.append(self, item) 24 | # max size reached, append becomes full_append 25 | if len(self) == self.size: 26 | self.append = self.full_append 27 | 28 | def get(self): 29 | """returns a list of size items (newest items)""" 30 | return list(self) 31 | 32 | # testing 33 | if __name__ == '__main__': 34 | size = 5 35 | ring = RingBuffer(size) 36 | for x in range(9): 37 | ring.append(x) 38 | print ring.get() # test 39 | 40 | """ 41 | notice that the left most item is popped to make room 42 | result = 43 | [0] 44 | [0, 1] 45 | [0, 1, 2] 46 | [0, 1, 2, 3] 47 | [0, 1, 2, 3, 4] 48 | [1, 2, 3, 4, 5] 49 | [2, 3, 4, 5, 6] 50 | [3, 4, 5, 6, 7] 51 | [4, 5, 6, 7, 8] 52 | """ -------------------------------------------------------------------------------- /modules/WebServer.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import threading 3 | import os 4 | 5 | server = None 6 | web_server_ip = "0.0.0.0" 7 | web_server_port = "8000" 8 | web_server_template = "www" 9 | 10 | 11 | def initialize_web_server(config): 12 | ''' 13 | Setup the web server, retrieving the configuration parameters 14 | and starting the web server thread 15 | ''' 16 | global web_server_ip, web_server_port, web_server_template 17 | 18 | # Check for custom web server address 19 | compositeWebServerAddress = config.get('BOT', 'customWebServerAddress', '0.0.0.0').split(":") 20 | 21 | # associate web server ip address 22 | web_server_ip = compositeWebServerAddress[0] 23 | 24 | # check for IP:PORT legacy format 25 | if (len(compositeWebServerAddress) > 1): 26 | # associate web server port 27 | web_server_port = compositeWebServerAddress[1] 28 | else: 29 | # Check for custom web server port 30 | web_server_port = config.get('BOT', 'customWebServerPort', '8000') 31 | 32 | # Check for custom web server template 33 | web_server_template = config.get('BOT', 'customWebServerTemplate', 'www') 34 | 35 | print('Starting WebServer at {0} on port {1} with template {2}' 36 | .format(web_server_ip, web_server_port, web_server_template)) 37 | 38 | thread = threading.Thread(target=start_web_server) 39 | thread.deamon = True 40 | thread.start() 41 | 42 | 43 | def start_web_server(): 44 | ''' 45 | Start the web server 46 | ''' 47 | import SimpleHTTPServer 48 | import SocketServer 49 | import socket 50 | 51 | try: 52 | port = int(web_server_port) 53 | host = web_server_ip 54 | 55 | # Do not attempt to fix code warnings in the below class, it is perfect. 56 | class QuietHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 57 | real_server_path = os.path.abspath(web_server_template) 58 | 59 | # quiet server logs 60 | def log_message(self, format, *args): 61 | return 62 | 63 | # serve from web_server_template folder under current working dir 64 | def translate_path(self, path): 65 | return SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, '/' + web_server_template + path) 66 | 67 | def send_head(self): 68 | local_path = self.translate_path(self.path) 69 | if os.path.commonprefix((os.path.abspath(local_path), self.real_server_path)) != self.real_server_path: 70 | self.send_error(404, "These aren't the droids you're looking for") 71 | return None 72 | return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self) 73 | 74 | global server 75 | SocketServer.TCPServer.allow_reuse_address = True 76 | server = SocketServer.TCPServer((host, port), QuietHandler) 77 | if host == "0.0.0.0": 78 | # Get all addresses that we could listen on the port specified 79 | addresses = [i[4][0] for i in socket.getaddrinfo(socket.gethostname().split('.')[0], port)] 80 | addresses = [i for i in addresses if ':' not in i] # Filter out all IPv6 addresses 81 | addresses.append('127.0.0.1') # getaddrinfo doesn't always get localhost 82 | hosts = list(set(addresses)) # Make list unique 83 | else: 84 | hosts = [host] 85 | serving_msg = "http://{0}:{1}/lendingbot.html".format(hosts[0], port) 86 | for host in hosts[1:]: 87 | serving_msg += ", http://{0}:{1}/lendingbot.html".format(host, port) 88 | print('Started WebServer, lendingbot status available at {0}'.format(serving_msg)) 89 | server.serve_forever() 90 | except Exception as ex: 91 | ex.message = ex.message if ex.message else str(ex) 92 | print('Failed to start WebServer: {0}'.format(ex.message)) 93 | 94 | 95 | def stop_web_server(): 96 | ''' 97 | Stop the web server 98 | ''' 99 | try: 100 | print("Stopping WebServer") 101 | threading.Thread(target=server.shutdown).start() 102 | except Exception as ex: 103 | ex.message = ex.message if ex.message else str(ex) 104 | print("Failed to stop WebServer: {0}".format(ex.message)) 105 | -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # We need this file so Python knows it is okay to import modules from this folder. Do not Delete. 3 | -------------------------------------------------------------------------------- /plugins/AccountStats.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from plugins.Plugin import Plugin 3 | import sqlite3 4 | 5 | DB_VERSION = 2 6 | BITCOIN_GENESIS_BLOCK_DATE = "2009-01-03 18:15:05" 7 | DB_DROP = "DROP TABLE IF EXISTS history" 8 | DB_CREATE = "CREATE TABLE IF NOT EXISTS history(" \ 9 | "id INTEGER NOT NULL, open TIMESTAMP, close TIMESTAMP," \ 10 | " duration NUMBER, interest NUMBER, rate NUMBER," \ 11 | " currency TEXT NOT NULL, amount NUMBER, earned NUMBER, fee NUMBER," \ 12 | " UNIQUE(id, currency) ON CONFLICT REPLACE )" 13 | DB_INSERT = "INSERT OR REPLACE INTO 'history'" \ 14 | "('id','open','close','duration','interest','rate','currency','amount','earned','fee')" \ 15 | " VALUES (?,?,?,?,?,?,?,?,?,?);" 16 | DB_GET_LAST_TIMESTAMP = "SELECT max(close) as last_timestamp FROM 'history'" 17 | DB_GET_FIRST_TIMESTAMP = "SELECT min(close) as first_timestamp FROM 'history'" 18 | DB_GET_TOTAL_EARNED = "SELECT sum(earned) as total_earned, currency FROM 'history' GROUP BY currency" 19 | DB_GET_YESTERDAY_EARNINGS = "SELECT sum(earned) as total_earned, currency FROM 'history' " \ 20 | "WHERE close BETWEEN datetime('now', 'start of day', '-1 day') " \ 21 | "AND datetime('now','start of day') GROUP BY currency" 22 | DB_GET_TODAYS_EARNINGS = "SELECT sum(earned) as total_earned, currency FROM 'history' " \ 23 | "WHERE close > datetime('now','start of day') GROUP BY currency" 24 | 25 | 26 | class AccountStats(Plugin): 27 | last_notification = 0 28 | earnings = {} 29 | report_interval = 86400 30 | 31 | def on_bot_init(self): 32 | super(AccountStats, self).on_bot_init() 33 | self.init_db() 34 | self.check_upgrade() 35 | self.report_interval = int(self.config.get("ACCOUNTSTATS", "ReportInterval", 86400)) 36 | 37 | def before_lending(self): 38 | for coin in self.earnings: 39 | for key in self.earnings[coin]: 40 | self.log.updateStatusValue(coin, key, self.earnings[coin][key]) 41 | 42 | def after_lending(self): 43 | if self.get_db_version() > 0 \ 44 | and self.last_notification != 0 \ 45 | and self.last_notification + self.report_interval > sqlite3.time.time(): 46 | return 47 | self.update_history() 48 | self.notify_stats() 49 | 50 | # noinspection PyAttributeOutsideInit 51 | def init_db(self): 52 | self.db = sqlite3.connect('market_data/loan_history.sqlite3') 53 | self.db.execute(DB_CREATE) 54 | self.db.commit() 55 | 56 | def check_upgrade(self): 57 | if 0 < self.get_db_version() < DB_VERSION: 58 | # drop table and set version to 0 to reinitialize db to new version. 59 | self.db.execute(DB_DROP) 60 | self.set_db_version(0) 61 | self.db.commit() 62 | self.db.execute(DB_CREATE) 63 | self.db.commit() 64 | self.log.log('Upgraded AccountStats DB to version ' + str(DB_VERSION)) 65 | 66 | def update_history(self): 67 | # timestamps are in UTC 68 | last_time_stamp = self.get_last_timestamp() 69 | 70 | if last_time_stamp is None: 71 | # no entries means db is empty and needs initialization 72 | last_time_stamp = BITCOIN_GENESIS_BLOCK_DATE 73 | self.db.execute("PRAGMA user_version = 0") 74 | 75 | self.fetch_history(self.api.create_time_stamp(last_time_stamp), sqlite3.time.time()) 76 | 77 | # Fetch history in batches, loop to make sure we got everything 78 | if (self.get_db_version() == 0) and (self.get_first_timestamp() is not None): 79 | last_time_stamp = BITCOIN_GENESIS_BLOCK_DATE 80 | loop = True 81 | while loop: 82 | sqlite3.time.sleep(10) # delay a bit, try not to annoy exchange 83 | first_time_stamp = self.get_first_timestamp() 84 | count = self.fetch_history(self.api.create_time_stamp(last_time_stamp), 85 | self.api.create_time_stamp(first_time_stamp)) 86 | loop = count != 0 87 | 88 | # if we reached here without errors means we managed to fetch all the history, db is ready. 89 | self.set_db_version(DB_VERSION) 90 | 91 | def set_db_version(self, version): 92 | self.db.execute("PRAGMA user_version = " + str(version)) 93 | 94 | def get_db_version(self): 95 | return self.db.execute("PRAGMA user_version").fetchone()[0] 96 | 97 | def fetch_history(self, first_time_stamp, last_time_stamp): 98 | history = self.api.return_lending_history(first_time_stamp, last_time_stamp - 1, 5000) 99 | loans = [] 100 | for loan in history: 101 | loans.append( 102 | [loan['id'], loan['open'], loan['close'], loan['duration'], loan['interest'], 103 | loan['rate'], loan['currency'], loan['amount'], loan['earned'], loan['fee']]) 104 | self.db.executemany(DB_INSERT, loans) 105 | self.db.commit() 106 | count = len(loans) 107 | self.log.log('Downloaded ' + str(count) + ' loans history ' 108 | + sqlite3.datetime.datetime.utcfromtimestamp(first_time_stamp).strftime('%Y-%m-%d %H:%M:%S') 109 | + ' to ' + sqlite3.datetime.datetime.utcfromtimestamp(last_time_stamp - 1).strftime( 110 | '%Y-%m-%d %H:%M:%S')) 111 | if count > 0: 112 | self.log.log('Last: ' + history[0]['close'] + ' First:' + history[count - 1]['close']) 113 | return count 114 | 115 | def get_last_timestamp(self): 116 | cursor = self.db.execute(DB_GET_LAST_TIMESTAMP) 117 | row = cursor.fetchone() 118 | cursor.close() 119 | return row[0] 120 | 121 | def get_first_timestamp(self): 122 | cursor = self.db.execute(DB_GET_FIRST_TIMESTAMP) 123 | row = cursor.fetchone() 124 | cursor.close() 125 | return row[0] 126 | 127 | def notify_stats(self): 128 | if (self.get_db_version() == 0) and (self.get_first_timestamp() is not None): 129 | # only log an error if there are actually loans in DB 130 | self.log.log_error('AccountStats DB isn\'t ready.') 131 | return 132 | 133 | self.earnings = {} 134 | output = '' 135 | 136 | # Today's earnings 137 | cursor = self.db.execute(DB_GET_TODAYS_EARNINGS) 138 | row = cursor.fetchone() 139 | if row is not None: 140 | while row is not None: 141 | output += self.format_value(row[0]) + ' ' + str(row[1]) + ' Today\n' 142 | if row[1] not in self.earnings: 143 | self.earnings[row[1]] = {} 144 | self.earnings[row[1]]['todayEarnings'] = row[0] 145 | row = cursor.fetchone() 146 | else: 147 | output += 'None Today\n' 148 | cursor.close() 149 | 150 | # Yesterday's earnings 151 | cursor = self.db.execute(DB_GET_YESTERDAY_EARNINGS) 152 | row = cursor.fetchone() 153 | if row is not None: 154 | while row is not None: 155 | output += self.format_value(row[0]) + ' ' + str(row[1]) + ' Yesterday\n' 156 | if row[1] not in self.earnings: 157 | self.earnings[row[1]] = {} 158 | self.earnings[row[1]]['yesterdayEarnings'] = row[0] 159 | row = cursor.fetchone() 160 | else: 161 | output += 'None Yesterday\n' 162 | cursor.close() 163 | 164 | # Total Earnings 165 | cursor = self.db.execute(DB_GET_TOTAL_EARNED) 166 | row = cursor.fetchone() 167 | if row is not None: 168 | while row is not None: 169 | output += self.format_value(row[0]) + ' ' + str(row[1]) + ' in total\n' 170 | if row[1] not in self.earnings: 171 | self.earnings[row[1]] = {} 172 | self.earnings[row[1]]['totalEarnings'] = row[0] 173 | row = cursor.fetchone() 174 | else: 175 | output += 'Unknown total earnings.\n' 176 | cursor.close() 177 | 178 | if output != '': 179 | self.last_notification = sqlite3.time.time() 180 | output = 'Earnings:\n----------\n' + output 181 | self.log.notify(output, self.notify_config) 182 | self.log.log(output) 183 | 184 | @staticmethod 185 | def format_value(value): 186 | return '{0:0.12f}'.format(float(value)).rstrip('0').rstrip('.') 187 | -------------------------------------------------------------------------------- /plugins/Charts.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from plugins.Plugin import Plugin 3 | import os 4 | import json 5 | import sqlite3 6 | 7 | DB_PATH = "market_data/loan_history.sqlite3" 8 | 9 | class Charts(Plugin): 10 | 11 | def on_bot_init(self): 12 | super(Charts, self).on_bot_init() 13 | 14 | # If there's no history database, can't use this 15 | if not os.path.isfile(DB_PATH): 16 | self.log.log_error("DB Doesn't Exist. 'AccountStats' plugin must be enabled.") 17 | return 18 | 19 | self.log.addSectionLog("plugins", "charts", { 'navbar': True }) 20 | 21 | self.db = sqlite3.connect('market_data/loan_history.sqlite3') 22 | self.last_dump = 0 23 | self.dump_interval = int(self.config.get("CHARTS", "DumpInterval", 21600)) 24 | self.history_file = self.config.get("CHARTS", "HistoryFile", "www/history.json") 25 | self.activeCurrencies = self.config.get_all_currencies() 26 | 27 | 28 | def before_lending(self): 29 | return 30 | 31 | 32 | def after_lending(self): 33 | if self.get_db_version() > 0 and self.last_dump + self.dump_interval < sqlite3.time.time(): 34 | self.log.log("Dumping Charts Data") 35 | self.dump_history() 36 | self.last_dump = sqlite3.time.time() 37 | 38 | 39 | def get_db_version(self): 40 | return self.db.execute("PRAGMA user_version").fetchone()[0] 41 | 42 | 43 | def dump_history(self): 44 | 45 | cursor = self.db.cursor() 46 | 47 | data = { } 48 | placeholder = '?' 49 | placeholders = ', '.join(placeholder for unused in self.activeCurrencies) 50 | 51 | # Get distinct coins 52 | query = "SELECT DISTINCT currency FROM history WHERE currency IN (%s) ORDER BY currency DESC" % placeholders 53 | cursor.execute(query, self.activeCurrencies) 54 | for i in cursor: 55 | data[i[0]] = [] 56 | 57 | # Loop over the coins and get data for each 58 | for coin in data: 59 | runningTotal = 0.0 60 | 61 | cursor.execute("SELECT strftime('%%s', strftime('%%Y-%%m-%%d 00:00:00', close)) ts, round(SUM(earned), 8) i " \ 62 | "FROM history WHERE currency = '%s' GROUP BY ts ORDER BY ts" % (coin)); 63 | for row in cursor: 64 | runningTotal += float(row[1]) 65 | data[coin].append([ int(row[0]), float(row[1]), float(runningTotal) ]) 66 | 67 | # Dump data to file 68 | with open(self.history_file, "w") as hist: 69 | hist.write(json.dumps(data)) 70 | 71 | self.log.log("Charts Plugin: History dumped. You can open charts.html.") 72 | cursor.close() 73 | -------------------------------------------------------------------------------- /plugins/Plugin.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | 4 | class Plugin(object): 5 | """ 6 | @type cfg1: modules.Configuration 7 | @type api1: modules.Poloniex.Poloniex 8 | @type log1: modules.Logger.Logger 9 | """ 10 | def __init__(self, cfg1, api1, log1, notify_config1): 11 | self.api = api1 12 | self.config = cfg1 13 | self.notify_config = notify_config1 14 | self.log = log1 15 | 16 | # override this to run plugin init code 17 | def on_bot_init(self): 18 | self.log.log(self.__class__.__name__ + ' plugin initializing...') 19 | 20 | # override this to run plugin loop code before lending 21 | def before_lending(self): 22 | pass 23 | 24 | # override this to run plugin loop code after lending 25 | def after_lending(self): 26 | pass 27 | 28 | # override this to run plugin stop code 29 | # since the bot can be killed, there is not guarantee this will be called. 30 | def on_bot_stop(self): 31 | pass 32 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | __all__ = [] 3 | 4 | import pkgutil 5 | import inspect 6 | 7 | for loader, name, is_pkg in pkgutil.walk_packages(__path__): 8 | module = loader.find_module(name).load_module(name) 9 | 10 | for name, value in inspect.getmembers(module): 11 | if name.startswith('__'): 12 | continue 13 | 14 | globals()[name] = value 15 | __all__.append(name) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | hypothesis 4 | requests 5 | pytz 6 | -------------------------------------------------------------------------------- /setup-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp -rp /usr/git-src/* /usr/src/app/ 4 | unlink /usr/git-src/${VIRTUAL_HOST}-botlog.json 5 | touch /usr/git-src/${VIRTUAL_HOST}-botlog.json 6 | rm /usr/src/app/www/botlog.json 7 | ln -s /usr/git-src/${VIRTUAL_HOST}-botlog.json /usr/src/app/www/botlog.json 8 | 9 | rm -rf /usr/src/app/market_data/ 10 | ln -s /usr/git-src/market_data/ /usr/src/app/market_data 11 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | #Poloniex lending bot tests 2 | 3 | All tests are written to work with pytest, they also use hypothesis. These are both available through PyPi using pip 4 | 5 | `pip install hypothesis pytest` 6 | 7 | For all the tests to work correctly you will also need numpy installed 8 | 9 | `pip install numpy` 10 | 11 | Currently there are only tests for the MarketAnalysis module though more will be added as time goes on. 12 | 13 | To run the tests, you need to have a config file that has a correct key to talk to Poloniex, it won't make any trades or do anything to your account, it's a problem with how we initialise modules at the minute. 14 | Then, cd to the root of the source code and run: 15 | 16 | `pytest tests/` 17 | 18 | That's it! If you come across any problems, make sure it's not something in your code. If you're sure it's an issue then please raise it on github. 19 | -------------------------------------------------------------------------------- /tests/test_BitfinexAPI.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | # Hack to get relative imports - probably need to fix the dir structure instead but we need this at the minute for 4 | # pytest to work 5 | import os, sys, inspect 6 | 7 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 8 | parentdir = os.path.dirname(currentdir) 9 | sys.path.insert(0, parentdir) 10 | 11 | from modules.Bitfinex import Bitfinex 12 | import modules.Configuration as Config 13 | import modules.Data as Data 14 | from modules.Logger import Logger 15 | import threading 16 | 17 | Config.init('default.cfg', Data) 18 | api = Bitfinex(Config, Logger()) 19 | start_time = time.time() 20 | 21 | 22 | def multiple_api_queries(n): 23 | try: 24 | for i in xrange(n): 25 | print 'Thread ' + str(i + 1) 26 | thread1 = threading.Thread(target=call_get_open_loan_offers, args=[(i+1)]) 27 | thread1.start() 28 | except Exception as e: 29 | assert False, 'Thread ' + str(i + 1) + ':' + e.message 30 | 31 | 32 | # Test fast api calls 33 | def test_multiple_calls(): 34 | multiple_api_queries(270) 35 | 36 | 37 | def call_get_open_loan_offers(i): 38 | api.return_open_loan_offers() 39 | print 'API Call ' + str(i) + ' sec:' + str(time.time() - start_time) 40 | 41 | 42 | # def api_rate_limit(n, start): 43 | # api.limit_request_rate() 44 | # # verify that the (N % 6) th request is delayed by (N / 6) sec from the start time 45 | # if n != 0 and n % 6 == 0: 46 | # print 'limit request ' + str(n) + ' ' + str(start) + ' ' + str(time.time()) + '\n' 47 | # assert time.time() - start >= int(n / 6), "rate limit failed" 48 | # 49 | # 50 | # # Test rate limiter 51 | # def test_rate_limiter(): 52 | # start = time.time() 53 | # for i in xrange(20): 54 | # thread1 = threading.Thread(target=api_rate_limit, args=(i, start)) 55 | # thread1.start() 56 | -------------------------------------------------------------------------------- /tests/test_MarketAnalysis.py: -------------------------------------------------------------------------------- 1 | from hypothesis import given, settings 2 | from hypothesis.strategies import floats, lists, integers 3 | from hypothesis.extra.datetime import datetimes 4 | 5 | import csv 6 | import datetime 7 | import time 8 | import pytest 9 | import sqlite3 as sqlite 10 | from random import randint 11 | import pandas as pd 12 | 13 | # Hack to get relative imports - probably need to fix the dir structure instead but we need this at the minute for 14 | # pytest to work 15 | import os, sys, inspect 16 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 17 | parentdir = os.path.dirname(currentdir) 18 | sys.path.insert(0, parentdir) 19 | 20 | from modules.MarketAnalysis import MarketAnalysis 21 | from modules.Configuration import get_all_currencies 22 | from modules.Poloniex import Poloniex 23 | import modules.Configuration as Config 24 | import modules.Data as Data 25 | 26 | Config.init('default.cfg', Data) 27 | api = Poloniex(Config, None) 28 | Data.init(api, None) 29 | MA = MarketAnalysis(Config, api) 30 | 31 | 32 | def new_db(): 33 | db_con = MA.create_connection(None, ':memory:') 34 | MA.create_rate_table(db_con, 3) 35 | return db_con 36 | 37 | 38 | def random_rates(): 39 | return lists(floats(min_value=0.00001, max_value=100, allow_nan=False, allow_infinity=False), min_size=0, max_size=100).example() 40 | 41 | 42 | def random_dates(min_len, max_len): 43 | max_year = datetime.datetime.now().year 44 | return lists(datetimes(min_year=2016, max_year=max_year), min_size=min_len, max_size=max_len).map(sorted).example() 45 | 46 | 47 | @pytest.fixture 48 | def populated_db(): 49 | price_levels = 3 50 | db_con = new_db() 51 | rates = random_rates() 52 | inserted_rates = [] 53 | for rate in rates: 54 | market_data = [] 55 | for level in range(price_levels): 56 | market_data.append("{0:.8f}".format(rate)) 57 | market_data.append("{0:.2f}".format(rate)) 58 | percentile = "{0:.8f}".format(rate) 59 | market_data.append(percentile) 60 | MA.insert_into_db(db_con, market_data) 61 | market_data = [float(x) for x in market_data] 62 | inserted_rates.append(market_data) 63 | return db_con, inserted_rates 64 | 65 | 66 | def test_new_db(): 67 | assert(isinstance(new_db(), sqlite.Connection)) 68 | 69 | 70 | def test_insert_into_db(populated_db): 71 | db_con, rates = populated_db 72 | query = "SELECT rate0, amnt0, rate1, amnt1, rate2, amnt2, percentile FROM loans;" 73 | db_rates = db_con.cursor().execute(query).fetchall() 74 | assert(len(rates) == len(db_rates)) 75 | for db_rate, rate in zip(db_rates, rates): 76 | assert(len(rate) == len(db_rate)) 77 | assert(len(rate) > 1) 78 | for level in range(len(rate)): 79 | assert(db_rate[level] == float(rate[level])) 80 | 81 | 82 | def test_get_rates_from_db(populated_db): 83 | db_con, rates = populated_db 84 | db_rates = MA.get_rates_from_db(db_con, from_date=time.time() - 10, price_levels=['rate0']) 85 | for db_rate, rate in zip(db_rates, rates): 86 | assert(len(db_rate) == 2) 87 | assert(db_rate[1] == float(rate[0])) 88 | 89 | 90 | def test_get_rate_list(populated_db): 91 | db_con, rates = populated_db 92 | db_rates = MA.get_rate_list(db_con, 1) 93 | assert(len(db_rates) == 1) 94 | 95 | 96 | def test_get_rate_suggestion(populated_db): 97 | db_con, rates = populated_db 98 | MA = MarketAnalysis(Config, api) 99 | MA.data_tolerance = 1 100 | 101 | rate_db = MA.get_rate_suggestion(db_con, method='percentile') 102 | assert(rate_db >= 0) 103 | 104 | df = pd.DataFrame(rates) 105 | df.columns = ['rate0', 'a0', 'r1', 'a1', 'r2', 'a2', 'p'] 106 | df.time = [time.time()] * len(df) 107 | rate_args = MA.get_rate_suggestion(db_con, df, 'percentile') 108 | assert(rate_args >= 0) 109 | 110 | rate = MA.get_rate_suggestion(db_con, method='MACD') 111 | assert(rate >= 0) 112 | 113 | 114 | @given(lists(floats(min_value=0, allow_nan=False, allow_infinity=False), min_size=3, max_size=100), 115 | integers(min_value=1, max_value=99)) 116 | def test_get_percentile(rates, lending_style): 117 | np_perc = MA.get_percentile(rates, lending_style, use_numpy=True) 118 | math_perc = MA.get_percentile(rates, lending_style, use_numpy=False) 119 | assert(np_perc == math_perc) 120 | -------------------------------------------------------------------------------- /tests/test_PoloniexAPI.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | # Hack to get relative imports - probably need to fix the dir structure instead but we need this at the minute for 4 | # pytest to work 5 | import os, sys, inspect 6 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 7 | parentdir = os.path.dirname(currentdir) 8 | sys.path.insert(0, parentdir) 9 | 10 | from modules.Poloniex import Poloniex 11 | import modules.Configuration as Config 12 | import modules.Data as Data 13 | from modules.Logger import Logger 14 | import threading 15 | 16 | Config.init('default.cfg', Data) 17 | api = Poloniex(Config, Logger()) 18 | 19 | 20 | # def multiple_api_queries(n): 21 | # try: 22 | # for i in xrange(n): 23 | # print 'api_query ' + str(i + 1) + '\n' 24 | # thread1 = threading.Thread(target=api.return_open_loan_offers) 25 | # thread1.start() 26 | # except Exception as e: 27 | # assert False, 'api_query ' + str(i + 1) + ':' + e.message 28 | # 29 | # 30 | # # Test fast api calls 31 | # def test_multiple_calls(): 32 | # multiple_api_queries(9) 33 | 34 | 35 | def api_rate_limit(n, start): 36 | api.limit_request_rate() 37 | # verify that the (N % 6) th request is delayed by (N / 6) sec from the start time 38 | if n != 0 and n % 6 == 0: 39 | print 'limit request ' + str(n) + ' ' + str(start) + ' ' + str(time.time()) + '\n' 40 | assert time.time() - start >= int(n / 6), "rate limit failed" 41 | 42 | 43 | # Test rate limiter 44 | def test_rate_limiter(): 45 | start = time.time() 46 | for i in xrange(20): 47 | thread1 = threading.Thread(target=api_rate_limit, args=(i, start)) 48 | thread1.start() 49 | -------------------------------------------------------------------------------- /www/charts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Lending Bot - Profit Charts 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 68 | 69 | 70 | 71 | 72 | 94 | 95 |
96 |
97 |
98 |

*Profits are displayed as the currency in which they were lent.

99 |
100 |
101 |
102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /www/images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBotFactory/MikaLendingBot/b59ab8709220b3be1481ca8949a80f944b05018e/www/images/icon.ico -------------------------------------------------------------------------------- /www/images/icon192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBotFactory/MikaLendingBot/b59ab8709220b3be1481ca8949a80f944b05018e/www/images/icon192.png -------------------------------------------------------------------------------- /www/images/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBotFactory/MikaLendingBot/b59ab8709220b3be1481ca8949a80f944b05018e/www/images/icon32.png -------------------------------------------------------------------------------- /www/images/icon384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBotFactory/MikaLendingBot/b59ab8709220b3be1481ca8949a80f944b05018e/www/images/icon384.png -------------------------------------------------------------------------------- /www/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBotFactory/MikaLendingBot/b59ab8709220b3be1481ca8949a80f944b05018e/www/images/icon48.png -------------------------------------------------------------------------------- /www/images/ios_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBotFactory/MikaLendingBot/b59ab8709220b3be1481ca8949a80f944b05018e/www/images/ios_splash.png -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | Lending Bot 11 | 12 | 13 | -------------------------------------------------------------------------------- /www/lendingbot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Lending Bot 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 81 | 82 | 83 | 84 | 85 | 86 | 123 | 124 | 125 | 126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
StatusN/A
135 | 138 |
139 |
Log (Reversed ordered):
148 |
149 |
150 | 151 | 152 | 153 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /www/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Lending Bot", 3 | "short_name" : "Lending Bot", 4 | "icons" : [{ 5 | "src" : "images/icon192.png", 6 | "sizes" : "192x192", 7 | "type" : "image/png" 8 | }, { 9 | "src" : "images/icon384.png", 10 | "sizes" : "384x384", 11 | "type" : "image/png" 12 | } 13 | ], 14 | "background_color": "#FAFAFA", 15 | "theme_color": "#e7e7e7", 16 | "start_url" : "lendingbot.html", 17 | "display" : "standalone", 18 | "orientation" : "portrait" 19 | } 20 | -------------------------------------------------------------------------------- /www/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | --------------------------------------------------------------------------------