├── arbitration ├── core │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_create_info_model.py │ ├── templatetags │ │ ├── __init__.py │ │ └── custom_filters.py │ ├── apps.py │ ├── urls.py │ ├── models.py │ ├── registration.py │ ├── views.py │ ├── api_views.py │ ├── serializers.py │ ├── filters.py │ └── tasks.py ├── banks │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_add_bybit_crypto_exchange.py │ │ └── 0001_create_banks_and_currency_markets_exchanges_model.py │ ├── banks_registration │ │ ├── __init__.py │ │ ├── bank_of_georgia.py │ │ ├── tbc.py │ │ ├── credo.py │ │ ├── qiwi.py │ │ ├── yoomoney.py │ │ ├── sberbank.py │ │ ├── tinkoff.py │ │ ├── wise.py │ │ └── raiffeisen.py │ ├── currency_markets_registration │ │ ├── __init__.py │ │ └── tinkoff_invest.py │ ├── apps.py │ ├── models.py │ ├── banks_config.py │ └── tasks.py ├── parsers │ ├── __init__.py │ ├── connection_types │ │ ├── __init__.py │ │ ├── direct.py │ │ ├── proxy.py │ │ └── tor.py │ ├── apps.py │ ├── cookie.py │ └── loggers.py ├── crypto_exchanges │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0005_create_marginality_percentages_model.py │ │ ├── 0006_update_inter_exchanges_updates.py │ │ ├── 0003_create_lists_fiats_model.py │ │ ├── 0001_create_intra_crypto_exchanges_model.py │ │ ├── 0002_create_crypto_exchanges_model.py │ │ └── 0004_create_inter_exchanges_model.py │ ├── crypto_exchanges_registration │ │ ├── __init__.py │ │ ├── bybit.py │ │ └── binance.py │ ├── apps.py │ ├── crypto_exchanges_config.py │ ├── tasks.py │ └── models.py ├── .dockerignore ├── arbitration │ ├── __init__.py │ ├── celery.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── .isort.cfg ├── static │ ├── img │ │ ├── logo.png │ │ └── fav │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ └── site.webmanifest │ ├── js │ │ ├── info_datatable.js │ │ └── main_datatable.js │ └── css │ │ └── custom_style.css ├── templates │ ├── core │ │ ├── 403csrf.html │ │ ├── 403.html │ │ ├── 500.html │ │ └── 404.html │ ├── includes │ │ ├── footer.html │ │ └── header.html │ ├── crypto_exchanges │ │ ├── includes │ │ │ ├── modal_includes │ │ │ │ ├── input_crypto_exchange.html │ │ │ │ ├── output_crypto_exchange.html │ │ │ │ ├── bank_exchange.html │ │ │ │ ├── interim_crypto_exchange.html │ │ │ │ └── diagram.html │ │ │ ├── seqence.html │ │ │ ├── product_filter.html │ │ │ └── modal_fade.html │ │ ├── info.html │ │ └── main.html │ └── base.html ├── requirements.txt ├── Dockerfile ├── scgi_params └── manage.py ├── runtime.txt ├── setup.cfg ├── infra ├── nginx │ ├── local_default.conf │ └── prod_default.conf ├── example.env ├── certbot │ └── init-letsencrypt.sh ├── local-docker-compose.yml └── prod-docker-compose.yml ├── LICENSE-MIT ├── .github └── workflows │ └── arbitration_workflow.yml ├── .gitignore └── README.md /arbitration/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.13 -------------------------------------------------------------------------------- /arbitration/banks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/banks/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/parsers/connection_types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/banks/currency_markets_registration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/crypto_exchanges_registration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/.dockerignore: -------------------------------------------------------------------------------- 1 | .Dockerfile 2 | .isort.cfg 3 | celery.log 4 | db.sqlite3 -------------------------------------------------------------------------------- /arbitration/arbitration/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ['celery_app'] 4 | -------------------------------------------------------------------------------- /arbitration/.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | force_grid_wrap=0 3 | use_parentheses=True 4 | line_length=79 5 | skip = migrations -------------------------------------------------------------------------------- /arbitration/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/logo.png -------------------------------------------------------------------------------- /arbitration/static/img/fav/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/fav/favicon.ico -------------------------------------------------------------------------------- /arbitration/static/img/fav/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/fav/favicon-16x16.png -------------------------------------------------------------------------------- /arbitration/static/img/fav/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/fav/favicon-32x32.png -------------------------------------------------------------------------------- /arbitration/static/img/fav/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/fav/apple-touch-icon.png -------------------------------------------------------------------------------- /arbitration/templates/core/403csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
Страницы с адресом {{ path }} не существует
8 | Идите на главную 9 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/static/img/fav/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /arbitration/static/js/info_datatable.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('#staticDatatable').DataTable({ 3 | "searching": false, 4 | "pageLength": 15, 5 | "lengthMenu": [[15, 30, 60, 100], [15, 30, 60, 100]], 6 | "info": true, 7 | // 'order': [[0, 'desc']], 8 | }); 9 | }); -------------------------------------------------------------------------------- /arbitration/requirements.txt: -------------------------------------------------------------------------------- 1 | celery==5.2.7 2 | Django==4.1.7 3 | django-bootstrap4>=22.3 4 | django-celery-beat==2.4.0 5 | django-filter>=22.1 6 | django-redis>=5.2.0 7 | django-select2>=8.0.0 8 | django-widget-tweaks>=1.4.12 9 | djangorestframework>=3.14.0 10 | fake-useragent>=1.1.3 11 | free-proxy>=1.1.1 12 | gunicorn>=20.1.0 13 | psycopg2-binary>=2.9.5 14 | python-dotenv>=0.21.0 15 | pysocks>=1.7.1 16 | requests>=2.28.2 17 | stem>=1.8.1 -------------------------------------------------------------------------------- /infra/nginx/local_default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_tokens off; 3 | listen 80; 4 | 5 | location /static/ { 6 | alias /var/html/static/; 7 | } 8 | location / { 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header Host $http_host; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_pass http://arbitration:8000; 13 | proxy_read_timeout 1; 14 | } 15 | } -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/modal_includes/input_crypto_exchange.html: -------------------------------------------------------------------------------- 1 | {% load custom_filters %} 2 | со счёта {{ loop_rate.input_crypto_exchange.bank.name }} нужно купить активы на криптобирже {{ loop_rate.crypto_exchange.name }}. За {{ loop_rate.input_crypto_exchange.fiat }} следует купить {{ loop_rate.input_crypto_exchange.asset }} методом {{ loop_rate.input_crypto_exchange.payment_channel }} по курсу {{ loop_rate.input_crypto_exchange.price|round_up }}. -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/modal_includes/output_crypto_exchange.html: -------------------------------------------------------------------------------- 1 | {% load custom_filters %} 2 | нужно перевести активы с {{ loop_rate.output_crypto_exchange.crypto_exchange.name }} на счёт {{ loop_rate.output_crypto_exchange.bank.name }} по методу {{ loop_rate.output_crypto_exchange.payment_channel }}. 3 | Перевести {{ loop_rate.output_crypto_exchange.asset }} в {{ loop_rate.output_crypto_exchange.fiat }} по курсу {{ loop_rate.output_crypto_exchange.price|round_up }}. -------------------------------------------------------------------------------- /arbitration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | LABEL author='Nezhinsky' 4 | 5 | WORKDIR /arbitration 6 | 7 | COPY requirements.txt . 8 | 9 | RUN apt-get update -y && apt-get upgrade -y && \ 10 | apt-get install -y iputils-ping && pip install --upgrade pip && \ 11 | pip install --root-user-action=ignore -r requirements.txt --no-cache-dir 12 | 13 | COPY . . 14 | 15 | CMD gunicorn --access-logfile - --workers 3 --bind 0.0.0.0:8000 --timeout 120 arbitration.wsgi:application -------------------------------------------------------------------------------- /arbitration/arbitration/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for arbitration project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'arbitration.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /arbitration/arbitration/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for arbitration project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault( 15 | 'DJANGO_SETTINGS_MODULE', 'arbitration.settings' 16 | ) 17 | 18 | application = get_wsgi_application() 19 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/bank_of_georgia.py: -------------------------------------------------------------------------------- 1 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 2 | BinanceP2PParser) 3 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 4 | 5 | BANK_NAME = 'Bank of Georgia' 6 | 7 | BOG_CURRENCIES = ( 8 | 'USD', 'EUR', 'GEL', 'GBP' 9 | ) 10 | 11 | 12 | class BOGBinanceP2PParser(BinanceP2PParser): 13 | bank_name: str = BANK_NAME 14 | 15 | 16 | class BOGBybitP2PParser(BybitP2PParser): 17 | bank_name: str = BANK_NAME 18 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/tbc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 4 | BinanceP2PParser) 5 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 6 | 7 | BANK_NAME = os.path.basename(__file__).split('.')[0].upper() 8 | 9 | TBC_CURRENCIES = ( 10 | 'USD', 'EUR', 'GEL', 'GBP' 11 | ) 12 | 13 | 14 | class TBCBinanceP2PParser(BinanceP2PParser): 15 | bank_name: str = BANK_NAME 16 | 17 | 18 | class TBCBybitP2PParser(BybitP2PParser): 19 | bank_name: str = BANK_NAME 20 | -------------------------------------------------------------------------------- /arbitration/banks/migrations/0002_add_bybit_crypto_exchange.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-16 12:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('banks', '0001_create_banks_and_currency_markets_exchanges_model'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='banks', 15 | name='bybit_name', 16 | field=models.CharField(blank=True, max_length=20, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/credo.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 4 | BinanceP2PParser) 5 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 6 | 7 | BANK_NAME = os.path.basename(__file__).split('.')[0].capitalize() 8 | 9 | CREDO_CURRENCIES = ( 10 | 'USD', 'EUR', 'GEL' 11 | ) 12 | 13 | 14 | class CredoBinanceP2PParser(BinanceP2PParser): 15 | bank_name: str = BANK_NAME 16 | 17 | 18 | class CredoBybitP2PParser(BybitP2PParser): 19 | bank_name: str = BANK_NAME 20 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/qiwi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 4 | BinanceP2PParser) 5 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 6 | 7 | BANK_NAME = os.path.basename(__file__).split('.')[0].upper() 8 | 9 | QIWI_CURRENCIES = ( 10 | 'USD', 'EUR', 'RUB', 'GBP', 'KZT' 11 | ) 12 | 13 | 14 | class QIWIBinanceP2PParser(BinanceP2PParser): 15 | bank_name: str = BANK_NAME 16 | 17 | 18 | class QIWIBybitP2PParser(BybitP2PParser): 19 | bank_name: str = BANK_NAME 20 | -------------------------------------------------------------------------------- /arbitration/arbitration/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 3 | from django.urls import include, path, re_path 4 | from django.views.static import serve 5 | 6 | urlpatterns = [ 7 | path('select2/', include('django_select2.urls')), 8 | # path('admin/', admin.site.urls), 9 | path('', include('core.urls', namespace='core')), 10 | re_path( 11 | r'^static/(?P17 | Число итераций: {{ object_list.count }} 18 |
19 |22 | Текущее количество процессоров: 2 23 |
24 |25 | Оптимальное количество процессоров: 5 26 |
27 || # | 34 |Status | 35 |Started | 36 |Stopped | 37 |Duration | 38 |
|---|---|---|---|---|
| {{ forloop.counter }} | 43 |{% if info_loop.value %}Started{% else %}Stopped{% endif %} | 44 |{{ info_loop.started }} | 45 |{{ info_loop.sopped }} | 46 |{{ info_loop.duration }} | 47 |
21 | * C 09.03.23 Binance закрыла для российских карт возможность покупки и продажи долларов и евро через свой P2P-сервис. Сервисы Card2Crypto и Card2Wallet2Crypto уже давно не поддерживают российские карты. 22 |
23 || Связка | 27 |Маржинальность | 28 |Обновлено | 29 |
|---|
6 | 10 | {% include 'crypto_exchanges/includes/seqence.html' %} 11 |
12 | 13 | 14 | 87 | -------------------------------------------------------------------------------- /arbitration/core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from banks.models import Banks, BanksExchangeRates, CurrencyMarkets 4 | from crypto_exchanges.models import (CryptoExchanges, CryptoExchangesRates, 5 | InterExchanges, InterExchangesUpdates, 6 | IntraCryptoExchangesRates) 7 | 8 | ROUND_TO = 10 9 | 10 | 11 | class RoundingDecimalField(serializers.DecimalField): 12 | """ 13 | Custom redefinition of the display of decimal numbers. 14 | """ 15 | MIN_NON_EMPTY_DECIMAL: int = 5 16 | 17 | def __init__(self, max_digits, decimal_places, percent=False, **kwargs): 18 | super().__init__(max_digits, decimal_places, **kwargs) 19 | self.percent = percent 20 | 21 | def validate_precision(self, value): 22 | return value 23 | 24 | def quantize(self, value): 25 | """ 26 | Quantize the decimal value to the configured precision. 27 | """ 28 | if self.percent: 29 | return round(value, self.decimal_places) 30 | integers, real_decimal_places = map( 31 | len, str(value).split('.')) 32 | max_decimal_places = self.max_digits - integers 33 | if real_decimal_places > max_decimal_places: 34 | self.decimal_places = max_decimal_places 35 | if integers >= self.max_digits: 36 | self.decimal_places = 1 37 | result = round(value, self.decimal_places) 38 | if result == int(result): 39 | return round(value, 1) 40 | return result 41 | 42 | 43 | class CryptoExchangesSerializer(serializers.ModelSerializer): 44 | """ 45 | A serializer for the CryptoExchanges model, which represents a 46 | cryptocurrency exchange. 47 | """ 48 | class Meta: 49 | model = CryptoExchanges 50 | fields = ('name',) 51 | 52 | 53 | class BanksSerializer(serializers.ModelSerializer): 54 | """ 55 | A serializer for the Banks model, which represents a bank. 56 | """ 57 | class Meta: 58 | model = Banks 59 | fields = ('name',) 60 | 61 | 62 | class IntraCryptoExchangesRatesSerializer(serializers.ModelSerializer): 63 | """ 64 | A serializer for the IntraCryptoExchangesRates model, which represents 65 | exchange rates between two cryptocurrencies on a single exchange. 66 | """ 67 | price = RoundingDecimalField(max_digits=ROUND_TO, decimal_places=None) 68 | 69 | class Meta: 70 | model = IntraCryptoExchangesRates 71 | exclude = ('id', 'update', 'crypto_exchange') 72 | 73 | 74 | class CryptoExchangesRatesSerializer(serializers.ModelSerializer): 75 | """ 76 | A serializer for the CryptoExchangesRates model, which represents exchange 77 | rates between a cryptocurrency on a single exchange and a fiat currency on 78 | banks. 79 | """ 80 | intra_crypto_exchange = IntraCryptoExchangesRatesSerializer(read_only=True) 81 | price = RoundingDecimalField(max_digits=ROUND_TO, decimal_places=None) 82 | 83 | class Meta: 84 | model = CryptoExchangesRates 85 | exclude = ( 86 | 'id', 'update', 'trade_type', 'crypto_exchange', 'bank' 87 | ) 88 | 89 | 90 | class CurrencyMarketsSerializer(serializers.ModelSerializer): 91 | """ 92 | A serializer for the CurrencyMarkets model, which represents a market for 93 | exchanging currencies. 94 | """ 95 | class Meta: 96 | model = CurrencyMarkets 97 | fields = ('name',) 98 | 99 | 100 | class BanksExchangeRatesSerializer(serializers.ModelSerializer): 101 | """ 102 | A serializer for the BanksExchangeRates model, which represents exchange 103 | rates between two fiat currencies at a bank. 104 | """ 105 | bank = BanksSerializer(read_only=True) 106 | currency_market = CurrencyMarketsSerializer(read_only=True) 107 | price = RoundingDecimalField(max_digits=ROUND_TO, decimal_places=None) 108 | 109 | class Meta: 110 | model = BanksExchangeRates 111 | exclude = ('id', 'update') 112 | 113 | 114 | class UpdateSerializer(serializers.ModelSerializer): 115 | """ 116 | A serializer for the InterExchangesUpdates model, which represents 117 | updates to the exchange rates between two fiat currencies on a bank or 118 | between a cryptocurrency and a fiat currency on an exchange. 119 | """ 120 | class Meta: 121 | model = InterExchangesUpdates 122 | fields = ('updated',) 123 | 124 | 125 | class InterExchangesSerializer(serializers.ModelSerializer): 126 | """ 127 | A serializer for the InterExchanges model, which represents an exchange of 128 | currency or cryptocurrency between two banks and between a bank and an 129 | exchange. 130 | """ 131 | crypto_exchange = CryptoExchangesSerializer(read_only=True) 132 | input_bank = BanksSerializer(read_only=True) 133 | output_bank = BanksSerializer(read_only=True) 134 | input_crypto_exchange = CryptoExchangesRatesSerializer(read_only=True) 135 | output_crypto_exchange = CryptoExchangesRatesSerializer(read_only=True) 136 | interim_crypto_exchange = IntraCryptoExchangesRatesSerializer( 137 | read_only=True) 138 | second_interim_crypto_exchange = IntraCryptoExchangesRatesSerializer( 139 | read_only=True) 140 | bank_exchange = BanksExchangeRatesSerializer(read_only=True) 141 | marginality_percentage = RoundingDecimalField( 142 | max_digits=4, decimal_places=2, percent=True 143 | ) 144 | update = UpdateSerializer(read_only=True) 145 | 146 | class Meta: 147 | model = InterExchanges 148 | fields = '__all__' 149 | -------------------------------------------------------------------------------- /arbitration/parsers/loggers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from datetime import datetime, timedelta, timezone 4 | 5 | from arbitration.settings import (LOGLEVEL_CALCULATING_END, 6 | LOGLEVEL_CALCULATING_START, 7 | LOGLEVEL_PARSING_END, LOGLEVEL_PARSING_START) 8 | 9 | 10 | class BaseLogger(ABC): 11 | """ 12 | The abstract base class for all logger classes. Defines common attributes 13 | and methods for all loggers. 14 | 15 | Attributes: 16 | duration (timedelta): The duration of the logger's operation. 17 | count_created_objects (int): The count of created objects during the 18 | logger's operation. 19 | count_updated_objects (int): The count of updated objects during the 20 | logger's operation. 21 | bank_name (str): The name of the bank related to the logger, if any. 22 | loglevel_start (str): Log level for start. 23 | loglevel_end (str): Log level for end. 24 | """ 25 | duration: timedelta 26 | count_created_objects: int 27 | count_updated_objects: int 28 | bank_name = None 29 | loglevel_start: str 30 | loglevel_end: str 31 | 32 | def __init__(self) -> None: 33 | """ 34 | Initializes the logger with the current datetime in UTC timezone and a 35 | logger object. 36 | """ 37 | self.start_time = datetime.now(timezone.utc) 38 | self.logger = logging.getLogger(self.__class__.__name__) 39 | 40 | def _logger_start(self) -> None: 41 | """ 42 | Logs a message with the start time of the logger. 43 | """ 44 | message = f'Start {self.__class__.__name__} at {self.start_time}.' 45 | getattr(self.logger, self.loglevel_start)(message) 46 | 47 | @abstractmethod 48 | def _get_count_created_objects(self) -> None: 49 | """ 50 | Abstract method for getting the count of created objects. 51 | """ 52 | pass 53 | 54 | @abstractmethod 55 | def _get_count_updated_objects(self) -> None: 56 | """ 57 | Abstract method for getting the count of updated objects. 58 | """ 59 | pass 60 | 61 | def _get_all_objects(self) -> None: 62 | """ 63 | Calls _get_count_created_objects() and _get_count_updated_objects(). 64 | """ 65 | self._get_count_created_objects() 66 | self._get_count_updated_objects() 67 | 68 | def _logger_error(self, error: Exception) -> None: 69 | """ 70 | Logs an error message with the count of created and updated objects and 71 | the error. 72 | """ 73 | self._get_all_objects() 74 | message = f'An error has occurred in {self.__class__.__name__}. ' 75 | if self.bank_name is not None: 76 | message += f'Bank name: {self.bank_name}. ' 77 | if self.count_created_objects + self.count_updated_objects > 0: 78 | message += f'Updated: {self.count_updated_objects}, ' 79 | message += f'Created: {self.count_created_objects}. ' 80 | else: 81 | message += (f'Has not been created and updated: ' 82 | f'{self.count_updated_objects}. ') 83 | message += f'Error: {error}' 84 | self.logger.error(message) 85 | 86 | def _logger_end(self, *args) -> None: 87 | """ 88 | Logs the end of the logger. 89 | """ 90 | self._get_all_objects() 91 | message = ( 92 | f'Finish {self.__class__.__name__} at {self.duration}. ' 93 | ) 94 | if self.bank_name is not None: 95 | message += f'Bank name: {self.bank_name}. ' 96 | for arg in args: 97 | message += arg 98 | if self.count_created_objects + self.count_updated_objects > 0: 99 | message += f'Updated: {self.count_updated_objects}, ' 100 | message += f'Created: {self.count_created_objects}. ' 101 | else: 102 | message += (f'Has not been Created and updated: ' 103 | f'{self.count_updated_objects}. ') 104 | getattr(self.logger, self.loglevel_end)(message) 105 | 106 | 107 | class ParsingLogger(BaseLogger, ABC): 108 | """ 109 | Logger for parsing operations. 110 | 111 | Attributes: 112 | loglevel_start (str): Log level for start. 113 | loglevel_end (str): Log level for end. 114 | connection_type(str): The type of the connection. Either Tor, Proxy 115 | or Direct. 116 | """ 117 | loglevel_start: str = LOGLEVEL_PARSING_START 118 | loglevel_end: str = LOGLEVEL_PARSING_END 119 | connection_type: str 120 | 121 | def __init__(self) -> None: 122 | """ 123 | Initializes the ParsingLogger with connections_duration and 124 | renew_connections_duration. 125 | """ 126 | super().__init__() 127 | self.connections_duration = 0 128 | self.renew_connections_duration = 0 129 | 130 | def __logger_connection_type(self) -> str: 131 | """ 132 | Adds a message to the logger about the type of connection. 133 | """ 134 | return f'Connection type: {self.connection_type}. ' 135 | 136 | def __logger_connections_duration(self) -> str: 137 | """ 138 | Adds a message to the logger about the duration of connection. 139 | """ 140 | message = '' 141 | if self.connections_duration > 0: 142 | message += (f'Duration of connections: ' 143 | f'{self.connections_duration}. ') 144 | return message 145 | 146 | def __logger_renew_connections_duration(self) -> str: 147 | """ 148 | Adds a message to the logger about the duration of renew connection. 149 | """ 150 | message = '' 151 | if self.renew_connections_duration > 0: 152 | message += (f'Duration of renew connections: ' 153 | f'{self.renew_connections_duration}. ') 154 | return message 155 | 156 | def _logger_end(self, *args) -> None: 157 | """ 158 | Logs the end of the parsing logger. 159 | """ 160 | super()._logger_end( 161 | self.__logger_connection_type(), 162 | self.__logger_connections_duration(), 163 | self.__logger_renew_connections_duration() 164 | ) 165 | 166 | 167 | class CalculatingLogger(BaseLogger, ABC): 168 | """ 169 | Logger for calculating operations. 170 | 171 | Attributes: 172 | loglevel_start (str): Log level for start. 173 | loglevel_end (str): Log level for end. 174 | simpl (bool): Determines whether the calculations will be simple or 175 | complex. 176 | international (bool): Specifies the list of output banks, only 177 | international or only local. 178 | """ 179 | loglevel_start: str = LOGLEVEL_CALCULATING_START 180 | loglevel_end: str = LOGLEVEL_CALCULATING_END 181 | simpl: bool 182 | international: bool 183 | full_update: bool 184 | 185 | def _logger_queue_overflowing(self): 186 | """ 187 | Logs a message for a skipped task due to queue overflow. 188 | """ 189 | message = ( 190 | f'The task was skipped due to the accumulation of identical tasks ' 191 | f'in the queue. {self.__class__.__name__}, Bank name: ' 192 | f'{self.bank_name}. simpl: {self.simpl}, international: ' 193 | f'{self.international}, full_update: {self.full_update}. ' 194 | ) 195 | self.logger.error(message) 196 | -------------------------------------------------------------------------------- /arbitration/core/filters.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from django_select2.forms import Select2MultipleWidget 3 | 4 | from banks.banks_config import BANKS_CONFIG 5 | from crypto_exchanges.crypto_exchanges_config import (ALL_ASSETS, ALL_FIATS, 6 | CRYPTO_EXCHANGES_CONFIG) 7 | from crypto_exchanges.models import InterExchanges 8 | 9 | BANK_CHOICES = tuple((bank, bank) for bank in BANKS_CONFIG.keys()) 10 | CRYPTO_EXCHANGE_CHOICES = tuple( 11 | (crypto_exchange, crypto_exchange) 12 | for crypto_exchange in CRYPTO_EXCHANGES_CONFIG.keys() 13 | ) 14 | CRYPTO_EXCHANGE_ASSET_CHOICES = tuple( 15 | (asset, asset) for asset in ALL_ASSETS 16 | ) 17 | PAYMENT_CHANNEL_CHOICES = ( 18 | ('P2P', 'P2P'), 19 | ('Card2CryptoExchange', 'Card2CryptoExchange'), 20 | ('Card2Wallet2CryptoExchange', 'Card2Wallet2CryptoExchange') 21 | ) 22 | ALL_FIAT_CHOICES = tuple( 23 | (fiat, fiat) for fiat in ALL_FIATS 24 | ) 25 | BANK_EXCHANGE_CHOICES = ( 26 | (0, 'Да'), 27 | (1, 'Нет'), 28 | ('banks', 'Только через банковский обмен'), 29 | ('Tinkoff invest', 'Только через валютные биржи') 30 | ) 31 | INTRA_CRYPTO_EXCHANGE_CHOICES = ( 32 | (0, 'Да'), 33 | (1, 'Нет'), 34 | ('one', 'Только c одной конвертацией'), 35 | ('two', 'Только с двумя конвертациями') 36 | ) 37 | 38 | 39 | class ExchangesFilter(django_filters.FilterSet): 40 | """ 41 | This class is responsible for filtering a list of cryptocurrency exchanges 42 | based on various criteria. 43 | """ 44 | gte = django_filters.NumberFilter( 45 | field_name='marginality_percentage', lookup_expr='gte', 46 | label='от', help_text='От' 47 | ) 48 | lte = django_filters.NumberFilter( 49 | field_name='marginality_percentage', lookup_expr='lte', 50 | label='до', help_text='До' 51 | ) 52 | crypto_exchange = django_filters.MultipleChoiceFilter( 53 | choices=CRYPTO_EXCHANGE_CHOICES, field_name='crypto_exchange__name', 54 | widget=Select2MultipleWidget, label='Крипто биржи' 55 | ) 56 | assets = django_filters.MultipleChoiceFilter( 57 | choices=CRYPTO_EXCHANGE_ASSET_CHOICES, 58 | method='asset_filter', 59 | widget=Select2MultipleWidget, label='Криптоактивы' 60 | ) 61 | input_payment_channel = django_filters.MultipleChoiceFilter( 62 | choices=PAYMENT_CHANNEL_CHOICES, 63 | method='input_payment_channel_filter', 64 | widget=Select2MultipleWidget, label='Платёжные методы в начале', 65 | help_text=( 66 | '•P2P (peer-to-peer) — прямая торговля пользователей ' 67 | 'друг с другом на бирже. Комиссия не взымается. ' 68 | '•Card2CryptoExchange — ввод / вывод криптоактивов ' 69 | 'напрямую через биржу. Предусмотрена комиссия. ' 70 | '•Card2Wallet2CryptoExchange — ввод на биржу сначала ' 71 | 'фиатных денег, с последующим обменом на СПОТовой бирже ' 72 | 'в криптоактивы или наоборот вывод с предворительным ' 73 | 'обменом криптоактивов в фиатные деньги. ' 74 | 'Предусмотрена комиссия.' 75 | ) 76 | ) 77 | output_payment_channel = django_filters.MultipleChoiceFilter( 78 | choices=PAYMENT_CHANNEL_CHOICES, 79 | method='output_payment_channel_filter', 80 | widget=Select2MultipleWidget, label='Платёжные методы в конце', 81 | help_text=( 82 | '•P2P (peer-to-peer) — прямая торговля пользователей ' 83 | 'друг с другом на бирже. Комиссия не взымается. ' 84 | '•Card2CryptoExchange — ввод / вывод криптоактивов ' 85 | 'напрямую через биржу. Предусмотрена комиссия. ' 86 | '•Card2Wallet2CryptoExchange — ввод на биржу сначала ' 87 | 'фиатных денег, с последующим обменом на СПОТовой бирже ' 88 | 'в криптоактивы или наоборот вывод с предворительным ' 89 | 'обменом криптоактивов в фиатные деньги. ' 90 | 'Предусмотрена комиссия.' 91 | ) 92 | ) 93 | input_bank = django_filters.MultipleChoiceFilter( 94 | choices=BANK_CHOICES, field_name='input_bank__name', 95 | widget=Select2MultipleWidget, label='Банки в начале' 96 | ) 97 | output_bank = django_filters.MultipleChoiceFilter( 98 | choices=BANK_CHOICES, field_name='output_bank__name', 99 | widget=Select2MultipleWidget, label='Банки в конце', 100 | ) 101 | fiats = django_filters.MultipleChoiceFilter( 102 | choices=ALL_FIAT_CHOICES, 103 | method='fiat_filter', 104 | widget=Select2MultipleWidget, label='Валюты' 105 | ) 106 | bank_exchange = django_filters.ChoiceFilter( 107 | choices=BANK_EXCHANGE_CHOICES, method='bank_exchange_filter', 108 | label='Конвертация внутри банка', empty_label='', help_text=( 109 | '•Да - только связки с внутрибанковскими конвертациями. ' 110 | '•Нет - только связки без внутрибанковских конвертаций. ' 111 | '*Внутрибанковская конвертация доступна не у всех банков. ' 112 | '*Через валютные биржи можно обменивать только по рабочим дням ' 113 | 'с 7:00 до 19:00 по Мск, в остальное время этот фильтр недоступен' 114 | ) 115 | ) 116 | intra_crypto_exchange = django_filters.ChoiceFilter( 117 | choices=INTRA_CRYPTO_EXCHANGE_CHOICES, 118 | method='intra_crypto_exchange_filter', 119 | label='Конвертация внутри криптобиржи', empty_label='', help_text=( 120 | '•Да - только связки с внутрибиржевыми конвертациями. ' 121 | '•Нет - только связки без внутрибиржевых конвертаций.' 122 | ) 123 | ) 124 | 125 | @staticmethod 126 | def asset_filter(queryset, _, values): 127 | return queryset.filter( 128 | input_crypto_exchange__asset__in=values, 129 | output_crypto_exchange__asset__in=values 130 | ) 131 | 132 | @staticmethod 133 | def input_payment_channel_filter(queryset, _, values): 134 | return queryset.filter( 135 | input_crypto_exchange__payment_channel__in=values 136 | ) 137 | 138 | @staticmethod 139 | def output_payment_channel_filter(queryset, _, values): 140 | return queryset.filter( 141 | output_crypto_exchange__payment_channel__in=values 142 | ) 143 | 144 | @staticmethod 145 | def fiat_filter(queryset, _, values): 146 | return queryset.filter( 147 | input_crypto_exchange__fiat__in=values, 148 | output_crypto_exchange__fiat__in=values 149 | ) 150 | 151 | @staticmethod 152 | def bank_exchange_filter(queryset, _, values): 153 | if values in ('1', '0'): 154 | return queryset.filter( 155 | bank_exchange__isnull=bool(int(values)) 156 | ) 157 | if values == 'banks': 158 | return queryset.filter( 159 | bank_exchange__isnull=False, 160 | bank_exchange__currency_market__isnull=True 161 | ) 162 | return queryset.filter( 163 | bank_exchange__isnull=False, 164 | bank_exchange__currency_market__isnull=False 165 | ) 166 | 167 | @staticmethod 168 | def intra_crypto_exchange_filter(queryset, _, values): 169 | if values in ('1', '0'): 170 | return queryset.filter( 171 | interim_crypto_exchange__isnull=bool(int(values)) 172 | ) 173 | if values == 'one': 174 | return queryset.filter( 175 | interim_crypto_exchange__isnull=False, 176 | second_interim_crypto_exchange__isnull=True 177 | ) 178 | return queryset.filter( 179 | interim_crypto_exchange__isnull=False, 180 | second_interim_crypto_exchange__isnull=False 181 | ) 182 | 183 | class Meta: 184 | model = InterExchanges 185 | fields = ( 186 | 'gte', 'lte', 'input_bank', 'output_bank', 'fiats', 187 | 'crypto_exchange', 'assets', 'input_payment_channel', 188 | 'output_payment_channel', 'bank_exchange', 'intra_crypto_exchange' 189 | ) 190 | -------------------------------------------------------------------------------- /arbitration/core/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timezone 3 | 4 | from celery import group 5 | 6 | from arbitration.celery import app 7 | from arbitration.settings import PARSING_WORKER_NAME 8 | from banks.banks_config import BANKS_CONFIG 9 | from banks.tasks import (get_bog_p2p_binance_exchanges, 10 | get_bog_p2p_bybit_exchanges, 11 | get_credo_p2p_binance_exchanges, 12 | get_credo_p2p_bybit_exchanges, 13 | get_qiwi_p2p_binance_exchanges, 14 | get_qiwi_p2p_bybit_exchanges, 15 | get_raiffeisen_p2p_binance_exchanges, 16 | get_raiffeisen_p2p_bybit_exchanges, 17 | get_sberbank_p2p_binance_exchanges, 18 | get_sberbank_p2p_bybit_exchanges, 19 | get_tbc_p2p_binance_exchanges, 20 | get_tbc_p2p_bybit_exchanges, 21 | get_tinkoff_p2p_binance_exchanges, 22 | get_tinkoff_p2p_bybit_exchanges, 23 | get_wise_p2p_binance_exchanges, 24 | get_wise_p2p_bybit_exchanges, 25 | get_yoomoney_p2p_binance_exchanges, 26 | get_yoomoney_p2p_bybit_exchanges, 27 | parse_internal_raiffeisen_rates, 28 | parse_internal_tinkoff_rates, 29 | parse_internal_wise_rates) 30 | from core.models import InfoLoop 31 | from core.registration import all_registration 32 | from crypto_exchanges.models import InterExchangesUpdates 33 | from crypto_exchanges.tasks import (get_all_binance_crypto_exchanges, 34 | get_all_bybit_crypto_exchanges, 35 | get_binance_card_2_crypto_exchanges_buy, 36 | get_binance_card_2_crypto_exchanges_sell, 37 | get_start_binance_fiat_crypto_list) 38 | from parsers.calculations import ( 39 | ComplexInterExchangesCalculating, 40 | ComplexInternationalInterExchangesCalculating, 41 | SimplInterExchangesCalculating, 42 | SimplInternationalInterExchangesCalculating) 43 | 44 | logger = logging.getLogger(__name__) 45 | 46 | 47 | @app.task(queue='parsing') 48 | def all_reg(): 49 | all_registration() 50 | get_start_binance_fiat_crypto_list.s().apply() 51 | logger.error('Finish registration') 52 | 53 | 54 | @app.task(bind=True, max_retries=4, queue='parsing') 55 | def delete_tasks_wait_for_idle(self): 56 | app.control.purge() 57 | stats = app.control.inspect().stats().get(PARSING_WORKER_NAME) 58 | try: 59 | active_tasks = ( 60 | stats.get('pool').get('writes').get('inqueues').get('active') 61 | ) 62 | if active_tasks > 0: 63 | logger.info(f'Есть активные задачи: {active_tasks}') 64 | app.control.wait_for_workers([PARSING_WORKER_NAME], 90, force=True) 65 | app.control.purge() 66 | new_stats = app.control.inspect().stats().get(PARSING_WORKER_NAME) 67 | new_active_tasks = ( 68 | new_stats.get('pool').get('writes').get('inqueues').get('active') 69 | ) 70 | if new_active_tasks > 0: 71 | self.retry() 72 | logger.info('Все задачи удалены из очереди') 73 | except Exception as error: 74 | logger.info(stats) 75 | message = f'Нет доступной статистики по воркерам: {error}' 76 | logger.error(message) 77 | raise Exception 78 | 79 | 80 | @app.task(queue='parsing') 81 | def assets_loop_stop(): 82 | try: 83 | InterExchangesUpdates.objects.filter(ended=False).delete() 84 | target_loop = InfoLoop.objects.first() 85 | target_loop.value = False 86 | datetime_now = datetime.now(timezone.utc) 87 | target_loop.stopped = datetime_now 88 | target_loop.duration = datetime_now - target_loop.started 89 | target_loop.save() 90 | except Exception as error: 91 | logger.error(error) 92 | raise Exception 93 | 94 | 95 | @app.task(queue='parsing') 96 | def assets_loop(): 97 | group( 98 | get_all_binance_crypto_exchanges.s(), 99 | get_all_bybit_crypto_exchanges.s(), 100 | get_tinkoff_p2p_binance_exchanges.s(), 101 | get_sberbank_p2p_binance_exchanges.s(), 102 | get_raiffeisen_p2p_binance_exchanges.s(), 103 | get_qiwi_p2p_binance_exchanges.s(), 104 | get_yoomoney_p2p_binance_exchanges.s(), 105 | get_bog_p2p_binance_exchanges.s(), 106 | get_tbc_p2p_binance_exchanges.s(), 107 | get_credo_p2p_binance_exchanges.s(), 108 | get_wise_p2p_binance_exchanges.s(), 109 | get_tinkoff_p2p_bybit_exchanges.s(), 110 | get_sberbank_p2p_bybit_exchanges.s(), 111 | get_raiffeisen_p2p_bybit_exchanges.s(), 112 | get_qiwi_p2p_bybit_exchanges.s(), 113 | get_yoomoney_p2p_bybit_exchanges.s(), 114 | get_bog_p2p_bybit_exchanges.s(), 115 | get_tbc_p2p_bybit_exchanges.s(), 116 | get_credo_p2p_bybit_exchanges.s(), 117 | get_wise_p2p_bybit_exchanges.s(), 118 | parse_internal_tinkoff_rates.s(), 119 | parse_internal_raiffeisen_rates.s(), 120 | parse_internal_wise_rates.s(), 121 | get_binance_card_2_crypto_exchanges_buy.s(), 122 | get_binance_card_2_crypto_exchanges_sell.s(), 123 | ).delay() 124 | 125 | 126 | # Inter exchanges calculating 127 | @app.task(queue='calculating') 128 | def simpl_inter_exchanges_calculating( 129 | crypto_exchange_name, bank_name, full_update): 130 | SimplInterExchangesCalculating( 131 | crypto_exchange_name, bank_name, full_update).main() 132 | 133 | 134 | @app.task 135 | def get_simpl_inter_exchanges_calculating(full_update): 136 | from crypto_exchanges.crypto_exchanges_config import ( 137 | CRYPTO_EXCHANGES_CONFIG) 138 | group( 139 | simpl_inter_exchanges_calculating.s( 140 | crypto_exchange_name, bank_name, full_update 141 | ) for crypto_exchange_name in CRYPTO_EXCHANGES_CONFIG.keys() 142 | for bank_name, config in BANKS_CONFIG.items() 143 | if 'Binance' in config['crypto_exchanges'] 144 | ).delay() 145 | 146 | 147 | @app.task(queue='calculating') 148 | def simpl_international_inter_exchanges_calculating( 149 | crypto_exchange_name, bank_name, full_update): 150 | SimplInternationalInterExchangesCalculating( 151 | crypto_exchange_name, bank_name, full_update).main() 152 | 153 | 154 | @app.task 155 | def get_simpl_international_inter_exchanges_calculating(full_update): 156 | from crypto_exchanges.crypto_exchanges_config import ( 157 | CRYPTO_EXCHANGES_CONFIG) 158 | group( 159 | simpl_international_inter_exchanges_calculating.s( 160 | crypto_exchange_name, bank_name, full_update 161 | ) for crypto_exchange_name in CRYPTO_EXCHANGES_CONFIG.keys() 162 | for bank_name, config in BANKS_CONFIG.items() 163 | if 'Binance' in config['crypto_exchanges'] 164 | ).delay() 165 | 166 | 167 | @app.task(queue='calculating') 168 | def complex_inter_exchanges_calculating( 169 | crypto_exchange_name, bank_name, full_update): 170 | ComplexInterExchangesCalculating( 171 | crypto_exchange_name, bank_name, full_update).main() 172 | 173 | 174 | @app.task 175 | def get_complex_inter_exchanges_calculating(full_update): 176 | from crypto_exchanges.crypto_exchanges_config import ( 177 | CRYPTO_EXCHANGES_CONFIG) 178 | group( 179 | complex_inter_exchanges_calculating.s( 180 | crypto_exchange_name, bank_name, full_update 181 | ) for crypto_exchange_name in CRYPTO_EXCHANGES_CONFIG.keys() 182 | for bank_name, config in BANKS_CONFIG.items() 183 | if config['bank_parser'] 184 | ).delay() 185 | 186 | 187 | @app.task(queue='calculating') 188 | def complex_international_inter_exchanges_calculating( 189 | crypto_exchange_name, bank_name, full_update): 190 | ComplexInternationalInterExchangesCalculating( 191 | crypto_exchange_name, bank_name, full_update).main() 192 | 193 | 194 | @app.task 195 | def get_complex_international_inter_exchanges_calculating(full_update): 196 | from crypto_exchanges.crypto_exchanges_config import ( 197 | CRYPTO_EXCHANGES_CONFIG) 198 | group( 199 | complex_international_inter_exchanges_calculating.s( 200 | crypto_exchange_name, bank_name, full_update 201 | ) for crypto_exchange_name in CRYPTO_EXCHANGES_CONFIG.keys() 202 | for bank_name, config in BANKS_CONFIG.items() 203 | if config['bank_parser'] 204 | ).delay() 205 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from arbitration.settings import (ASSET_LENGTH, CHANNEL_LENGTH, DIAGRAM_LENGTH, 4 | FIAT_LENGTH, NAME_LENGTH, TRADE_TYPE_LENGTH) 5 | from banks.models import Banks, BanksExchangeRates 6 | from core.models import UpdatesModel 7 | 8 | 9 | class CryptoExchanges(models.Model): 10 | """ 11 | Model to represent crypto exchanges. 12 | """ 13 | name = models.CharField(max_length=NAME_LENGTH) 14 | 15 | 16 | class IntraCryptoExchangesRatesUpdates(UpdatesModel): 17 | """ 18 | Model to store the last update date of intra crypto exchange rates. 19 | """ 20 | crypto_exchange = models.ForeignKey( 21 | CryptoExchanges, 22 | related_name='crypto_exchanges_update', 23 | on_delete=models.CASCADE 24 | ) 25 | 26 | 27 | class IntraCryptoExchangesRates(models.Model): 28 | """ 29 | Model to store the intra crypto exchange rates. 30 | """ 31 | crypto_exchange = models.ForeignKey( 32 | CryptoExchanges, 33 | related_name='crypto_exchanges', 34 | on_delete=models.CASCADE 35 | ) 36 | from_asset = models.CharField(max_length=ASSET_LENGTH) 37 | to_asset = models.CharField(max_length=ASSET_LENGTH) 38 | price = models.FloatField() 39 | update = models.ForeignKey( 40 | IntraCryptoExchangesRatesUpdates, 41 | related_name='datas', 42 | on_delete=models.CASCADE 43 | ) 44 | spot_fee = models.FloatField(default=None) 45 | 46 | class Meta: 47 | constraints = [ 48 | models.UniqueConstraint( 49 | fields=( 50 | 'crypto_exchange', 'from_asset', 'to_asset' 51 | ), name='unique_intra_crypto_exchanges' 52 | ) 53 | ] 54 | 55 | 56 | class CryptoExchangesRatesUpdates(UpdatesModel): 57 | """ 58 | Model to store the last update date of crypto exchange rates. 59 | """ 60 | crypto_exchange = models.ForeignKey( 61 | CryptoExchanges, 62 | related_name='crypto_exchange_rates_update', 63 | on_delete=models.CASCADE 64 | ) 65 | bank = models.ForeignKey( 66 | Banks, 67 | related_name='crypto_exchange_rates_update', 68 | null=True, 69 | blank=True, 70 | on_delete=models.CASCADE 71 | ) 72 | payment_channel = models.CharField( 73 | max_length=CHANNEL_LENGTH, 74 | null=True, 75 | blank=True 76 | ) 77 | 78 | 79 | class CryptoExchangesRates(models.Model): 80 | """ 81 | Model to store the crypto exchange rates. 82 | """ 83 | crypto_exchange = models.ForeignKey( 84 | CryptoExchanges, related_name='crypto_exchange_rates', 85 | on_delete=models.CASCADE 86 | ) 87 | asset = models.CharField(max_length=ASSET_LENGTH) 88 | fiat = models.CharField(max_length=FIAT_LENGTH) 89 | trade_type = models.CharField(max_length=TRADE_TYPE_LENGTH) 90 | bank = models.ForeignKey( 91 | Banks, 92 | related_name='crypto_exchange_rates', 93 | on_delete=models.CASCADE 94 | ) 95 | payment_channel = models.CharField( 96 | max_length=CHANNEL_LENGTH, 97 | null=True, 98 | blank=True 99 | ) 100 | transaction_method = models.CharField( 101 | max_length=CHANNEL_LENGTH, 102 | null=True, 103 | blank=True 104 | ) 105 | transaction_fee = models.FloatField( 106 | null=True, 107 | blank=True, 108 | default=None 109 | ) 110 | price = models.FloatField( 111 | null=True, 112 | blank=True, 113 | default=None 114 | ) 115 | pre_price = models.FloatField( 116 | null=True, 117 | blank=True, 118 | default=None 119 | ) 120 | intra_crypto_exchange = models.ForeignKey( 121 | IntraCryptoExchangesRates, 122 | related_name='card_2_wallet_2_crypto_exchange_rates', 123 | blank=True, 124 | null=True, 125 | on_delete=models.CASCADE 126 | ) 127 | update = models.ForeignKey( 128 | CryptoExchangesRatesUpdates, 129 | related_name='datas', 130 | on_delete=models.CASCADE 131 | ) 132 | 133 | class Meta: 134 | constraints = [ 135 | models.UniqueConstraint( 136 | fields=( 137 | 'crypto_exchange', 'bank', 'asset', 'trade_type', 138 | 'fiat', 'transaction_method', 'payment_channel' 139 | ), name='unique_crypto_exchange_rates' 140 | ) 141 | ] 142 | 143 | 144 | class ListsFiatCryptoUpdates(UpdatesModel): 145 | """ 146 | Model to store the last update date of fiat-crypto pairs lists. 147 | """ 148 | crypto_exchange = models.ForeignKey( 149 | CryptoExchanges, 150 | related_name='list_fiat_crypto_update', 151 | on_delete=models.CASCADE 152 | ) 153 | 154 | 155 | class ListsFiatCrypto(models.Model): 156 | """ 157 | Model to store the list of fiat-crypto pairs for a crypto exchange. 158 | """ 159 | crypto_exchange = models.ForeignKey( 160 | CryptoExchanges, 161 | related_name='list_fiat_crypto', 162 | on_delete=models.CASCADE 163 | ) 164 | list_fiat_crypto = models.JSONField() 165 | trade_type = models.CharField(max_length=TRADE_TYPE_LENGTH) 166 | update = models.ForeignKey( 167 | ListsFiatCryptoUpdates, 168 | related_name='datas', 169 | on_delete=models.CASCADE 170 | ) 171 | 172 | 173 | class InterExchangesUpdates(UpdatesModel): 174 | """ 175 | Model to store the last update date of inter-exchange rates. 176 | """ 177 | crypto_exchange = models.ForeignKey( 178 | CryptoExchanges, 179 | related_name='inter_exchanges_update', 180 | on_delete=models.CASCADE 181 | ) 182 | bank = models.ForeignKey( 183 | Banks, 184 | related_name='inter_exchanges_update', 185 | on_delete=models.CASCADE 186 | ) 187 | full_update = models.BooleanField(default=True) 188 | international = models.BooleanField(default=True) 189 | simpl = models.BooleanField(default=True) 190 | ended = models.BooleanField(default=False) 191 | 192 | 193 | class InterExchanges(models.Model): 194 | """ 195 | Model to store the inter-exchange rates for a crypto exchange. 196 | """ 197 | crypto_exchange = models.ForeignKey( 198 | CryptoExchanges, 199 | related_name='inter_exchanges', 200 | on_delete=models.CASCADE 201 | ) 202 | input_bank = models.ForeignKey( 203 | Banks, 204 | related_name='input_bank_inter_exchanges', 205 | on_delete=models.CASCADE 206 | ) 207 | output_bank = models.ForeignKey( 208 | Banks, 209 | related_name='output_bank_inter_exchanges', 210 | on_delete=models.CASCADE 211 | ) 212 | input_crypto_exchange = models.ForeignKey( 213 | CryptoExchangesRates, 214 | related_name='input_crypto_exchange_inter_exchanges', 215 | on_delete=models.CASCADE 216 | ) 217 | interim_crypto_exchange = models.ForeignKey( 218 | IntraCryptoExchangesRates, 219 | related_name='interim_exchange_inter_exchanges', 220 | blank=True, 221 | null=True, 222 | on_delete=models.CASCADE 223 | ) 224 | second_interim_crypto_exchange = models.ForeignKey( 225 | IntraCryptoExchangesRates, 226 | related_name='second_interim_exchange_inter_exchanges', 227 | blank=True, 228 | null=True, 229 | on_delete=models.CASCADE 230 | ) 231 | output_crypto_exchange = models.ForeignKey( 232 | CryptoExchangesRates, 233 | related_name='output_crypto_exchange_inter_exchanges', 234 | on_delete=models.CASCADE 235 | ) 236 | bank_exchange = models.ForeignKey( 237 | BanksExchangeRates, 238 | related_name='bank_rate_inter_exchanges', 239 | blank=True, 240 | null=True, 241 | on_delete=models.CASCADE 242 | ) 243 | marginality_percentage = models.FloatField( 244 | verbose_name='Marginality percentage' 245 | ) 246 | diagram = models.CharField( 247 | max_length=DIAGRAM_LENGTH, 248 | null=True, 249 | blank=True 250 | ) 251 | dynamics = models.CharField( 252 | max_length=TRADE_TYPE_LENGTH, 253 | null=True, 254 | default=None 255 | ) 256 | new = models.BooleanField( 257 | null=True, 258 | default=True 259 | ) 260 | update = models.ForeignKey( 261 | InterExchangesUpdates, 262 | related_name='datas', 263 | on_delete=models.CASCADE 264 | ) 265 | 266 | class Meta: 267 | ordering = ['-marginality_percentage'] 268 | constraints = [ 269 | models.UniqueConstraint( 270 | fields=( 271 | 'crypto_exchange', 'input_bank', 'output_bank', 272 | 'input_crypto_exchange', 'interim_crypto_exchange', 273 | 'second_interim_crypto_exchange', 'output_crypto_exchange', 274 | 'bank_exchange' 275 | ), name='unique_inter_exchanges' 276 | ) 277 | ] 278 | 279 | 280 | class RelatedMarginalityPercentages(models.Model): 281 | """ 282 | A model for storing the percentage of margin at the time of each update. 283 | """ 284 | updated = models.DateTimeField( 285 | verbose_name='Update date', 286 | auto_now_add=True, 287 | db_index=True 288 | ) 289 | marginality_percentage = models.FloatField( 290 | verbose_name='Marginality percentage' 291 | ) 292 | inter_exchange = models.ForeignKey( 293 | InterExchanges, 294 | related_name='marginality_percentages', 295 | on_delete=models.CASCADE 296 | ) 297 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/crypto_exchanges_registration/binance.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from abc import ABC 5 | from itertools import combinations 6 | from typing import List, Tuple 7 | 8 | from arbitration.settings import (API_BINANCE_CARD_2_CRYPTO_BUY, 9 | API_BINANCE_CARD_2_CRYPTO_SELL, 10 | API_BINANCE_CRYPTO, 11 | API_BINANCE_LIST_FIAT_BUY, 12 | API_BINANCE_LIST_FIAT_SELL, API_P2P_BINANCE, 13 | CONNECTION_TYPE_BINANCE_CARD_2_CRYPTO, 14 | CONNECTION_TYPE_BINANCE_CRYPTO, 15 | CONNECTION_TYPE_BINANCE_LIST_FIAT, 16 | CONNECTION_TYPE_P2P_BINANCE) 17 | from parsers.calculations import Card2Wallet2CryptoExchangesCalculating 18 | from parsers.parsers import (Card2CryptoExchangesParser, CryptoExchangesParser, 19 | ListsFiatCryptoParser, P2PParser) 20 | 21 | CRYPTO_EXCHANGES_NAME = os.path.basename(__file__).split('.')[0].capitalize() 22 | 23 | BINANCE_ASSETS = ('ADA', 'BNB', 'ETH', 'BTC', 'SHIB', 'BUSD', 'USDT') # 'DAI', 24 | BINANCE_ASSETS_FOR_FIAT = { 25 | 'all': ('USDT', 'BTC', 'BUSD', 'ETH'), 26 | 'RUB': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH', 'SHIB', 'RUB'), 27 | 'USD': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH',), # 'DAI'), 28 | 'EUR': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH',), # 'DAI', 'SHIB'), 29 | 'GBP': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH',), # 'DAI'), 30 | 'CHF': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH',), # 'DAI'), 31 | 'CAD': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH',), # 'DAI'), 32 | 'AUD': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH', 'SHIB', 'ADA'), 33 | 'GEL': ('USDT', 'BTC', 'BUSD', 'BNB', 'ETH') 34 | } 35 | BINANCE_INVALID_PARAMS_LIST = ( 36 | ('DAI', 'AUD'), ('DAI', 'BRL'), ('DAI', 'EUR'), ('DAI', 'GBP'), 37 | ('DAI', 'RUB'), ('DAI', 'TRY'), ('DAI', 'UAH'), ('BNB', 'SHIB'), 38 | ('ETH', 'SHIB'), ('BTC', 'SHIB'), ('DAI', 'SHIB'), ('ADA', 'DAI'), 39 | ('ADA', 'SHIB'), ('ADA', 'UAH') 40 | ) 41 | BINANCE_CRYPTO_FIATS = ('AUD', 'BRL', 'EUR', 'GBP', 'RUB', 'TRY', 'UAH') 42 | BINANCE_DEPOSIT_FIATS = { 43 | 'RUB': (('Bank Card (Visa/MS/МИР)', 1.2),), 44 | 'UAH': (('SettlePay (Visa/MC)', 1.5),), 45 | 'EUR': (('Bank Card (Visa/MC)', 1.8),), 46 | 'GBP': (('Bank Card (Visa/MC)', 1.8),), 47 | 'TRY': (('Turkish Bank Transfer', 0),), 48 | } 49 | BINANCE_WITHDRAW_FIATS = { 50 | 'RUB': (('Bank Card (Visa/MS/МИР)', 0),), 51 | 'UAH': (('SettlePay (Visa/MC)', 1),), 52 | 'EUR': (('Bank Card (Visa)', 1.8),), 53 | 'GBP': (('Bank Card (Visa)', 1.8),), 54 | 'TRY': (('Turkish Bank Transfer', 0),), 55 | } 56 | BINANCE_SPOT_ZERO_FEES = { 57 | 'BTC': [ 58 | 'AUD', 'BIDR', 'BRL', 'BUSD', 'EUR', 'GBP', 'RUB', 'TRY', 'TUSD', 59 | 'UAH', 'USDC', 'USDP', 'USDT' 60 | ] 61 | } 62 | 63 | 64 | class BinanceP2PParser(P2PParser, ABC): 65 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 66 | endpoint: str = API_P2P_BINANCE 67 | connection_type: str = CONNECTION_TYPE_P2P_BINANCE 68 | need_cookies: bool = False 69 | page: int = 1 70 | rows: int = 1 71 | # custom_settings 72 | exception_fiats: Tuple[str] = ('USD', 'EUR') 73 | 74 | def _check_supports_fiat(self, fiat: str) -> bool: 75 | from banks.banks_config import RUS_BANKS 76 | if self.bank_name in RUS_BANKS and fiat in self.exception_fiats: 77 | return False 78 | return True 79 | 80 | def _create_body(self, asset: str, fiat: str, trade_type: str) -> dict: 81 | return { 82 | "page": self.page, 83 | "rows": self.rows, 84 | "publisherType": "merchant", 85 | "asset": asset, 86 | "tradeType": trade_type, 87 | "fiat": fiat, 88 | "payTypes": [self.bank.binance_name] 89 | } 90 | 91 | @staticmethod 92 | def _extract_price_from_json(json_data: dict) -> float | None: 93 | data = json_data.get('data') 94 | if len(data) == 0: 95 | return None 96 | internal_data = data[0] 97 | adv = internal_data.get('adv') 98 | return float(adv.get('price')) 99 | 100 | 101 | class BinanceCryptoParser(CryptoExchangesParser): 102 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 103 | endpoint: str = API_BINANCE_CRYPTO 104 | connection_type: str = CONNECTION_TYPE_BINANCE_CRYPTO 105 | need_cookies: bool = False 106 | fake_useragent: bool = False 107 | exceptions: tuple = ('SHIBRUB', 'RUBSHIB', 'SHIBGBP', 'GBPSHIB') 108 | name_from: str = 'symbol' 109 | base_spot_fee: float = 0.1 110 | zero_fees: dict = BINANCE_SPOT_ZERO_FEES 111 | # custom_settings 112 | stablecoins: Tuple[str] = ('USDT', 'BUSD') 113 | 114 | @staticmethod 115 | def _extract_price_from_json(json_data: dict) -> float: 116 | return float(json_data['price']) 117 | 118 | def _generate_unique_params(self) -> List[dict[str, str]]: 119 | """ 120 | Method that generates unique parameters for the cryptocurrency exchange 121 | API endpoint. 122 | """ 123 | currencies_combinations = list(combinations(self.assets, 124 | self.CURRENCY_PAIR)) 125 | invalid_params_list = self.crypto_exchanges_configs.get( 126 | 'invalid_params_list') 127 | for crypto_fiat in self.crypto_fiats: 128 | for asset in self.assets: 129 | if asset in self.stablecoins and crypto_fiat in ( 130 | 'EUR', 'GBP', 'AUD'): 131 | currencies_combinations.append((crypto_fiat, asset)) 132 | else: 133 | currencies_combinations.append((asset, crypto_fiat)) 134 | currencies_combinations = tuple( 135 | currencies_combination for currencies_combination 136 | in currencies_combinations 137 | if currencies_combination not in invalid_params_list 138 | ) 139 | return self._create_params(currencies_combinations) 140 | 141 | 142 | class BinanceCard2CryptoExchangesParser(Card2CryptoExchangesParser): 143 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 144 | endpoint_sell: str = API_BINANCE_CARD_2_CRYPTO_SELL 145 | endpoint_buy: str = API_BINANCE_CARD_2_CRYPTO_BUY 146 | connection_type: str = CONNECTION_TYPE_BINANCE_CARD_2_CRYPTO 147 | need_cookies: bool = False 148 | 149 | @staticmethod 150 | def _create_body_sell(fiat: str, asset: str, amount: int) -> dict: 151 | return { 152 | "baseCurrency": fiat, 153 | "cryptoCurrency": asset, 154 | "payType": "Ask", 155 | "paymentChannel": "card", 156 | "rail": "card", 157 | "requestedAmount": amount, 158 | "requestedCurrency": fiat 159 | } 160 | 161 | @staticmethod 162 | def _create_body_buy(fiat: str, asset: str, amount: int) -> dict: 163 | return { 164 | "baseCurrency": fiat, 165 | "cryptoCurrency": asset, 166 | "payType": "Ask", 167 | "paymentChannel": "card", 168 | "rail": "card", 169 | "requestedAmount": amount, 170 | "requestedCurrency": fiat 171 | } 172 | 173 | @staticmethod 174 | def _create_params_buy(fiat: str, asset: str) -> dict: 175 | return { 176 | 'channelCode': 'card', 177 | 'fiatCode': fiat, 178 | 'cryptoAsset': asset 179 | } 180 | 181 | def _extract_values_from_json(self, json_data: dict, amount: int 182 | ) -> tuple | None: 183 | if self.trade_type == 'SELL': 184 | data = json_data['data'].get('rows') 185 | pre_price = data.get('quotePrice') 186 | if not pre_price: 187 | return None 188 | commission = data.get('totalFee') / amount 189 | price = pre_price / (1 + commission) 190 | commission *= 100 191 | else: # BUY 192 | data = json_data['data'] 193 | pre_price = data['price'] 194 | if not pre_price: 195 | return None 196 | pre_price = float(pre_price) 197 | commission = 0.02 198 | price = 1 / (pre_price * (1 + commission)) 199 | commission *= 100 200 | return price, pre_price, commission 201 | 202 | 203 | class BinanceListsFiatCryptoParser(ListsFiatCryptoParser): 204 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 205 | endpoint_sell: str = API_BINANCE_LIST_FIAT_SELL 206 | endpoint_buy: str = API_BINANCE_LIST_FIAT_BUY 207 | connection_type: str = CONNECTION_TYPE_BINANCE_LIST_FIAT 208 | need_cookies: bool = False 209 | 210 | @staticmethod 211 | def _create_body_sell(asset: str) -> dict: 212 | return { 213 | "channels": ["card"], 214 | "crypto": asset, 215 | "transactionType": "SELL" 216 | } 217 | 218 | @staticmethod 219 | def _create_body_buy(fiat: str) -> dict: 220 | return { 221 | "channels": ["card"], 222 | "fiat": fiat, 223 | "transactionType": "BUY" 224 | } 225 | 226 | def _extract_buy_or_sell_list_from_json(self, json_data: dict, 227 | trade_type: str) -> List[list]: 228 | general_list = json_data.get('data') 229 | valid_asset = self.assets if trade_type == 'BUY' else self.all_fiats 230 | if general_list == '': 231 | return [] 232 | valid_list = [] 233 | for asset_data in general_list: 234 | asset = asset_data.get('assetCode') 235 | if asset_data.get('quotation') != '' and asset in valid_asset: 236 | max_limit = int(float(asset_data['maxLimit']) * 0.9) 237 | valid_list.append([asset, max_limit]) 238 | return valid_list 239 | 240 | 241 | class BinanceCard2Wallet2CryptoExchangesCalculating( 242 | Card2Wallet2CryptoExchangesCalculating 243 | ): 244 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 245 | -------------------------------------------------------------------------------- /arbitration/banks/tasks.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | from arbitration.celery import app 4 | from arbitration.settings import (INTERNAL_BANKS_UPDATE_FREQUENCY, 5 | P2P_BINANCE_UPDATE_FREQUENCY, 6 | P2P_BYBIT_UPDATE_FREQUENCY, UPDATE_RATE) 7 | from banks.banks_registration.bank_of_georgia import (BOGBinanceP2PParser, 8 | BOGBybitP2PParser) 9 | from banks.banks_registration.credo import (CredoBinanceP2PParser, 10 | CredoBybitP2PParser) 11 | from banks.banks_registration.qiwi import (QIWIBinanceP2PParser, 12 | QIWIBybitP2PParser) 13 | from banks.banks_registration.raiffeisen import (RaiffeisenBinanceP2PParser, 14 | RaiffeisenBybitP2PParser, 15 | RaiffeisenParser) 16 | from banks.banks_registration.sberbank import (SberbankBinanceP2PParser, 17 | SberbankBybitP2PParser) 18 | from banks.banks_registration.tbc import TBCBinanceP2PParser, TBCBybitP2PParser 19 | from banks.banks_registration.tinkoff import (TinkoffBinanceP2PParser, 20 | TinkoffBybitP2PParser, 21 | TinkoffParser) 22 | from banks.banks_registration.wise import (WiseBinanceP2PParser, 23 | WiseBybitP2PParser, WiseParser) 24 | from banks.banks_registration.yoomoney import (YoomoneyBinanceP2PParser, 25 | YoomoneyBybitP2PParser) 26 | from banks.currency_markets_registration.tinkoff_invest import ( 27 | TinkoffCurrencyMarketParser) 28 | 29 | 30 | # Banks internal rates 31 | @app.task( 32 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 33 | retry_backoff=True 34 | ) 35 | def parse_internal_tinkoff_rates(self): 36 | TinkoffParser().main() 37 | self.retry( 38 | countdown=INTERNAL_BANKS_UPDATE_FREQUENCY * UPDATE_RATE[ 39 | datetime.now(timezone.utc).hour 40 | ] 41 | ) 42 | 43 | 44 | @app.task( 45 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 46 | retry_backoff=True 47 | ) 48 | def parse_internal_raiffeisen_rates(self): 49 | RaiffeisenParser().main() 50 | self.retry( 51 | countdown=INTERNAL_BANKS_UPDATE_FREQUENCY * UPDATE_RATE[ 52 | datetime.now(timezone.utc).hour 53 | ] 54 | ) 55 | 56 | 57 | @app.task( 58 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 59 | retry_backoff=True 60 | ) 61 | def parse_internal_wise_rates(self): 62 | WiseParser().main() 63 | self.retry( 64 | countdown=INTERNAL_BANKS_UPDATE_FREQUENCY * UPDATE_RATE[ 65 | datetime.now(timezone.utc).hour 66 | ] 67 | ) 68 | 69 | 70 | # Banks P2P rates 71 | # Binance 72 | @app.task( 73 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 74 | retry_backoff=True 75 | ) 76 | def get_tinkoff_p2p_binance_exchanges(self): 77 | TinkoffBinanceP2PParser().main() 78 | self.retry( 79 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 80 | datetime.now(timezone.utc).hour 81 | ] 82 | ) 83 | 84 | 85 | @app.task( 86 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 87 | retry_backoff=True 88 | ) 89 | def get_sberbank_p2p_binance_exchanges(self): 90 | SberbankBinanceP2PParser().main() 91 | self.retry( 92 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 93 | datetime.now(timezone.utc).hour 94 | ] 95 | ) 96 | 97 | 98 | @app.task( 99 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 100 | retry_backoff=True 101 | ) 102 | def get_raiffeisen_p2p_binance_exchanges(self): 103 | RaiffeisenBinanceP2PParser().main() 104 | self.retry( 105 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 106 | datetime.now(timezone.utc).hour 107 | ] 108 | ) 109 | 110 | 111 | @app.task( 112 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 113 | retry_backoff=True 114 | ) 115 | def get_qiwi_p2p_binance_exchanges(self): 116 | QIWIBinanceP2PParser().main() 117 | self.retry( 118 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 119 | datetime.now(timezone.utc).hour 120 | ] 121 | ) 122 | 123 | 124 | @app.task( 125 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 126 | retry_backoff=True 127 | ) 128 | def get_yoomoney_p2p_binance_exchanges(self): 129 | YoomoneyBinanceP2PParser().main() 130 | self.retry( 131 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 132 | datetime.now(timezone.utc).hour 133 | ] 134 | ) 135 | 136 | 137 | @app.task( 138 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 139 | retry_backoff=True 140 | ) 141 | def get_bog_p2p_binance_exchanges(self): 142 | BOGBinanceP2PParser().main() 143 | self.retry( 144 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 145 | datetime.now(timezone.utc).hour 146 | ] 147 | ) 148 | 149 | 150 | @app.task( 151 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 152 | retry_backoff=True 153 | ) 154 | def get_tbc_p2p_binance_exchanges(self): 155 | TBCBinanceP2PParser().main() 156 | self.retry( 157 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 158 | datetime.now(timezone.utc).hour 159 | ] 160 | ) 161 | 162 | 163 | @app.task( 164 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 165 | retry_backoff=True 166 | ) 167 | def get_credo_p2p_binance_exchanges(self): 168 | CredoBinanceP2PParser().main() 169 | self.retry( 170 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 171 | datetime.now(timezone.utc).hour 172 | ] 173 | ) 174 | 175 | 176 | @app.task( 177 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 178 | retry_backoff=True 179 | ) 180 | def get_wise_p2p_binance_exchanges(self): 181 | WiseBinanceP2PParser().main() 182 | self.retry( 183 | countdown=P2P_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 184 | datetime.now(timezone.utc).hour 185 | ] 186 | ) 187 | 188 | 189 | # Bybit 190 | @app.task( 191 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 192 | retry_backoff=True 193 | ) 194 | def get_tinkoff_p2p_bybit_exchanges(self): 195 | TinkoffBybitP2PParser().main() 196 | self.retry( 197 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 198 | datetime.now(timezone.utc).hour 199 | ] 200 | ) 201 | 202 | 203 | @app.task( 204 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 205 | retry_backoff=True 206 | ) 207 | def get_sberbank_p2p_bybit_exchanges(self): 208 | SberbankBybitP2PParser().main() 209 | self.retry( 210 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 211 | datetime.now(timezone.utc).hour 212 | ] 213 | ) 214 | 215 | 216 | @app.task( 217 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 218 | retry_backoff=True 219 | ) 220 | def get_raiffeisen_p2p_bybit_exchanges(self): 221 | RaiffeisenBybitP2PParser().main() 222 | self.retry( 223 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 224 | datetime.now(timezone.utc).hour 225 | ] 226 | ) 227 | 228 | 229 | @app.task( 230 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 231 | retry_backoff=True 232 | ) 233 | def get_qiwi_p2p_bybit_exchanges(self): 234 | QIWIBybitP2PParser().main() 235 | self.retry( 236 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 237 | datetime.now(timezone.utc).hour 238 | ] 239 | ) 240 | 241 | 242 | @app.task( 243 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 244 | retry_backoff=True 245 | ) 246 | def get_yoomoney_p2p_bybit_exchanges(self): 247 | YoomoneyBybitP2PParser().main() 248 | self.retry( 249 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 250 | datetime.now(timezone.utc).hour 251 | ] 252 | ) 253 | 254 | 255 | @app.task( 256 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 257 | retry_backoff=True 258 | ) 259 | def get_bog_p2p_bybit_exchanges(self): 260 | BOGBybitP2PParser().main() 261 | self.retry( 262 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 263 | datetime.now(timezone.utc).hour 264 | ] 265 | ) 266 | 267 | 268 | @app.task( 269 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 270 | retry_backoff=True 271 | ) 272 | def get_tbc_p2p_bybit_exchanges(self): 273 | TBCBybitP2PParser().main() 274 | self.retry( 275 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 276 | datetime.now(timezone.utc).hour 277 | ] 278 | ) 279 | 280 | 281 | @app.task( 282 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 283 | retry_backoff=True 284 | ) 285 | def get_credo_p2p_bybit_exchanges(self): 286 | CredoBybitP2PParser().main() 287 | self.retry( 288 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 289 | datetime.now(timezone.utc).hour 290 | ] 291 | ) 292 | 293 | 294 | @app.task( 295 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 296 | retry_backoff=True 297 | ) 298 | def get_wise_p2p_bybit_exchanges(self): 299 | WiseBybitP2PParser().main() 300 | self.retry( 301 | countdown=P2P_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 302 | datetime.now(timezone.utc).hour 303 | ] 304 | ) 305 | 306 | 307 | # Currency markets 308 | @app.task 309 | def parse_currency_market_tinkoff_rates(): 310 | TinkoffCurrencyMarketParser().main() 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Assets-Loop - https://assetsloop.com 2 | 3 |  4 | 5 | Assets Loop is a free Open Source web application (built with Python + JavaScript) designed to assist in trading across cryptocurrency exchanges, currency markets, and banks using an arbitrage strategy. It is intended to search for all possible transaction chains and display them on the website's homepage. Currency exchange rates are continuously and concurrently parsed through a network of open and closed APIs using Tor network, proxys and direct, ensuring the rates are always up-to-date. 6 | 7 | --- 8 | 9 | [Sponsorship](#sponsorship) | [Structure](#structure) | [Supported marketplaces](#supported-exchange-marketplaces) | [Technologies](#technologies) | [Requirements](#requirements) | [Quick start](#quick-start) | [Support](#support) | [Disclaimer](#disclaimer) | [License](#license) | [Author](#author) | [Copyright](#copyright) 10 | 11 | ## Sponsorship 12 | 13 | This project requires financial support for its development and maintenance, primarily to cover server costs. If I could afford to pay for the server from donations, I could add many more banks, currency markets, and crypto exchanges. I'm aware that many people are parsing data from my resource, and I'm pleased that the project is helpful. However, if you could consider donating 5-10$ monthly, it would go a long way in supporting the project. 14 | 15 | Assets Loop is not supported by any company and is developed in my spare time, and the server is paid from my personal funds. 16 | 17 | - Wise balance by my mail M.Nezhinsy@yandex.ru 18 | - Binance balance Pay ID: 366620204 19 | - Tinkoff balance 20 | - Patreon 21 | - Boosty 22 | 23 | ## Structure 24 | 25 |26 | . 27 | ├── .github/workflows ─ Workflow for Git Actions CI 28 | ├── arbitration ─ Django project 29 | │ ├── arbitration ─ Django settings module 30 | │ ├── banks ─ Banks & Currency Markets creation module 31 | │ ├── core ─ View module 32 | │ ├── crypto_exchanges ─ Crypto Exchanges creation module 33 | │ ├── parsers ─ Business logic module 34 | │ ├── static ─ css, javascript, favicon 35 | │ └── templates ─ HTML pages 36 | └── infra ─ Project infrastructure setup 37 |38 | 39 | ## Supported Exchange marketplaces 40 | 41 | ### Supported crypto exchanges and supported deposit/withdrawal methods between crypto assets and fiat currencies: 42 | 43 | - [x] [Binance](https://www.binance.com/ru/activity/referral-entry/CPA?fromActivityPage=true&ref=CPA_00ALEQ9QW0): 44 | - [x] [P2P](https://p2p.binance.com/) 45 | - [x] [Card2CryptoExchange](https://www.binance.com/ru/buy-sell-crypto/) 46 | - [x] [Card2Wallet2CryptoExchange](https://www.binance.com/ru/fiat/deposit/) 47 | 48 | - [x] [Bybit](https://www.bybit.com/): 49 | - [x] [P2P](https://www.bybit.com/fiat/trade/otc/) 50 | - [ ] [Card2CryptoExchange](https://www.bybit.com/fiat/trade/express/home/) 51 | - [ ] [Card2Wallet2CryptoExchange](https://www.bybit.com/fiat/trade/deposit/home/) 52 | 53 | ### Supported Banks: 54 | 55 | #### Banks supporting currency conversion within the bank: 56 | 57 | - [x] [Wise](https://wise.com/invite/ih/mikhailn114/) 58 | - [x] [Tinkoff](https://www.tinkoff.ru/) 59 | - [x] [Raiffeisen Bank](https://www.raiffeisen.ru/) 60 | 61 | #### Other supported banks: 62 | 63 | - [x] [Bank of Georgia](https://bankofgeorgia.ge/) 64 | - [x] [TBC Bank](https://www.tbcbank.ge/) 65 | - [x] [Credo Bank](https://credobank.ge/) 66 | - [x] [Sberbank](http://www.sberbank.ru/) 67 | - [x] [Yoomoney](https://yoomoney.ru/) 68 | - [x] [QIWI](https://qiwi.com/) 69 | 70 | ### Supported currency markets: 71 | 72 | - [x] [Tinkoff Invest](https://www.tinkoff.ru/invest/) 73 | 74 | ## Technologies 75 | 76 | This is a web application project using Docker Compose for containerization. The project includes several services such as a Tor proxy to bypass parsing locks, Redis, PostgreSQL database, Nginx web server, Certbot for SSL certification, and Celery workflows for parsing, parsing and computing data. These services are connected to the user's network. 77 | 78 | - Python 3 79 | - Django, Django REST framework 80 | - Celery, Celery Beat, 81 | - Redis 82 | - Docker, Docker-Compose 83 | - PostgreSQL 84 | - NGINX, Certbot, Gunicorn 85 | - JavaScript, jQuery 86 | - DataTables, Ajax 87 | - Bootstrap, Select2, Twix 88 | - CI/CD, Git Actions 89 | - Digital Ocean 90 | 91 | ## Requirements 92 | 93 | ### Up-to-date clock 94 | 95 | The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. 96 | 97 | ### Minimum hardware required 98 | 99 | To run this service I recommend you a cloud instance with a minimum of: 100 | 101 | - Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU 102 | 103 | ### Software requirements 104 | 105 | - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 106 | - [Docker & Docker Compose](https://www.docker.com/products/docker) 107 | - [Python](http://docs.python-guide.org/en/latest/starting/installation/) >= 3.9 108 | - [pip](https://pip.pypa.io/en/stable/installing/) 109 | - [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) 110 | 111 | ## Quick start 112 | 113 | ### Install Docker & Docker Compose 114 | 115 | - Install Docker and Docker Compose on the server or local (for ubuntu): 116 | 117 | ``` 118 | sudo apt install curl # installing a file download utility 119 | curl -fsSL https://get.docker.com -o get-docker.sh # download script for installation 120 | sh get-docker.sh # running the script 121 | sudo apt-get install docker-compose-plugin # install docker compose 122 | ``` 123 | 124 | ### Local development 125 | 126 | - Clone repository: 127 | ``` 128 | git clone https://github.com/Nezhinskiy/Assets-Loop.git 129 | ``` 130 | 131 | - Go to the [infra](infra) directory: 132 | ``` 133 | cd Assets-Loop/infra/ 134 | ``` 135 | 136 | - Create an .env file in the [infra](infra) directory and fill it with your data as in the 137 | [example.env](infra/example.env) file. 138 | 139 | - Create and run Docker containers, run command: 140 | ``` 141 | docker-compose -f local-docker-compose.yml up 142 | ``` 143 | 144 | - After a successful build, run the migrations: 145 | ``` 146 | docker compose exec arbitration python manage.py migrate 147 | ``` 148 | 149 | - Collect static: 150 | ``` 151 | docker compose exec arbitration python manage.py collectstatic --noinput 152 | ``` 153 | 154 | ### Deploy to server 155 | 156 | - Clone repository: 157 | ``` 158 | git clone https://github.com/Nezhinskiy/Assets-Loop.git 159 | ``` 160 | 161 | - Go to the [infra](infra) directory: 162 | ``` 163 | cd Assets-Loop/infra/ 164 | ``` 165 | 166 | - Copy the [prod-docker-compose.yml](infra/prod-docker-compose.yml) file and the 167 | [nginx](infra/nginx) directory and the [certbot](infra/certbot) directory from the 168 | [infra](infra) directory to the server (execute commands while in the 169 | [infra](infra) directory): 170 | ``` 171 | scp prod-docker-compose.yml username@IP:/home/username/Assets-Loop/ # username - server username 172 | scp -r nginx username@IP:/home/username/Assets-Loop/ # IP - server public IP 173 | scp -r certbot username@IP:/home/username/Assets-Loop/ 174 | ``` 175 | 176 | - Go to your server, to the Assets-Loop directory: 177 | ``` 178 | ssh username@IP # username - server username 179 | cd Assets-Loop/ # IP - server public IP 180 | ``` 181 | 182 | - Create an .env file in the Assets-Loop directory and fill it with your data as in the 183 | [example.env](infra/example.env) file. 184 | 185 | - Create and run Docker containers, run command on server: 186 | ``` 187 | docker-compose -f prod-docker-compose.yml up 188 | ``` 189 | 190 | - After a successful build, run the migrations: 191 | ``` 192 | docker compose exec arbitration python manage.py migrate 193 | ``` 194 | 195 | - Collect static: 196 | ``` 197 | docker compose exec arbitration python manage.py collectstatic --noinput 198 | ``` 199 | 200 | ### Populating the database and starting the service 201 | 202 | - In the .env file, you specified the path in the REGISTRATION_URL variable. Follow it to populate the database with the necessary data. 203 | 204 | - In the .env file, you specified the path in the START_URL variable. Follow it to start the service. 205 | 206 | ### GitHub Actions CI 207 | 208 | - To work with GitHub Actions, you need to create environment variables in the Secrets > Actions section of the repository: 209 | ``` 210 | DOCKER_PASSWORD # Docker Hub password 211 | DOCKER_USERNAME # Docker Hub login 212 | DOCKER_PROJECT # Project name on Docker Hub 213 | HOST # server public IP 214 | USER # server username 215 | PASSPHRASE # *if ssh key is password protected 216 | SSH_KEY # private ssh key 217 | TELEGRAM_TO # Telegram account ID to send a message 218 | TELEGRAM_TOKEN # token of the bot sending the message 219 | ``` 220 | 221 | ## Support 222 | 223 | ### Help 224 | 225 | For any questions not covered by the documentation, or for more information about the service, write to me in 226 | Telegram. 227 | 228 | ### [Pull Requests](https://github.com/Nezhinskiy/Assets-Loop/pulls) 229 | 230 | Feel like the service is missing a feature? I welcome your pull requests! 231 | 232 | Please, message me on Telegram, before you start working on any new feature. 233 | 234 | ## Disclaimer 235 | 236 | This software is for educational purposes only. Do not risk money which 237 | you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS 238 | AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. 239 | 240 | ## License 241 | 242 | MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 243 | 244 | ## Author 245 | 246 | Assets Loop is owned and maintained by Mikhail Nezhinsky. 247 | 248 | You can follow me on 249 | Linkedin to keep up to date with project updates and releases. Or you can write to me on 250 | Telegram. 251 | 252 | ## Copyright 253 | 254 | Copyright (c) 2022-2023 Mikhail Nezhinsky 255 | -------------------------------------------------------------------------------- /arbitration/static/js/main_datatable.js: -------------------------------------------------------------------------------- 1 | function roundToTwo(num) { 2 | if (Number.isInteger(num) || num.toFixed(1) === num) { 3 | return num; 4 | } 5 | if (num.toFixed(2) !== num) { 6 | return +num.toFixed(2); 7 | } 8 | return num; 9 | } 10 | 11 | function updateTime(updated) { 12 | let seconds = Math.ceil((new Date() - new Date(updated)) / 1000); 13 | return seconds > 999 ? 999 : seconds; 14 | } 15 | 16 | function colorSelector(marginality_percentage) { 17 | return marginality_percentage > 0 ? 'green' : 'red'; 18 | } 19 | 20 | function bankExchange(row, bank) { 21 | if (!row.bank_exchange.currency_market) { 22 | return `внутри банка ${bank} нужно поменять 23 | ${row.bank_exchange.from_fiat} на ${row.bank_exchange.to_fiat} 24 | по курсу ${row.bank_exchange.price}, с учётом комиссии.`; 25 | } else { 26 | return `внутри банка ${bank} нужно поменять 27 | ${row.bank_exchange.from_fiat} на ${row.bank_exchange.to_fiat} 28 | через биржу ${row.bank_exchange.currency_market.name}, по курсу 29 | ${row.bank_exchange.price}, с учётом комиссии.`; 30 | } 31 | } 32 | 33 | function inputP2P(row) { 34 | return `со счёта ${row.input_bank.name} нужно купить активы на криптобирже 35 | ${row.crypto_exchange.name}. За 36 | ${row.input_crypto_exchange.fiat} следует купить 37 | ${row.input_crypto_exchange.asset} методом P2P по курсу 38 | ${row.input_crypto_exchange.price}. P2P не облагается комиссией.`; 39 | } 40 | 41 | function inputCard2CryptoExchange(row) { 42 | return `со счёта ${row.input_bank.name} нужно купить активы на криптобирже 43 | ${row.crypto_exchange.name}. За 44 | ${row.input_crypto_exchange.fiat} следует купить 45 | ${row.input_crypto_exchange.asset} методом 46 | Card2CryptoExchange через ${row.input_crypto_exchange.transaction_method}, 47 | по курсу ${row.input_crypto_exchange.price}. В стоимость включена комиссия 48 | биржи ${roundToTwo(row.input_crypto_exchange.transaction_fee)}%.`; 49 | } 50 | 51 | function inputCard2Wallet2CryptoExchange(row) { 52 | return `со счёта ${row.input_bank.name} нужно купить активы на криптобирже 53 | ${row.crypto_exchange.name}. За 54 | ${row.input_crypto_exchange.fiat} следует купить 55 | ${row.input_crypto_exchange.asset} методом 56 | Card2Wallet2CryptoExchange по итоговому курсу с учётом всех комиссий - 57 | ${row.input_crypto_exchange.price}. Этот метод состоит из двух транзакций: 58 | 1. Нужно перевести ${row.input_crypto_exchange.fiat} на свой 59 | ${row.crypto_exchange.name} кошелёк через 60 | ${row.input_crypto_exchange.transaction_method}. Комиссия составит 61 | ${roundToTwo(row.input_crypto_exchange.transaction_fee)}%. 62 | 2. Далее, уже внутри биржи надо конвертировать через Спот 63 | ${row.input_crypto_exchange.fiat} в ${row.input_crypto_exchange.asset} 64 | по курсу ${row.input_crypto_exchange.intra_crypto_exchange.price} c учётом 65 | комиссии ${row.input_crypto_exchange.intra_crypto_exchange.spot_fee}%.`; 66 | } 67 | 68 | function inputCryptoExchange(row) { 69 | if (row.input_crypto_exchange.payment_channel === 'P2P') { 70 | return inputP2P(row); 71 | } 72 | if (row.input_crypto_exchange.payment_channel === 'Card2CryptoExchange') { 73 | return inputCard2CryptoExchange(row); 74 | } 75 | if (row.input_crypto_exchange.payment_channel === 'Card2Wallet2CryptoExchange') { 76 | return inputCard2Wallet2CryptoExchange(row); 77 | } 78 | return row.input_crypto_exchange.payment_channel 79 | } 80 | 81 | function interimCryptoExchange(row) { 82 | if (row.second_interim_crypto_exchange) { 83 | return `Теперь внутри ${row.crypto_exchange.name} через Спот надо 84 | сначала конвертировать ${row.interim_crypto_exchange.from_asset} в 85 | ${row.interim_crypto_exchange.to_asset} по курсу 86 | ${row.interim_crypto_exchange.price} (комиссия биржи 87 | ${row.interim_crypto_exchange.spot_fee}%), а потом 88 | ${row.interim_crypto_exchange.to_asset} в 89 | ${row.second_interim_crypto_exchange.to_asset} по курсу 90 | ${row.second_interim_crypto_exchange.price} (комиссия биржи 91 | ${row.second_interim_crypto_exchange.spot_fee}%). Все комиссии 92 | включены в стоимость.`; 93 | } else { 94 | return `Теперь внутри ${row.crypto_exchange.name} через Спот надо 95 | конвертировать ${row.interim_crypto_exchange.from_asset} в 96 | ${row.interim_crypto_exchange.to_asset} по курсу 97 | ${row.interim_crypto_exchange.price}. Комиссия биржи 98 | ${row.interim_crypto_exchange.spot_fee}% включена в стоимость.`; 99 | } 100 | } 101 | 102 | function outputP2P(row) { 103 | return `нужно перевести активы с ${row.crypto_exchange.name} на счёт 104 | ${row.output_bank.name} по методу 105 | ${row.output_crypto_exchange.payment_channel}. Перевести 106 | ${row.output_crypto_exchange.asset} в ${row.output_crypto_exchange.fiat} 107 | по курсу ${row.output_crypto_exchange.price}. P2P не облагается 108 | комиссией.`; 109 | } 110 | 111 | function outputCard2CryptoExchange(row) { 112 | return `нужно перевести активы с ${row.crypto_exchange.name} на счёт 113 | ${row.output_bank.name} по методу CryptoExchange2Card через 114 | ${row.output_crypto_exchange.transaction_method}. Перевести 115 | ${row.output_crypto_exchange.asset} в ${row.output_crypto_exchange.fiat} 116 | по курсу ${row.output_crypto_exchange.price}. В стоимость включена 117 | комиссия биржи 118 | ${roundToTwo(row.output_crypto_exchange.transaction_fee)}%.`; 119 | } 120 | 121 | function outputCard2Wallet2CryptoExchange(row) { 122 | return `нужно перевести активы с ${row.crypto_exchange.name} на счёт 123 | ${row.output_bank.name} по методу CryptoExchange2Wallet2Card. Перевести 124 | ${row.output_crypto_exchange.asset} в ${row.output_crypto_exchange.fiat} 125 | по итоговому курсу ${row.output_crypto_exchange.price} с учётом всех 126 | комиссий. Этот метод состоит из двух транзакций: 1. Нужно внутри биржи 127 | через Спот конвертировать ${row.output_crypto_exchange.asset} в 128 | ${row.output_crypto_exchange.fiat} 129 | на свой ${row.crypto_exchange.name} кошелёк, по курсу 130 | ${row.output_crypto_exchange.intra_crypto_exchange.price} c учётом 131 | комиссии ${row.output_crypto_exchange.intra_crypto_exchange.spot_fee}%. 132 | 2. Далее, нужно вывести с ${row.crypto_exchange.name} кошелька 133 | ${row.output_crypto_exchange.fiat} на свой ${row.output_bank.name} счёт 134 | через ${row.output_crypto_exchange.transaction_method}. Комиссия составит 135 | ${roundToTwo(row.output_crypto_exchange.transaction_fee)}%.`; 136 | } 137 | 138 | function outputCryptoExchange(row) { 139 | if (row.output_crypto_exchange.payment_channel === 'P2P') { 140 | return outputP2P(row); 141 | } 142 | if (row.output_crypto_exchange.payment_channel === 'Card2CryptoExchange') { 143 | return outputCard2CryptoExchange(row); 144 | } 145 | if (row.output_crypto_exchange.payment_channel === 'Card2Wallet2CryptoExchange') { 146 | return outputCard2Wallet2CryptoExchange(row); 147 | } 148 | return row.output_crypto_exchange.payment_channel 149 | } 150 | 151 | function modalWrite(row) { 152 | let modal = '' 153 | if (row.bank_exchange && row.bank_exchange.bank.name === row.input_bank.name) { 154 | modal += `
1. Сначала ${bankExchange(row, row.input_bank.name)}
2. Далее, ${inputCryptoExchange(row)}
` 155 | if (row.interim_crypto_exchange) { 156 | modal += `3. ${interimCryptoExchange(row)}
4. Последнее, ${outputCryptoExchange(row)}
` 157 | } else { 158 | modal += `3. Последнее, ${outputCryptoExchange(row)}
` 159 | } 160 | } else { 161 | modal += `1. Сначала ${inputCryptoExchange(row)}
` 162 | if (row.interim_crypto_exchange) { 163 | modal += `2. ${interimCryptoExchange(row)}
` 164 | if (!row.bank_exchange) { 165 | modal += `3. Последнее, ${outputCryptoExchange(row)}
` 166 | } else { 167 | modal += `3. Далее, ${outputCryptoExchange(row)}
4. Последнее, ${bankExchange(row, row.output_bank.name)}
` 168 | } 169 | } else { 170 | if (!row.bank_exchange) { 171 | modal += `2. Последнее, ${outputCryptoExchange(row)}
` 172 | } else { 173 | modal += `2. Далее, ${outputCryptoExchange(row)}
3. Последнее, ${bankExchange(row, row.output_bank.name)}
` 174 | } 175 | } 176 | } 177 | return modal; 178 | } 179 | 180 | $(document).ready(function () { 181 | var refreshTable = $('#dynamicDatatable').DataTable({ 182 | "language": dt_language, 183 | "searching": false, 184 | "pageLength": 10, 185 | "lengthMenu": [[10, 25, 50, 100], [10, 25, 50, 100]], 186 | "info": true, 187 | // "sDom": '<"top"<"actions">fpi<"clear">><"clear">rt<"bottom">', 188 | // "aaSorting": [], 189 | 'order': [], //[[1, 'desc']] 190 | 'processing': false, 191 | 'serverSide': true, 192 | 'ajax': { 193 | url: $('#dynamicDatatable').data('url') + $('#dynamicDatatable').data('filter'), 194 | dataSrc: 'data' 195 | }, 196 | "rowCallback": function( row, data, index ) { 197 | $('.tooltip').remove(); 198 | var updatedTime = updateTime(data.update.updated); 199 | if (updatedTime > 120) { 200 | $('td', row).addClass('obsolete'); 201 | } else if (data.new === true && updatedTime < 4) { 202 | $('td', row).addClass('stylish'); 203 | setTimeout(function() { 204 | $('td', row).removeClass('stylish'); 205 | }, 500); 206 | } else if (data.dynamics === 'fall' && updatedTime < 4) { 207 | $('td:eq(1)', row).addClass('fall'); 208 | setTimeout(function() { 209 | $('td:eq(1)', row).removeClass('fall'); 210 | }, 500); 211 | } else if (data.dynamics === 'rise' && updatedTime < 4) { 212 | $('td:eq(1)', row).addClass('rise'); 213 | setTimeout(function() { 214 | $('td:eq(1)', row).removeClass('rise'); 215 | }, 500); 216 | }}, 217 | columns: [ 218 | { 219 | data: null, 220 | render: function (data, type, row){ 221 | return '' + 222 | '' + 223 | row.diagram + 224 | '
' + 225 | '' 226 | }, 227 | orderable: true 228 | }, 229 | { 230 | data: null, 231 | render: function (data, type, row){ 232 | return '' + 233 | '' + 234 | ''+row.marginality_percentage+'% ' + 235 | '' + 239 | '
' + 240 | '' 241 | }, 242 | orderable: true 243 | }, 244 | { 245 | data: null, 246 | render: function (data, type, row){ 247 | return `< ${updateTime(row.update.updated)} cек.
` 248 | }, 249 | orderable: false 250 | }, 251 | ] 252 | }); 253 | setInterval( function () { 254 | refreshTable.ajax.reload( null, false ); 255 | }, 3000 ); 256 | refreshTable.on('page.dt', function() { 257 | $('html, body').animate({ 258 | scrollTop: $(".dataTables_wrapper").offset().top 259 | }, 'slow'); 260 | $('thead tr th:first-child').focus().blur(); 261 | }); 262 | }); 263 | 264 | $("#myModal").on('show.bs.modal', function (e) { 265 | var triggerLink = $(e.relatedTarget); 266 | var diagram = triggerLink.data("diagram"); 267 | var content = triggerLink.data("content"); 268 | $("#modalTitle").html('Инструкция к связке: