diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 5be02db..6cfabd5 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 28817aa..69b1eb0 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..5c208b3 100644 --- a/config/settings.py +++ b/config/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'signals', ] MIDDLEWARE = [ diff --git a/config/urls.py b/config/urls.py index bcfc074..c19926c 100644 --- a/config/urls.py +++ b/config/urls.py @@ -22,6 +22,7 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), path("", include("core.urls")), + path("api/", include("signals.urls")), ] if settings.DEBUG: diff --git a/requirements.txt b/requirements.txt index e22994c..c494051 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +ccxt diff --git a/signals/__init__.py b/signals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/signals/__pycache__/__init__.cpython-311.pyc b/signals/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f10361a Binary files /dev/null and b/signals/__pycache__/__init__.cpython-311.pyc differ diff --git a/signals/__pycache__/admin.cpython-311.pyc b/signals/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000..3c6228f Binary files /dev/null and b/signals/__pycache__/admin.cpython-311.pyc differ diff --git a/signals/__pycache__/apps.cpython-311.pyc b/signals/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000..cc6ca60 Binary files /dev/null and b/signals/__pycache__/apps.cpython-311.pyc differ diff --git a/signals/__pycache__/engine.cpython-311.pyc b/signals/__pycache__/engine.cpython-311.pyc new file mode 100644 index 0000000..5cec905 Binary files /dev/null and b/signals/__pycache__/engine.cpython-311.pyc differ diff --git a/signals/__pycache__/models.cpython-311.pyc b/signals/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..038ab33 Binary files /dev/null and b/signals/__pycache__/models.cpython-311.pyc differ diff --git a/signals/__pycache__/urls.cpython-311.pyc b/signals/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000..0a032de Binary files /dev/null and b/signals/__pycache__/urls.cpython-311.pyc differ diff --git a/signals/__pycache__/views.cpython-311.pyc b/signals/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000..1073f34 Binary files /dev/null and b/signals/__pycache__/views.cpython-311.pyc differ diff --git a/signals/admin.py b/signals/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/signals/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/signals/apps.py b/signals/apps.py new file mode 100644 index 0000000..c819cdb --- /dev/null +++ b/signals/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SignalsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'signals' diff --git a/signals/engine.py b/signals/engine.py new file mode 100644 index 0000000..87e665a --- /dev/null +++ b/signals/engine.py @@ -0,0 +1,67 @@ + +import ccxt +import pandas as pd + +def get_ohlcv(symbol, timeframe): + exchange = ccxt.binance() + ohlcv = exchange.fetch_ohlcv(symbol, timeframe) + df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + return df + +def get_trend(df): + df['ema20'] = df['close'].ewm(span=20, adjust=False).mean() + df['ema50'] = df['close'].ewm(span=50, adjust=False).mean() + df['rsi'] = 100 - (100 / (1 + df['close'].diff().apply(lambda x: x if x > 0 else 0).ewm(alpha=1/14, adjust=False).mean() / df['close'].diff().apply(lambda x: abs(x) if x < 0 else 0).ewm(alpha=1/14, adjust=False).mean())) + + last_row = df.iloc[-1] + if last_row['ema20'] > last_row['ema50'] and last_row['close'] > last_row['ema20'] and last_row['rsi'] > 50: + return 'UP' + elif last_row['ema20'] < last_row['ema50'] and last_row['close'] < last_row['ema20'] and last_row['rsi'] < 50: + return 'DOWN' + else: + return None + +def get_setup(df): + df['rsi'] = 100 - (100 / (1 + df['close'].diff().apply(lambda x: x if x > 0 else 0).ewm(alpha=1/14, adjust=False).mean() / df['close'].diff().apply(lambda x: abs(x) if x < 0 else 0).ewm(alpha=1/14, adjust=False).mean())) + + last_row = df.iloc[-1] + if last_row['rsi'] > 40 and last_row['rsi'] < 60: + return True + else: + return False + +def get_entry(df): + df['ema20'] = df['close'].ewm(span=20, adjust=False).mean() + df['ema50'] = df['close'].ewm(span=50, adjust=False).mean() + df['rsi'] = 100 - (100 / (1 + df['close'].diff().apply(lambda x: x if x > 0 else 0).ewm(alpha=1/14, adjust=False).mean() / df['close'].diff().apply(lambda x: abs(x) if x < 0 else 0).ewm(alpha=1/14, adjust=False).mean())) + df['volume_sma20'] = df['volume'].rolling(window=20).mean() + + last_row = df.iloc[-1] + if last_row['ema20'] > last_row['ema50'] and last_row['rsi'] > 50 and last_row['close'] > last_row['ema20'] and last_row['volume'] > last_row['volume_sma20']: + return 'BUY' + elif last_row['ema20'] < last_row['ema50'] and last_row['rsi'] < 50 and last_row['close'] < last_row['ema20'] and last_row['volume'] > last_row['volume_sma20']: + return 'SELL' + else: + return 'WAIT' + +def generate_signal(symbol): + # 1h trend + df_1h = get_ohlcv(symbol, '1h') + trend = get_trend(df_1h) + + if trend: + # 15m setup + df_15m = get_ohlcv(symbol, '15m') + setup = get_setup(df_15m) + + if setup: + # 5m entry + df_5m = get_ohlcv(symbol, '5m') + entry = get_entry(df_5m) + + if (trend == 'UP' and entry == 'BUY') or (trend == 'DOWN' and entry == 'SELL'): + return entry + + return 'WAIT' + diff --git a/signals/migrations/0001_initial.py b/signals/migrations/0001_initial.py new file mode 100644 index 0000000..2b050a3 --- /dev/null +++ b/signals/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.7 on 2025-12-28 14:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Backtest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('symbol', models.CharField(max_length=20)), + ('start_date', models.DateTimeField()), + ('end_date', models.DateTimeField()), + ('profit_loss', models.FloatField()), + ('win_rate', models.FloatField()), + ('max_drawdown', models.FloatField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Signal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('symbol', models.CharField(max_length=20)), + ('signal', models.CharField(choices=[('BUY', 'Buy'), ('SELL', 'Sell'), ('WAIT', 'Wait')], max_length=4)), + ('timeframe', models.CharField(max_length=10)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='ConfidenceScore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('score', models.FloatField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('signal', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='confidence_score', to='signals.signal')), + ], + ), + ] diff --git a/signals/migrations/__init__.py b/signals/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/signals/migrations/__pycache__/0001_initial.cpython-311.pyc b/signals/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..e45db15 Binary files /dev/null and b/signals/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/signals/migrations/__pycache__/__init__.cpython-311.pyc b/signals/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..216d77b Binary files /dev/null and b/signals/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/signals/models.py b/signals/models.py new file mode 100644 index 0000000..21887ff --- /dev/null +++ b/signals/models.py @@ -0,0 +1,36 @@ +from django.db import models + +class Signal(models.Model): + SIGNAL_CHOICES = [ + ('BUY', 'Buy'), + ('SELL', 'Sell'), + ('WAIT', 'Wait'), + ] + + symbol = models.CharField(max_length=20) + signal = models.CharField(max_length=4, choices=SIGNAL_CHOICES) + timeframe = models.CharField(max_length=10) + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.symbol} - {self.signal} ({self.timeframe})" + +class ConfidenceScore(models.Model): + signal = models.OneToOneField(Signal, on_delete=models.CASCADE, related_name='confidence_score') + score = models.FloatField() + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.signal} - {self.score}" + +class Backtest(models.Model): + symbol = models.CharField(max_length=20) + start_date = models.DateTimeField() + end_date = models.DateTimeField() + profit_loss = models.FloatField() + win_rate = models.FloatField() + max_drawdown = models.FloatField() + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Backtest for {self.symbol} from {self.start_date} to {self.end_date}" \ No newline at end of file diff --git a/signals/tests.py b/signals/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/signals/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/signals/urls.py b/signals/urls.py new file mode 100644 index 0000000..258e3c6 --- /dev/null +++ b/signals/urls.py @@ -0,0 +1,9 @@ + +from django.urls import path +from . import views + +app_name = 'signals' + +urlpatterns = [ + path('signal//', views.get_signal, name='get_signal'), +] diff --git a/signals/views.py b/signals/views.py new file mode 100644 index 0000000..1ba781e --- /dev/null +++ b/signals/views.py @@ -0,0 +1,6 @@ +from django.http import JsonResponse +from .engine import generate_signal + +def get_signal(request, symbol): + signal = generate_signal(symbol) + return JsonResponse({'symbol': symbol, 'signal': signal}) \ No newline at end of file diff --git a/trading/__init__.py b/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trading/__pycache__/__init__.cpython-311.pyc b/trading/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6094d67 Binary files /dev/null and b/trading/__pycache__/__init__.cpython-311.pyc differ diff --git a/trading/__pycache__/admin.cpython-311.pyc b/trading/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000..deb7627 Binary files /dev/null and b/trading/__pycache__/admin.cpython-311.pyc differ diff --git a/trading/__pycache__/apps.cpython-311.pyc b/trading/__pycache__/apps.cpython-311.pyc new file mode 100644 index 0000000..b122138 Binary files /dev/null and b/trading/__pycache__/apps.cpython-311.pyc differ diff --git a/trading/__pycache__/models.cpython-311.pyc b/trading/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..3ad8e50 Binary files /dev/null and b/trading/__pycache__/models.cpython-311.pyc differ diff --git a/trading/__pycache__/signals.cpython-311.pyc b/trading/__pycache__/signals.cpython-311.pyc new file mode 100644 index 0000000..072687e Binary files /dev/null and b/trading/__pycache__/signals.cpython-311.pyc differ diff --git a/trading/admin.py b/trading/admin.py new file mode 100644 index 0000000..b676649 --- /dev/null +++ b/trading/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User +from .models import UserProfile, Signal, Trade, Backtest + +class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + verbose_name_plural = 'Profile' + fk_name = 'user' + +class CustomUserAdmin(UserAdmin): + inlines = (UserProfileInline, ) + + def get_inline_instances(self, request, obj=None): + if not obj: + return list() + return super(CustomUserAdmin, self).get_inline_instances(request, obj) + + +admin.site.unregister(User) +admin.site.register(User, CustomUserAdmin) + +admin.site.register(Signal) +admin.site.register(Trade) +admin.site.register(Backtest) \ No newline at end of file diff --git a/trading/apps.py b/trading/apps.py new file mode 100644 index 0000000..abf4355 --- /dev/null +++ b/trading/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class TradingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'trading' + + def ready(self): + import trading.signals diff --git a/trading/migrations/0001_initial.py b/trading/migrations/0001_initial.py new file mode 100644 index 0000000..f4687d2 --- /dev/null +++ b/trading/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.7 on 2025-12-28 13:55 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Signal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('symbol', models.CharField(max_length=20)), + ('signal', models.CharField(choices=[('BUY', 'Buy'), ('SELL', 'Sell'), ('WAIT', 'Wait')], max_length=4)), + ('confidence', models.FloatField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='Backtest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateTimeField()), + ('end_date', models.DateTimeField()), + ('results', models.JSONField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Trade', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('symbol', models.CharField(max_length=20)), + ('entry_price', models.FloatField()), + ('exit_price', models.FloatField(blank=True, null=True)), + ('entry_timestamp', models.DateTimeField()), + ('exit_timestamp', models.DateTimeField(blank=True, null=True)), + ('signal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='trading.signal')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('Free', 'Free'), ('Pro', 'Pro'), ('Admin', 'Admin')], default='Free', max_length=10)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/trading/migrations/__init__.py b/trading/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trading/migrations/__pycache__/0001_initial.cpython-311.pyc b/trading/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..827589f Binary files /dev/null and b/trading/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/trading/migrations/__pycache__/__init__.cpython-311.pyc b/trading/migrations/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..20b9a48 Binary files /dev/null and b/trading/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/trading/models.py b/trading/models.py new file mode 100644 index 0000000..e6a9f0c --- /dev/null +++ b/trading/models.py @@ -0,0 +1,50 @@ +from django.db import models +from django.contrib.auth.models import User + +class UserProfile(models.Model): + USER_ROLE_CHOICES = ( + ('Free', 'Free'), + ('Pro', 'Pro'), + ('Admin', 'Admin'), + ) + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + role = models.CharField(max_length=10, choices=USER_ROLE_CHOICES, default='Free') + + def __str__(self): + return f'{self.user.username} - {self.role}' + +class Signal(models.Model): + SIGNAL_CHOICES = ( + ('BUY', 'Buy'), + ('SELL', 'Sell'), + ('WAIT', 'Wait'), + ) + symbol = models.CharField(max_length=20) + signal = models.CharField(max_length=4, choices=SIGNAL_CHOICES) + confidence = models.FloatField() + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f'{self.timestamp} - {self.symbol} - {self.signal}' + +class Trade(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + symbol = models.CharField(max_length=20) + entry_price = models.FloatField() + exit_price = models.FloatField(null=True, blank=True) + entry_timestamp = models.DateTimeField() + exit_timestamp = models.DateTimeField(null=True, blank=True) + signal = models.ForeignKey(Signal, on_delete=models.CASCADE) + + def __str__(self): + return f'{self.user.username} - {self.symbol}' + +class Backtest(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + start_date = models.DateTimeField() + end_date = models.DateTimeField() + results = models.JSONField() + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f'{self.user.username} - {self.timestamp}' \ No newline at end of file diff --git a/trading/signals.py b/trading/signals.py new file mode 100644 index 0000000..f4951ff --- /dev/null +++ b/trading/signals.py @@ -0,0 +1,16 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.contrib.auth.models import User +from .models import UserProfile + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + try: + instance.profile.save() + except UserProfile.DoesNotExist: + UserProfile.objects.create(user=instance) diff --git a/trading/tests.py b/trading/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/trading/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/trading/views.py b/trading/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/trading/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.