diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa99304 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media + + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/README.md b/README.md index 1fb5bf9..e3383ac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ +# CryptoTracker project description +We implement a crypto tracker project using djano rest framework. + +Check List of requested features: +- [x] User able to signup (name, username, password and KuCoin information:{api_key, secret_key and passphrase}) +- [x] User able to sign in (using django rest token authentication) +- [x] User able to request position tracking and cancel it. (using **APScheduler** library every 30 secs fetch user's positions) +- [x] User able to see list of open position +- [x] Application track user's positions (if user requested for it) every 30 secs +- [x] Application stores users that requested for positions tracking and track their positions in a interval (Handeling muti users) +- [x] Application start position tracking by starting application (resume traking with restarting application) + +Also you can see the API documentation created using postman in the link below: + +[CryptoTracker API documentation](https://documenter.getpostman.com/view/2983315/UzR1L3Ai) + + + # In name of Allah ## Introduction diff --git a/cryptoTracker/accounts/__init__.py b/cryptoTracker/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoTracker/accounts/admin.py b/cryptoTracker/accounts/admin.py new file mode 100644 index 0000000..7e2c4ff --- /dev/null +++ b/cryptoTracker/accounts/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import CustomUser +# Register your models here. + +admin.site.register(CustomUser) + diff --git a/cryptoTracker/accounts/apps.py b/cryptoTracker/accounts/apps.py new file mode 100644 index 0000000..7681965 --- /dev/null +++ b/cryptoTracker/accounts/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' + \ No newline at end of file diff --git a/cryptoTracker/accounts/migrations/0001_initial.py b/cryptoTracker/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..4ca308e --- /dev/null +++ b/cryptoTracker/accounts/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 4.0.6 on 2022-07-17 13:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('name', models.CharField(max_length=100)), + ('username', models.CharField(max_length=100)), + ('api_key', models.CharField(max_length=200)), + ('secret_key', models.CharField(max_length=200)), + ('api_passphrase', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + ] diff --git a/cryptoTracker/accounts/migrations/0002_delete_profile_alter_customuser_options_and_more.py b/cryptoTracker/accounts/migrations/0002_delete_profile_alter_customuser_options_and_more.py new file mode 100644 index 0000000..1e8c3ce --- /dev/null +++ b/cryptoTracker/accounts/migrations/0002_delete_profile_alter_customuser_options_and_more.py @@ -0,0 +1,96 @@ +# Generated by Django 4.0.6 on 2022-07-20 08:22 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone +import fernet_fields.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='Profile', + ), + migrations.AlterModelOptions( + name='customuser', + options={'verbose_name': 'user', 'verbose_name_plural': 'users'}, + ), + migrations.AlterModelManagers( + name='customuser', + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.AddField( + model_name='customuser', + name='date_joined', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), + ), + migrations.AddField( + model_name='customuser', + name='email', + field=models.EmailField(blank=True, max_length=254, verbose_name='email address'), + ), + migrations.AddField( + model_name='customuser', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + migrations.AddField( + model_name='customuser', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), + ), + migrations.AddField( + model_name='customuser', + name='is_active', + field=models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active'), + ), + migrations.AddField( + model_name='customuser', + name='is_staff', + field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'), + ), + migrations.AddField( + model_name='customuser', + name='is_superuser', + field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'), + ), + migrations.AddField( + model_name='customuser', + name='last_name', + field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + ), + migrations.AddField( + model_name='customuser', + name='user_permissions', + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), + ), + migrations.AlterField( + model_name='customuser', + name='api_key', + field=fernet_fields.fields.EncryptedCharField(max_length=200), + ), + migrations.AlterField( + model_name='customuser', + name='api_passphrase', + field=fernet_fields.fields.EncryptedCharField(max_length=200), + ), + migrations.AlterField( + model_name='customuser', + name='secret_key', + field=fernet_fields.fields.EncryptedCharField(max_length=200), + ), + migrations.AlterField( + model_name='customuser', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/cryptoTracker/accounts/migrations/0003_order_activetrackinguser.py b/cryptoTracker/accounts/migrations/0003_order_activetrackinguser.py new file mode 100644 index 0000000..960576a --- /dev/null +++ b/cryptoTracker/accounts/migrations/0003_order_activetrackinguser.py @@ -0,0 +1,40 @@ +# Generated by Django 4.0.6 on 2022-07-21 09:21 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_delete_profile_alter_customuser_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order_id', models.CharField(max_length=100)), + ('symbol', models.CharField(max_length=50)), + ('opType', models.CharField(max_length=20)), + ('type', models.CharField(max_length=20)), + ('side', models.CharField(max_length=20)), + ('price', models.CharField(max_length=20)), + ('size', models.CharField(max_length=20)), + ('fee', models.CharField(max_length=20)), + ('isActive', models.CharField(max_length=20)), + ('order_createdAt', models.IntegerField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ActiveTrackingUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('track', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/cryptoTracker/accounts/migrations/0004_remove_order_user_delete_activetrackinguser_and_more.py b/cryptoTracker/accounts/migrations/0004_remove_order_user_delete_activetrackinguser_and_more.py new file mode 100644 index 0000000..16f56cc --- /dev/null +++ b/cryptoTracker/accounts/migrations/0004_remove_order_user_delete_activetrackinguser_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.6 on 2022-07-21 12:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_order_activetrackinguser'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='user', + ), + migrations.DeleteModel( + name='ActiveTrackingUser', + ), + migrations.DeleteModel( + name='Order', + ), + ] diff --git a/cryptoTracker/accounts/migrations/__init__.py b/cryptoTracker/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoTracker/accounts/models.py b/cryptoTracker/accounts/models.py new file mode 100644 index 0000000..4a555d3 --- /dev/null +++ b/cryptoTracker/accounts/models.py @@ -0,0 +1,14 @@ +from symtable import Symbol +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, AbstractUser +from cryptography.fernet import Fernet +from django.conf import settings +from fernet_fields import EncryptedCharField +from django.contrib.auth.models import UserManager + +class CustomUser(AbstractUser): + name = models.CharField(max_length=100) + api_key = EncryptedCharField(max_length=200) + secret_key = EncryptedCharField(max_length=200) + api_passphrase = EncryptedCharField(max_length=200) + diff --git a/cryptoTracker/accounts/serializers.py b/cryptoTracker/accounts/serializers.py new file mode 100644 index 0000000..e6a293f --- /dev/null +++ b/cryptoTracker/accounts/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers +from .models import CustomUser +from rest_framework.validators import UniqueValidator + + +class RegisterSerializer(serializers.Serializer): + name = serializers.CharField(required=True, write_only=True) + username = serializers.CharField( + required=True, write_only=True, + validators=[UniqueValidator(queryset=CustomUser.objects.all(), message='a user is already registered with this username!')] + ) + api_key = serializers.CharField(required=True, write_only=True) + secret_key = serializers.CharField(required=True, write_only=True) + api_passphrase = serializers.CharField(required=True, write_only=True) + password = serializers.CharField(required=True, write_only=True, style={"input_type": "password"}) + + def create(self, validated_data): + user = CustomUser.objects.create(**validated_data) + user.set_password(validated_data['password']) + user.save() + return user + +class LoginSerializer(serializers.Serializer): + username = serializers.CharField(required=True, write_only=True) + password = serializers.CharField(required=True, write_only=True, style={"input_type": "password"}) + + def validate(self, attrs): + return super().validate(attrs) + diff --git a/cryptoTracker/accounts/tests.py b/cryptoTracker/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cryptoTracker/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cryptoTracker/accounts/urls.py b/cryptoTracker/accounts/urls.py new file mode 100644 index 0000000..700a5b5 --- /dev/null +++ b/cryptoTracker/accounts/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from .views import ( + RegisterAPIView, + LogoutAPIView +) +from rest_framework.authtoken.views import obtain_auth_token + + +urlpatterns = [ + path('register/', RegisterAPIView.as_view()), + path('login/', view=obtain_auth_token), + path('logout/', LogoutAPIView.as_view()), + +] diff --git a/cryptoTracker/accounts/views.py b/cryptoTracker/accounts/views.py new file mode 100644 index 0000000..646b5ae --- /dev/null +++ b/cryptoTracker/accounts/views.py @@ -0,0 +1,24 @@ +from rest_framework.generics import ( + CreateAPIView, +) +from .models import CustomUser +from .serializers import RegisterSerializer +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + + +class RegisterAPIView(CreateAPIView): + serializer_class = RegisterSerializer + permission_classes = (AllowAny,) + queryset = CustomUser.objects.all() + + +class LogoutAPIView(APIView): + permission_classes = (IsAuthenticated, ) + + def post(self, request): + request.user.auth_token.delete() + return Response(data={'message': f" {request.user.username} logged out"}, status=status.HTTP_200_OK) + diff --git a/cryptoTracker/cryptoTracker/__init__.py b/cryptoTracker/cryptoTracker/__init__.py new file mode 100644 index 0000000..aa60bed --- /dev/null +++ b/cryptoTracker/cryptoTracker/__init__.py @@ -0,0 +1,3 @@ +import pymysql + +pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/cryptoTracker/cryptoTracker/asgi.py b/cryptoTracker/cryptoTracker/asgi.py new file mode 100644 index 0000000..dc9fc74 --- /dev/null +++ b/cryptoTracker/cryptoTracker/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for cryptoTracker project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cryptoTracker.settings') + +application = get_asgi_application() diff --git a/cryptoTracker/cryptoTracker/settings.py b/cryptoTracker/cryptoTracker/settings.py new file mode 100644 index 0000000..2d5a353 --- /dev/null +++ b/cryptoTracker/cryptoTracker/settings.py @@ -0,0 +1,151 @@ +""" +Django settings for cryptoTracker project. + +Generated by 'django-admin startproject' using Django 4.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Secret Key for Fernet Encryption +# FERNET_KEYS = [ +# 'F782I-1B-1PUwdXH7NeC87xVznRz4GaQIzjPocqaY2A=', +# 'F782I-1B-1PUwdXH7NeC87xVznRz4GaQIzjPocqaY2A=', +# ] + +# FERNET_KEY = b'F782I-1B-1PUwdXH7NeC87xVznRz4GaQIzjPocqaY2A=' +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-&wtglv3v#$g8z4jluy4nf!i+(1ea^_l3sn1a9@uual&^ilid2d' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +# Custom user authentication +AUTH_USER_MODEL = 'accounts.CustomUser' +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'accounts', + 'tracker', + 'rest_framework.authtoken' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'cryptoTracker.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'cryptoTracker.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.mysql', +# 'NAME': BASE_DIR / 'db.sqlite3', +# } +# } + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'cryptodb', + 'USER': 'root', + 'PASSWORD': 'password', + 'HOST': 'localhost', + 'PORT': '3306' + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +#Rest framework settings +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + ], +} + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/cryptoTracker/cryptoTracker/urls.py b/cryptoTracker/cryptoTracker/urls.py new file mode 100644 index 0000000..d5fc774 --- /dev/null +++ b/cryptoTracker/cryptoTracker/urls.py @@ -0,0 +1,24 @@ +"""cryptoTracker URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from accounts import urls + +urlpatterns = [ + path('admin/', admin.site.urls), + path('accounts/', include('accounts.urls')), + path('tracker/', include('tracker.urls')), +] diff --git a/cryptoTracker/cryptoTracker/wsgi.py b/cryptoTracker/cryptoTracker/wsgi.py new file mode 100644 index 0000000..dec945b --- /dev/null +++ b/cryptoTracker/cryptoTracker/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for cryptoTracker project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cryptoTracker.settings') + +application = get_wsgi_application() diff --git a/cryptoTracker/manage.py b/cryptoTracker/manage.py new file mode 100755 index 0000000..f9d1c8e --- /dev/null +++ b/cryptoTracker/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cryptoTracker.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/cryptoTracker/tracker/__init__.py b/cryptoTracker/tracker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoTracker/tracker/admin.py b/cryptoTracker/tracker/admin.py new file mode 100644 index 0000000..44aa840 --- /dev/null +++ b/cryptoTracker/tracker/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import ActiveTrackingUser, Order + +# Register your models here. +admin.site.register(Order) +admin.site.register(ActiveTrackingUser) \ No newline at end of file diff --git a/cryptoTracker/tracker/apps.py b/cryptoTracker/tracker/apps.py new file mode 100644 index 0000000..5949f48 --- /dev/null +++ b/cryptoTracker/tracker/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + + +class TrackerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tracker' + def ready(self): + print('start scheduler ...') + from tracker.track_scheduler.scheduler import PositionTrackingSchduler + position_schduler = PositionTrackingSchduler() + position_schduler.start() diff --git a/cryptoTracker/tracker/migrations/0001_initial.py b/cryptoTracker/tracker/migrations/0001_initial.py new file mode 100644 index 0000000..2ebfb65 --- /dev/null +++ b/cryptoTracker/tracker/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 4.0.6 on 2022-07-21 12:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order_id', models.CharField(max_length=100)), + ('symbol', models.CharField(max_length=50)), + ('opType', models.CharField(max_length=20)), + ('type', models.CharField(max_length=20)), + ('side', models.CharField(max_length=20)), + ('price', models.CharField(max_length=20)), + ('size', models.CharField(max_length=20)), + ('fee', models.CharField(max_length=20)), + ('isActive', models.CharField(max_length=20)), + ('order_createdAt', models.IntegerField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ActiveTrackingUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('track', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/cryptoTracker/tracker/migrations/__init__.py b/cryptoTracker/tracker/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoTracker/tracker/models.py b/cryptoTracker/tracker/models.py new file mode 100644 index 0000000..c676b1c --- /dev/null +++ b/cryptoTracker/tracker/models.py @@ -0,0 +1,21 @@ +from django.db import models +from accounts.models import CustomUser + +# we just add some fields of a real position. other fields can added in the same way +class Order(models.Model): + user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + order_id = models.CharField(max_length=100) + symbol = models.CharField(max_length=50) + opType = models.CharField(max_length=20) + type = models.CharField(max_length=20) + side = models.CharField(max_length=20) + price = models.CharField(max_length=20) + size = models.CharField(max_length=20) + fee = models.CharField(max_length=20) + isActive = models.CharField(max_length=20) + order_createdAt = models.IntegerField() + +# check user must be tracked or not +class ActiveTrackingUser(models.Model): + user = models.ForeignKey(CustomUser, on_delete=models.CASCADE) + track = models.BooleanField(default=False) diff --git a/cryptoTracker/tracker/serializers.py b/cryptoTracker/tracker/serializers.py new file mode 100644 index 0000000..6292e5a --- /dev/null +++ b/cryptoTracker/tracker/serializers.py @@ -0,0 +1,7 @@ +from rest_framework import serializers + +class PositionTrackingSerializer(serializers.Serializer): + track = serializers.BooleanField(required=True, write_only=True) + + def validate(self, attrs): + return super().validate(attrs) \ No newline at end of file diff --git a/cryptoTracker/tracker/tests.py b/cryptoTracker/tracker/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cryptoTracker/tracker/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cryptoTracker/tracker/track_scheduler/__init__.py b/cryptoTracker/tracker/track_scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoTracker/tracker/track_scheduler/scheduler.py b/cryptoTracker/tracker/track_scheduler/scheduler.py new file mode 100644 index 0000000..ec5df2f --- /dev/null +++ b/cryptoTracker/tracker/track_scheduler/scheduler.py @@ -0,0 +1,21 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from tracker.views import PositionTrackingAPIView + +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + +# create a scheduler to run save_position_data function every 0.5 minutes (30 seconds) +class PositionTrackingSchduler(metaclass=Singleton): + + def __init__(self): + self.scheduler = BackgroundScheduler() + self.position_tracking_view = PositionTrackingAPIView() + + def start(self): + self.scheduler.add_job(self.position_tracking_view.save_position_data, 'interval', minutes=0.5, id='position001', replace_existing=True) + self.scheduler.start() + diff --git a/cryptoTracker/tracker/urls.py b/cryptoTracker/tracker/urls.py new file mode 100644 index 0000000..4c66d26 --- /dev/null +++ b/cryptoTracker/tracker/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from tracker.views import ( + OpenPositionsAPIView, + PositionTrackingAPIView +) + +urlpatterns = [ + path('open-positions/', OpenPositionsAPIView.as_view()), + path('position-tracking/', PositionTrackingAPIView.as_view()), +] diff --git a/cryptoTracker/tracker/utils.py b/cryptoTracker/tracker/utils.py new file mode 100644 index 0000000..932c91d --- /dev/null +++ b/cryptoTracker/tracker/utils.py @@ -0,0 +1,26 @@ +import base64 +import time +import hmac +import requests +import hashlib + +# fetch data from kucoin +def api_kucoin(api_key, api_secret, api_passphrase, endpoint): + api_key = api_key + api_secret = api_secret + api_passphrase = api_passphrase + url = 'https://api.kucoin.com' + endpoint + now = int(time.time() * 1000) + str_to_sign = str(now) + 'GET' + endpoint + signature = base64.b64encode( + hmac.new(api_secret.encode('utf-8'), str_to_sign.encode('utf-8'), hashlib.sha256).digest()) + passphrase = base64.b64encode(hmac.new(api_secret.encode('utf-8'), api_passphrase.encode('utf-8'), hashlib.sha256).digest()) + headers = { + "KC-API-SIGN": signature, + "KC-API-TIMESTAMP": str(now), + "KC-API-KEY": api_key, + "KC-API-PASSPHRASE": passphrase, + "KC-API-KEY-VERSION": "2" + } + response = requests.request('get', url, headers=headers) + return response.status_code, response.json() diff --git a/cryptoTracker/tracker/views.py b/cryptoTracker/tracker/views.py new file mode 100644 index 0000000..e108021 --- /dev/null +++ b/cryptoTracker/tracker/views.py @@ -0,0 +1,72 @@ +from rest_framework.views import ( + APIView +) +from .models import Order, ActiveTrackingUser +from .serializers import PositionTrackingSerializer +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from .utils import api_kucoin + +# get list of open positions +class OpenPositionsAPIView(APIView): + permission_classes = (IsAuthenticated, ) + + def get(self, request): + endpoint = '/api/v1/orders?status=active' + api_key = request.user.api_key + api_secret = request.user.secret_key + api_passphrase = request.user.api_passphrase + status_code, response = api_kucoin(api_key, api_secret, api_passphrase, endpoint) + return Response(data=response, status=status_code) + +class PositionTrackingAPIView(APIView): + permission_classes = (IsAuthenticated,) + + # get user's positions from kucoin + def _get_position_data(self, user): + endpoint = '/api/v1/orders' + api_key = user.api_key + api_secret = user.secret_key + api_passphrase = user.api_passphrase + try: + status_code, response = api_kucoin(api_key, api_secret, api_passphrase, endpoint) + return status_code, response + except: + pass + + # save positions for all users that requested for position tracking + def save_position_data(self): + active_tracking_users = ActiveTrackingUser.objects.filter(track=True) + for activ_user in active_tracking_users: + user = activ_user.user + status_code, response = self._get_position_data(user) + if len(response['data']['items']) > 0: + for item in response['data']['items']: + Order.objects.create( + user=user, order_id=item['id'], symbol=item['id'], + opType=item['opType'], type=item['type'], + side=item['side'], price=item['price'], + size=item['size'], fee=item['fee'], + isActive=item['isActive'], order_createdAt=item['createdAt'], + ) + + # get user's positions + def get(self, request): + user_orders = Order.objects.filter(user=request.user).values() + return Response(user_orders, status=200) + + # Set position tracking flag (track=True for start and track = False for stop tracking) + def post(self, request): + serializer = PositionTrackingSerializer(data=request.data) + if serializer.is_valid(): + user = ActiveTrackingUser.objects.update_or_create( + user=request.user, + defaults={'track':request.data['track']} + ) + if request.data['track'] == 'True': + message = f'start tracking {request.user}\'s posisions' + else: + message = f'stop tracking {request.user}\'s posisions' + return Response({'message': message}) + else: + return Response(serializer.errors) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5a94371 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +APScheduler==3.9.1 +asgiref==3.5.2 +backports.zoneinfo==0.2.1 +certifi==2022.6.15 +cffi==1.15.1 +charset-normalizer==2.1.0 +cryptography==37.0.4 +Django==4.0.6 +django-fernet-fields==0.6 +djangorestframework==3.13.1 +idna==3.3 +polling2==0.5.0 +pycparser==2.21 +PyMySQL==1.0.2 +pytz==2022.1 +pytz-deprecation-shim==0.1.0.post0 +requests==2.28.1 +six==1.16.0 +sqlparse==0.4.2 +tzdata==2022.1 +tzlocal==4.2 +urllib3==1.26.10