├── 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 |

Custom CSRF check error. 403

5 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/static/img/fav/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/fav/android-chrome-192x192.png -------------------------------------------------------------------------------- /arbitration/static/img/fav/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nezhinskiy/Assets-Loop/HEAD/arbitration/static/img/fav/android-chrome-512x512.png -------------------------------------------------------------------------------- /arbitration/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'core' 7 | -------------------------------------------------------------------------------- /arbitration/templates/core/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Custom 403{% endblock %} 4 | 5 | {% block content %} 6 |

Custom 403

7 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/templates/core/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Custom 500{% endblock %} 4 | 5 | {% block content %} 6 |

Custom 500

7 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/banks/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BanksConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'banks' 7 | -------------------------------------------------------------------------------- /arbitration/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /arbitration/parsers/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ParsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'parsers' 7 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CryptoExchangesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'crypto_exchanges' 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | R503 4 | C901 5 | exclude = 6 | tests/, 7 | */migrations/, 8 | venv/, 9 | env/ 10 | per-file-ignores = 11 | */settings.py:E501 12 | max-complexity = 10 -------------------------------------------------------------------------------- /arbitration/arbitration/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "arbitration.settings") 6 | app = Celery("arbitration") 7 | app.config_from_object("django.conf:settings", namespace="CELERY") 8 | app.autodiscover_tasks() 9 | -------------------------------------------------------------------------------- /arbitration/templates/core/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Custom 404{% endblock %} 4 | 5 | {% block content %} 6 |

Custom 404

7 |

