├── .gitignore ├── ArbitrageBot.py ├── LICENSE ├── README.md ├── config ├── __init__.py └── base.py ├── crawler ├── __init__.py ├── crawler.py └── data_loader.py ├── crawler_launch.py ├── messages.py ├── mongo_queries.py ├── notify_users.py ├── requirements.txt └── services ├── __init__.py ├── alerts.py ├── commands.py ├── core.py ├── helpers.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | .idea/ 3 | *.pyc 4 | config/local.py 5 | __pycache__/ -------------------------------------------------------------------------------- /ArbitrageBot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 4 | 5 | This file is part of CindicatorArbitrageBot. 6 | 7 | CindicatorArbitrageBot is free software: you can redistribute it and/or 8 | modify it under the terms of the GNU General Public License as published 9 | by the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with CindicatorArbitrageBot. If not, see . 19 | """ 20 | 21 | """This module contains ArbitrageBot class and method for launching bot""" 22 | 23 | import telegram.bot 24 | from telegram.ext import messagequeue as mqueue 25 | from telegram.ext import Updater, CommandHandler, ConversationHandler, RegexHandler 26 | from telegram.utils.request import Request 27 | 28 | import mongo_queries as mq 29 | from config import local as local_config 30 | from services import ( 31 | core, 32 | commands, 33 | helpers, 34 | settings, 35 | alerts 36 | ) 37 | 38 | 39 | class ArbitrageBot(telegram.bot.Bot): 40 | """ 41 | Subclass of that implements 42 | to avoid spam limits 43 | 44 | Attributes: 45 | :param is_queued_def: [optional=True] use MessageQueue (True) or not (False) 46 | :param msg_queue: [optional=None] 47 | MessageQueue instance 48 | 49 | """ 50 | def __init__(self, *args, is_queued_def=True, msg_queue=None, **kwargs): 51 | super().__init__(*args, **kwargs) 52 | # For decorator usage 53 | self._is_messages_queued_default = is_queued_def 54 | self._msg_queue = msg_queue or mqueue.MessageQueue() 55 | 56 | @mqueue.queuedmessage 57 | def send_message(self, *args, **kwargs): 58 | super().send_message(*args, **kwargs) 59 | 60 | 61 | def launch(): 62 | msg_queue = mqueue.MessageQueue(all_burst_limit=29, all_time_limit_ms=1017) 63 | request = Request(con_pool_size=4 + local_config.WORKERS_NUM) 64 | bot = ArbitrageBot(local_config.TOKEN, request=request, msg_queue=msg_queue) 65 | updater = Updater(bot=bot, workers=local_config.WORKERS_NUM) 66 | dispatcher = updater.dispatcher 67 | 68 | available_commands = [CommandHandler('start', commands.start, 69 | pass_args=True, 70 | pass_job_queue=True, 71 | pass_chat_data=True), 72 | CommandHandler('switch_on', commands.switch_on, 73 | pass_job_queue=True, 74 | pass_chat_data=True), 75 | CommandHandler('switch_off', commands.switch_off, 76 | pass_job_queue=True, 77 | pass_chat_data=True), 78 | CommandHandler('set_interval', commands.set_interval, 79 | pass_args=True, 80 | pass_job_queue=True, 81 | pass_chat_data=True), 82 | CommandHandler('set_threshold', commands.set_threshold, pass_args=True), 83 | CommandHandler('add_coin', commands.add_coin, pass_args=True), 84 | CommandHandler('remove_coin', commands.remove_coin, pass_args=True), 85 | CommandHandler('show_coins', commands.show_your_coins), 86 | CommandHandler('add_exchange', commands.add_exchange, pass_args=True), 87 | CommandHandler('remove_exchange', commands.remove_exchange, pass_args=True), 88 | CommandHandler('show_exchanges', commands.show_your_exchanges), 89 | RegexHandler('(?!⬅Back)', commands.default_response) 90 | ] 91 | 92 | # Menus constructor 93 | conv_handler = ConversationHandler( 94 | entry_points=[CommandHandler('start', commands.start, 95 | pass_args=True, 96 | pass_job_queue=True, 97 | pass_chat_data=True)], 98 | states={ 99 | core.MAIN_MENU: [RegexHandler('^(🤖Settings)$', helpers.settings), 100 | RegexHandler('^(🔬FAQ)$', helpers.faq), 101 | RegexHandler('^(📱Contacts)$', helpers.contacts), 102 | RegexHandler('^(👾About)$', helpers.about) 103 | ] + available_commands, 104 | 105 | core.SETTINGS_MENU: [RegexHandler('^(🎚Set threshold)$', settings.threshold), 106 | RegexHandler('^(⏱Set interval)$', settings.interval), 107 | RegexHandler('^(🎛Alerts settings)$', settings.alerts_settings), 108 | RegexHandler('^(🔊Turn off/on notification)$', settings.switch, 109 | pass_job_queue=True, 110 | pass_chat_data=True) 111 | ] + available_commands, 112 | 113 | core.ALERTS_MENU: [RegexHandler('^(💎Add/remove coins)$', alerts.add_remove_coin), 114 | RegexHandler('^(⚖️Add/remove exchanges)$', alerts.add_remove_exchange), 115 | RegexHandler('^(⚙️Show my settings)$', alerts.show_settings), 116 | RegexHandler('^(⬅Back)$', settings.back_to_settings) 117 | ] + available_commands, 118 | 119 | core.SET_INTERVAL: [RegexHandler('^([0-9]+)$', settings.set_interval_dialog, 120 | pass_chat_data=True, 121 | pass_job_queue=True), 122 | RegexHandler('^(⬅Back)$', settings.back_to_settings), 123 | CommandHandler('start', commands.start, 124 | pass_args=True, 125 | pass_job_queue=True, 126 | pass_chat_data=True), 127 | RegexHandler('\w*', settings.interval_help) 128 | ], 129 | 130 | core.SET_THRESHOLD: [RegexHandler('^([0-9]+(\.[0-9]*){0,1})$', settings.set_threshold_dialog), 131 | RegexHandler('^(⬅Back)$', settings.back_to_settings), 132 | CommandHandler('start', commands.start, 133 | pass_args=True, 134 | pass_job_queue=True, 135 | pass_chat_data=True), 136 | RegexHandler('\w*', settings.threshold_help) 137 | ], 138 | 139 | core.ADD_RM_COINS: [RegexHandler('^([aA][dD][dD][ \t\n\r]+[A-Za-z]{3}/[A-Za-z]{3})$', alerts.add_coin_dialog), 140 | RegexHandler('^([rR][mM][ \t\n\r]+[A-Za-z]{3}/[A-Za-z]{3})$', alerts.remove_coin_dialog), 141 | RegexHandler('^([aA][lL][lL])$', alerts.show_all_coins), 142 | RegexHandler('^(⬅Back)$', alerts.back_to_alerts), 143 | CommandHandler('start', commands.start, 144 | pass_args=True, 145 | pass_job_queue=True, 146 | pass_chat_data=True), 147 | RegexHandler('\w*', alerts.coins_help) 148 | ], 149 | 150 | core.ADD_RM_EX: [RegexHandler('^([aA][dD][dD][ \t\n\r]+[-\w]+)$', alerts.add_exchange_dialog), 151 | RegexHandler('^([rR][mM][ \t\n\r]+[-\w]+)$', alerts.remove_exchange_dialog), 152 | RegexHandler('^([aA][lL][lL])$', alerts.show_all_exchanges), 153 | RegexHandler('^(⬅Back)$', alerts.back_to_alerts), 154 | CommandHandler('start', commands.start, 155 | pass_args=True, 156 | pass_job_queue=True, 157 | pass_chat_data=True), 158 | RegexHandler('\w*', alerts.ex_help) 159 | ], 160 | 161 | core.BLACK_HOLE: [CommandHandler('start', commands.start, 162 | pass_args=True, 163 | pass_job_queue=True, 164 | pass_chat_data=True), 165 | RegexHandler('\w*', commands.get_registration)] 166 | }, 167 | fallbacks=[RegexHandler('⬅Back', helpers.back)] 168 | ) 169 | 170 | dispatcher.add_handler(conv_handler) 171 | # Log all errors 172 | dispatcher.add_error_handler(commands.error) 173 | # Restart users notifications 174 | core.restart_jobs(dispatcher, mq.get_users()) 175 | 176 | updater.start_webhook(listen='0.0.0.0', 177 | port=local_config.PORT, 178 | url_path=local_config.TOKEN, 179 | key=local_config.WEBHOOK_PKEY, 180 | cert=local_config.WEBHOOK_CERT, 181 | webhook_url=local_config.URL+local_config.TOKEN) 182 | updater.idle() 183 | 184 | if __name__ == '__main__': 185 | launch() 186 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CindicatorArbitrageBot 2 | 3 | The bot is used for sending notifications when the price difference for a currency between different exchanges is higher than a set threshold. 4 | You can use these signals to monitor the possibility of favorable arbitrage between exchanges. 5 | 6 | ### What is arbitrage? 7 | 8 | Arbitrage is the simultaneous purchase and sale of an asset to profit from a difference in its price. 9 | It is a trade that profits by exploiting the price differences of identical or similar financial 10 | instruments on different markets or in different forms. Arbitrage exists as a result of market inefficiencies. 11 | 12 | ### Registration 13 | 14 | To start the bot working, a user must register on https://cindicator.com/arbitrage-bot by entering their email address. 15 | Users will then receive a link to the telegram bot with a randomly generated unique hash key. 16 | By following the link, users will be automatically registered in the system and can start to communicate with the bot and receive notifications. 17 | 18 | ### Deployment 19 | 20 | To deploy the bot, first, you need to create a local.py module in the config folder with server settings. 21 | The module must contain these values: 22 | 23 | TOKEN - bot token 24 | 25 | URL - server url in format: 'https://url:port/' \ 26 | PORT - server port 27 | 28 | WEBHOOK_CERT - path to webhook certificate \ 29 | WEBHOOK_PKEY - path to webhook pkey 30 | 31 | MONGO_HOST - mongodb host \ 32 | MONGO_PORT - mongodb port \ 33 | MONGO_DB - mongodb name 34 | 35 | MONGO_BOT_USER - mongodb user \ 36 | MONGO_BOT_PASSWORD - mongodb password 37 | 38 | Run the bot with the command: *python ArbitrageBot.py* 39 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ -------------------------------------------------------------------------------- /config/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains constants (urls for exchanges and fields name in DB)""" 21 | 22 | # Exchanges url 23 | POLONIEX_URL = 'https://poloniex.com/public?command=returnTicker' 24 | KRAKEN_URL = 'https://api.kraken.com/0/public/Ticker?pair=' 25 | OKCOIN_URL = 'https://www.okcoin.com/api/v1/ticker.do?symbol=' 26 | GEMINI_URL = 'https://api.gemini.com/v1/pubticker/' 27 | BITSTAMP_URL = 'https://www.bitstamp.net/api/v2/ticker/' 28 | BITTREX_URL = 'https://bittrex.com/api/v1.1/public/getticker?market=' 29 | BITFINEX_URL = 'https://api.bitfinex.com/v1/pubticker/' 30 | 31 | # Names in DB 32 | CHAT_ID = 'chat_id' 33 | USERNAME = 'username' 34 | FIRST_NAME = 'first_name' 35 | LAST_NAME = 'last_name' 36 | EMAIL = 'email' 37 | SETTINGS = 'settings' 38 | # Names in settings 39 | NOTIFICATIONS = 'notifications' 40 | THRESHOLD = 'threshold' 41 | INTERVAL = 'interval' 42 | COINS = 'coins' 43 | EXCHANGES = 'exchanges' 44 | -------------------------------------------------------------------------------- /crawler/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | -------------------------------------------------------------------------------- /crawler/crawler.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains main logic for crawler.""" 21 | 22 | import asyncio 23 | from datetime import datetime, timedelta 24 | from traceback import format_exc 25 | 26 | from crawler.data_loader import loader, exchange_loader 27 | 28 | 29 | class Crawler: 30 | """ 31 | Crawler that checks new prices on various coins/exchanges pairs, update this price in DB, and create 32 | price history. 33 | 34 | Attributes: 35 | :param coin_map: where keys is coin name (in db) and values is 36 | with structure like {exchange name (in db): coin name (on exchange), ...} 37 | :param db: to interact with DB that contain next functions: 38 | get_coin(coin), add_coin(coin), 39 | get_exchange(coin, exchange), add_exchange(coin, exchange), update_exchange(coin, exchange, ask, bid), 40 | get_coin_h(coin), add_coin_h(coin), 41 | get_exchange(coin, exchange), add_exchange_h(coin, exchange), get_exchange_history(coin, exchange), 42 | add_price_to_exchange_h(coin, exchange, time, ask, bid), 43 | update_exchange_h(coin, exchange, history, current_time) 44 | :param logger: crawler's logger 45 | :param timeout: [optional=1] frequency (in seconds) of requesting new data 46 | :param history: [optional=True] will write coins price history if True 47 | :param h_update_time: [optional=3600] frequency (in seconds) of history cleaning 48 | :param h_threshold_time: [optional=1] time (in days) that price will exist in history 49 | 50 | """ 51 | 52 | def __init__(self, coin_map, db, logger, timeout=1, history=True, h_update_time=3600, h_threshold_time=1): 53 | self.coin_map = coin_map 54 | self.db = db 55 | self.timeout = timeout 56 | self.loop = asyncio.get_event_loop() 57 | self.logger = logger 58 | self.history = history 59 | self.h_update_time = h_update_time 60 | self.h_threshold_time = h_threshold_time 61 | 62 | async def _load(self, coin, exchange): 63 | """ 64 | Return bid and ask price on specific coin/exchange pair. 65 | 66 | Args: 67 | :param coin: coin name (on exchange) 68 | :param exchange: exchange name 69 | 70 | Returns: 71 | :return: with structure like: {'ask': best ask price, 'bid': best bid price} 72 | 73 | """ 74 | return await loader(coin, exchange, self.logger) 75 | 76 | async def _load_exchange(self, coins, exchange): 77 | """ 78 | Return bid and ask prices of specific coins on specific exchange. 79 | 80 | Args: 81 | :param coins: of coin name (in db) 82 | :param exchange: exchange name 83 | 84 | Returns: 85 | :return: where keys is coins name and values is with 86 | structure like: {'ask': best ask price, 'bid': best bid price} 87 | 88 | """ 89 | return await exchange_loader(coins, exchange, self.logger) 90 | 91 | def _update(self, coin, exchange, ask, bid): 92 | """ 93 | Update price of specific coin on specific exchange in db 94 | 95 | Args: 96 | :param coin: coin name (in db) 97 | :param exchange: exchange name 98 | :param ask: ask price 99 | :param bid: bid price 100 | 101 | """ 102 | self.db.update_exchange(coin=coin, exchange=exchange, ask=ask, bid=bid) 103 | if self.history: 104 | self.db.add_price_to_exchange_h(coin=coin, exchange=exchange, time=datetime.utcnow(), 105 | ask=ask, bid=bid) 106 | 107 | def _check_existing(self, coin, exchange): 108 | """ 109 | Check existence of coin/exchange pair in db and if not exist add them. 110 | 111 | Args: 112 | :param coin: coin name (in db) 113 | :param exchange: exchange name 114 | 115 | """ 116 | if not self.db.get_coin(coin=coin): 117 | self.db.add_coin(coin=coin) 118 | if not self.db.get_exchange(coin=coin, exchange=exchange): 119 | self.db.add_exchange(coin=coin, exchange=exchange) 120 | 121 | if self.history: 122 | if not self.db.get_coin_h(coin=coin): 123 | self.db.add_coin_h(coin=coin) 124 | if not self.db.get_exchange_h(coin=coin, exchange=exchange): 125 | self.db.add_exchange_h(coin=coin, exchange=exchange) 126 | 127 | async def load_and_update(self, coin, exchange): 128 | """ 129 | Check availability of coin/exchange pair. If available start infinite loop 130 | that gets new coin prices from exchange site and updates this prices in db 131 | 132 | Args: 133 | :param coin: coin name (in db) 134 | :param exchange: exchange name 135 | 136 | """ 137 | if exchange == 'bitfinex': 138 | sleep_time = len(self.coin_map) 139 | else: 140 | sleep_time = self.timeout 141 | try: 142 | exchange_coin_name = self.coin_map[coin].get(exchange) 143 | if exchange_coin_name is not None: 144 | self._check_existing(coin=coin, exchange=exchange) 145 | while True: 146 | try: 147 | stime = datetime.now() 148 | coin_data = await self._load(coin=exchange_coin_name, exchange=exchange) 149 | self._update(coin=coin, exchange=exchange, ask=coin_data['ask'], bid=coin_data['bid']) 150 | await asyncio.sleep(sleep_time - (datetime.now() - stime).total_seconds()) 151 | except Exception as e: 152 | self.logger.warning('Exception in thread\'s loop ({} {}): {}\n' 153 | '{}'.format(exchange, coin, str(e), format_exc())) 154 | await asyncio.sleep(sleep_time) 155 | except Exception as e: 156 | self.logger.critical('Exception in crawler\'s thread ({} {}): {}\n' 157 | '{}'.format(exchange, coin, str(e), format_exc())) 158 | finally: 159 | self.loop.stop() 160 | 161 | async def load_and_update_exchange(self, coins, exchange): 162 | """ 163 | Check availability of coins on exchange. For all available start infinite loop 164 | that gets new coins prices from exchange site and updates this prices in db 165 | 166 | Args: 167 | :param coins: of coin name (in db) 168 | :param exchange: exchange name 169 | 170 | """ 171 | try: 172 | # coin_names_map: {exchange coin name: db coin name} 173 | coin_names_map = {} 174 | for coin in coins: 175 | coin_name = self.coin_map[coin].get(exchange) 176 | if coin_name is not None: 177 | self._check_existing(coin=coin, exchange=exchange) 178 | coin_names_map[coin_name] = coin 179 | while True: 180 | try: 181 | stime = datetime.now() 182 | coins_data = await self._load_exchange(coins=coin_names_map.keys(), 183 | exchange=exchange) 184 | for coin_name in coin_names_map.keys(): 185 | self._update(coin=coin_names_map[coin_name], exchange=exchange, 186 | ask=coins_data[coin_name]['ask'], 187 | bid=coins_data[coin_name]['bid']) 188 | await asyncio.sleep(self.timeout - (datetime.now() - stime).total_seconds()) 189 | except Exception as e: 190 | self.logger.warning('Exception in thread\'s loop ({}): {}\n' 191 | '{}'.format(exchange, str(e), format_exc())) 192 | await asyncio.sleep(self.timeout) 193 | except Exception as e: 194 | self.logger.critical('Exception in crawler\'s thread ({}): {}\n' 195 | '{}'.format(exchange, str(e), format_exc())) 196 | finally: 197 | self.loop.stop() 198 | 199 | async def history_cleaner(self): 200 | """Clean history from old prices""" 201 | while True: 202 | try: 203 | for coin in self.coin_map.keys(): 204 | for exchange in self.coin_map[coin].keys(): 205 | try: 206 | new_history = [] 207 | time = datetime.utcnow() - timedelta(days=self.h_threshold_time) 208 | for timestamp in self.db.get_exchange_history(coin=coin, exchange=exchange): 209 | if timestamp['time'] > time: 210 | new_history.append(timestamp) 211 | self.db.update_exchange_h(coin=coin, exchange=exchange, history=new_history, 212 | current_time=datetime.utcnow()) 213 | except Exception as e: 214 | self.logger.warning('Exception in history_cleaner ({} {}): {}\n' 215 | '{}'.format(exchange, coin, str(e), format_exc())) 216 | except Exception as e: 217 | self.logger.warning('Exception in history_cleaner: {}\n' 218 | '{}'.format(str(e), format_exc())) 219 | finally: 220 | await asyncio.sleep(self.h_update_time) 221 | 222 | def launch(self): 223 | """ 224 | Start function of Crawler (start asynchronous load_and_update() tasks for 225 | all available combinations of coins and exchanges). 226 | """ 227 | try: 228 | full_exchanges = {'poloniex': []} 229 | for coin in self.coin_map.keys(): 230 | for exchange in self.coin_map[coin].keys(): 231 | if exchange in full_exchanges.keys(): 232 | full_exchanges[exchange].append(coin) 233 | else: 234 | asyncio.ensure_future(self.load_and_update(coin=coin, 235 | exchange=exchange)) 236 | for exchange in full_exchanges.keys(): 237 | asyncio.ensure_future(self.load_and_update_exchange(coins=full_exchanges[exchange], 238 | exchange=exchange)) 239 | if self.history: 240 | asyncio.ensure_future(self.history_cleaner()) 241 | self.loop.run_forever() 242 | except Exception as e: 243 | self.logger.critical('Exception in creating crawler\'s threads: {}\n' 244 | '{}'.format(str(e), format_exc())) 245 | finally: 246 | self.loop.stop() 247 | self.loop.close() 248 | -------------------------------------------------------------------------------- /crawler/data_loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains functions for loading best bid and best ask coins prices""" 21 | 22 | import json 23 | import aiohttp 24 | from traceback import format_exc 25 | from json.decoder import JSONDecodeError 26 | from concurrent.futures._base import TimeoutError 27 | 28 | from config import base as base_config 29 | 30 | 31 | # Dict map, where keys are exchanges and values are functions with: 32 | # Args: 33 | # - coin name 34 | # Returns: 35 | # - url where is coin price at which the last order executed 36 | 37 | URL_MAP = { 38 | 'poloniex': lambda coin: base_config.POLONIEX_URL, 39 | 'kraken': lambda coin: base_config.KRAKEN_URL + coin, 40 | 'okcoin': lambda coin: base_config.OKCOIN_URL + coin, 41 | 'gemini': lambda coin: base_config.GEMINI_URL + coin, 42 | 'bitstamp': lambda coin: base_config.BITSTAMP_URL + coin, 43 | 'bittrex': lambda coin: base_config.BITTREX_URL + coin, 44 | 'bitfinex': lambda coin: base_config.BITFINEX_URL + coin 45 | } 46 | 47 | 48 | def parse_kraken(data, coin): 49 | real_name = list(data['result'].keys())[0] 50 | return {'ask': float(data['result'][real_name]['a'][0]), 51 | 'bid': float(data['result'][real_name]['b'][0])} 52 | 53 | 54 | # Dict map, where keys are exchanges and values are functions with: 55 | # Args: 56 | # - data crawled data from exchange API 57 | # - coin name 58 | # Returns: 59 | # - with structure like: {'ask': best ask price, 'bid': best bid price} 60 | 61 | PARSE_MAP = { 62 | 'poloniex': lambda data, coin: {'ask': float(data[coin]['lowestAsk']), 63 | 'bid': float(data[coin]['highestBid'])}, 64 | 'kraken': parse_kraken, 65 | 'okcoin': lambda data, coin: {'ask': float(data['ticker']['sell']), 66 | 'bid': float(data['ticker']['buy'])}, 67 | 'gemini': lambda data, coin: {'ask': float(data['ask']), 68 | 'bid': float(data['bid'])}, 69 | 'bitstamp': lambda data, coin: {'ask': float(data['ask']), 70 | 'bid': float(data['bid'])}, 71 | 'bittrex': lambda data, coin: {'ask': float(data['result']['Ask']), 72 | 'bid': float(data['result']['Bid'])}, 73 | 'bitfinex': lambda data, coin: {'ask': float(data['ask']), 74 | 'bid': float(data['bid'])} 75 | } 76 | 77 | 78 | async def loader(coin, exchange, logger): 79 | """ 80 | Retrieve data from exchange and return best bid and ask prices on specific coin. 81 | 82 | Args: 83 | :param coin: coin name (on exchange) 84 | :param exchange: exchange name 85 | :param logger: crawler's logger 86 | 87 | Returns: 88 | :return: with structure like: {'ask': best ask price, 'bid': best bid price} 89 | ({'ask': None, 'bid': None} if an exception was raised while getting data from url) 90 | 91 | """ 92 | result = {'ask': None, 'bid': None} 93 | try: 94 | url = URL_MAP[exchange](coin) 95 | async with aiohttp.ClientSession() as session: 96 | response = await session.get(url) 97 | data = json.loads(await response.text()) 98 | result = PARSE_MAP[exchange](data, coin) 99 | except TimeoutError as time_out_e: 100 | logger.warning('Parse/GET exception in {} exchange for {} coin: {}' 101 | '(concurrent.futures._base.TimeoutError)'.format(exchange, coin, str(time_out_e))) 102 | except JSONDecodeError as json_e: 103 | logger.warning('Parse/GET exception in {} exchange for {} coin: {}\n' 104 | 'response: {}'.format(exchange, coin, str(json_e), await response.status)) 105 | except Exception as e: 106 | logger.warning('Parse/GET exception in {} exchange for {} coin: {}\n' 107 | '{}'.format(exchange, coin, str(e), format_exc())) 108 | finally: 109 | return result 110 | 111 | 112 | async def exchange_loader(coins, exchange, logger): 113 | """ 114 | Retrieve data from exchange and return best bid and ask prices on exchange coins. 115 | 116 | Args: 117 | :param coins: of coin name (in db) 118 | :param exchange: exchange name 119 | :param logger: crawler's logger 120 | 121 | Returns: 122 | :return: where keys is coins names and value is with structure like: 123 | {'ask': best ask price, 'bid': best bid price} ({'ask': None, 'bid': None} if 124 | an exception was raised while getting data from url) 125 | 126 | """ 127 | result = {coin: {'ask': None, 'bid': None} for coin in coins} 128 | try: 129 | url = URL_MAP[exchange](None) 130 | async with aiohttp.ClientSession() as session: 131 | response = await session.get(url) 132 | data = json.loads(await response.text()) 133 | for coin in coins: 134 | try: 135 | result[coin] = PARSE_MAP[exchange](data, coin) 136 | except Exception as e: 137 | logger.warning('Parse data exception in {} exchange for {} coin: {}\n' 138 | '{}'.format(exchange, coin, str(e), format_exc())) 139 | except TimeoutError as time_out_e: 140 | logger.warning('Parse/GET exception in {} exchange: {}' 141 | '(concurrent.futures._base.TimeoutError)'.format(exchange, str(time_out_e))) 142 | except JSONDecodeError as json_e: 143 | logger.warning('Parse/GET exception in {} exchange: {}\n' 144 | 'response: {}'.format(exchange, str(json_e), await response.status)) 145 | except Exception as e: 146 | logger.warning('Parse/GET exception in {} exchange: {}\n' 147 | '{}'.format(exchange, str(e), format_exc())) 148 | finally: 149 | return result 150 | -------------------------------------------------------------------------------- /crawler_launch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains entry point and logger for crawler""" 21 | 22 | import logging 23 | import mongo_queries as mq 24 | from crawler.crawler import Crawler 25 | 26 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 27 | level=logging.INFO) 28 | crawler_logger = logging.getLogger(__name__) 29 | 30 | if __name__ == '__main__': 31 | crawler = Crawler(coin_map=mq.coin_map, db=mq, logger=crawler_logger) 32 | crawler.launch() -------------------------------------------------------------------------------- /messages.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | MARKDOWN = 'Markdown' 21 | 22 | HELLO_TEXT = """ 23 | Hello! 24 | 25 | Thank you for joining [Cindicator](cindicator.com) 26 | 27 | Your account has been activated! You will start receiving signals soon. 28 | 29 | I send notifications when the price difference for a currency between different \ 30 | exchanges is higher than your threshold. Use these signals \ 31 | to monitor the possibility of favorable arbitrage between exchanges. \ 32 | You can set your own threshold but make sure it covers the transaction costs. 33 | 34 | I am currently monitoring seven exchanges: [Poloniex](poloniex.com), \ 35 | [Kraken](kraken.com), [Okcoin](okcoin.com), \ 36 | [Gemini](gemini.com), [Bitstamp](bitstamp.net), [Bittrex](bittrex.com) and \ 37 | [Bitfinex](bitfinex.com), but planning to add more very soon. \ 38 | However, you can turn some exchanges off individually in your dashboard if you would like. 39 | 40 | Don’t forget to follow us on [Twitter](twitter.com/Crowd_indicator) for the latest news! 41 | """ 42 | 43 | ALERT_TEXT = """ 44 | #{} 45 | Bid is higher on *{}* 46 | than ask on *{}* by *{}%* 47 | """ 48 | 49 | CONTACTS_TEXT = """ 50 | This bot is made by Cindicator team. 51 | Contact us: bot@cindicator.com 52 | Here is the tipjar to show your love: 53 | BTC: 16APoVRR2SKD7XfP7sau84FHVgTPLwqak7 54 | ETH: 0xfe5309Fa6f3Df393d88BB449AaCbc78551664a55 55 | """ 56 | 57 | FAQ_TEXT = """ 58 | *Who am I?* 59 | I am an arbitrage bot created by [Cindicator](cindicator.com) team. 60 | 61 | *What is arbitrage?* 62 | Arbitrage is the simultaneous purchase and sale of an asset to profit from a \ 63 | difference in its price. It is a trade that profits by exploiting the price \ 64 | differences of identical or similar financial instruments on different markets \ 65 | or in different forms. Arbitrage exists as a result of market inefficiencies. \ 66 | ([Investopedia](http://investopedia.com/terms/a/arbitrage.asp)) 67 | 68 | *So what do I do?* 69 | I send notifications when the price difference for a cryptocurrency \ 70 | between different exchanges is higher than a threshold (it is 3.5% by default). 71 | 72 | 73 | *What does that all mean?* 74 | This means that by buying the cryptocurrency on the cheaper exchange and selling it on \ 75 | the more expensive one, you will make a gross profit of 3.5%. However, don’t forget \ 76 | about the transaction costs, which can greatly reduce your profits or even make the trade \ 77 | unprofitable. You can find information about transaction costs on the exchanges. 78 | 79 | 80 | *What else?* 81 | You can also set your own threshold. However, the higher the threshold, the less \ 82 | often you can make a trade. On rare occasions, when the market dives, the threshold \ 83 | can be as high as 10–12%. 84 | 85 | 86 | *What currencies and exchanges are available?* 87 | Available exchanges: [Poloniex](poloniex.com), \ 88 | [Kraken](kraken.com), [Okcoin](okcoin.com), \ 89 | [Gemini](gemini.com), [Bitstamp](bitstamp.net), [Bittrex](bittrex.com) and \ 90 | [Bitfinex](bitfinex.com) \ 91 | Available coins: [BTC](http://investopedia.com/terms/b/bitcoin.asp), \ 92 | [ETH](http://investopedia.com/terms/e/ethereum.asp), \ 93 | [LTC](http://investopedia.com/terms/l/litecoin.asp) (all against USD or USDT) 94 | 95 | If you are not interested in some currencies, you can turn off alerts for them. 96 | 97 | *Notifications are too rare or too frequent. How can I change that?* 98 | You can set your own interval between notifications. It is 15 minutes by default, \ 99 | which means that the bot will send messages no more than once in 15 minutes. 100 | 101 | You can also turn off notifications completely. 102 | 103 | *Where can I find my current settings?* 104 | All your settings can be seen at settings - alert settings - show my settings 105 | """ 106 | 107 | ABOUT_TEXT = """ 108 | Hello! I am an arbitrage bot created by [Cindicator](cindicator.com) team! 109 | 110 | I send notifications when the price difference for a currency \ 111 | between different exchanges is higher than your threshold. Use these signals to \ 112 | monitor the possibility of favorable arbitrage between exchanges. You can \ 113 | set your own threshold but make sure it covers the transaction costs. 114 | 115 | 116 | Available exchanges: [Poloniex](poloniex.com), \ 117 | [Kraken](kraken.com), [Okcoin](okcoin.com), \ 118 | [Gemini](gemini.com), [Bitstamp](bitstamp.net), [Bittrex](bittrex.com) and \ 119 | [Bitfinex](bitfinex.com) \ 120 | Available coins: [BTC](http://investopedia.com/terms/b/bitcoin.asp), \ 121 | [ETH](http://investopedia.com/terms/e/ethereum.asp), \ 122 | [LTC](http://investopedia.com/terms/l/litecoin.asp) (all against USD or USDT) 123 | 124 | If you just signed up, I recommend checking out the FAQ first. 125 | 126 | Enjoy and don’t forget to follow us on [Twitter](twitter.com/Crowd_indicator) for the latest news! 127 | """ 128 | 129 | # start texto 130 | GET_REGISTRATION_TEXT = 'Please, get registration on cindicator.com/arbitrage-bot' 131 | AUTHORIZATION_FAIL_TEXT = 'Authorization failed!' 132 | AUTHORIZATION_SUCC_TEXT = 'Authorization succeed!' 133 | 134 | # switch on text 135 | ALREADY_ON_TEXT = 'Notifications already `enabled`!' 136 | NOTIFICATIONS_ON_TEXT = '*Success!* Notifications `enabled`.' 137 | 138 | # switch off text 139 | ALREADY_OFF_TEXT = 'Notifications already `disabled`!' 140 | NOTIFICATIONS_OFF_TEXT = '*Success!* Notifications `disabled`.' 141 | 142 | # set interval text 143 | SET_INTERVAL_BAD_VALUE_TEXT = 'Interval must be bigger then 0!' 144 | SET_INTERVAL_SUCC_TEXT = '*Success!* New interval value: {}s.' 145 | SET_INTERVAL_HELP_CONV_TEXT = 'Please enter a *integer* number or press Back.' 146 | SET_INTERVAL_BIG_VALUE_EXCEPTION = 'Too big value for interval' 147 | SET_INTERVAL_HELP_TEXT = """ 148 | *Usage*: /set\_interval <`seconds`> 149 | Example: `/set_interval 900` 150 | """ 151 | 152 | # set threshold text 153 | SET_THRESHOLD_BAD_VALUE_TEXT = 'Threshold must be bigger then 0' 154 | SET_THRESHOLD_SUCC_TEXT = '*Success!* New threshold value: {}%' 155 | SET_THRESHOLD_HELP_CONV_TEXT = 'Please enter a *float* number or press Back.' 156 | SET_THRESHOLD_BIG_VALUE_EXCEPTION = 'Too big value for threshold' 157 | SET_THRESHOLD_HELP_TEXT = """ 158 | *Usage*: /set\_threshold <`percents`> 159 | Example: `/set_threshold 5` 160 | """ 161 | 162 | # add coin text 163 | UNSUPPORTED_COIN_TEXT = """ 164 | Sorry, we do not support this coin. 165 | Please, choose from: 166 | {} 167 | """ 168 | ALREADY_ENABLED_COIN = 'You already have notification on coin `{}`' 169 | ADD_COIN_SUCC_TEXT = """ 170 | *Success!* Coin added. 171 | Now you have notifications on this coins: 172 | {} 173 | """ 174 | ADD_COIN_HELP_TEXT = """ 175 | *Usage*: /add\_coin <`coin`> 176 | Example: `/add_coin BTC/USD` 177 | """ 178 | 179 | # remove coin text 180 | ALREADY_DISABLED_COIN = """ 181 | You have notification only on next coins: 182 | {} 183 | """ 184 | REMOVE_COIN_SUCC_TEXT = """ 185 | *Success!* Coin removed. 186 | Now you have notifications on this coins: 187 | {} 188 | """ 189 | REMOVE_COIN_HELP_TEXT = """ 190 | *Usage*: /remove\_coin <`coin`> 191 | Example: `/remove_coin BTC/USD` 192 | """ 193 | 194 | UNSUPPORTED_COIN_CONV_TEXT = """ 195 | Sorry, coin `{}` not available. 196 | To see all available coins print: all 197 | """ 198 | ADD_REMOVE_COIN_HELP = """ 199 | Please enter a valid coin or press Back. 200 | To see all available coins print: all 201 | """ 202 | 203 | # add exchange text 204 | UNSUPPORTED_EXCHANGE_TEXT = """ 205 | Sorry, we do not support this exchange. 206 | Please, choose from: 207 | {} 208 | """ 209 | ALREADY_ENABLED_EXCHANGE_TEXT = 'You already have notification on exchange `{}`' 210 | ADD_EXCHANGE_SUCC_TEXT = """ 211 | *Success!* Exchange added. 212 | Now you have notifications on this exchanges: 213 | {} 214 | """ 215 | ADD_EXCHANGE_HELP_TEXT = """ 216 | *Usage*: /add\_exchange <`exchange`> 217 | Example: `/add_exchange Poloniex` 218 | """ 219 | 220 | # remove exchange text 221 | ALREADY_DISABLED_EXCHANGE_TEXT = """ 222 | You have notification only on this exchanges: 223 | {} 224 | """ 225 | REMOVE_EXCHANGE_SUCC_TEXT = """ 226 | *Success!* Exchange removed. 227 | Now you have notifications on this exchanges: 228 | {} 229 | """ 230 | REMOVE_EXCHANGE_HELP_TEXT = """ 231 | *Usage*: /remove\_exchange <`exchange`> 232 | Example: `/remove_exchange Poloniex` 233 | """ 234 | 235 | UNSUPPORTED_EXCHANGE_CONV_TEXTS = """ 236 | Sorry, exchange `{}` not available. 237 | To see all available exchanges print: all 238 | """ 239 | ADD_REMOVE_EXCHANGE_HELP_TEXT = """ 240 | Please enter a valid exchange or press Back. 241 | To see all available exchanges print: all 242 | """ 243 | 244 | SETTINGS_TEXT = """ 245 | *Notifications*: {} 246 | *Interval*: {}s. 247 | *Threshold*: {}% 248 | *Coins*: 249 | {} 250 | *Exchanges*: 251 | {} 252 | """ 253 | 254 | SET_INTERVAL_TEXT = """ 255 | *Current interval value*: {}s. 256 | Please, enter new value as number or press Back. 257 | """ 258 | 259 | SET_THRESHOLD_TEXT = """ 260 | *Current threshold value*: {}% 261 | Please, enter new value as number or press Back. 262 | """ 263 | 264 | ADD_RM_COINS_TEXT = """ 265 | *You have notifications on this coins*: 266 | {} 267 | *To add coin print*: add <`coin`> 268 | *To remove coin print*: rm <`coin`> 269 | *To see all available coins print*: all 270 | *Example*: `add BTC/USD` 271 | """ 272 | 273 | ADD_RM_EX_TEXT = """ 274 | *You have notifications on this exchanges*: 275 | {} 276 | *To add exchange print*: add <`exchange`> 277 | *To remove exchange print*: rm <`exchange`> 278 | *To see all available exchanges print*: all 279 | *Example*: `add Poloniex` 280 | """ 281 | 282 | SETTINGS_MENU_TEXT = 'Here is your settings' 283 | 284 | ALERTS_MENU_TEXT = 'Here you can see your settings and add or remove coins and exchanges' 285 | 286 | RESTART_TEXT = """ 287 | Hello, my friend! I was updated and became a little better. Your notifications and settings \ 288 | are the same. Just type /start and we can communicate again. 289 | """ 290 | 291 | OFFLINE_TEXT = """ 292 | Hello, my friend! I'm currently updating and offline because of that. \ 293 | But soon I'll become better and come back. 294 | """ 295 | 296 | ERROR_TEXT = 'Update {} caused error {}' 297 | 298 | BACK_TEXT = 'Ok' 299 | -------------------------------------------------------------------------------- /mongo_queries.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains DB connection and functions for work with collections in it""" 21 | 22 | from math import ceil 23 | from pymongo import MongoClient 24 | from config import base as base_config 25 | from config import local as local_config 26 | 27 | # Mongo DB 28 | mongo_client = MongoClient(local_config.MONGO_HOST, local_config.MONGO_PORT) 29 | db = mongo_client[local_config.MONGO_DB] 30 | db.authenticate(local_config.MONGO_BOT_USER, local_config.MONGO_BOT_PASSWORD) 31 | 32 | 33 | # Dictionary that compares coin name with coin name on specific exchange 34 | def _coin_map(): 35 | return db.settings.find_one()['coin_map'] 36 | coin_map = _coin_map() 37 | 38 | 39 | # All available coins 40 | def _coins(): 41 | return db.settings.find_one()['coin_map'].keys() 42 | coins = _coins() 43 | 44 | 45 | # Dictionary that compares exchange name from DB with real exchange name 46 | def _exchange_map(): 47 | return db.settings.find_one()['exchange_map'] 48 | exchange_map = _exchange_map() 49 | # All available exchanges 50 | exchanges = exchange_map.keys() 51 | 52 | 53 | # Default settings for users 54 | def _default_settings(): 55 | return db.settings.find_one()['default_settings'] 56 | default_settings = _default_settings() 57 | 58 | 59 | # Users collection 60 | 61 | def get_users(): 62 | """ 63 | Return all users data 64 | 65 | Returns: 66 | :return: of dict with user's data 67 | 68 | """ 69 | return list(db['users'].find({})) 70 | 71 | 72 | def add_user(msg, email): 73 | """ 74 | Add new user to collection 75 | 76 | Args: 77 | :param msg: telegram's message with user data 78 | :param email: users e-mail 79 | 80 | """ 81 | db['users'].insert_one({base_config.CHAT_ID: str(msg.chat_id), 82 | base_config.USERNAME: str(msg['chat']['username']), 83 | base_config.EMAIL: str(email), 84 | base_config.FIRST_NAME: str(msg['chat']['first_name']), 85 | base_config.LAST_NAME: str(msg['chat']['last_name']), 86 | base_config.SETTINGS: default_settings 87 | }) 88 | 89 | 90 | def get_user(chat_id): 91 | """ 92 | Return user's data 93 | 94 | Args: 95 | :param chat_id: or user's chat id 96 | 97 | Returns: 98 | :return: with user's data or None if user doesn't exist 99 | 100 | """ 101 | return db['users'].find_one({base_config.CHAT_ID: str(chat_id)}) 102 | 103 | 104 | def get_user_by_email(email): 105 | """ 106 | Return user's data 107 | 108 | Args: 109 | :param email: user's email 110 | 111 | Returns 112 | :return: with user's data or None if user doesn't exist 113 | 114 | """ 115 | return db['users'].find_one({base_config.EMAIL: email}) 116 | 117 | 118 | def get_user_settings(chat_id): 119 | """ 120 | Return settings for specific user 121 | 122 | Args: 123 | :param chat_id: or user's chat id 124 | 125 | Returns: 126 | :return: with user's settings or None if user doesn't exist 127 | 128 | """ 129 | return get_user(chat_id).get(base_config.SETTINGS) 130 | 131 | 132 | def update_setting(chat_id, setting, value): 133 | """ 134 | Update user's setting with new value 135 | 136 | Args: 137 | :param chat_id: or user's chat id 138 | :param setting: setting name 139 | :param value: , , new setting value 140 | 141 | """ 142 | db['users'].find_one_and_update({base_config.CHAT_ID: str(chat_id)}, 143 | {'$set': {base_config.SETTINGS + '.' + setting: value}}) 144 | 145 | 146 | def add_to_list(chat_id, list, value): 147 | """ 148 | Add new value to user's list 149 | 150 | Args: 151 | :param chat_id: or user's chat id 152 | :param list: list name 153 | :param value: value that will added to list 154 | 155 | """ 156 | db['users'].update({base_config.CHAT_ID: str(chat_id)}, 157 | {'$push': {base_config.SETTINGS + '.' + list: value}}) 158 | 159 | 160 | def remove_from_list(chat_id, list, value): 161 | """ 162 | Remove value from user's list 163 | 164 | Args: 165 | :param chat_id: or user's chat id 166 | :param list: list name 167 | :param value: value that will removed from list 168 | 169 | """ 170 | db['users'].update({base_config.CHAT_ID: str(chat_id)}, 171 | {'$pull': {base_config.SETTINGS + '.' + list: value}}) 172 | 173 | 174 | # Coins collection 175 | 176 | def get_coin(coin): 177 | """ 178 | Return coin's data 179 | 180 | Args: 181 | :param coin: coin name 182 | 183 | Returns: 184 | :return: with coin's data or None if coin doesn't exist 185 | 186 | """ 187 | return db['coins'].find_one({'name': coin}) 188 | 189 | 190 | def add_coin(coin): 191 | """ 192 | Add new coin with emtpy exchange list to collection 193 | 194 | Args: 195 | :param coin: coin name 196 | 197 | """ 198 | db['coins'].insert_one({'name': coin, 'exchanges': []}) 199 | 200 | 201 | def get_exchange(coin, exchange): 202 | """ 203 | Return coin's data where coin contains specific exchange 204 | 205 | Args: 206 | :param coin: coin name 207 | :param exchange: exchange name 208 | 209 | Returns: 210 | :return: with coin's data or None if that coin doesn't exist 211 | 212 | """ 213 | coin = get_coin(coin) 214 | if coin: 215 | for exchange_ in coin['exchanges']: 216 | if exchange_['name'] == exchange: 217 | return exchange_ 218 | else: 219 | return None 220 | 221 | 222 | def add_exchange(coin, exchange): 223 | """ 224 | Add new exchange to coin's list 225 | 226 | Args: 227 | :param coin: coin name 228 | :param exchange: exchange name 229 | 230 | """ 231 | db['coins'].update({'name': coin}, 232 | {'$push': {'exchanges': {'name': exchange, 233 | 'ask': None, 234 | 'bid': None}}}) 235 | 236 | 237 | def update_exchange(coin, exchange, ask, bid): 238 | """ 239 | Update coin's price on exchange 240 | 241 | Args: 242 | :param coin: coin name 243 | :param exchange: exchange name 244 | :param ask: coin's ask price on exchange 245 | :param bid: coin's bid price on exchange 246 | 247 | """ 248 | db['coins'].find_and_modify(query={'name': coin, 'exchanges.name': exchange}, 249 | update={'$set': {'exchanges.$.ask': ask, 250 | 'exchanges.$.bid': bid}}) 251 | 252 | 253 | # Coins history collection 254 | 255 | def get_coin_h(coin): 256 | """ 257 | Return coin's data 258 | 259 | Args: 260 | :param coin: coin name 261 | 262 | Returns: 263 | :return: with coin's data or None if coin doesn't exist 264 | 265 | """ 266 | return db['coins_history'].find_one({'name': coin}) 267 | 268 | 269 | def add_coin_h(coin): 270 | """ 271 | Add new coin with emtpy exchange list to collection 272 | 273 | Args: 274 | :param coin: coin name 275 | 276 | """ 277 | db['coins_history'].insert_one({'name': coin, 'exchanges': []}) 278 | 279 | 280 | def get_exchange_h(coin, exchange): 281 | """ 282 | Return coin's data where coin contains specific exchange 283 | 284 | Args: 285 | :param coin: coin name 286 | :param exchange: exchange name 287 | 288 | Returns: 289 | :return: with coin's data or None if that coin doesn't exist 290 | 291 | """ 292 | return db['coins_history'].find_one({'name': coin, 'exchanges.name': exchange}) 293 | 294 | 295 | def add_exchange_h(coin, exchange): 296 | """ 297 | Add new exchange with empty coin's price history to coin's list 298 | 299 | Args: 300 | :param coin: coin name 301 | :param exchange: exchange name 302 | 303 | """ 304 | db['coins_history'].update({'name': coin}, 305 | {'$push': {'exchanges': {'name': exchange, 306 | 'history': {str(hour): [] for hour in range(25)}}}}) 307 | 308 | 309 | def add_price_to_exchange_h(coin, exchange, time, ask, bid): 310 | """ 311 | Add coin's price on exchange and time when this price get to history 312 | 313 | Args: 314 | :param coin: coin name 315 | :param exchange: exchange name 316 | :param time: UTC time 317 | :param ask: coin's ask price on exchange 318 | :param bid: coin's bid price on exchange 319 | 320 | """ 321 | db['coins_history'].find_and_modify(query={'name': coin, 'exchanges.name': exchange}, 322 | update={'$push': {'exchanges.$.history.0': {'time': time, 323 | 'ask': ask, 324 | 'bid': bid}}}) 325 | 326 | 327 | def get_exchange_history(coin, exchange): 328 | """ 329 | Return coin's price history on exchange 330 | 331 | Args: 332 | :param coin: coin name 333 | :param exchange: exchange name 334 | 335 | Returns: 336 | :return: of coin's price history 337 | 338 | """ 339 | db_history = db['coins_history'].find_one({'name': coin, 'exchanges.name': exchange}, 340 | {'exchanges.$': 1})['exchanges'][0]['history'] 341 | history = [] 342 | for hour in range(25): 343 | history += db_history[str(hour)] 344 | return history 345 | 346 | 347 | def update_exchange_h(coin, exchange, history, current_time): 348 | """ 349 | Update coin's history on exchange 350 | 351 | Args: 352 | :param coin: coin name 353 | :param exchange: exchange name 354 | :param history: of dictionaries with next structure: 355 | {'time': , 'price': {'ask': , 'bid': }} 356 | :param current_time: current UTC time 357 | 358 | """ 359 | db_history = {str(hour): [] for hour in range(25)} 360 | for timestamp in history: 361 | hour = ceil((current_time - timestamp['time']).total_seconds() / 3600) 362 | db_history[str(hour)].append(timestamp) 363 | db['coins_history'].find_and_modify(query={'name': coin, 'exchanges.name': exchange}, 364 | update={'$set': {'exchanges.$.history': db_history}}) 365 | 366 | 367 | # Subscribers collection 368 | 369 | def get_user_email(key): 370 | """ 371 | Return subscriber's e-mail 372 | 373 | Args: 374 | :param key: 375 | 376 | Returns: 377 | :return: user's e-mail or None if subscriber doesn't exist 378 | 379 | """ 380 | user = db['subscribers'].find_one({'key': str(key)}) 381 | if user: 382 | return user['email'] 383 | else: 384 | return None 385 | -------------------------------------------------------------------------------- /notify_users.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains method for notifying all users with specific text""" 21 | 22 | import sys 23 | import time 24 | import telegram 25 | from datetime import datetime 26 | 27 | import messages 28 | import mongo_queries as mq 29 | import config.base as base_config 30 | import config.local as local_config 31 | 32 | 33 | def notify_users(bot, users, text, msg_num_limit=29, msg_time_limit=1): 34 | """ 35 | Send all users specific text (in 'msg_time_limit' can be send only 36 | 'msg_num_limit' messages) 37 | 38 | Args: 39 | :param bot: instance 40 | :param users: of dict with user's data 41 | :param text: that will send to users 42 | :param msg_num_limit: [optional=29] num of message 43 | :param msg_time_limit: [optional=1] time in seconds 44 | """ 45 | # Num of message that was send in 'msg_num_limit' seconds 46 | msg_num = 0 47 | for user in users: 48 | # New second time starts if num of message equal to zero 49 | if msg_num == 0: 50 | s_time = datetime.utcnow() 51 | 52 | try: 53 | bot.send_message(chat_id=int(user[base_config.CHAT_ID]), text=text) 54 | except Exception as e: 55 | print('chat_id: {}; error: {}'.format(user[base_config.CHAT_ID], str(e))) 56 | finally: 57 | msg_num += 1 58 | 59 | # If was sent 'msg_time_limit' messages in less than 'msg_num_limit' seconds 60 | if msg_num >= msg_num_limit and (datetime.utcnow() - s_time).total_seconds() < msg_time_limit: 61 | time.sleep(msg_time_limit) 62 | msg_num = 0 63 | # If was sent less than 'msg_time_limit' messages in 'msg_num_limit' seconds 64 | elif (datetime.utcnow() - s_time).total_seconds() > msg_time_limit: 65 | msg_num = 0 66 | 67 | if __name__ == '__main__': 68 | if len(sys.argv) > 1: 69 | notify_text = sys.argv[1] 70 | else: 71 | notify_text = messages.OFFLINE_TEXT 72 | bot = telegram.bot.Bot(local_config.TOKEN) 73 | 74 | notify_users(bot, mq.get_users(), notify_text) 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==2.2.0 2 | async-timeout==1.2.1 3 | certifi==2017.4.17 4 | chardet==3.0.4 5 | future==0.16.0 6 | idna==2.5 7 | multidict==3.1.0 8 | pymongo==3.4.0 9 | python-telegram-bot==7.0.1 10 | requests==2.18.1 11 | urllib3==1.21.1 12 | yarl==0.10.3 13 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ -------------------------------------------------------------------------------- /services/alerts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains logic and interactive replies for user's bot alerts menu. 21 | All methods has the same arguments signature which described in add_remove_coin method.""" 22 | 23 | from telegram.ext.dispatcher import run_async 24 | 25 | import messages 26 | import mongo_queries as mq 27 | from config import base as base_config 28 | from services.core import ( 29 | exchange_convert, 30 | kb_back, 31 | kb_alerts, 32 | ALERTS_MENU, 33 | ADD_RM_COINS, 34 | ADD_RM_EX 35 | ) 36 | 37 | 38 | @run_async 39 | def add_remove_coin(bot, update): 40 | """ 41 | Begin dialog for change coin's list 42 | 43 | Args: 44 | :param bot: bot instance 45 | :param update: update instance 46 | 47 | Returns: 48 | :return: change coin's list dialog identifier 49 | 50 | """ 51 | msg = update.message 52 | user_coins = '`' + '`, `'.join(mq.get_user_settings(msg.chat_id)[base_config.COINS]) + '`' 53 | msg.reply_text(messages.ADD_RM_COINS_TEXT.format(user_coins), reply_markup=kb_back, parse_mode=messages.MARKDOWN) 54 | return ADD_RM_COINS 55 | 56 | 57 | @run_async 58 | def add_coin_dialog(bot, update): 59 | """Add coin to user's list and send message to user about operation's result""" 60 | 61 | msg = update.message 62 | coin = msg.text[4:].strip(' \t\n\r').upper() 63 | if coin not in mq.coins: 64 | msg.reply_text(messages.UNSUPPORTED_COIN_CONV_TEXT.format(coin), parse_mode=messages.MARKDOWN) 65 | return ADD_RM_COINS 66 | user_coins = mq.get_user_settings(msg.chat_id)[base_config.COINS] 67 | if coin in user_coins: 68 | msg.reply_text(messages.ALREADY_ENABLED_COIN.format(coin), parse_mode=messages.MARKDOWN) 69 | return ADD_RM_COINS 70 | mq.add_to_list(msg.chat_id, list=base_config.COINS, value=coin) 71 | new_user_coins = '`' + '`, `'.join(set(user_coins + [coin])) + '`' 72 | msg.reply_text(messages.ADD_COIN_SUCC_TEXT.format(new_user_coins), parse_mode=messages.MARKDOWN) 73 | return ADD_RM_COINS 74 | 75 | 76 | @run_async 77 | def remove_coin_dialog(bot, update): 78 | """Remove coin from user's list and send message to user about operation's result""" 79 | 80 | msg = update.message 81 | coin = msg.text[3:].strip(' \t\n\r').upper() 82 | user_coins = mq.get_user_settings(msg.chat_id)[base_config.COINS] 83 | if coin not in user_coins: 84 | msg.reply_text(messages.ALREADY_DISABLED_COIN.format('`' + '`, `'.join(user_coins) + '`'), 85 | parse_mode=messages.MARKDOWN) 86 | return ADD_RM_COINS 87 | mq.remove_from_list(msg.chat_id, list=base_config.COINS, value=coin) 88 | new_user_coins = '`' + '`, `'.join(set(user_coins) - {coin}) + '`' 89 | msg.reply_text(messages.REMOVE_COIN_SUCC_TEXT.format(new_user_coins), parse_mode=messages.MARKDOWN) 90 | return ADD_RM_COINS 91 | 92 | 93 | @run_async 94 | def show_all_coins(bot, update): 95 | """Send message to user with all available coins""" 96 | 97 | update.message.reply_text('`' + '`, `'.join(mq.coins) + '`', parse_mode=messages.MARKDOWN) 98 | return ADD_RM_COINS 99 | 100 | 101 | @run_async 102 | def coins_help(bot, update): 103 | """Send message to user when bot gets unavailable coin""" 104 | 105 | update.message.reply_text(messages.ADD_REMOVE_COIN_HELP, parse_mode=messages.MARKDOWN) 106 | return ADD_RM_COINS 107 | 108 | 109 | @run_async 110 | def add_remove_exchange(bot, update): 111 | """ 112 | Begin dialog for change exchange's list 113 | 114 | Returns: 115 | :return: change exchange's list dialog identifier 116 | 117 | """ 118 | msg = update.message 119 | user_ex = '`' + '`, `'.join(exchange_convert(mq.get_user_settings(msg.chat_id)[base_config.EXCHANGES])) + '`' 120 | msg.reply_text(messages.ADD_RM_EX_TEXT.format(user_ex), reply_markup=kb_back, parse_mode=messages.MARKDOWN) 121 | return ADD_RM_EX 122 | 123 | 124 | @run_async 125 | def add_exchange_dialog(bot, update): 126 | """Add exchange to user's list and send message to user about operation's result """ 127 | 128 | msg = update.message 129 | exchange = msg.text[4:].strip(' \t\n\r').replace('-', '_').lower() 130 | if exchange not in mq.exchanges: 131 | msg.reply_text(messages.UNSUPPORTED_EXCHANGE_CONV_TEXTS.format(exchange), parse_mode=messages.MARKDOWN) 132 | return ADD_RM_EX 133 | user_exchanges = mq.get_user_settings(msg.chat_id)[base_config.EXCHANGES] 134 | if exchange in user_exchanges: 135 | msg.reply_text(messages.ALREADY_ENABLED_EXCHANGE_TEXT.format(exchange_convert(exchange)), parse_mode=messages.MARKDOWN) 136 | return ADD_RM_EX 137 | mq.add_to_list(msg.chat_id, list=base_config.EXCHANGES, value=exchange) 138 | new_user_exchanges = '`' + '`, `'.join(set(exchange_convert(user_exchanges + [exchange]))) + '`' 139 | msg.reply_text(messages.ADD_EXCHANGE_SUCC_TEXT.format(new_user_exchanges), parse_mode=messages.MARKDOWN) 140 | return ADD_RM_EX 141 | 142 | 143 | @run_async 144 | def remove_exchange_dialog(bot, update): 145 | """Remove exchange from user's list and send message to user about operation's result""" 146 | 147 | msg = update.message 148 | exchange = msg.text[3:].strip(' \t\n\r').replace('-', '_').lower() 149 | user_exchanges = mq.get_user_settings(msg.chat_id)[base_config.EXCHANGES] 150 | if exchange not in user_exchanges: 151 | user_exchanges = exchange_convert(user_exchanges) 152 | msg.reply_text(messages.ALREADY_DISABLED_EXCHANGE_TEXT.format('`' + '`, `'.join(user_exchanges) + '`'), 153 | parse_mode=messages.MARKDOWN) 154 | return ADD_RM_EX 155 | mq.remove_from_list(msg.chat_id, list=base_config.EXCHANGES, value=exchange) 156 | new_user_exchanges = '`' + '`, `'.join(exchange_convert(set(user_exchanges) - {exchange})) + '`' 157 | msg.reply_text(messages.REMOVE_EXCHANGE_SUCC_TEXT.format(new_user_exchanges), parse_mode=messages.MARKDOWN) 158 | return ADD_RM_EX 159 | 160 | 161 | @run_async 162 | def show_all_exchanges(bot, update): 163 | """ 164 | Send message to user with all available exchanges 165 | """ 166 | update.message.reply_text('`' + '`, `'.join(exchange_convert(mq.exchanges)) + '`', parse_mode=messages.MARKDOWN) 167 | return ADD_RM_EX 168 | 169 | 170 | @run_async 171 | def ex_help(bot, update): 172 | """ 173 | Send message to user when bot gets unavailable exchange 174 | """ 175 | update.message.reply_text(messages.ADD_REMOVE_EXCHANGE_HELP_TEXT, parse_mode=messages.MARKDOWN) 176 | return ADD_RM_EX 177 | 178 | 179 | @run_async 180 | def show_settings(bot, update): 181 | """ 182 | Send message to user with his current settings 183 | """ 184 | msg = update.message 185 | user_settings = mq.get_user_settings(msg.chat_id) 186 | coins = '`' + '`, `'.join(user_settings[base_config.COINS]) + '`' 187 | exchanges = '`' + '`, `'.join(exchange_convert(user_settings[base_config.EXCHANGES])) + '`' 188 | threshold = str(user_settings[base_config.THRESHOLD]) 189 | interval = str(user_settings[base_config.INTERVAL]) 190 | notifications = 'enabled 🔊' if user_settings[base_config.NOTIFICATIONS] else 'disabled 🔇' 191 | settings_message = messages.SETTINGS_TEXT.format(notifications, interval, threshold, coins, exchanges) 192 | msg.reply_text(settings_message, parse_mode=messages.MARKDOWN) 193 | return ALERTS_MENU 194 | 195 | 196 | @run_async 197 | def back_to_alerts(bot, update): 198 | """Return user to alerts menu 199 | 200 | Returns: 201 | :return: alerts menu identifier 202 | 203 | """ 204 | update.message.reply_text(messages.BACK_TEXT, reply_markup=kb_alerts, parse_mode=messages.MARKDOWN) 205 | return ALERTS_MENU 206 | -------------------------------------------------------------------------------- /services/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains user commands to interact with the bot. Commands starts with '/' sign. 21 | All methods has the same arguments signature which described in start method. Not all methods 22 | may contain the whole list of described arguments""" 23 | 24 | from telegram.ext.dispatcher import run_async 25 | 26 | import messages 27 | import mongo_queries as mq 28 | from config import base as base_config 29 | from services.core import ( 30 | logger, 31 | notify, 32 | exchange_convert, 33 | kb_main, 34 | MAIN_MENU, 35 | BLACK_HOLE, 36 | kb_entry_point 37 | ) 38 | 39 | 40 | # @run_async 41 | def start(bot, update, args, job_queue, chat_data): 42 | """ 43 | Main entry point of the bot. This command starts the bot interface and checks user authorization. 44 | 45 | Args: 46 | :param bot: bot instance 47 | :param update: update object that contains updates from user chat 48 | :param args: arguments that user enters after command. In this case command /start receives 49 | the unique hash key for user authentication 50 | :param job_queue: queue of jobs. The notify job automatically adds to queue after start 51 | :param chat_data: stores the data for the chat. In this case a job object stores in it. 52 | 53 | Returns: 54 | :return: Main menu identifier 55 | """ 56 | msg = update.message 57 | user = mq.get_user(msg.chat_id) 58 | if not user: 59 | if len(args) == 0: 60 | msg.reply_text(messages.ABOUT_TEXT, parse_mode=messages.MARKDOWN) 61 | msg.reply_text(messages.GET_REGISTRATION_TEXT, parse_mode=messages.MARKDOWN) 62 | return BLACK_HOLE 63 | else: 64 | key = args[0] 65 | email = mq.get_user_email(key) 66 | if not email or mq.get_user_by_email(email): 67 | msg.reply_text(messages.AUTHORIZATION_FAIL_TEXT, parse_mode=messages.MARKDOWN) 68 | return BLACK_HOLE 69 | else: 70 | mq.add_user(msg, email) 71 | msg.reply_text(messages.AUTHORIZATION_SUCC_TEXT, parse_mode=messages.MARKDOWN) 72 | 73 | msg.reply_text(messages.HELLO_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 74 | 75 | # Add job to queue 76 | if 'job' not in chat_data: 77 | user_settings = mq.get_user_settings(msg.chat_id) 78 | if user_settings[base_config.NOTIFICATIONS]: 79 | job = job_queue.run_repeating(notify, int(user_settings[base_config.INTERVAL]), context={'chat_id': msg.chat_id}) 80 | chat_data['job'] = job 81 | 82 | return MAIN_MENU 83 | 84 | 85 | def switch_on(bot, update, job_queue, chat_data): 86 | """Add the job to the queue""" 87 | 88 | msg = update.message 89 | if mq.get_user_settings(msg.chat_id)[base_config.NOTIFICATIONS]: 90 | msg.reply_text(messages.ALREADY_ON_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 91 | return MAIN_MENU 92 | mq.update_setting(msg.chat_id, setting=base_config.NOTIFICATIONS, value=True) 93 | if 'job' in chat_data: 94 | chat_data['job'].schedule_removal() 95 | job = job_queue.run_repeating(notify, int(mq.get_user_settings(msg.chat_id)[base_config.INTERVAL]), 96 | context={'chat_id': msg.chat_id}) 97 | chat_data['job'] = job 98 | msg.reply_text(messages.NOTIFICATIONS_ON_TEXT, parse_mode=messages.MARKDOWN) 99 | 100 | 101 | def switch_off(bot, update, job_queue, chat_data): 102 | """Remove the job if the user changed their mind""" 103 | 104 | msg = update.message 105 | if not mq.get_user_settings(msg.chat_id)[base_config.NOTIFICATIONS]: 106 | msg.reply_text(messages.ALREADY_OFF_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 107 | return MAIN_MENU 108 | mq.update_setting(msg.chat_id, setting=base_config.NOTIFICATIONS, value=False) 109 | chat_data['job'].schedule_removal() 110 | msg.reply_text(messages.NOTIFICATIONS_OFF_TEXT, parse_mode=messages.MARKDOWN) 111 | 112 | 113 | @run_async 114 | def set_interval(bot, update, args, chat_data, job_queue): 115 | """Set interval between notifications in seconds""" 116 | 117 | msg = update.message 118 | try: 119 | interval = int(args[0]) 120 | if interval <= 0: 121 | msg.reply_text(messages.SET_INTERVAL_BAD_VALUE_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 122 | return MAIN_MENU 123 | mq.update_setting(msg.chat_id, setting=base_config.INTERVAL, value=interval) 124 | if mq.get_user_settings(msg.chat_id)[base_config.NOTIFICATIONS]: 125 | chat_data['job'].schedule_removal() 126 | job = job_queue.run_repeating(notify, interval, context={'chat_id': msg.chat_id}) 127 | chat_data['job'] = job 128 | msg.reply_text(messages.SET_INTERVAL_SUCC_TEXT.format(interval), 129 | reply_markup=kb_main,parse_mode=messages.MARKDOWN) 130 | return MAIN_MENU 131 | except (IndexError, ValueError, IndexError): 132 | msg.reply_text(messages.SET_INTERVAL_HELP_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 133 | return MAIN_MENU 134 | except OverflowError: 135 | msg.reply_text(messages.SET_INTERVAL_BIG_VALUE_EXCEPTION, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 136 | return MAIN_MENU 137 | 138 | 139 | @run_async 140 | def set_threshold(bot, update, args): 141 | """Set the threshold between cryptocurrencies by which the bot will notify the user""" 142 | 143 | msg = update.message 144 | try: 145 | threshold = float(args[0]) 146 | if threshold <= 0: 147 | msg.reply_text(messages.SET_THRESHOLD_BAD_VALUE_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 148 | return MAIN_MENU 149 | if threshold > 100: 150 | msg.reply_text(messages.SET_THRESHOLD_BAD_VALUE_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 151 | return MAIN_MENU 152 | mq.update_setting(msg.chat_id, setting=base_config.THRESHOLD, value=threshold) 153 | msg.reply_text(messages.SET_THRESHOLD_SUCC_TEXT.format(threshold), reply_markup=kb_main, 154 | parse_mode=messages.MARKDOWN) 155 | return MAIN_MENU 156 | except (IndexError, ValueError): 157 | msg.reply_text(messages.SET_THRESHOLD_HELP_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 158 | return MAIN_MENU 159 | except OverflowError: 160 | msg.reply_text(messages.SET_THRESHOLD_BIG_VALUE_EXCEPTION, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 161 | return MAIN_MENU 162 | 163 | 164 | @run_async 165 | def add_coin(bot, update, args): 166 | """Add coin to coins list by which the bot will notify the user""" 167 | 168 | msg = update.message 169 | try: 170 | coin = args[0].upper() 171 | if coin not in mq.coins: 172 | msg.reply_text(messages.UNSUPPORTED_COIN_TEXT.format('`' + '`, `'.join(mq.coins) + '`'), 173 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 174 | return MAIN_MENU 175 | user_coins = mq.get_user_settings(msg.chat_id)[base_config.COINS] 176 | if coin in user_coins: 177 | msg.reply_text(messages.ALREADY_ENABLED_COIN.format('`' + coin + '`'), 178 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 179 | return MAIN_MENU 180 | mq.add_to_list(msg.chat_id, list=base_config.COINS, value=coin) 181 | new_user_coins = '`' + '`, `'.join(set(user_coins + [coin])) + '`' 182 | msg.reply_text(messages.ADD_COIN_SUCC_TEXT.format(new_user_coins), 183 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 184 | return MAIN_MENU 185 | except (IndexError, ValueError): 186 | msg.reply_text(messages.ADD_COIN_HELP_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 187 | return MAIN_MENU 188 | 189 | 190 | @run_async 191 | def remove_coin(bot, update, args): 192 | """Remove coin from coins list by which the bot will notify the user""" 193 | 194 | msg = update.message 195 | try: 196 | coin = args[0].upper() 197 | user_coins = mq.get_user_settings(msg.chat_id)[base_config.COINS] 198 | if coin not in user_coins: 199 | msg.reply_text(messages.ALREADY_DISABLED_COIN.format('`' + '`, `'.join(user_coins) + '`'), 200 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 201 | return MAIN_MENU 202 | mq.remove_from_list(msg.chat_id, list=base_config.COINS, value=coin) 203 | new_user_coins = '`' + '`, `'.join(set(user_coins) - {coin}) + '`' 204 | msg.reply_text(messages.REMOVE_COIN_SUCC_TEXT.format(new_user_coins), 205 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 206 | return MAIN_MENU 207 | except (IndexError, ValueError): 208 | msg.reply_text(messages.REMOVE_COIN_HELP_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 209 | return MAIN_MENU 210 | 211 | 212 | @run_async 213 | def show_your_coins(bot, update): 214 | """Show the user's coins list""" 215 | 216 | msg = update.message 217 | msg.reply_text('`' + '`, `'.join(mq.get_user_settings(msg.chat_id)[base_config.COINS]) + '`', 218 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 219 | return MAIN_MENU 220 | 221 | 222 | @run_async 223 | def add_exchange(bot, update, args): 224 | """Add exchange to exchanges list by which the bot will notify the user""" 225 | 226 | msg = update.message 227 | try: 228 | exchange = args[0].replace('-', '_').lower() 229 | if exchange not in mq.exchanges: 230 | exchanges = exchange_convert(mq.exchanges) 231 | msg.reply_text(messages.UNSUPPORTED_EXCHANGE_TEXT.format('`' + '`, `'.join(exchanges) + '`'), 232 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 233 | return MAIN_MENU 234 | user_exchanges = mq.get_user_settings(msg.chat_id)[base_config.EXCHANGES] 235 | if exchange in user_exchanges: 236 | msg.reply_text(messages.ALREADY_ENABLED_EXCHANGE_TEXT.format('`' + exchange_convert(exchange) + '`'), 237 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 238 | return MAIN_MENU 239 | mq.add_to_list(msg.chat_id, list=base_config.EXCHANGES, value=exchange) 240 | new_user_exchanges = '`' + '`, `'.join(exchange_convert(set(user_exchanges + [exchange]))) + '`' 241 | msg.reply_text(messages.ADD_EXCHANGE_SUCC_TEXT.format(new_user_exchanges), 242 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 243 | return MAIN_MENU 244 | except (IndexError, ValueError): 245 | msg.reply_text(messages.ADD_EXCHANGE_HELP_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 246 | return MAIN_MENU 247 | 248 | 249 | @run_async 250 | def remove_exchange(bot, update, args): 251 | """Remove exchange to exchanges list by which the bot will notify the user""" 252 | 253 | msg = update.message 254 | try: 255 | exchange = args[0].replace('-', '_').lower() 256 | user_exchanges = mq.get_user_settings(msg.chat_id)[base_config.EXCHANGES] 257 | if exchange not in user_exchanges: 258 | user_exchanges = exchange_convert(user_exchanges) 259 | msg.reply_text(messages.ALREADY_DISABLED_EXCHANGE_TEXT.format('`' + '`, `'.join(user_exchanges) + '`'), 260 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 261 | return MAIN_MENU 262 | mq.remove_from_list(msg.chat_id, list=base_config.EXCHANGES, value=exchange) 263 | new_user_exchanges = '`' + '`, `'.join(exchange_convert(set(user_exchanges) - {exchange})) + '`' 264 | msg.reply_text(messages.REMOVE_EXCHANGE_SUCC_TEXT.format(new_user_exchanges), 265 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 266 | return MAIN_MENU 267 | except (IndexError, ValueError): 268 | msg.reply_text(messages.REMOVE_EXCHANGE_HELP_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 269 | return MAIN_MENU 270 | 271 | 272 | @run_async 273 | def show_your_exchanges(bot, update): 274 | """Show user's exchanges list""" 275 | 276 | user_exchanges = mq.get_user_settings(update.message.chat_id)[base_config.EXCHANGES] 277 | update.message.reply_text('`' + '`, `'.join(user_exchanges) + '`', 278 | reply_markup=kb_main, parse_mode=messages.MARKDOWN) 279 | return MAIN_MENU 280 | 281 | 282 | @run_async 283 | def default_response(bot, update): 284 | """Return user to the main menu if user text something unexpectable""" 285 | update.message.reply_text(messages.HELLO_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 286 | return MAIN_MENU 287 | 288 | 289 | @run_async 290 | def get_registration(bot, update): 291 | """Show user about and get registration text if user is unauthorized""" 292 | update.message.reply_text(messages.ABOUT_TEXT, parse_mode=messages.MARKDOWN) 293 | update.message.reply_text(messages.GET_REGISTRATION_TEXT, parse_mode=messages.MARKDOWN) 294 | return BLACK_HOLE 295 | 296 | 297 | @run_async 298 | def error(bot, update, error): 299 | """Throw out user from conversation if error occur""" 300 | logger.warning(messages.ERROR_TEXT.format(str(update), str(error))) 301 | if update and update.message: 302 | update.message.reply_text('Error occur. Please type /start and we can communicate again.', 303 | reply_markup=kb_entry_point, parse_mode=messages.MARKDOWN) 304 | return -1 305 | -------------------------------------------------------------------------------- /services/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains constants (objects of all menus and keyboards), methods to get notifications, and 21 | restart method""" 22 | 23 | import logging 24 | from traceback import format_exc 25 | from itertools import combinations 26 | 27 | from telegram import KeyboardButton, ReplyKeyboardMarkup 28 | from telegram.error import Unauthorized, TimedOut 29 | from telegram.ext.dispatcher import run_async 30 | 31 | import messages 32 | import config.base as base_config 33 | import mongo_queries as mq 34 | 35 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 36 | level=logging.INFO) 37 | logger = logging.getLogger(__name__) 38 | 39 | # Menus objects 40 | MAIN_MENU, SETTINGS_MENU, ALERTS_MENU, SET_INTERVAL, SET_THRESHOLD, ADD_RM_COINS, ADD_RM_EX, BLACK_HOLE = range(8) 41 | 42 | # Keyboards objects 43 | kb_main = ReplyKeyboardMarkup([[KeyboardButton('🤖Settings'), KeyboardButton('🔬FAQ')], 44 | [KeyboardButton('👾About'), KeyboardButton('📱Contacts')]], 45 | resize_keyboard=True) 46 | 47 | kb_settings = ReplyKeyboardMarkup([[KeyboardButton('⏱Set interval'), KeyboardButton('🎚Set threshold')], 48 | [KeyboardButton('🎛Alerts settings'), KeyboardButton('🔊Turn off/on notification')], 49 | [KeyboardButton('⬅Back')]], 50 | resize_keyboard=True) 51 | 52 | kb_alerts = ReplyKeyboardMarkup([[KeyboardButton('💎Add/remove coins'), KeyboardButton('⚖️Add/remove exchanges')], 53 | [KeyboardButton('⚙️Show my settings'), KeyboardButton('⬅Back')]], 54 | resize_keyboard=True) 55 | 56 | kb_back = ReplyKeyboardMarkup([[KeyboardButton('⬅Back')]], resize_keyboard=True) 57 | 58 | kb_entry_point = ReplyKeyboardMarkup([[KeyboardButton('/start')]], resize_keyboard=True) 59 | 60 | 61 | def _price_checker(exchanges_pair, threshold, coin_name): 62 | """ 63 | If bid price on one of two exchanges is bigger than ask price on another exchange, 64 | than return tuple with next structure: (coin name, bid exchange name, ask exchange name, 65 | diff in percent between bid and ask) 66 | 67 | Args: 68 | :param exchanges_pair: with two with next structure: 69 | {'name': exchange name, 'ask': best ask price, 'bid': best bid price} 70 | :param threshold: notification threshold 71 | :param coin_name: coin name 72 | 73 | Returns: 74 | :return: coin_name: coin name 75 | :return: bid_exchange: exchange name 76 | :return: ask_exchange: exchange name 77 | :return: delta: diff in percent between bid and ask 78 | 79 | """ 80 | for bid_exchange, ask_exchange in [(exchanges_pair[0], exchanges_pair[1]), 81 | (exchanges_pair[1], exchanges_pair[0])]: 82 | if bid_exchange['bid'] > ask_exchange['ask']: 83 | delta = round((bid_exchange['bid'] - ask_exchange['ask']) / ask_exchange['ask'] * 100, 2) 84 | if delta > threshold: 85 | return coin_name, bid_exchange['name'], ask_exchange['name'], delta 86 | return None 87 | 88 | 89 | def _generate_string(data): 90 | """ 91 | Generate notification text 92 | 93 | Args: 94 | :param data: of lists that contains 4 items: [coin name, first exchange name, 95 | second exchange name, diff in percent between bid and ask coin's price on exchanges], 96 | bid price is higher than ask price 97 | 98 | Returns: 99 | :return: notification text 100 | 101 | """ 102 | message = '' 103 | for element in data: 104 | message += messages.ALERT_TEXT.format(element[0], element[1], element[2], element[3]) 105 | return message 106 | 107 | 108 | def crawl(chat_id): 109 | """ 110 | Return list of lists that contains coin name, two exchanges names, and diff in percent between 111 | bid and ask on coin on this exchanges (diff always positive, bid bigger than ask) 112 | 113 | Args: 114 | :param chat_id: or user's chat id 115 | 116 | Returns: 117 | :return: of lists that contains 4 items: [coin name, first exchange name, 118 | second exchange name, diff in percent between bid and ask coin's price on exchanges], 119 | bid price is higher than ask price 120 | 121 | """ 122 | user_settings = mq.get_user_settings(chat_id) 123 | threshold = user_settings[base_config.THRESHOLD] 124 | user_coins = user_settings[base_config.COINS] 125 | user_exchanges = user_settings[base_config.EXCHANGES] 126 | res = [] 127 | for user_coin in user_coins: 128 | try: 129 | coin_doc = mq.get_coin(user_coin) 130 | exchanges_list = [] 131 | for user_exch in user_exchanges: 132 | try: 133 | exch_doc_list = list(filter(lambda coin_exch: coin_exch['name'] == user_exch, 134 | coin_doc['exchanges'])) 135 | if len(exch_doc_list) > 0: 136 | exch_doc = exch_doc_list[0] 137 | name = exch_doc['name'] 138 | ask = exch_doc['ask'] 139 | bid = exch_doc['bid'] 140 | if ask and bid: 141 | exchanges_list.append({'name': name, 'ask': ask, 'bid': bid}) 142 | except Exception as e: 143 | logger.warning('chat_id: {}; error: {}\n' 144 | '{}'.format(chat_id, str(e), format_exc())) 145 | if len(exchanges_list) > 1: 146 | combined_exchanges = combinations(exchanges_list, 2) 147 | for exchanges_pair in combined_exchanges: 148 | try: 149 | check_res = _price_checker(exchanges_pair=exchanges_pair, threshold=threshold, 150 | coin_name=coin_doc['name'].replace('/', '\_')) 151 | if check_res: 152 | res.append(check_res) 153 | except Exception as e: 154 | logger.warning('chat_id: {}; error: {}\n' 155 | '{}'.format(chat_id, str(e), format_exc())) 156 | except Exception as e: 157 | logger.warning('chat_id: {}; error: {}\n' 158 | '{}'.format(chat_id, str(e), format_exc())) 159 | return res 160 | 161 | 162 | def notify(bot, job): 163 | """ 164 | Send notification to user 165 | 166 | Args: 167 | :param bot: bot instance 168 | :param job: user's job instance 169 | 170 | """ 171 | res = crawl(job.context['chat_id']) 172 | if len(res) > 0: 173 | try: 174 | bot.send_message(chat_id=job.context['chat_id'], text=_generate_string(res), parse_mode=messages.MARKDOWN) 175 | except Unauthorized: 176 | job.schedule_removal() 177 | mq.update_setting(job.context['chat_id'], setting=base_config.NOTIFICATIONS, value=False) 178 | except TimedOut: 179 | logger.warning('chat_id: {}; error: {}'.format(job.context['chat_id'], 180 | 'Time out while sending notification')) 181 | except Exception as e: 182 | logger.warning('chat_id: {}; error: {}\n' 183 | '{}'.format(job.context['chat_id'], str(e), format_exc())) 184 | 185 | 186 | def restart_jobs(dispatcher, users): 187 | """ 188 | Notify users about bot's restart and restart users notifications 189 | 190 | Args: 191 | :param dispatcher: bot's dispatcher instance 192 | :param users: of dict with user's data 193 | 194 | """ 195 | job_queue = dispatcher.job_queue 196 | chat_data = dispatcher.chat_data 197 | for user in users: 198 | try: 199 | if user[base_config.SETTINGS][base_config.NOTIFICATIONS]: 200 | job = job_queue.run_repeating(notify, user[base_config.SETTINGS][base_config.INTERVAL], 201 | context={'chat_id': user[base_config.CHAT_ID]}) 202 | chat_data[int(user[base_config.CHAT_ID])]['job'] = job 203 | try: 204 | job_queue.bot.send_message(chat_id=int(user[base_config.CHAT_ID]), text=messages.RESTART_TEXT, 205 | reply_markup=kb_entry_point, parse_mode=messages.MARKDOWN) 206 | except Exception as e: 207 | if 'job' in chat_data[int(user[base_config.CHAT_ID])]: 208 | chat_data[int(user[base_config.CHAT_ID])]['job'].schedule_removal() 209 | logger.warning('chat_id: {}; error: {}\n' 210 | '{}'.format(user[base_config.CHAT_ID], str(e), format_exc())) 211 | except Exception as e: 212 | logger.critical('chat_id: {}; error: {}\n' 213 | '{}'.format(user[base_config.CHAT_ID], str(e), format_exc())) 214 | 215 | 216 | def exchange_convert(exchanges): 217 | """ 218 | Convert exchange name (or list of exchanges names) to name (names) from DB 219 | 220 | Args: 221 | :param exchanges: (or of 's) exchange name 222 | 223 | Returns: 224 | :return: (or of 's) DB exchange name 225 | 226 | """ 227 | if type(exchanges) == str: 228 | return mq.exchange_map[exchanges] 229 | return [mq.exchange_map[exchange] for exchange in exchanges] 230 | -------------------------------------------------------------------------------- /services/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains static replies for user messages. Method name speak for itself""" 21 | 22 | from telegram.ext.dispatcher import run_async 23 | 24 | import messages 25 | from services.core import ( 26 | kb_main, 27 | kb_settings, 28 | MAIN_MENU, 29 | SETTINGS_MENU 30 | ) 31 | 32 | 33 | @run_async 34 | def settings(bot, update): 35 | update.message.reply_text(messages.SETTINGS_MENU_TEXT, reply_markup=kb_settings, parse_mode=messages.MARKDOWN) 36 | return SETTINGS_MENU 37 | 38 | 39 | @run_async 40 | def faq(bot, update): 41 | update.message.reply_text(messages.FAQ_TEXT, parse_mode=messages.MARKDOWN) 42 | return MAIN_MENU 43 | 44 | 45 | @run_async 46 | def about(bot, update): 47 | update.message.reply_text(messages.ABOUT_TEXT, parse_mode=messages.MARKDOWN) 48 | return MAIN_MENU 49 | 50 | 51 | @run_async 52 | def contacts(bot, update): 53 | update.message.reply_text(messages.CONTACTS_TEXT, parse_mode=messages.MARKDOWN) 54 | return MAIN_MENU 55 | 56 | 57 | @run_async 58 | def back(bot, update): 59 | update.message.reply_text(messages.BACK_TEXT, reply_markup=kb_main, parse_mode=messages.MARKDOWN) 60 | return MAIN_MENU 61 | -------------------------------------------------------------------------------- /services/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Evgeniy Koltsov, Sergey Zhirnov. 3 | 4 | This file is part of CindicatorArbitrageBot. 5 | 6 | CindicatorArbitrageBot is free software: you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License as published 8 | by the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | CindicatorArbitrageBot is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with CindicatorArbitrageBot. If not, see . 18 | """ 19 | 20 | """This module contains logic and interactive replies for user's bot settings. 21 | All methods has the same arguments signature which described in set_interval_dialog method. Not all methods 22 | may contain the whole list of described arguments""" 23 | 24 | from telegram.ext.dispatcher import run_async 25 | 26 | import messages 27 | import mongo_queries as mq 28 | from config import base as base_config 29 | from services.commands import switch_on, switch_off 30 | from services.core import ( 31 | notify, 32 | kb_back, 33 | kb_settings, 34 | kb_alerts, 35 | SETTINGS_MENU, 36 | ALERTS_MENU, 37 | SET_INTERVAL, 38 | SET_THRESHOLD 39 | ) 40 | 41 | 42 | @run_async 43 | def set_interval_dialog(bot, update, chat_data, job_queue): 44 | """ 45 | Begin the dialog with for setting an interval value 46 | 47 | Args: 48 | :param bot: bot instance 49 | :param update: update object that contains updates from user chat 50 | :param chat_data: stores the data for the chat. In this case a job object stores in it 51 | :param job_queue: queue of jobs. The notify job automatically adds to queue after start 52 | 53 | Returns: 54 | :return: settings menu identifier 55 | """ 56 | 57 | msg = update.message 58 | interval = int(msg.text) 59 | if interval <= 0: 60 | msg.reply_text(messages.SET_INTERVAL_BAD_VALUE_TEXT, parse_mode=messages.MARKDOWN) 61 | else: 62 | mq.update_setting(msg.chat_id, setting=base_config.INTERVAL, value=interval) 63 | if mq.get_user_settings(msg.chat_id)[base_config.NOTIFICATIONS]: 64 | chat_data['job'].schedule_removal() 65 | job = job_queue.run_repeating(notify, interval, context={"chat_id": msg.chat_id}) 66 | chat_data['job'] = job 67 | msg.reply_text(messages.SET_INTERVAL_SUCC_TEXT.format(interval), 68 | reply_markup=kb_settings, parse_mode=messages.MARKDOWN) 69 | return SETTINGS_MENU 70 | return SET_INTERVAL 71 | 72 | 73 | @run_async 74 | def set_threshold_dialog(bot, update): 75 | """ 76 | Begin the dialog with for setting an threshold value 77 | 78 | Returns: 79 | :return: settings menu identifier 80 | """ 81 | 82 | msg = update.message 83 | threshold = float(msg.text) 84 | if threshold <= 0: 85 | msg.reply_text(messages.SET_THRESHOLD_BAD_VALUE_TEXT, parse_mode=messages.MARKDOWN) 86 | else: 87 | mq.update_setting(msg.chat_id, setting=base_config.THRESHOLD, value=threshold) 88 | msg.reply_text(messages.SET_THRESHOLD_SUCC_TEXT.format(threshold), 89 | reply_markup=kb_settings, parse_mode=messages.MARKDOWN) 90 | return SETTINGS_MENU 91 | return SET_THRESHOLD 92 | 93 | 94 | @run_async 95 | def switch(bot, update, job_queue, chat_data): 96 | """Switch notifications on/off""" 97 | 98 | if mq.get_user_settings(update.message.chat_id)[base_config.NOTIFICATIONS]: 99 | switch_off(bot, update, job_queue, chat_data) 100 | else: 101 | switch_on(bot, update, job_queue, chat_data) 102 | return SETTINGS_MENU 103 | 104 | 105 | @run_async 106 | def interval(bot, update): 107 | """ 108 | Send static message for interval setting 109 | 110 | Returns: 111 | :return: interval menu identifier 112 | """ 113 | 114 | msg = update.message 115 | interval = mq.get_user_settings(msg.chat_id)[base_config.INTERVAL] 116 | msg.reply_text(messages.SET_INTERVAL_TEXT.format(interval), reply_markup=kb_back, parse_mode=messages.MARKDOWN) 117 | return SET_INTERVAL 118 | 119 | 120 | @run_async 121 | def interval_help(bot, update): 122 | """Send static helper message for interval setting""" 123 | 124 | update.message.reply_text(messages.SET_INTERVAL_HELP_CONV_TEXT, parse_mode=messages.MARKDOWN) 125 | return SET_INTERVAL 126 | 127 | 128 | @run_async 129 | def threshold(bot, update): 130 | """ 131 | Send static message for threshold setting 132 | 133 | Returns: 134 | :return: threshold menu identifier 135 | """ 136 | 137 | msg = update.message 138 | threshold = mq.get_user_settings(msg.chat_id)[base_config.THRESHOLD] 139 | msg.reply_text(messages.SET_THRESHOLD_TEXT.format(threshold), reply_markup=kb_back, parse_mode=messages.MARKDOWN) 140 | return SET_THRESHOLD 141 | 142 | 143 | @run_async 144 | def threshold_help(bot, update): 145 | """Send static helper message for threshold setting""" 146 | 147 | update.message.reply_text(messages.SET_THRESHOLD_HELP_CONV_TEXT, parse_mode=messages.MARKDOWN) 148 | return SET_THRESHOLD 149 | 150 | 151 | @run_async 152 | def alerts_settings(bot, update): 153 | """ 154 | Open the menu for alerts settings 155 | 156 | Returns: 157 | :return: alerts settings menu identifier 158 | """ 159 | 160 | update.message.reply_text(messages.ALERTS_MENU_TEXT, reply_markup=kb_alerts, parse_mode=messages.MARKDOWN) 161 | return ALERTS_MENU 162 | 163 | 164 | @run_async 165 | def back_to_settings(bot, update): 166 | """ 167 | Return user to settings menu 168 | 169 | Returns: 170 | :return: settings menu identifier 171 | """ 172 | 173 | update.message.reply_text(messages.BACK_TEXT, reply_markup=kb_settings, parse_mode=messages.MARKDOWN) 174 | return SETTINGS_MENU 175 | --------------------------------------------------------------------------------