Страницы с адресом {{ 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/(?P.*)$', serve, 12 | {'document_root': settings.STATIC_ROOT} 13 | ), 14 | ] 15 | 16 | urlpatterns += staticfiles_urlpatterns() 17 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/yoomoney.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 | YOOMONEY_CURRENCIES = ( 10 | 'USD', 'EUR', 'RUB', 'GBP', 'KZT', 'BYN', 11 | ) 12 | 13 | 14 | class YoomoneyBinanceP2PParser(BinanceP2PParser): 15 | bank_name: str = BANK_NAME 16 | 17 | 18 | class YoomoneyBybitP2PParser(BybitP2PParser): 19 | bank_name: str = BANK_NAME 20 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/sberbank.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 | SBERBANK_CURRENCIES = ( 10 | 'USD', 'EUR', 'RUB', 'GBP', 'CHF', 'CAD', 'SGD', 'DKK', 11 | 'NOK', 'SEK', 'JPY' 12 | ) 13 | 14 | 15 | class SberbankBinanceP2PParser(BinanceP2PParser): 16 | bank_name: str = BANK_NAME 17 | 18 | 19 | class SberbankBybitP2PParser(BybitP2PParser): 20 | bank_name: str = BANK_NAME 21 | -------------------------------------------------------------------------------- /arbitration/static/css/custom_style.css: -------------------------------------------------------------------------------- 1 | td { 2 | background-color: white !important; 3 | transition: background-color 1s !important; 4 | } 5 | 6 | .stylish { 7 | background-color: orange !important; 8 | transition: background-color 1s !important; 9 | } 10 | .fall { 11 | background-color: #ff6565 !important; 12 | transition: background-color 1s !important; 13 | } 14 | .rise { 15 | background-color: #66fc66 !important; 16 | transition: background-color 1s !important; 17 | } 18 | .obsolete { 19 | background-color: #dadada !important; 20 | } 21 | @media (min-width: 1300px) { 22 | .container{ 23 | max-width: 1300px; 24 | } 25 | } -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/modal_includes/bank_exchange.html: -------------------------------------------------------------------------------- 1 | {% load custom_filters %} 2 | {% if not loop_rate.bank_exchange.currency_market %} 3 | внутри банка {{ loop_rate.input_bank.name }} нужно поменять {{ loop_rate.bank_exchange.from_fiat }} на {{ loop_rate.bank_exchange.to_fiat }} по курсу {{ loop_rate.bank_exchange.price|round_up }}, с учётом комиссии. 4 | {% else %} 5 | внутри банка {{ loop_rate.input_bank.name }} нужно поменять {{ loop_rate.bank_exchange.from_fiat }} на {{ loop_rate.bank_exchange.to_fiat }} через биржу {{ loop_rate.bank_exchange.currency_market.name }}, по курсу {{ loop_rate.bank_exchange.price|round_up }}, с учётом комиссии. 6 | {% endif %} -------------------------------------------------------------------------------- /arbitration/scgi_params: -------------------------------------------------------------------------------- 1 | 2 | scgi_param REQUEST_METHOD $request_method; 3 | scgi_param REQUEST_URI $request_uri; 4 | scgi_param QUERY_STRING $query_string; 5 | scgi_param CONTENT_TYPE $content_type; 6 | 7 | scgi_param DOCUMENT_URI $document_uri; 8 | scgi_param DOCUMENT_ROOT $document_root; 9 | scgi_param SCGI 1; 10 | scgi_param SERVER_PROTOCOL $server_protocol; 11 | scgi_param REQUEST_SCHEME $scheme; 12 | scgi_param HTTPS $https if_not_empty; 13 | 14 | scgi_param REMOTE_ADDR $remote_addr; 15 | scgi_param REMOTE_PORT $remote_port; 16 | scgi_param SERVER_PORT $server_port; 17 | scgi_param SERVER_NAME $server_name; -------------------------------------------------------------------------------- /arbitration/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'arbitration.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /arbitration/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from arbitration.settings import (INFO_URL, REGISTRATION_URL, START_URL, 4 | STOP_URL) 5 | from core.api_views import InterExchangesAPIView 6 | from core.views import (InfoLoopList, InterExchangesListNew, registration, 7 | start, stop) 8 | 9 | app_name = 'core' 10 | 11 | urlpatterns = [ 12 | path('', InterExchangesListNew.as_view(), name='inter_exchanges_list_new'), 13 | path('data/', InterExchangesAPIView.as_view(), 14 | name='inter_exchanges_data'), 15 | path(INFO_URL, InfoLoopList.as_view(), name="info"), 16 | path(START_URL, start, name="start"), 17 | path(STOP_URL, stop, name="stop"), 18 | path(REGISTRATION_URL, registration, name="registration"), 19 | ] 20 | -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/modal_includes/interim_crypto_exchange.html: -------------------------------------------------------------------------------- 1 | {% load custom_filters %} 2 | {% if loop_rate.second_interim_crypto_exchange %} 3 | Теперь внутри {{ loop_rate.crypto_exchange.name }} через Спот надо сначала конвертировать {{ loop_rate.interim_crypto_exchange.from_asset }} в {{ loop_rate.interim_crypto_exchange.to_asset }} по курсу {{ loop_rate.interim_crypto_exchange.price|round_up }}, а потом {{ loop_rate.interim_crypto_exchange.to_asset }} в {{ loop_rate.second_interim_crypto_exchange.to_asset }} по курсу {{ loop_rate.second_interim_crypto_exchange.price|round_up }}, комиссии учтены. 4 | {% else %} 5 | Теперь внутри {{ loop_rate.crypto_exchange.name }} через Спот надо конвертировать {{ loop_rate.interim_crypto_exchange.from_asset }} в {{ loop_rate.interim_crypto_exchange.to_asset }} по курсу {{ loop_rate.interim_crypto_exchange.price|round_up }}, комиссия учтена. 6 | {% endif %} -------------------------------------------------------------------------------- /arbitration/core/migrations/0001_create_info_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:52 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='InfoLoop', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('value', models.BooleanField(default=False)), 20 | ('started', models.DateTimeField(auto_now_add=True, verbose_name='Started date')), 21 | ('stopped', models.DateTimeField(blank=True, null=True, verbose_name='Stopped date')), 22 | ('duration', models.DurationField(default=datetime.timedelta(0))), 23 | ], 24 | options={ 25 | 'ordering': ['-started'], 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /arbitration/parsers/connection_types/direct.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import requests 4 | 5 | 6 | class Direct: 7 | """ 8 | A class for direct connection to make HTTP requests using the requests 9 | library. 10 | Attributes: 11 | request_timeout (int): The timeout value for HTTP requests. 12 | """ 13 | request_timeout: int = 5 14 | 15 | def __init__(self) -> None: 16 | """ 17 | Initializes a Direct object and creates a session object. 18 | """ 19 | self.session: requests.sessions.Session = self.__set_direct_session() 20 | 21 | @staticmethod 22 | def __set_direct_session() -> requests.sessions.Session: 23 | """ 24 | Static method that creates and returns a session object for the Direct 25 | object. 26 | """ 27 | return requests.session() 28 | 29 | def renew_connection(self) -> None: 30 | """ 31 | Placeholder method that will be used to renew the HTTP connection. 32 | """ 33 | time.sleep(2) 34 | -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/seqence.html: -------------------------------------------------------------------------------- 1 | {% if loop_rate.input_bank == loop_rate.bank_exchange.bank %} 2 | {% if loop_rate.bank_exchange.currency_market %}{{ loop_rate.bank_exchange.currency_market.name }}{% else %}{{ loop_rate.input_bank.name }}{% endif %} {{ loop_rate.bank_exchange.from_fiat }} ⇨ 3 | {% endif %} 4 | {{ loop_rate.input_bank.name }} 5 | {{ loop_rate.input_crypto_exchange.fiat }} ⇨ {{ loop_rate.input_crypto_exchange.asset }} ⇨ 6 | {% if loop_rate.interim_crypto_exchange %} 7 | {{ loop_rate.interim_crypto_exchange.to_asset }} ⇨ 8 | {% endif %} 9 | {% if loop_rate.second_interim_crypto_exchange %} 10 | {{ loop_rate.second_interim_crypto_exchange.from_asset }} ⇨ 11 | {% endif %} 12 | {{ loop_rate.output_bank.name }} {{ loop_rate.output_crypto_exchange.fiat }} 13 | {% if loop_rate.output_bank == loop_rate.bank_exchange.bank %} 14 | ⇨ {% if loop_rate.bank_exchange.currency_market %}{{ loop_rate.bank_exchange.currency_market.name }}{% else %}{{ loop_rate.output_bank.name }}{% endif %} {{ loop_rate.bank_exchange.to_fiat }} 15 | {% endif %} -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/0005_create_marginality_percentages_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:57 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('crypto_exchanges', '0004_create_inter_exchanges_model'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='RelatedMarginalityPercentages', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('updated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Update date')), 19 | ('marginality_percentage', models.FloatField(verbose_name='Marginality percentage')), 20 | ('inter_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='marginality_percentages', to='crypto_exchanges.interexchanges')), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /arbitration/core/models.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.db import models 4 | 5 | 6 | class UpdatesModel(models.Model): 7 | """ 8 | Creates an abstract model to store the date and time of the last update. 9 | """ 10 | updated = models.DateTimeField( 11 | verbose_name='Update date', 12 | auto_now_add=True, 13 | db_index=True 14 | ) 15 | duration = models.DurationField(default=timedelta()) 16 | 17 | class Meta: 18 | abstract = True 19 | 20 | 21 | class InfoLoop(models.Model): 22 | """ 23 | Creates a model to store data about the launch and run time of the 24 | application. 25 | """ 26 | value = models.BooleanField(default=False) 27 | started = models.DateTimeField( 28 | verbose_name='Started date', 29 | auto_now_add=True 30 | ) 31 | stopped = models.DateTimeField( 32 | verbose_name='Stopped date', 33 | blank=True, 34 | null=True 35 | ) 36 | duration = models.DurationField(default=timedelta()) 37 | 38 | class Meta: 39 | ordering = ['-started'] 40 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mikhail Nezhinsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/0006_update_inter_exchanges_updates.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 20:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('crypto_exchanges', '0005_create_marginality_percentages_model'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='interexchangesupdates', 15 | name='ended', 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AddField( 19 | model_name='interexchangesupdates', 20 | name='full_update', 21 | field=models.BooleanField(default=True), 22 | ), 23 | migrations.AddField( 24 | model_name='interexchangesupdates', 25 | name='international', 26 | field=models.BooleanField(default=True), 27 | ), 28 | migrations.AddField( 29 | model_name='interexchangesupdates', 30 | name='simpl', 31 | field=models.BooleanField(default=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/modal_includes/diagram.html: -------------------------------------------------------------------------------- 1 | {% load custom_filters %} 2 | {% if loop_rate.input_bank == loop_rate.bank_exchange.bank %} 3 | {{ loop_rate.input_bank.name }}-{{ loop_rate.bank_exchange.from_fiat }} 4 | {% if loop_rate.bank_exchange.currency_market %}({{ loop_rate.bank_exchange.currency_market.name }}){% endif %} -> 5 | {% endif %} 6 | {{ loop_rate.input_bank.name }} 7 | {{ loop_rate.input_crypto_exchange.fiat }} ({{ loop_rate.input_crypto_exchange.payment_channel }})-> {{ loop_rate.input_crypto_exchange.asset }} -> 8 | {% if loop_rate.interim_crypto_exchange %} 9 | {{ loop_rate.interim_crypto_exchange.to_asset }} -> 10 | {% endif %} 11 | {% if loop_rate.second_interim_crypto_exchange %} 12 | {{ loop_rate.second_interim_crypto_exchange.from_asset }} -> 13 | {% endif %} 14 | {{ loop_rate.output_crypto_exchange.asset }} ({{ loop_rate.output_crypto_exchange.payment_channel }})-> {{ loop_rate.output_bank.name }}-{{ loop_rate.output_crypto_exchange.fiat }} 15 | {% if loop_rate.output_bank == loop_rate.bank_exchange.bank %} 16 | {% if loop_rate.bank_exchange.currency_market %}({{ loop_rate.bank_exchange.currency_market.name }}){% endif %} -> {{ loop_rate.output_bank.name }}-{{ loop_rate.bank_exchange.to_fiat }} 17 | {% endif %} -------------------------------------------------------------------------------- /arbitration/core/registration.py: -------------------------------------------------------------------------------- 1 | from banks.banks_config import BANKS_CONFIG 2 | from banks.currency_markets_registration.tinkoff_invest import ( 3 | CURRENCY_MARKET_NAME) 4 | from banks.models import Banks, CurrencyMarkets 5 | from crypto_exchanges.models import CryptoExchanges 6 | 7 | 8 | def banks(): 9 | for bank_name in BANKS_CONFIG.keys(): 10 | if not Banks.objects.filter(name=bank_name).exists(): 11 | bank_config = BANKS_CONFIG[bank_name] 12 | binance_name = bank_config['binance_name'] 13 | bybit_name = bank_config['bybit_name'] 14 | Banks.objects.create( 15 | name=bank_name, binance_name=binance_name, 16 | bybit_name=bybit_name 17 | ) 18 | 19 | 20 | def currency_markets(): 21 | if not CurrencyMarkets.objects.filter(name=CURRENCY_MARKET_NAME 22 | ).exists(): 23 | CurrencyMarkets.objects.create(name=CURRENCY_MARKET_NAME) 24 | 25 | 26 | def crypto_exchanges(): 27 | from crypto_exchanges.crypto_exchanges_config import ( 28 | CRYPTO_EXCHANGES_CONFIG) 29 | for crypto_exchange_name in CRYPTO_EXCHANGES_CONFIG.keys(): 30 | if not CryptoExchanges.objects.filter( 31 | name=crypto_exchange_name 32 | ).exists(): 33 | CryptoExchanges.objects.create(name=crypto_exchange_name) 34 | 35 | 36 | def all_registration(): 37 | banks() 38 | currency_markets() 39 | crypto_exchanges() 40 | -------------------------------------------------------------------------------- /arbitration/core/views.py: -------------------------------------------------------------------------------- 1 | 2 | from django.shortcuts import redirect 3 | from django.views.generic import ListView 4 | from django_filters.views import FilterView 5 | 6 | from core.filters import ExchangesFilter 7 | from core.models import InfoLoop 8 | from core.tasks import all_reg, assets_loop, assets_loop_stop 9 | from crypto_exchanges.models import InterExchanges 10 | 11 | 12 | def registration(request): 13 | all_reg.s().delay() 14 | if InfoLoop.objects.all().count() == 0: 15 | InfoLoop.objects.create(value=False) 16 | return redirect('core:info') 17 | 18 | 19 | def start(request): 20 | if InfoLoop.objects.first().value == 0: 21 | InfoLoop.objects.create(value=True) 22 | assets_loop.s().delay() 23 | return redirect('core:inter_exchanges_list_new') 24 | 25 | 26 | def stop(request): 27 | if InfoLoop.objects.first().value == 1: 28 | assets_loop_stop.s().delay() 29 | return redirect('core:info') 30 | 31 | 32 | class InfoLoopList(ListView): 33 | model = InfoLoop 34 | template_name = 'crypto_exchanges/info.html' 35 | 36 | 37 | class InterExchangesListNew(FilterView): 38 | """ 39 | View displays a list of InterExchanges objects using a FilterView and a 40 | template called main.html. It also filters the list based on user search 41 | queries using a filterset called ExchangesFilter. 42 | """ 43 | model = InterExchanges 44 | template_name = 'crypto_exchanges/main.html' 45 | filterset_class = ExchangesFilter 46 | -------------------------------------------------------------------------------- /arbitration/core/templatetags/custom_filters.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter 9 | def last_url_path(value, num): 10 | print(value.split('/')[-1]) 11 | return value.split('/')[-num - 1] 12 | 13 | 14 | @register.filter 15 | def is_empty(value, channel): 16 | from banks.banks_config import BANKS_CONFIG 17 | currency_markets = BANKS_CONFIG[value][channel] 18 | if not currency_markets: 19 | return True 20 | return False 21 | 22 | 23 | @register.filter 24 | def round_up(value): 25 | if value is None: 26 | return None 27 | target_length = 10 28 | length = len(str(int(value))) 29 | round_length = target_length - length 30 | return round(value, round_length) 31 | 32 | 33 | @register.filter 34 | def payment_channel_name(value, trade_type): 35 | if value == 'P2PCryptoExchangesRates': 36 | return 'P2P' 37 | if value == 'Card2Wallet2CryptoExchanges': 38 | if trade_type == 'BUY': 39 | return 'Card-Wallet-Crypto' 40 | return 'Crypto-Wallet-Card' 41 | if value == 'Card2CryptoExchanges': 42 | if trade_type == 'BUY': 43 | return 'Card-Crypto' 44 | return 'Crypto-Card' 45 | 46 | 47 | @register.filter 48 | def updated_time(value): 49 | seconds = int( 50 | (datetime.now() - value.replace(tzinfo=None)).total_seconds() 51 | ) 52 | if seconds > 999: 53 | return 999 54 | return seconds 55 | -------------------------------------------------------------------------------- /infra/nginx/prod_default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | root /var/www/html/${SERVER_NAME}; 5 | server_name ${SERVER_NAME} ${WWW_SERVER_NAME}; 6 | rewrite ^/(.*) https://${SERVER_NAME}/$1 permanent; 7 | 8 | server_tokens off; 9 | 10 | location /static/ { 11 | alias /var/html/static/; 12 | } 13 | 14 | location /.well-known/acme-challenge/ { 15 | alias /var/www/certbot; 16 | } 17 | 18 | location / { 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header Host $http_host; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | proxy_pass http://arbitration:8000; 23 | } 24 | } 25 | 26 | server { 27 | listen 443 ssl; 28 | root /var/www/html/${SERVER_NAME}; 29 | server_name ${SERVER_NAME} ${WWW_SERVER_NAME}; 30 | 31 | ssl_certificate /etc/letsencrypt/live/${SERVER_NAME}/fullchain.pem; 32 | ssl_certificate_key /etc/letsencrypt/live/${SERVER_NAME}/privkey.pem; 33 | 34 | include /etc/letsencrypt/options-ssl-nginx.conf; 35 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 36 | 37 | location /static/ { 38 | alias /var/html/static/; 39 | } 40 | 41 | location /.well-known/acme-challenge/ { 42 | alias /var/www/certbot; 43 | } 44 | 45 | location / { 46 | proxy_set_header X-Real-IP $remote_addr; 47 | proxy_set_header Host $http_host; 48 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 49 | proxy_pass http://arbitration:8000; 50 | } 51 | } -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/info.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load custom_filters %} 4 | 5 | {% block title %} 6 | Инфорация по запускам 7 | {% endblock %} 8 | 9 | {% block content %} 10 |

11 | Инфорация по работе сервиса 12 |

13 |
14 |
15 |
16 |

17 | Число итераций: {{ object_list.count }} 18 |

19 |
20 |
21 |

22 | Текущее количество процессоров: 2 23 |

24 |

25 | Оптимальное количество процессоров: 5 26 |

27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for info_loop in object_list %} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% endfor %} 49 |
#StatusStartedStoppedDuration
{{ forloop.counter }}{% if info_loop.value %}Started{% else %}Stopped{% endif %}{{ info_loop.started }}{{ info_loop.sopped }}{{ info_loop.duration }}
50 | {% endblock %} 51 | 52 | {% block extra_js %} 53 | 54 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/crypto_exchanges_config.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from banks.banks_config import BANKS_CONFIG 4 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 5 | BINANCE_ASSETS, BINANCE_ASSETS_FOR_FIAT, BINANCE_CRYPTO_FIATS, 6 | BINANCE_DEPOSIT_FIATS, BINANCE_INVALID_PARAMS_LIST, BINANCE_WITHDRAW_FIATS) 7 | from crypto_exchanges.crypto_exchanges_registration.bybit import ( 8 | BYBIT_ASSETS, BYBIT_ASSETS_FOR_FIAT, BYBIT_CRYPTO_FIATS, 9 | BYBIT_INVALID_PARAMS_LIST) 10 | 11 | TRADE_TYPES = ('BUY', 'SELL') 12 | 13 | CRYPTO_EXCHANGES_CONFIG = { 14 | 'Binance': { 15 | 'assets': BINANCE_ASSETS, 16 | 'assets_for_fiats': BINANCE_ASSETS_FOR_FIAT, 17 | 'invalid_params_list': BINANCE_INVALID_PARAMS_LIST, 18 | 'crypto_fiats': BINANCE_CRYPTO_FIATS, 19 | 'deposit_fiats': BINANCE_DEPOSIT_FIATS, 20 | 'withdraw_fiats': BINANCE_WITHDRAW_FIATS, 21 | }, 22 | 'Bybit': { 23 | 'assets': BYBIT_ASSETS, 24 | 'assets_for_fiats': BYBIT_ASSETS_FOR_FIAT, 25 | 'invalid_params_list': BYBIT_INVALID_PARAMS_LIST, 26 | 'crypto_fiats': BYBIT_CRYPTO_FIATS, 27 | 28 | } 29 | } 30 | ALL_FIATS = tuple( 31 | OrderedDict.fromkeys( 32 | fiat for bank_info in BANKS_CONFIG.values() 33 | for fiat in bank_info['currencies'] 34 | ) 35 | ) 36 | ALL_ASSETS = tuple( 37 | OrderedDict.fromkeys( 38 | asset for crypto_exchange_info in CRYPTO_EXCHANGES_CONFIG.values() 39 | for asset in crypto_exchange_info['assets'] 40 | ) 41 | ) 42 | -------------------------------------------------------------------------------- /arbitration/parsers/connection_types/proxy.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import requests 4 | from fp.fp import FreeProxy 5 | 6 | from arbitration.settings import COUNTRIES_NEAR_SERVER 7 | 8 | 9 | class Proxy: 10 | """ 11 | A class to manage proxy connections for making HTTP requests. 12 | 13 | Attributes: 14 | request_timeout (int): The timeout value for HTTP requests. 15 | country_id (List[str]): A list of country codes to limit proxy 16 | selection to. 17 | """ 18 | request_timeout: int = 4 19 | country_id: List[str] = COUNTRIES_NEAR_SERVER 20 | 21 | def __init__(self) -> None: 22 | """ 23 | Initializes the Proxy class and sets up a new session with a proxy. 24 | """ 25 | self.proxy_list = FreeProxy(country_id=self.country_id, elite=True) 26 | self.proxy_url: str = self.__set_proxy_url() 27 | self.session: requests.sessions.Session = self.__set_proxy_session() 28 | 29 | def __set_proxy_session(self) -> requests.sessions.Session: 30 | """ 31 | Private method to set up a new requests session with the currently 32 | selected proxy. 33 | """ 34 | with requests.session() as session: 35 | session.proxies = {'http': self.proxy_url} 36 | return session 37 | 38 | def __set_proxy_url(self) -> str: 39 | """ 40 | Private method to get a new proxy URL from the proxy list. 41 | """ 42 | return self.proxy_list.get() 43 | 44 | def renew_connection(self) -> None: 45 | """ 46 | Public method to renew the proxy connection by getting a new proxy URL 47 | and setting up a new session. 48 | """ 49 | self.proxy_url = self.__set_proxy_url() 50 | self.session = self.__set_proxy_session() 51 | -------------------------------------------------------------------------------- /arbitration/templates/includes/header.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% with request.resolver_match.view_name as view_name %} 3 |
4 | 33 |
34 | {% endwith %} -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/0003_create_lists_fiats_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:57 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('crypto_exchanges', '0002_create_crypto_exchanges_model'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ListsFiatCryptoUpdates', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('updated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Update date')), 20 | ('duration', models.DurationField(default=datetime.timedelta(0))), 21 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='list_fiat_crypto_update', to='crypto_exchanges.cryptoexchanges')), 22 | ], 23 | options={ 24 | 'abstract': False, 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='ListsFiatCrypto', 29 | fields=[ 30 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('list_fiat_crypto', models.JSONField()), 32 | ('trade_type', models.CharField(max_length=4)), 33 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='list_fiat_crypto', to='crypto_exchanges.cryptoexchanges')), 34 | ('update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datas', to='crypto_exchanges.listsfiatcryptoupdates')), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/tinkoff.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from arbitration.settings import API_TINKOFF, CONNECTION_TYPE_TINKOFF 4 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 5 | BinanceP2PParser) 6 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 7 | from parsers.parsers import BankParser 8 | 9 | BANK_NAME = os.path.basename(__file__).split('.')[0].capitalize() 10 | 11 | TINKOFF_CURRENCIES = ( 12 | 'USD', 'EUR', 'RUB', 'GBP', 'KZT', 'BYN', 'AMD', 'TRY', 'CNY', 'JPY', 13 | 'CHF', 'HKD' 14 | ) 15 | 16 | 17 | class TinkoffParser(BankParser): 18 | bank_name: str = BANK_NAME 19 | endpoint: str = API_TINKOFF 20 | buy_and_sell: bool = True 21 | name_from: str = 'from' 22 | name_to: str = 'to' 23 | connection_type: str = CONNECTION_TYPE_TINKOFF 24 | need_cookies: bool = False 25 | 26 | def _create_params(self, fiats_combinations): 27 | params = [ 28 | dict([(self.name_from, params[0]), (self.name_to, params[-1])]) 29 | for params in fiats_combinations 30 | ] 31 | return params 32 | 33 | def _extract_buy_and_sell_from_json(self, json_data: dict 34 | ) -> tuple[float, float] or None: 35 | if not json_data: 36 | return None 37 | payload = json_data['payload'] 38 | rates = payload['rates'] 39 | buy = sell = float() 40 | for category in rates: 41 | if category['category'] == 'DepositPayments': 42 | buy = category.get('buy') 43 | sell = category.get('sell') 44 | if category['category'] == 'CUTransfersPremium': 45 | buy_premium = category.get('buy') 46 | sell_premium = category.get('sell') 47 | if buy_premium and sell_premium: 48 | return buy_premium, sell_premium 49 | return buy, sell 50 | 51 | 52 | class TinkoffBinanceP2PParser(BinanceP2PParser): 53 | bank_name: str = BANK_NAME 54 | 55 | 56 | class TinkoffBybitP2PParser(BybitP2PParser): 57 | bank_name: str = BANK_NAME 58 | -------------------------------------------------------------------------------- /arbitration/parsers/cookie.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | import requests 4 | 5 | 6 | class Cookie: 7 | """ 8 | This class represents a set of cookies that can be added to the headers of 9 | HTTP requests made using a requests session object. 10 | 11 | Attributes: 12 | endpoint (str): The URL of the endpoint that will be requested to 13 | obtain the cookies. 14 | session (requests.sessions.Session): A requests session object to use 15 | for making HTTP requests. 16 | cookies (dict): Cookies in the form of a dictionary. 17 | cookies_names (Optional[Iterable[str]]): An optional iterable of cookie 18 | names to include in the headers. If not provided, all cookies 19 | obtained from the endpoint will be included. 20 | """ 21 | def __init__(self, endpoint: str, session: requests.sessions.Session, 22 | cookies_names=None) -> None: 23 | """ 24 | Initializes a new Cookie instance by obtaining the cookies from the 25 | specified endpoint using the provided session object. If cookies_names 26 | is not provided, all cookies obtained will be used. 27 | """ 28 | self.endpoint: str = endpoint 29 | self.session: requests.sessions.Session = session 30 | self.cookies: Dict[str, Any] = self.__get_cookies() 31 | self.cookies_names: str = cookies_names or self.cookies.keys() 32 | 33 | def __get_cookies(self) -> Dict[str, Any]: 34 | """ 35 | Private method that obtains the cookies from the endpoint using the 36 | session object and returns them as a dictionary. 37 | """ 38 | self.session.get(self.endpoint) 39 | return self.session.cookies.get_dict() 40 | 41 | def add_cookies_to_headers(self) -> None: 42 | """ 43 | Adds the cookies to the headers of the session object by updating the 44 | 'Cookie' field in the headers dictionary with the values of the 45 | specified cookie names. 46 | """ 47 | headers_with_cookies = { 48 | 'Cookie': '; '.join( 49 | [f'{name}={self.cookies[name]}' for name in self.cookies_names] 50 | ) 51 | } 52 | self.session.headers.update(headers_with_cookies) 53 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/wise.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from arbitration.settings import API_WISE, CONNECTION_TYPE_WISE 4 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 5 | BinanceP2PParser) 6 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 7 | from parsers.parsers import BankParser 8 | 9 | BANK_NAME = os.path.basename(__file__).split('.')[0].capitalize() 10 | 11 | WISE_CURRENCIES = ( 12 | 'USD', 'EUR', 'UAH', 'ILS', 'GBP', 'GEL', 'TRY', 'CHF', 'AUD' 13 | ) # 'CZK', 'RON', 'NZD', 'AED', 'CLP', 'INR', 'SGD', 'HUF', 'PLN', 'CAD', 14 | # 'CHF', 'AUD', 'CNY', 'JPY' 15 | 16 | 17 | class WiseParser(BankParser): 18 | bank_name: str = BANK_NAME 19 | endpoint: str = API_WISE 20 | name_from: str = 'sourceCurrency' 21 | name_to: str = 'targetCurrency' 22 | buy_and_sell: bool = False 23 | # custom_settings 24 | source_amount: int = 10000 25 | profile_country: str = 'RU' 26 | connection_type: str = CONNECTION_TYPE_WISE 27 | need_cookies: bool = False 28 | 29 | def _create_params(self, fiats_combinations): 30 | params = [dict([('sourceAmount', self.source_amount), 31 | (self.name_from, params[0]), 32 | (self.name_to, params[-1]), 33 | ('profileCountry', self.profile_country)]) 34 | for params in fiats_combinations] 35 | return params 36 | 37 | def _extract_price_from_json(self, json_data: list) -> float: 38 | if json_data and len(json_data) > 1: 39 | for exchange_data in json_data: 40 | pay_in_method = exchange_data.get('payInMethod') 41 | pay_out_method = exchange_data.get('payOutMethod') 42 | if pay_in_method == pay_out_method == 'BALANCE': 43 | price_before_commission = exchange_data.get('midRate') 44 | fee = exchange_data.get('total') 45 | commission = fee / self.source_amount * 100 46 | price = price_before_commission * 100 / (100 + commission) 47 | return price 48 | 49 | 50 | class WiseBinanceP2PParser(BinanceP2PParser): 51 | bank_name: str = BANK_NAME 52 | 53 | 54 | class WiseBybitP2PParser(BybitP2PParser): 55 | bank_name: str = BANK_NAME 56 | -------------------------------------------------------------------------------- /arbitration/parsers/connection_types/tor.py: -------------------------------------------------------------------------------- 1 | import re 2 | import subprocess 3 | from time import sleep 4 | 5 | import requests 6 | from stem import Signal 7 | from stem.control import Controller 8 | from stem.util import log 9 | 10 | 11 | class Tor: 12 | """ 13 | This class sets up a Tor proxy on a running Tor host and initializes a 14 | request session with the proxy. 15 | 16 | Attributes: 17 | request_timeout (int): The timeout value for HTTP requests. 18 | TOR_HOSTNAME (str): Hostname docker container Tor. 19 | """ 20 | request_timeout: int = None 21 | TOR_HOSTNAME: str = 'tor_proxy' 22 | 23 | def __init__(self) -> None: 24 | """ 25 | Initializes the Tor class by setting the container IP address and 26 | creating a new Tor session. 27 | """ 28 | log.get_logger().propagate = False # Disable Tor's redundant logging. 29 | self.container_ip: str = self.__get_tor_ip() 30 | self.session: requests.sessions.Session = self.__set_tor_session() 31 | 32 | def __set_tor_session(self) -> requests.sessions.Session: 33 | """ 34 | Set up a proxy for http and https on the running Tor host: port 9050 35 | and initialize the request session. 36 | """ 37 | with requests.session() as session: 38 | session.proxies = {'http': f'socks5h://{self.TOR_HOSTNAME}:9050', 39 | 'https': f'socks5h://{self.TOR_HOSTNAME}:9050'} 40 | return session 41 | 42 | def __get_tor_ip(self) -> str: 43 | """ 44 | Retrieves the IP address of the running Tor host. 45 | """ 46 | cmd = f'ping -c 1 {self.TOR_HOSTNAME}' 47 | output = subprocess.check_output(cmd, shell=True).decode().strip() 48 | return re.findall(r'\(.*?\)', output)[0][1:-1] 49 | 50 | def renew_connection(self) -> None: 51 | """ 52 | Renews the connection with the running Tor host by sending a signal to 53 | the Tor control port and creating a new Tor session with a new IP 54 | address. 55 | """ 56 | with Controller.from_port(address=self.container_ip) as controller: 57 | controller.authenticate() 58 | controller.signal(Signal.NEWNYM) 59 | sleep(controller.get_newnym_wait()) 60 | self.session = self.__set_tor_session() 61 | -------------------------------------------------------------------------------- /arbitration/banks/banks_registration/raiffeisen.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict, List, Optional 3 | 4 | from arbitration.settings import API_RAIFFEISEN, CONNECTION_TYPE_RAIFFEISEN 5 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 6 | BinanceP2PParser) 7 | from crypto_exchanges.crypto_exchanges_registration.bybit import BybitP2PParser 8 | from parsers.parsers import BankParser 9 | 10 | BANK_NAME = os.path.basename(__file__).split('.')[0].capitalize() 11 | 12 | RAIFFEISEN_CURRENCIES = ( 13 | 'USD', 'EUR', 'RUB', 'GBP' 14 | ) 15 | 16 | 17 | class RaiffeisenParser(BankParser): 18 | bank_name: str = BANK_NAME 19 | endpoint: str = API_RAIFFEISEN 20 | all_values: bool = True 21 | connection_type: str = CONNECTION_TYPE_RAIFFEISEN 22 | need_cookies: bool = False 23 | LIMIT_TRY: int = 6 24 | 25 | def _extract_all_values_from_json(self, json_data: dict 26 | ) -> Optional[List[Dict[str, Any]]]: 27 | if not json_data: 28 | return None 29 | value_lst = [] 30 | data = json_data['data'] 31 | rates = data['rates'][0] 32 | main_currency = rates['code'] 33 | exchanges = rates['exchange'] 34 | for exchange in exchanges: 35 | second_currency = exchange['code'] 36 | buy = exchange['rates']['buy']['value'] 37 | sell = exchange['rates']['sell']['value'] 38 | value_lst.append( 39 | { 40 | 'from_fiat': main_currency, 41 | 'to_fiat': second_currency, 42 | 'price': 1 / sell 43 | } 44 | ) 45 | value_lst.append( 46 | { 47 | 'from_fiat': second_currency, 48 | 'to_fiat': main_currency, 49 | 'price': buy 50 | } 51 | ) 52 | return value_lst 53 | 54 | def _get_all_api_answers(self) -> None: 55 | values = self._choice_buy_and_sell_or_price() 56 | if not values: 57 | return 58 | for value_dict in values: 59 | price = value_dict.pop('price') 60 | self._add_to_bulk_update_or_create(value_dict, price) 61 | 62 | 63 | class RaiffeisenBinanceP2PParser(BinanceP2PParser): 64 | bank_name: str = BANK_NAME 65 | 66 | 67 | class RaiffeisenBybitP2PParser(BybitP2PParser): 68 | bank_name: str = BANK_NAME 69 | -------------------------------------------------------------------------------- /arbitration/banks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from arbitration.settings import FIAT_LENGTH, NAME_LENGTH 4 | from core.models import UpdatesModel 5 | 6 | 7 | class Banks(models.Model): 8 | """ 9 | Model to represent banks. 10 | """ 11 | name = models.CharField( 12 | max_length=NAME_LENGTH, 13 | null=True, 14 | blank=True 15 | ) 16 | binance_name = models.CharField( 17 | max_length=NAME_LENGTH, 18 | null=True, 19 | blank=True 20 | ) 21 | bybit_name = models.CharField( 22 | max_length=NAME_LENGTH, 23 | null=True, 24 | blank=True 25 | ) 26 | 27 | 28 | class CurrencyMarkets(models.Model): 29 | """ 30 | Model to represent currency markets. 31 | """ 32 | name = models.CharField(max_length=NAME_LENGTH) 33 | 34 | 35 | class BanksExchangeRatesUpdates(UpdatesModel): 36 | """ 37 | Model to represent the last update time for bank exchange rates. 38 | """ 39 | bank = models.ForeignKey( 40 | Banks, 41 | related_name='bank_rates_update', 42 | blank=True, 43 | null=True, 44 | on_delete=models.CASCADE 45 | ) 46 | currency_market = models.ForeignKey( 47 | CurrencyMarkets, 48 | related_name='currency_market_rates_update', 49 | blank=True, 50 | null=True, 51 | on_delete=models.CASCADE 52 | ) 53 | 54 | 55 | class BanksExchangeRates(models.Model): 56 | """ 57 | Model to represent bank exchange rates. 58 | """ 59 | bank = models.ForeignKey( 60 | Banks, 61 | related_name='bank_rates', 62 | on_delete=models.CASCADE 63 | ) 64 | currency_market = models.ForeignKey( 65 | CurrencyMarkets, 66 | related_name='currency_market_rates', 67 | blank=True, 68 | null=True, 69 | on_delete=models.CASCADE 70 | ) 71 | from_fiat = models.CharField(max_length=FIAT_LENGTH) 72 | to_fiat = models.CharField(max_length=FIAT_LENGTH) 73 | price = models.FloatField() 74 | update = models.ForeignKey( 75 | BanksExchangeRatesUpdates, 76 | related_name='datas', 77 | on_delete=models.CASCADE 78 | ) 79 | 80 | class Meta: 81 | constraints = [ 82 | models.UniqueConstraint( 83 | fields=( 84 | 'bank', 'from_fiat', 'to_fiat', 'currency_market' 85 | ), 86 | name='unique_bank_exchanges' 87 | ) 88 | ] 89 | -------------------------------------------------------------------------------- /arbitration/banks/currency_markets_registration/tinkoff_invest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime, time 3 | 4 | import pytz 5 | from _decimal import Decimal 6 | 7 | from arbitration.settings import (API_TINKOFF_INVEST, 8 | CONNECTION_TYPE_TINKOFF_INVEST) 9 | from parsers.parsers import BankInvestParser 10 | 11 | CURRENCY_MARKET_NAME = ( 12 | os.path.basename(__file__).split('.')[0].capitalize().replace('_', ' ')) 13 | 14 | 15 | class TinkoffCurrencyMarketParser(BankInvestParser): 16 | currency_markets_name = CURRENCY_MARKET_NAME 17 | endpoint = API_TINKOFF_INVEST 18 | link_ends = ('USDRUB', 'EURRUB') 19 | # 'GBPRUB', 'HKDRUB', 'TRYRUB', 'KZTRUB_TOM', 'BYNRUB_TOM', 'AMDRUB_TOM', 20 | # 'CHFRUB', 'JPYRUB', 21 | connection_type: str = CONNECTION_TYPE_TINKOFF_INVEST 22 | need_cookies: bool = False 23 | fake_useragent: bool = False 24 | LIMIT_TRY: int = 6 25 | 26 | @staticmethod 27 | def _get_utc_work_time() -> bool: 28 | start_work_time = time(hour=7) 29 | end_work_time = time(hour=19) 30 | time_zone = 'Europe/Moscow' 31 | local_datatime = datetime.now(pytz.timezone(time_zone)) 32 | if_work_day = local_datatime.weekday() in range(0, 5) 33 | if_work_time = start_work_time <= local_datatime.time() < end_work_time 34 | return if_work_day and if_work_time 35 | 36 | @staticmethod 37 | def _extract_buy_and_sell_from_json(json_data: dict, link_end: str 38 | ) -> tuple[Decimal, Decimal]: 39 | items = json_data['payload']['items'] 40 | for item in items: 41 | content = item['content'] 42 | instruments = content['instruments'] 43 | for instrument in instruments: 44 | if not instrument: 45 | continue 46 | ticker = instrument.get('ticker') 47 | if ticker == link_end: 48 | relative_yield = Decimal(instrument['relativeYield']) 49 | pre_price = Decimal(instrument['price']) 50 | price = pre_price + pre_price / 100 * relative_yield 51 | if link_end[0:3] == 'KZT': 52 | price /= 100 53 | elif link_end[0:3] == 'AMD': 54 | price /= 100 55 | buy_price = price - price * Decimal('0.003') 56 | sell_price = (1 / price) - (1 / price) * Decimal('0.003') 57 | return buy_price, sell_price 58 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/0001_create_intra_crypto_exchanges_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:55 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='CryptoExchanges', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=20)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='IntraCryptoExchangesRatesUpdates', 25 | fields=[ 26 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('updated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Update date')), 28 | ('duration', models.DurationField(default=datetime.timedelta(0))), 29 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crypto_exchanges_update', to='crypto_exchanges.cryptoexchanges')), 30 | ], 31 | options={ 32 | 'abstract': False, 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='IntraCryptoExchangesRates', 37 | fields=[ 38 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('from_asset', models.CharField(max_length=4)), 40 | ('to_asset', models.CharField(max_length=4)), 41 | ('price', models.FloatField()), 42 | ('spot_fee', models.FloatField(default=None)), 43 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crypto_exchanges', to='crypto_exchanges.cryptoexchanges')), 44 | ('update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datas', to='crypto_exchanges.intracryptoexchangesratesupdates')), 45 | ], 46 | ), 47 | migrations.AddConstraint( 48 | model_name='intracryptoexchangesrates', 49 | constraint=models.UniqueConstraint(fields=('crypto_exchange', 'from_asset', 'to_asset'), name='unique_intra_crypto_exchanges'), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /.github/workflows/arbitration_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Arbitration workflow 2 | 3 | on: [push] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up Python 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: 3.9 15 | 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install flake8 pep8-naming flake8-broken-line flake8-return flake8-isort 20 | pip install -r arbitration/requirements.txt 21 | - name: Test with flake8 and django tests 22 | run: python -m flake8 23 | 24 | build_and_push_to_docker_hub: 25 | name: Push Docker image to Docker Hub 26 | runs-on: ubuntu-latest 27 | needs: tests 28 | steps: 29 | - name: Check out the repo 30 | uses: actions/checkout@v3 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@v2 33 | - name: Login to Docker 34 | uses: docker/login-action@v2 35 | with: 36 | username: ${{ secrets.DOCKER_USERNAME }} 37 | password: ${{ secrets.DOCKER_PASSWORD }} 38 | - name: Push project to Docker Hub 39 | uses: docker/build-push-action@v2 40 | with: 41 | context: arbitration 42 | push: true 43 | tags: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_PROJECT }}:latest 44 | 45 | deploy: 46 | runs-on: ubuntu-latest 47 | needs: build_and_push_to_docker_hub 48 | steps: 49 | - name: executing remote ssh commands to deploy 50 | uses: appleboy/ssh-action@master 51 | with: 52 | host: ${{ secrets.HOST }} 53 | username: ${{ secrets.USER }} 54 | key: ${{ secrets.SSH_KEY }} 55 | script: | 56 | cd infra/ 57 | docker-compose down 58 | docker image rm ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_PROJECT }} 59 | docker volume rm infra_static_value 60 | docker-compose up -d --build 61 | docker-compose run ${{ secrets.DOCKER_PROJECT }} python manage.py migrate 62 | docker-compose run ${{ secrets.DOCKER_PROJECT }} python manage.py collectstatic --noinput 63 | 64 | send_message: 65 | runs-on: ubuntu-latest 66 | needs: deploy 67 | steps: 68 | - name: send message 69 | uses: appleboy/telegram-action@master 70 | with: 71 | to: ${{ secrets.TELEGRAM_TO }} 72 | token: ${{ secrets.TELEGRAM_TOKEN }} 73 | message: ${{ secrets.DOCKER_PROJECT }} deployment completed successfully -------------------------------------------------------------------------------- /infra/example.env: -------------------------------------------------------------------------------- 1 | # Django 2 | LOCAL=# True / False 3 | DEBUG=# True / False 4 | DEVELOPMENT_MODE=# True / False 5 | ALLOWED_HOSTS=example.com www.example.com # example 6 | ALLOWED_PERCENTAGE=999 # example 7 | COUNTRIES_NEAR_SERVER=GL RU FN # example 8 | MINIMUM_PERCENTAGE=-3 # example 9 | 10 | # Update frequency 11 | UPDATE_RATE= 5, 5, 5, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 5, 5 # example 12 | P2P_BINANCE_UPDATE_FREQUENCY=900 # example 13 | P2P_BYBIT_UPDATE_FREQUENCY=900 # example 14 | INTERNAL_BANKS_UPDATE_FREQUENCY=900 # example 15 | EXCHANGES_BINANCE_UPDATE_FREQUENCY=900 # example 16 | EXCHANGES_BYBIT_UPDATE_FREQUENCY=900 # example 17 | CARD_2_CRYPTO_BINANCE_UPDATE_FREQUENCY=900 # example 18 | 19 | # URLs 20 | INFO_URL=example_info # example 21 | START_URL=example_start # example 22 | STOP_URL=example_stop # example 23 | REGISTRATION_URL=example_registrations # example 24 | 25 | # Logger 26 | LOGLEVEL_PARSING_START=# info / error / warning / debug 27 | LOGLEVEL_PARSING_END=# info / error / warning / debug 28 | LOGLEVEL_CALCULATING_START=# info / error / warning / debug 29 | LOGLEVEL_CALCULATING_END=# info / error / warning / debug 30 | 31 | # Endpoints 32 | API_P2P_BINANCE=https://example.com # example 33 | API_P2P_BYBIT=https://example.com # example 34 | API_BINANCE_CARD_2_CRYPTO_SELL=https://example.com # example 35 | API_BINANCE_CARD_2_CRYPTO_BUY=https://example.com # example 36 | API_BINANCE_LIST_FIAT_SELL=https://example.com # example 37 | API_BINANCE_LIST_FIAT_BUY=https://example.com # example 38 | API_BINANCE_CRYPTO=https://example.com # example 39 | API_BYBIT_CRYPTO=https://example.com # example 40 | API_WISE=https://example.com # example 41 | API_RAIFFEISEN=https://example.com # example 42 | API_TINKOFF=https://example.com # example 43 | API_TINKOFF_INVEST=https://example.com # example 44 | 45 | # Connection types 46 | CONNECTION_TYPE_P2P_BINANCE=# Direct / Tor / Proxy 47 | CONNECTION_TYPE_P2P_BYBIT=# Direct / Tor / Proxy 48 | CONNECTION_TYPE_BINANCE_CARD_2_CRYPTO=# Direct / Tor / Proxy 49 | CONNECTION_TYPE_BINANCE_LIST_FIAT=# Direct / Tor / Proxy 50 | CONNECTION_TYPE_BINANCE_CRYPTO=# Direct / Tor / Proxy 51 | CONNECTION_TYPE_BYBIT_CRYPTO=# Direct / Tor / Proxy 52 | CONNECTION_TYPE_WISE=# Direct / Tor / Proxy 53 | CONNECTION_TYPE_RAIFFEISEN=# Direct / Tor / Proxy 54 | CONNECTION_TYPE_TINKOFF=# Direct / Tor / Proxy 55 | CONNECTION_TYPE_TINKOFF_INVEST=# Direct / Tor / Proxy 56 | 57 | # Data Base 58 | DB_ENGINE=django.db.backends.postgresql # example 59 | DB_NAME=postgres # example 60 | POSTGRES_USER=postgres # example 61 | POSTGRES_PASSWORD=postgres # example 62 | DB_HOST=db # example 63 | DB_PORT=5432 # example 64 | 65 | # Nginx 66 | SERVER_NAME=example.com # example 67 | WWW_SERVER_NAME=www.example.com # example -------------------------------------------------------------------------------- /infra/certbot/init-letsencrypt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! [ -x "$(command -v docker-compose)" ]; then 4 | echo 'Error: docker-compose is not installed.' >&2 5 | exit 1 6 | fi 7 | 8 | domains=(${SERVER_NAME} ${WWW_SERVER_NAME}) 9 | rsa_key_size=4096 10 | data_path="./certbot" 11 | email=${EMAIL} 12 | staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits 13 | 14 | if [ -d "$data_path" ]; then 15 | read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision 16 | if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then 17 | exit 18 | fi 19 | fi 20 | 21 | if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then 22 | echo "### Downloading recommended TLS parameters ..." 23 | mkdir -p "$data_path/conf" 24 | curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" 25 | curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" 26 | echo 27 | fi 28 | 29 | echo "### Creating dummy certificate for $domains ..." 30 | path="/etc/letsencrypt/live/$domains" 31 | mkdir -p "$data_path/conf/live/$domains" 32 | docker-compose run --rm --entrypoint "\ 33 | openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\ 34 | -keyout '$path/privkey.pem' \ 35 | -out '$path/fullchain.pem' \ 36 | -subj '/CN=localhost'" certbot 37 | echo 38 | 39 | echo "### Starting nginx ..." 40 | docker-compose up --force-recreate -d nginx 41 | echo 42 | 43 | echo "### Deleting dummy certificate for $domains ..." 44 | docker-compose run --rm --entrypoint "\ 45 | rm -Rf /etc/letsencrypt/live/$domains && \ 46 | rm -Rf /etc/letsencrypt/archive/$domains && \ 47 | rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot 48 | echo 49 | 50 | 51 | echo "### Requesting Let's Encrypt certificate for $domains ..." 52 | #Join $domains to -d args 53 | domain_args="" 54 | for domain in "${domains[@]}"; do 55 | domain_args="$domain_args -d $domain" 56 | done 57 | 58 | # Select appropriate email arg 59 | case "$email" in 60 | "") email_arg="--register-unsafely-without-email" ;; 61 | *) email_arg="--email $email" ;; 62 | esac 63 | 64 | # Enable staging mode if needed 65 | if [ $staging != "0" ]; then staging_arg="--staging"; fi 66 | 67 | docker-compose run --rm --entrypoint "\ 68 | certbot certonly --webroot -w /var/www/certbot \ 69 | $staging_arg \ 70 | $email_arg \ 71 | $domain_args \ 72 | --rsa-key-size $rsa_key_size \ 73 | --agree-tos \ 74 | --force-renewal" certbot 75 | echo 76 | 77 | echo "### Reloading nginx ..." 78 | docker-compose exec nginx nginx -s reload -------------------------------------------------------------------------------- /arbitration/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% load static %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block title %} 17 | {{ title }} 18 | {% endblock %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% include 'includes/header.html' %} 33 |
34 |
35 | {% block content %} 36 | {% endblock %} 37 |
38 |
39 | 42 | {# #} 43 | {# #} 44 | {# #} 45 | {# #} 46 | {% block extra_js %} 47 | {% endblock %} 48 | 49 | 50 | -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/product_filter.html: -------------------------------------------------------------------------------- 1 | {% block style %} 2 | {{ filter.form.media.css }} 3 | {% endblock %} 4 | 5 | {% load bootstrap4 %} 6 | {% load widget_tweaks %} 7 | 8 |
9 |
10 |
11 |

12 | Расширенный фильтр: 13 |

14 |
15 |

Процент маржинальности:

16 |
17 | {% for field in filter.form %} 18 | {% if forloop.counter == 1 %} 19 |
20 |

21 | 22 |

23 |
24 | {% elif forloop.counter == 2 %} 25 |
26 |

27 | 28 |

29 |
30 | 31 | {% else %} 32 |
33 | {% if forloop.counter == 8 or forloop.counter == 9 or forloop.counter == 10 or forloop.counter == 11 %} 34 |

35 | 44 |

45 | {% else %} 46 |

47 | 48 |

49 | {% endif %} 50 |
51 |
52 |

53 | {% render_field field class="form-control" %} 54 |

55 |
56 | {% endif %} 57 | {% endfor %} 58 |
59 | 62 |
63 |
64 | 65 | {% block extra_js %} 66 | {{ filter.form.media.js }} 67 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/tasks.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | from arbitration.celery import app 4 | from arbitration.settings import (CARD_2_CRYPTO_BINANCE_UPDATE_FREQUENCY, 5 | EXCHANGES_BINANCE_UPDATE_FREQUENCY, 6 | EXCHANGES_BYBIT_UPDATE_FREQUENCY, 7 | UPDATE_RATE) 8 | from crypto_exchanges.crypto_exchanges_registration.binance import ( 9 | BinanceCard2CryptoExchangesParser, 10 | BinanceCard2Wallet2CryptoExchangesCalculating, BinanceCryptoParser, 11 | BinanceListsFiatCryptoParser) 12 | from crypto_exchanges.crypto_exchanges_registration.bybit import ( 13 | BybitCryptoParser) 14 | 15 | 16 | # Intra crypto exchanges 17 | # Binance 18 | @app.task( 19 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 20 | retry_backoff=True 21 | ) 22 | def get_all_binance_crypto_exchanges(self): 23 | BinanceCryptoParser().main() 24 | self.retry( 25 | countdown=EXCHANGES_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 26 | datetime.now(timezone.utc).hour 27 | ] 28 | ) 29 | 30 | 31 | # Bybit 32 | @app.task( 33 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 34 | retry_backoff=True 35 | ) 36 | def get_all_bybit_crypto_exchanges(self): 37 | BybitCryptoParser().main() 38 | self.retry( 39 | countdown=EXCHANGES_BYBIT_UPDATE_FREQUENCY * UPDATE_RATE[ 40 | datetime.now(timezone.utc).hour 41 | ] 42 | ) 43 | 44 | 45 | @app.task( 46 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 47 | retry_backoff=True 48 | ) 49 | def get_binance_card_2_crypto_exchanges_buy(self): 50 | BinanceCard2CryptoExchangesParser('BUY').main() 51 | self.retry( 52 | countdown=CARD_2_CRYPTO_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 53 | datetime.now(timezone.utc).hour 54 | ] 55 | ) 56 | 57 | 58 | @app.task( 59 | bind=True, max_retries=None, queue='parsing', autoretry_for=(Exception,), 60 | retry_backoff=True 61 | ) 62 | def get_binance_card_2_crypto_exchanges_sell(self): 63 | BinanceCard2CryptoExchangesParser('SELL').main() 64 | self.retry( 65 | countdown=CARD_2_CRYPTO_BINANCE_UPDATE_FREQUENCY * UPDATE_RATE[ 66 | datetime.now(timezone.utc).hour 67 | ] 68 | ) 69 | 70 | 71 | @app.task(max_retries=2, queue='parsing', autoretry_for=(Exception,)) 72 | def get_start_binance_fiat_crypto_list(): 73 | BinanceListsFiatCryptoParser().main() 74 | 75 | 76 | # Calculating 77 | @app.task 78 | def get_binance_fiat_crypto_list(): 79 | BinanceListsFiatCryptoParser().main() 80 | 81 | 82 | @app.task 83 | def get_all_card_2_wallet_2_crypto_exchanges_buy(): 84 | BinanceCard2Wallet2CryptoExchangesCalculating('BUY').main() 85 | 86 | 87 | @app.task 88 | def get_all_card_2_wallet_2_crypto_exchanges_sell(): 89 | BinanceCard2Wallet2CryptoExchangesCalculating('SELL').main() 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test.py / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | #db.sqlite3 62 | #db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Папки, создаваемые средой разработки 132 | .idea 133 | .DS_Store 134 | .AppleDouble 135 | .LSOverride 136 | 137 | *.sublime-project 138 | *.sublime-workspace 139 | 140 | .vscode/ 141 | *.code-workspace 142 | 143 | # Local History for Visual Studio Code 144 | .history/ 145 | 146 | .mypy_cache 147 | 148 | # папки со статикой и медиа 149 | media/ 150 | 151 | #db 152 | *.sqlite3 153 | 154 | # certbot 155 | /infra/certbot/conf/ 156 | /infra/certbot/www/ 157 | /infra/arbitration/ 158 | -------------------------------------------------------------------------------- /infra/local-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | 4 | tor_proxy: 5 | image: dockage/tor-privoxy:latest 6 | container_name: tor_proxy 7 | hostname: tor_proxy 8 | restart: always 9 | ports: 10 | - "9050:9050" 11 | - "9051:9051" 12 | - "8118:8118" 13 | networks: 14 | - arbitration_web 15 | 16 | redis: 17 | restart: always 18 | image: redis:latest 19 | container_name: redis 20 | networks: 21 | - arbitration_web 22 | 23 | db: 24 | image: postgres:15.2-alpine 25 | container_name: db 26 | restart: always 27 | volumes: 28 | - db_arbitration:/var/lib/postgresql/data 29 | env_file: 30 | - .env 31 | networks: 32 | - arbitration_web 33 | 34 | arbitration: 35 | build: 36 | context: ../arbitration 37 | container_name: arbitration 38 | restart: always 39 | volumes: 40 | - static_value:/arbitration/static/ 41 | depends_on: 42 | - db 43 | env_file: 44 | - .env 45 | networks: 46 | - arbitration_web 47 | 48 | nginx: 49 | image: nginx:1.23.3-alpine 50 | container_name: nginx 51 | restart: always 52 | ports: 53 | - "80:80" 54 | - "443:443" 55 | volumes: 56 | - ./nginx/local_default.conf:/etc/nginx/conf.d/local_default.conf 57 | - static_value:/var/html/static/ 58 | - ./certbot/conf:/etc/letsencrypt 59 | - ./certbot/www:/var/www/certbot 60 | env_file: 61 | - .env 62 | depends_on: 63 | - arbitration 64 | networks: 65 | - arbitration_web 66 | 67 | celery-parsing: 68 | restart: always 69 | build: 70 | context: ../arbitration 71 | entrypoint: celery 72 | command: -A arbitration worker --loglevel=ERROR -Q parsing -c 8 -n parsing_worker 73 | env_file: 74 | - .env 75 | networks: 76 | - arbitration_web 77 | links: 78 | - redis 79 | depends_on: 80 | - arbitration 81 | - redis 82 | 83 | celery-calculating: 84 | restart: always 85 | build: 86 | context: ../arbitration 87 | container_name: celery-calculating 88 | entrypoint: celery 89 | command: -A arbitration worker --loglevel=ERROR -Q calculating -c 3 -n calculating_worker 90 | env_file: 91 | - .env 92 | networks: 93 | - arbitration_web 94 | links: 95 | - redis 96 | depends_on: 97 | - arbitration 98 | - redis 99 | 100 | celery-beat: 101 | restart: always 102 | build: 103 | context: ../arbitration 104 | container_name: celery-beat 105 | entrypoint: celery 106 | command: -A arbitration beat --loglevel=ERROR 107 | env_file: 108 | - .env 109 | networks: 110 | - arbitration_web 111 | links: 112 | - celery-parsing 113 | - celery-calculating 114 | - redis 115 | depends_on: 116 | - celery-parsing 117 | - celery-calculating 118 | - redis 119 | - tor_proxy 120 | - nginx 121 | 122 | networks: 123 | arbitration_web: 124 | driver: bridge 125 | 126 | volumes: 127 | db_arbitration: 128 | static_value: -------------------------------------------------------------------------------- /arbitration/banks/migrations/0001_create_banks_and_currency_markets_exchanges_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:52 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Banks', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(blank=True, max_length=20, null=True)), 21 | ('binance_name', models.CharField(blank=True, max_length=20, null=True)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='CurrencyMarkets', 26 | fields=[ 27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=20)), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name='BanksExchangeRatesUpdates', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('updated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Update date')), 36 | ('duration', models.DurationField(default=datetime.timedelta(0))), 37 | ('bank', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bank_rates_update', to='banks.banks')), 38 | ('currency_market', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='currency_market_rates_update', to='banks.currencymarkets')), 39 | ], 40 | options={ 41 | 'abstract': False, 42 | }, 43 | ), 44 | migrations.CreateModel( 45 | name='BanksExchangeRates', 46 | fields=[ 47 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 48 | ('from_fiat', models.CharField(max_length=3)), 49 | ('to_fiat', models.CharField(max_length=3)), 50 | ('price', models.FloatField()), 51 | ('bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bank_rates', to='banks.banks')), 52 | ('currency_market', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='currency_market_rates', to='banks.currencymarkets')), 53 | ('update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datas', to='banks.banksexchangeratesupdates')), 54 | ], 55 | ), 56 | migrations.AddConstraint( 57 | model_name='banksexchangerates', 58 | constraint=models.UniqueConstraint(fields=('bank', 'from_fiat', 'to_fiat', 'currency_market'), name='unique_bank_exchanges'), 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/main.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load custom_filters %} 4 | {% load i18n static %} 5 | 6 | {% block title %} 7 | Арбитражные связки 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |
14 | {% include 'crypto_exchanges/includes/product_filter.html' %} 15 |
16 |
17 |

18 | Актуальные арбитражные связки: 19 |

20 |

21 | * C 09.03.23 Binance закрыла для российских карт возможность покупки и продажи долларов и евро через свой P2P-сервис. Сервисы Card2Crypto и Card2Wallet2Crypto уже давно не поддерживают российские карты. 22 |

23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
СвязкаМаржинальностьОбновлено
34 |
35 |
36 |
37 | 38 | 52 | {% endblock %} 53 | 54 | {% block extra_js %} 55 | 80 | 81 | {% endblock %} -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/0002_create_crypto_exchanges_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:56 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('banks', '0001_create_banks_and_currency_markets_exchanges_model'), 12 | ('crypto_exchanges', '0001_create_intra_crypto_exchanges_model'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='CryptoExchangesRatesUpdates', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('updated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Update date')), 21 | ('duration', models.DurationField(default=datetime.timedelta(0))), 22 | ('payment_channel', models.CharField(blank=True, max_length=30, null=True)), 23 | ('bank', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='crypto_exchange_rates_update', to='banks.banks')), 24 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crypto_exchange_rates_update', to='crypto_exchanges.cryptoexchanges')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='CryptoExchangesRates', 32 | fields=[ 33 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('asset', models.CharField(max_length=4)), 35 | ('fiat', models.CharField(max_length=3)), 36 | ('trade_type', models.CharField(max_length=4)), 37 | ('payment_channel', models.CharField(blank=True, max_length=30, null=True)), 38 | ('transaction_method', models.CharField(blank=True, max_length=30, null=True)), 39 | ('transaction_fee', models.FloatField(blank=True, default=None, null=True)), 40 | ('price', models.FloatField(blank=True, default=None, null=True)), 41 | ('pre_price', models.FloatField(blank=True, default=None, null=True)), 42 | ('bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crypto_exchange_rates', to='banks.banks')), 43 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crypto_exchange_rates', to='crypto_exchanges.cryptoexchanges')), 44 | ('intra_crypto_exchange', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='card_2_wallet_2_crypto_exchange_rates', to='crypto_exchanges.intracryptoexchangesrates')), 45 | ('update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datas', to='crypto_exchanges.cryptoexchangesratesupdates')), 46 | ], 47 | ), 48 | migrations.AddConstraint( 49 | model_name='cryptoexchangesrates', 50 | constraint=models.UniqueConstraint(fields=('crypto_exchange', 'bank', 'asset', 'trade_type', 'fiat', 'transaction_method', 'payment_channel'), name='unique_crypto_exchange_rates'), 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /arbitration/core/api_views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | from django_filters.views import FilterView 4 | from rest_framework import throttling 5 | from rest_framework.generics import ListAPIView 6 | from rest_framework.response import Response 7 | 8 | from arbitration.settings import INTER_EXCHANGES_OBSOLETE_IN_MINUTES 9 | from core.filters import ExchangesFilter 10 | from core.serializers import InterExchangesSerializer 11 | from crypto_exchanges.models import InterExchanges 12 | 13 | 14 | class InterExchangesAPIView(ListAPIView, FilterView): 15 | """ 16 | View returns a list of serialized InterExchanges objects in JSON format, 17 | which can be used to display exchange information in a web application. 18 | It also filters the list based on user search queries using the same 19 | ExchangesFilter. It uses a ListAPIView, and provides pagination and 20 | ordering capabilities for the data using a custom list method. It also 21 | implements rate limiting using the throttle_classes attribute. 22 | """ 23 | model = InterExchanges 24 | serializer_class = InterExchangesSerializer 25 | filterset_class = ExchangesFilter 26 | throttle_classes = (throttling.AnonRateThrottle,) 27 | 28 | def get_queryset(self): 29 | qs = self.model.objects.prefetch_related( 30 | 'input_bank', 'output_bank', 'bank_exchange', 31 | 'input_crypto_exchange', 'output_crypto_exchange', 32 | 'interim_crypto_exchange', 'second_interim_crypto_exchange', 33 | 'update' 34 | ).filter( 35 | update__updated__gte=( 36 | datetime.now(timezone.utc) - timedelta( 37 | minutes=INTER_EXCHANGES_OBSOLETE_IN_MINUTES 38 | ) 39 | ) 40 | ) 41 | self.filter = self.filterset_class(self.request.GET, queryset=qs) 42 | return self.filter.qs 43 | 44 | def filter_for_datatable(self, queryset): 45 | # ordering 46 | ordering_column = self.request.query_params.get('order[0][column]') 47 | ordering_direction = self.request.query_params.get('order[0][dir]') 48 | ordering = None 49 | if ordering_column == '0': 50 | ordering = 'diagram' 51 | if ordering_column == '1': 52 | ordering = 'marginality_percentage' 53 | if ordering and ordering_direction == 'desc': 54 | ordering = f"-{ordering}" 55 | if ordering: 56 | return queryset.order_by(ordering) 57 | return queryset 58 | 59 | def list(self, request, *args, **kwargs): 60 | draw = request.query_params.get('draw') 61 | queryset = self.get_queryset() 62 | records_total = queryset.count() 63 | filtered_queryset = self.filter_for_datatable(queryset) 64 | try: 65 | start = int(request.query_params.get('start')) 66 | except ValueError: 67 | start = 0 68 | try: 69 | length = int(request.query_params.get('length')) 70 | except ValueError: 71 | length = 10 72 | if length not in [10, 25, 50, 100]: 73 | length = 10 74 | end = length + start 75 | serializer = self.get_serializer(filtered_queryset[start:end], 76 | many=True) 77 | response = { 78 | 'draw': draw, 79 | 'recordsTotal': records_total, 80 | 'recordsFiltered': filtered_queryset.count(), 81 | 'data': serializer.data 82 | } 83 | return Response(response) 84 | -------------------------------------------------------------------------------- /infra/prod-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | 4 | tor_proxy: 5 | image: dockage/tor-privoxy:latest 6 | container_name: tor_proxy 7 | hostname: tor_proxy 8 | restart: always 9 | ports: 10 | - "9050:9050" 11 | - "9051:9051" 12 | - "8118:8118" 13 | networks: 14 | - arbitration_web 15 | 16 | redis: 17 | restart: always 18 | image: redis:latest 19 | container_name: redis 20 | networks: 21 | - arbitration_web 22 | 23 | db: 24 | image: postgres:15.2-alpine 25 | container_name: db 26 | restart: always 27 | volumes: 28 | - db_arbitration:/var/lib/postgresql/data 29 | env_file: 30 | - .env 31 | networks: 32 | - arbitration_web 33 | 34 | arbitration: 35 | image: nezhinsky/arbitration:latest 36 | container_name: arbitration 37 | restart: always 38 | volumes: 39 | - static_value:/arbitration/static/ 40 | depends_on: 41 | - db 42 | env_file: 43 | - .env 44 | networks: 45 | - arbitration_web 46 | 47 | nginx: 48 | image: nginx:1.23.3-alpine 49 | container_name: nginx 50 | restart: always 51 | ports: 52 | - "80:80" 53 | - "443:443" 54 | volumes: 55 | - ./nginx/prod_default.conf:/etc/nginx/conf.d/prod_default.conf 56 | - static_value:/var/html/static/ 57 | - ./certbot/conf:/etc/letsencrypt 58 | - ./certbot/www:/var/www/certbot 59 | env_file: 60 | - .env 61 | depends_on: 62 | - arbitration 63 | networks: 64 | - arbitration_web 65 | 66 | certbot: 67 | image: certbot/certbot 68 | container_name: certbot 69 | restart: always 70 | entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" 71 | command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" 72 | volumes: 73 | - ./certbot/conf:/etc/letsencrypt 74 | - ./certbot/www:/var/www/certbot 75 | env_file: 76 | - .env 77 | depends_on: 78 | - nginx 79 | networks: 80 | - arbitration_web 81 | 82 | 83 | celery-parsing: 84 | restart: always 85 | image: nezhinsky/arbitration:latest 86 | entrypoint: celery 87 | command: -A arbitration worker --loglevel=ERROR -Q parsing -c 8 -n parsing_worker 88 | env_file: 89 | - .env 90 | networks: 91 | - arbitration_web 92 | links: 93 | - redis 94 | depends_on: 95 | - arbitration 96 | - redis 97 | 98 | celery-calculating: 99 | restart: always 100 | image: nezhinsky/arbitration:latest 101 | container_name: celery-calculating 102 | entrypoint: celery 103 | command: -A arbitration worker --loglevel=ERROR -Q calculating -c 3 -n calculating_worker 104 | env_file: 105 | - .env 106 | networks: 107 | - arbitration_web 108 | links: 109 | - redis 110 | depends_on: 111 | - arbitration 112 | - redis 113 | 114 | celery-beat: 115 | restart: always 116 | image: nezhinsky/arbitration:latest 117 | container_name: celery-beat 118 | entrypoint: celery 119 | command: -A arbitration beat --loglevel=ERROR 120 | env_file: 121 | - .env 122 | networks: 123 | - arbitration_web 124 | links: 125 | - celery-parsing 126 | - celery-calculating 127 | - redis 128 | depends_on: 129 | - celery-parsing 130 | - celery-calculating 131 | - redis 132 | - tor_proxy 133 | - nginx 134 | 135 | networks: 136 | arbitration_web: 137 | driver: bridge 138 | 139 | volumes: 140 | db_arbitration: 141 | static_value: -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/crypto_exchanges_registration/bybit.py: -------------------------------------------------------------------------------- 1 | import os 2 | from abc import ABC 3 | from typing import Tuple 4 | 5 | from arbitration.settings import (API_BYBIT_CRYPTO, API_P2P_BYBIT, 6 | CONNECTION_TYPE_BYBIT_CRYPTO, 7 | CONNECTION_TYPE_P2P_BYBIT) 8 | from banks.models import BanksExchangeRates 9 | from parsers.parsers import CryptoExchangesParser, P2PParser 10 | 11 | CRYPTO_EXCHANGES_NAME = os.path.basename(__file__).split('.')[0].capitalize() 12 | 13 | BYBIT_ASSETS = ('ETH', 'BTC', 'USDC', 'USDT') 14 | BYBIT_CRYPTO_FIATS = ('EUR',) 15 | BYBIT_ASSETS_FOR_FIAT = {'all': ('BTC', 'ETH', 'USDC', 'USDT')} 16 | BYBIT_SPOT_ZERO_FEES = { 17 | 'USDC': [ 18 | 'ADA', 'APE', 'APEX', 'BTC', 'DOGE', 'CHZ', 'DOT', 'EOS', 'ETH', 'BIT', 19 | 'HFT', 'LDO', 'LTC', 'LUNC', 'SHIB', 'SLG', 'SOL', 'TRX', 'XRP' 20 | ], 21 | 'USDT': [ 22 | 'BUSD', 'DAI', 'USDC' 23 | ], 24 | 'BTC': [ 25 | 'WBTC' 26 | ] 27 | } 28 | BYBIT_INVALID_PARAMS_LIST = (('BTC', 'EUR'), ('ETH', 'EUR'), ('USDC', 'EUR')) 29 | 30 | 31 | class BybitP2PParser(P2PParser, ABC): 32 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 33 | endpoint: str = API_P2P_BYBIT 34 | user_agent_browser: str = 'safari' 35 | connection_type: str = CONNECTION_TYPE_P2P_BYBIT 36 | need_cookies: bool = True 37 | cookies_names: Tuple[str] = None 38 | page: int = 1 39 | rows: int = 1 40 | fake_useragent: bool = True 41 | custom_user_agent: Tuple[str] = ( 42 | 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; pt-pt) AppleWebKit/418.9.1 ' 43 | '(KHTML, like Gecko) Safari/419.3', 44 | ) 45 | # custom_settings 46 | fiat_for_amount: str = 'USD' 47 | min_amount: int = 100 48 | base_exchange_name: str = 'Wise' 49 | 50 | def __get_min_amount(self, fiat: str) -> str: 51 | if fiat == self.fiat_for_amount: 52 | return str(self.min_amount) 53 | target_exchange = BanksExchangeRates.objects.filter( 54 | bank__name=self.base_exchange_name, from_fiat=self.fiat_for_amount, 55 | to_fiat=fiat 56 | ) 57 | if target_exchange.exists(): 58 | return str(int(target_exchange.get().price * self.min_amount)) 59 | return '' 60 | 61 | def _check_supports_fiat(self, _) -> bool: 62 | return True 63 | 64 | def _create_body(self, asset: str, fiat: str, trade_type: str) -> dict: 65 | if trade_type == 'SELL': 66 | side = '0' 67 | else: 68 | side = '1' 69 | amount = self.__get_min_amount(fiat) 70 | return { 71 | "userId": "", 72 | "size": str(self.rows), 73 | "page": str(self.page), 74 | "tokenId": asset, 75 | "side": side, 76 | "currencyId": fiat, 77 | "payment": [self.bank.bybit_name], 78 | "amount": amount 79 | } 80 | 81 | @staticmethod 82 | def _extract_price_from_json(json_data: dict) -> float or None: 83 | result = json_data['result'] 84 | items = result['items'] 85 | count = result['count'] 86 | if count == 0: 87 | return None 88 | first_item = items[0] 89 | return float(first_item['price']) 90 | 91 | 92 | class BybitCryptoParser(CryptoExchangesParser): 93 | crypto_exchange_name: str = CRYPTO_EXCHANGES_NAME 94 | endpoint: str = API_BYBIT_CRYPTO 95 | connection_type: str = CONNECTION_TYPE_BYBIT_CRYPTO 96 | need_cookies: bool = False 97 | fake_useragent: bool = False 98 | name_from: str = 'symbol' 99 | base_spot_fee: float = 0.1 100 | zero_fees: dict = BYBIT_SPOT_ZERO_FEES 101 | 102 | @staticmethod 103 | def _extract_price_from_json(json_data: dict) -> float: 104 | result = json_data['result'] 105 | return float(result['price']) 106 | -------------------------------------------------------------------------------- /arbitration/crypto_exchanges/migrations/0004_create_inter_exchanges_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.7 on 2023-03-06 12:57 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('banks', '0001_create_banks_and_currency_markets_exchanges_model'), 12 | ('crypto_exchanges', '0003_create_lists_fiats_model'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='InterExchangesUpdates', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('updated', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Update date')), 21 | ('duration', models.DurationField(default=datetime.timedelta(0))), 22 | ('bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inter_exchanges_update', to='banks.banks')), 23 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inter_exchanges_update', to='crypto_exchanges.cryptoexchanges')), 24 | ], 25 | options={ 26 | 'abstract': False, 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='InterExchanges', 31 | fields=[ 32 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('marginality_percentage', models.FloatField(verbose_name='Marginality percentage')), 34 | ('diagram', models.CharField(blank=True, max_length=100, null=True)), 35 | ('dynamics', models.CharField(default=None, max_length=4, null=True)), 36 | ('new', models.BooleanField(default=True, null=True)), 37 | ('bank_exchange', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bank_rate_inter_exchanges', to='banks.banksexchangerates')), 38 | ('crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inter_exchanges', to='crypto_exchanges.cryptoexchanges')), 39 | ('input_bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='input_bank_inter_exchanges', to='banks.banks')), 40 | ('input_crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='input_crypto_exchange_inter_exchanges', to='crypto_exchanges.cryptoexchangesrates')), 41 | ('interim_crypto_exchange', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interim_exchange_inter_exchanges', to='crypto_exchanges.intracryptoexchangesrates')), 42 | ('output_bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='output_bank_inter_exchanges', to='banks.banks')), 43 | ('output_crypto_exchange', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='output_crypto_exchange_inter_exchanges', to='crypto_exchanges.cryptoexchangesrates')), 44 | ('second_interim_crypto_exchange', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='second_interim_exchange_inter_exchanges', to='crypto_exchanges.intracryptoexchangesrates')), 45 | ('update', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datas', to='crypto_exchanges.interexchangesupdates')), 46 | ], 47 | options={ 48 | 'ordering': ['-marginality_percentage'], 49 | }, 50 | ), 51 | migrations.AddConstraint( 52 | model_name='interexchanges', 53 | constraint=models.UniqueConstraint(fields=('crypto_exchange', 'input_bank', 'output_bank', 'input_crypto_exchange', 'interim_crypto_exchange', 'second_interim_crypto_exchange', 'output_crypto_exchange', 'bank_exchange'), name='unique_inter_exchanges'), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /arbitration/banks/banks_config.py: -------------------------------------------------------------------------------- 1 | from banks.banks_registration.bank_of_georgia import BOG_CURRENCIES 2 | from banks.banks_registration.credo import CREDO_CURRENCIES 3 | from banks.banks_registration.qiwi import QIWI_CURRENCIES 4 | from banks.banks_registration.raiffeisen import RAIFFEISEN_CURRENCIES 5 | from banks.banks_registration.sberbank import SBERBANK_CURRENCIES 6 | from banks.banks_registration.tbc import TBC_CURRENCIES 7 | from banks.banks_registration.tinkoff import TINKOFF_CURRENCIES 8 | from banks.banks_registration.wise import WISE_CURRENCIES 9 | from banks.banks_registration.yoomoney import YOOMONEY_CURRENCIES 10 | 11 | INTERNATIONAL_BANKS = ('Wise', 'Bank of Georgia', 'TBC', 'Credo') 12 | RUS_BANKS = ('Tinkoff', 'Sberbank', 'Raiffeisen', 'QIWI', 'Yoomoney') 13 | 14 | BANKS_CONFIG = { 15 | 'Tinkoff': { 16 | 'bank_parser': True, 17 | 'currencies': TINKOFF_CURRENCIES, 18 | 'crypto_exchanges': ('Binance',), 19 | 'binance_name': 'TinkoffNew', 20 | 'bybit_name': '75', 21 | 'payment_channels': ( 22 | 'Card2CryptoExchange', 'Card2Wallet2CryptoExchange', 'P2P' 23 | ), 24 | 'transaction_methods': ( 25 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 26 | ), 27 | 'bank_invest_exchanges': ['Tinkoff invest'] 28 | }, 29 | 'Sberbank': { 30 | 'bank_parser': False, 31 | 'currencies': SBERBANK_CURRENCIES, 32 | 'crypto_exchanges': ('Binance',), 33 | 'binance_name': 'RosBankNew', 34 | 'bybit_name': '185', 35 | 'payment_channels': ( 36 | 'Card2CryptoExchange', 'Card2Wallet2CryptoExchange', 'P2P' 37 | ), 38 | 'transaction_methods': ( 39 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 40 | ), 41 | 'bank_invest_exchanges': [] 42 | }, 43 | 'Raiffeisen': { 44 | 'bank_parser': True, 45 | 'currencies': RAIFFEISEN_CURRENCIES, 46 | 'crypto_exchanges': ('Binance',), 47 | 'binance_name': 'RaiffeisenBank', 48 | 'bybit_name': '64', 49 | 'payment_channels': ( 50 | 'Card2CryptoExchange', 'Card2Wallet2CryptoExchange', 'P2P' 51 | ), 52 | 'transaction_methods': ( 53 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 54 | ), 55 | 'bank_invest_exchanges': () 56 | }, 57 | 'QIWI': { 58 | 'bank_parser': False, 59 | 'currencies': QIWI_CURRENCIES, 60 | 'crypto_exchanges': ('Binance',), 61 | 'binance_name': 'QIWI', 62 | 'bybit_name': '62', 63 | 'payment_channels': ( 64 | 'Card2CryptoExchange', 'Card2Wallet2CryptoExchange', 'P2P' 65 | ), 66 | 'transaction_methods': ( 67 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 68 | ), 69 | 'bank_invest_exchanges': () 70 | }, 71 | 'Yoomoney': { 72 | 'bank_parser': False, 73 | 'currencies': YOOMONEY_CURRENCIES, 74 | 'crypto_exchanges': ('Binance',), 75 | 'binance_name': 'YandexMoneyNew', 76 | 'bybit_name': '274', 77 | 'payment_channels': ( 78 | 'Card2CryptoExchange', 'Card2Wallet2CryptoExchange', 'P2P' 79 | ), 80 | 'transaction_methods': ( 81 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 82 | ), 83 | 'bank_invest_exchanges': () 84 | }, 85 | 'Bank of Georgia': { 86 | 'bank_parser': False, 87 | 'currencies': BOG_CURRENCIES, 88 | 'crypto_exchanges': ('Binance',), 89 | 'binance_name': 'BankofGeorgia', 90 | 'bybit_name': '11', 91 | 'payment_channels': ( 92 | 'Card2CryptoExchange', 'P2P' 93 | ), 94 | 'transaction_methods': ( 95 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 96 | ), 97 | 'bank_invest_exchanges': () 98 | }, 99 | 'TBC': { 100 | 'bank_parser': False, 101 | 'currencies': TBC_CURRENCIES, 102 | 'crypto_exchanges': ('Binance',), 103 | 'binance_name': 'TBCbank', 104 | 'bybit_name': '165', 105 | 'payment_channels': ( 106 | 'Card2CryptoExchange', 'P2P' 107 | ), 108 | 'transaction_methods': ( 109 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 110 | ), 111 | 'bank_invest_exchanges': () 112 | }, 113 | 'Credo': { 114 | 'bank_parser': False, 115 | 'currencies': CREDO_CURRENCIES, 116 | 'crypto_exchanges': ('Binance',), 117 | 'binance_name': 'CREDOBANK', 118 | 'bybit_name': '359', 119 | 'payment_channels': ( 120 | 'Card2CryptoExchange', 'P2P' 121 | ), 122 | 'transaction_methods': ( 123 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 124 | ), 125 | 'bank_invest_exchanges': () 126 | }, 127 | 'Wise': { 128 | 'bank_parser': True, 129 | 'currencies': WISE_CURRENCIES, 130 | 'crypto_exchanges': ('Binance',), 131 | 'binance_name': 'Wise', 132 | 'bybit_name': '78', 133 | 'payment_channels': ( 134 | 'Card2CryptoExchange', 'Card2Wallet2CryptoExchange', 'P2P' 135 | ), 136 | 'transaction_methods': ( 137 | 'Bank Card (Visa/MC)', 'Bank Card (Visa)' 138 | ), 139 | 'bank_invest_exchanges': () 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /arbitration/templates/crypto_exchanges/includes/modal_fade.html: -------------------------------------------------------------------------------- 1 | {% load custom_filters %} 2 | 3 | {##} 4 | 5 |

6 | 7 | 8 | 9 | 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 | ![example workflow](https://github.com/Nezhinskiy/Assets-Loop/actions/workflows/arbitration_workflow.yml/badge.svg) 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 | │   ├── parsersBusiness 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 | '' + 236 | '' + 237 | '' + 238 | '' + 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('Инструкция к связке:

'+diagram+'

'); 269 | $(this).find(".modal-body").html("
"+content+"
"); 270 | }); 271 | 272 | $( document ).ajaxComplete(function( event, request, settings ) { 273 | $('[data-toggle="tooltip"]').not( '[data-original-title]' 274 | ).tooltip(); 275 | }); -------------------------------------------------------------------------------- /arbitration/arbitration/settings.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import os 3 | import random 4 | import sys 5 | from datetime import timedelta, timezone 6 | from pathlib import Path 7 | from typing import List 8 | 9 | from celery.schedules import crontab 10 | from django.core.management.utils import get_random_secret_key 11 | from django.utils.log import DEFAULT_LOGGING 12 | from dotenv import load_dotenv 13 | 14 | load_dotenv() 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', get_random_secret_key()) 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = os.getenv('DEBUG', 'False') == 'True' 24 | 25 | LOCAL = os.getenv('LOCAL', 'True') == 'True' 26 | 27 | ALLOWED_HOSTS = ['*'] if LOCAL else os.getenv('ALLOWED_HOSTS').split() 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'django_celery_beat', 39 | 'django_filters', 40 | 'rest_framework', 41 | 'bootstrap4', 42 | 'django_select2', 43 | 'widget_tweaks', 44 | 'crypto_exchanges', 45 | 'banks', 46 | 'core', 47 | 'parsers', 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'django.middleware.security.SecurityMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.middleware.csrf.CsrfViewMiddleware', 55 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'arbitration.urls' 61 | 62 | TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates') 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [TEMPLATES_DIR], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'arbitration.wsgi.application' 81 | 82 | # Database 83 | 84 | DEVELOPMENT_MODE = os.getenv("DEVELOPMENT_MODE", "True") == "True" 85 | 86 | if DEVELOPMENT_MODE: 87 | DATABASES = { 88 | "default": { 89 | "ENGINE": "django.db.backends.sqlite3", 90 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 91 | } 92 | } 93 | elif len(sys.argv) > 0 and sys.argv[1] != 'collectstatic': 94 | DATABASES = { 95 | 'default': { 96 | 'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'), 97 | 'NAME': os.getenv('DB_NAME'), 98 | 'USER': os.getenv('POSTGRES_USER'), 99 | 'PASSWORD': os.getenv('POSTGRES_PASSWORD'), 100 | 'HOST': os.getenv('DB_HOST'), 101 | 'PORT': os.getenv('DB_PORT'), 102 | }, 103 | } 104 | 105 | # Password validation 106 | 107 | AUTH_PASSWORD_VALIDATORS = [ 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 119 | }, 120 | ] 121 | 122 | # Disable Django's logging setup 123 | 124 | LOGGING_CONFIG = None 125 | 126 | # DRF config 127 | 128 | REST_FRAMEWORK = { 129 | 'DEFAULT_THROTTLE_CLASSES': [ 130 | 'rest_framework.throttling.UserRateThrottle', 131 | ], 132 | 133 | 'DEFAULT_THROTTLE_RATES': { 134 | 'anon': '1300/hour', 135 | }, 136 | } 137 | 138 | # Logging config 139 | 140 | LOGLEVEL = os.environ.get('LOGLEVEL', 'error').upper() 141 | 142 | logging.config.dictConfig({ 143 | 'version': 1, 144 | 'disable_existing_loggers': False, 145 | 'formatters': { 146 | 'default': { 147 | # exact format is not important, this is the minimum information 148 | 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', 149 | }, 150 | 'django.server': DEFAULT_LOGGING['formatters']['django.server'], 151 | }, 152 | 'handlers': { 153 | # console logs to stderr 154 | 'console': { 155 | 'class': 'logging.StreamHandler', 156 | 'formatter': 'default', 157 | }, 158 | 'django.server': DEFAULT_LOGGING['handlers']['django.server'], 159 | }, 160 | 'loggers': { 161 | # default for all undefined Python modules 162 | '': { 163 | 'level': 'WARNING', 164 | 'handlers': ['console'], 165 | }, 166 | # Our application code 167 | 'app': { 168 | 'level': LOGLEVEL, 169 | 'handlers': ['console'], 170 | # Avoid double logging because of root logger 171 | 'propagate': False, 172 | }, 173 | # Default runserver request logging 174 | 'django.server': DEFAULT_LOGGING['loggers']['django.server'], 175 | }, 176 | }) 177 | 178 | # Internationalization 179 | LANGUAGE_CODE = 'ru-ru' # 'en-us' 180 | 181 | TIME_ZONE = 'UTC' 182 | 183 | USE_I18N = True 184 | 185 | USE_L10N = True 186 | 187 | USE_TZ = True 188 | 189 | 190 | # Static files (CSS, JavaScript, Images) 191 | STATIC_URL = '/static/' 192 | 193 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 194 | 195 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 196 | 197 | # Custom setting 198 | 199 | PARSING_WORKER_NAME: str = 'celery@parsing_worker' 200 | BASE_ASSET: str = 'USDT' # Preferred cryptocurrency for internal exchanges on a crypto exchanges. 201 | DATA_OBSOLETE_IN_MINUTES: int = 10 # The time in minutes since the last update, after which the data is considered out of date and does not participate in calculations. 202 | INTER_EXCHANGES_OBSOLETE_IN_MINUTES: int = 15 # The time in minutes since the last update, after which the interexchange exchange is considered obsolete and is not displayed on the page. 203 | INTER_EXCHANGES_BEGIN_OBSOLETE_MINUTES: int = 2 # The time in minutes since the last update, after which the inter-exchange exchange becomes obsolete and is displayed on the page in gray. 204 | ALLOWED_PERCENTAGE: int = int(os.getenv('ALLOWED_PERCENTAGE', '0')) # The maximum margin percentage above which data is considered invalid. (Due to an error in the crypto exchange data) 205 | MINIMUM_PERCENTAGE: int = int(os.getenv('MINIMUM_PERCENTAGE', '-3')) 206 | COUNTRIES_NEAR_SERVER: List[str] = os.getenv('COUNTRIES_NEAR_SERVER', '0').split() 207 | 208 | # Update frequency 209 | UPDATE_RATE: tuple[int] = tuple(map(int, os.getenv('UPDATE_RATE', '0').replace(',', '').split())) # Update frequency schedule. 210 | P2P_BINANCE_UPDATE_FREQUENCY: int = int(os.getenv('P2P_BINANCE_UPDATE_FREQUENCY', '0')) 211 | P2P_BYBIT_UPDATE_FREQUENCY: int = int(os.getenv('P2P_BYBIT_UPDATE_FREQUENCY', '0')) 212 | INTERNAL_BANKS_UPDATE_FREQUENCY: int = int(os.getenv('INTERNAL_BANKS_UPDATE_FREQUENCY', '0')) 213 | EXCHANGES_BINANCE_UPDATE_FREQUENCY: int = int(os.getenv('EXCHANGES_BINANCE_UPDATE_FREQUENCY', '0')) 214 | EXCHANGES_BYBIT_UPDATE_FREQUENCY: int = int(os.getenv('EXCHANGES_BYBIT_UPDATE_FREQUENCY', '0')) 215 | CARD_2_CRYPTO_BINANCE_UPDATE_FREQUENCY: int = int(os.getenv('CARD_2_CRYPTO_BINANCE_UPDATE_FREQUENCY', '0')) 216 | 217 | 218 | # Models 219 | FIAT_LENGTH: int = 3 220 | ASSET_LENGTH: int = 4 221 | TRADE_TYPE_LENGTH: int = 4 222 | NAME_LENGTH: int = 20 223 | CHANNEL_LENGTH: int = 30 224 | DIAGRAM_LENGTH: int = 100 225 | 226 | # Logger 227 | LOGLEVEL_PARSING_START: str = os.getenv('LOGLEVEL_PARSING_START', '') 228 | LOGLEVEL_PARSING_END: str = os.getenv('LOGLEVEL_PARSING_END', '') 229 | LOGLEVEL_CALCULATING_START: str = os.getenv('LOGLEVEL_CALCULATING_START', '') 230 | LOGLEVEL_CALCULATING_END: str = os.getenv('LOGLEVEL_CALCULATING_END', '') 231 | 232 | 233 | # Endpoints 234 | API_P2P_BINANCE: str = os.getenv('API_P2P_BINANCE', '') 235 | API_P2P_BYBIT: str = os.getenv('API_P2P_BYBIT', '') 236 | API_BINANCE_CARD_2_CRYPTO_SELL: str = os.getenv('API_BINANCE_CARD_2_CRYPTO_SELL', '') 237 | API_BINANCE_CARD_2_CRYPTO_BUY: str = os.getenv('API_BINANCE_CARD_2_CRYPTO_BUY', '') 238 | API_BINANCE_LIST_FIAT_SELL: str = os.getenv('API_BINANCE_LIST_FIAT_SELL', '') 239 | API_BINANCE_LIST_FIAT_BUY: str = os.getenv('API_BINANCE_LIST_FIAT_BUY', '') 240 | API_BINANCE_CRYPTO: str = os.getenv('API_BINANCE_CRYPTO', '') 241 | API_BYBIT_CRYPTO: str = os.getenv('API_BYBIT_CRYPTO', '') 242 | API_WISE: str = os.getenv('API_WISE', '') 243 | API_RAIFFEISEN: str = os.getenv('API_RAIFFEISEN', '') 244 | API_TINKOFF: str = os.getenv('API_TINKOFF', '') 245 | API_TINKOFF_INVEST: str = os.getenv('API_TINKOFF_INVEST', '') 246 | 247 | # Connection types 248 | CONNECTION_TYPE_P2P_BINANCE: str = os.getenv('CONNECTION_TYPE_P2P_BINANCE', '') 249 | CONNECTION_TYPE_P2P_BYBIT: str = os.getenv('CONNECTION_TYPE_P2P_BYBIT', '') 250 | CONNECTION_TYPE_BINANCE_CARD_2_CRYPTO: str = os.getenv('CONNECTION_TYPE_BINANCE_CARD_2_CRYPTO', '') 251 | CONNECTION_TYPE_BINANCE_LIST_FIAT: str = os.getenv('CONNECTION_TYPE_BINANCE_LIST_FIAT', '') 252 | CONNECTION_TYPE_BINANCE_CRYPTO: str = os.getenv('CONNECTION_TYPE_BINANCE_CRYPTO', '') 253 | CONNECTION_TYPE_BYBIT_CRYPTO: str = os.getenv('CONNECTION_TYPE_BYBIT_CRYPTO', '') 254 | CONNECTION_TYPE_WISE: str = os.getenv('CONNECTION_TYPE_WISE', '') 255 | CONNECTION_TYPE_RAIFFEISEN: str = os.getenv('CONNECTION_TYPE_RAIFFEISEN', '') 256 | CONNECTION_TYPE_TINKOFF: str = os.getenv('CONNECTION_TYPE_TINKOFF', '') 257 | CONNECTION_TYPE_TINKOFF_INVEST: str = os.getenv('CONNECTION_TYPE_TINKOFF_INVEST', '') 258 | 259 | 260 | # URLs 261 | INFO_URL: str = os.getenv('INFO_URL', '') 262 | START_URL: str = os.getenv('START_URL', '') 263 | STOP_URL: str = os.getenv('STOP_URL', '') 264 | REGISTRATION_URL: str = os.getenv('REGISTRATION_URL', '') 265 | 266 | 267 | # Celery settings 268 | 269 | CELERY_TASK_ACKS_LATE = True 270 | CELERY_TIMEZONE = timezone.utc 271 | 272 | CELERY_BROKER_URL = "redis://redis:6379" 273 | CELERY_RESULT_BACKEND = "redis://redis:6379" 274 | 275 | # Celery beat settings 276 | 277 | CELERY_BEAT_SCHEDULE = { 278 | 'get_binance_fiat_crypto_list': { 279 | 'task': 'crypto_exchanges.tasks.get_binance_fiat_crypto_list', 280 | 'schedule': crontab(hour='*/12'), 281 | 'options': {'queue': 'parsing'} 282 | }, 283 | 'parse_currency_market_tinkoff_rates': { 284 | 'task': 'banks.tasks.parse_currency_market_tinkoff_rates', 285 | 'schedule': crontab(minute='*/3', hour='4-15', day_of_week='1-5'), 286 | 'options': {'queue': 'parsing'} 287 | }, 288 | 'get_all_card_2_wallet_2_crypto_exchanges_buy': { 289 | 'task': 'crypto_exchanges.tasks.get_all_card_2_wallet_2_crypto_exchanges_buy', 290 | 'schedule': timedelta(seconds=random.randint(45, 50)), 291 | 'options': {'queue': 'calculating'} 292 | }, 293 | 'get_all_card_2_wallet_2_crypto_exchanges_sell': { 294 | 'task': 'crypto_exchanges.tasks.get_all_card_2_wallet_2_crypto_exchanges_sell', 295 | 'schedule': timedelta(seconds=random.randint(45, 50)), 296 | 'options': {'queue': 'calculating'} 297 | }, 298 | 'get_simpl_inter_exchanges_calculating': { 299 | 'task': 'core.tasks.get_simpl_inter_exchanges_calculating', 300 | 'schedule': timedelta(seconds=random.randint(20, 25)), 301 | 'options': {'queue': 'calculating'}, 302 | 'args': (False,), 303 | }, 304 | 'get_simpl_international_inter_exchanges_calculating': { 305 | 'task': 'core.tasks.get_simpl_international_inter_exchanges_calculating', 306 | 'schedule': timedelta(seconds=random.randint(20, 25)), 307 | 'options': {'queue': 'calculating'}, 308 | 'args': (False,), 309 | }, 310 | 'get_complex_inter_exchanges_calculating': { 311 | 'task': 'core.tasks.get_complex_inter_exchanges_calculating', 312 | 'schedule': timedelta(seconds=random.randint(30, 35)), 313 | 'options': {'queue': 'calculating'}, 314 | 'args': (False,), 315 | }, 316 | 'get_complex_international_inter_exchanges_calculating': { 317 | 'task': 'core.tasks.get_complex_international_inter_exchanges_calculating', 318 | 'schedule': timedelta(seconds=random.randint(30, 35)), 319 | 'options': {'queue': 'calculating'}, 320 | 'args': (False,), 321 | }, 322 | 'get_simpl_full_update_inter_exchanges_calculating': { 323 | 'task': 'core.tasks.get_simpl_inter_exchanges_calculating', 324 | 'schedule': timedelta(minutes=random.randint(10, 15)), 325 | 'options': {'queue': 'calculating'}, 326 | 'args': (True,), 327 | }, 328 | 'get_simpl_full_update_international_inter_exchanges_calculating': { 329 | 'task': 'core.tasks.get_simpl_international_inter_exchanges_calculating', 330 | 'schedule': timedelta(minutes=random.randint(10, 15)), 331 | 'options': {'queue': 'calculating'}, 332 | 'args': (True,), 333 | }, 334 | 'get_complex_full_update_inter_exchanges_calculating': { 335 | 'task': 'core.tasks.get_complex_inter_exchanges_calculating', 336 | 'schedule': timedelta(minutes=random.randint(15, 20)), 337 | 'options': {'queue': 'calculating'}, 338 | 'args': (True,), 339 | }, 340 | 'get_complex_full_update_international_inter_exchanges_calculating': { 341 | 'task': 'core.tasks.get_complex_international_inter_exchanges_calculating', 342 | 'schedule': timedelta(minutes=random.randint(15, 20)), 343 | 'options': {'queue': 'calculating'}, 344 | 'args': (True,), 345 | }, 346 | } 347 | 348 | # Tell select2 which cache configuration to use: 349 | 350 | CACHES = { 351 | 'default': { 352 | "BACKEND": "django_redis.cache.RedisCache", 353 | "LOCATION": "redis://redis:6379/2", 354 | }, 355 | # … default cache config and others 356 | "select2": { 357 | "BACKEND": "django_redis.cache.RedisCache", 358 | "LOCATION": "redis://redis:6379/2", 359 | "OPTIONS": { 360 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 361 | } 362 | } 363 | } 364 | 365 | SELECT2_CACHE_BACKEND = 'select2' 366 | --------------------------------------------------------------------------------