Compare commits

..

2 Commits

Author SHA1 Message Date
Flatlogic Bot
2750ec443e Using Kraken as Market provider 2025-12-29 13:31:35 +00:00
Flatlogic Bot
2d3a86ad45 First Trading with SaaS 2025-12-28 2025-12-28 21:36:27 +00:00
28 changed files with 349 additions and 165 deletions

View File

@ -49,13 +49,10 @@ CSRF_COOKIE_SAMESITE = "None"
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core',
'signals',
]
MIDDLEWARE = [
@ -63,8 +60,6 @@ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@ -81,8 +76,6 @@ TEMPLATES = [
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
'core.context_processors.project_context',
],
@ -98,36 +91,15 @@ WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.getenv('DB_NAME', ''),
'USER': os.getenv('DB_USER', ''),
'PASSWORD': os.getenv('DB_PASS', ''),
'HOST': os.getenv('DB_HOST', '127.0.0.1'),
'PORT': os.getenv('DB_PORT', '3306'),
'OPTIONS': {
'charset': 'utf8mb4',
},
},
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/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',
},
]
# Internationalization

View File

@ -14,14 +14,13 @@ 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 include, path
from django.conf import settings
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:

View File

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %}
{% block title %}Trading Signal: {{ active_symbol }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
@ -8,11 +8,15 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
--bg-color: #121212;
--text-color: #e0e0e0;
--card-bg-color: #1e1e1e;
--card-border-color: #2c2c2c;
--primary-color: #4A90E2; /* A modern blue */
--buy-color: #4CAF50;
--sell-color: #F44336;
--wait-color: #FFC107;
--font-family: 'Inter', sans-serif;
}
* {
@ -21,125 +25,132 @@
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
font-family: var(--font-family);
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
header {
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
max-width: 600px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
.symbol-selector-form {
display: flex;
gap: 1rem;
align-items: center;
}
.symbol-selector-form label {
font-size: 1rem;
font-weight: 700;
}
.symbol-selector {
padding: 0.75rem 1rem;
font-size: 1rem;
background-color: var(--card-bg-color);
color: var(--text-color);
border: 1px solid var(--card-border-color);
border-radius: 8px;
cursor: pointer;
}
.submit-btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 700;
background-color: var(--primary-color);
color: #ffffff;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.submit-btn:hover {
background-color: #357ABD;
}
main {
width: 100%;
max-width: 600px;
display: flex;
flex-direction: column;
align-items: center;
}
.signal-card {
width: 100%;
background-color: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 12px;
padding: 2.5rem;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.signal-card h1 {
margin: 0;
font-size: 1.5rem;
color: var(--primary-color);
font-weight: 700;
margin-bottom: 0.5rem;
}
.signal-card p {
margin: 0;
font-size: 4rem;
font-weight: 700;
}
.signal-BUY {
color: var(--buy-color);
}
.signal-SELL {
color: var(--sell-color);
}
.signal-WAIT {
color: var(--wait-color);
}
</style>
{% endblock %}
{% block content %}
<header>
<!-- 2.A. MANUAL SYMBOL SELECTOR -->
<form action="{% url 'home' %}" method="get" class="symbol-selector-form">
<label for="symbol-select">Active Symbol:</label>
<select name="symbol" id="symbol-select" class="symbol-selector">
{% for sym in available_symbols %}
<option value="{{ sym }}" {% if sym == active_symbol %}selected{% endif %}>
{{ sym }}
</option>
{% endfor %}
</select>
<button type="submit" class="submit-btn">Analyze</button>
</form>
</header>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
<!-- 4. FRONTEND BEHAVIOR: Display data for the active symbol -->
<div class="signal-card">
<h1>{{ active_symbol }}</h1>
<p class="signal-{{ signal }}">{{ signal }}</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}
{% endblock %}

View File

@ -1,25 +1,25 @@
import os
import platform
from django import get_version as django_version
from django.shortcuts import render
from django.utils import timezone
from signals.engine import generate_signal
def home(request):
"""Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
"""
Renders the home page with a trading signal for the active symbol.
"""
# 1. ACTIVE SYMBOL STATE: Get symbol from URL parameter, default to BTC/USDT
symbol = request.GET.get('symbol', 'BTC/USDT').upper()
try:
# 3. BACKEND INTEGRATION: Generate signal for the active symbol
signal = generate_signal(symbol)
except Exception as e:
signal = f'Error: {e}'
# A list of symbols for the dropdown selector
available_symbols = ['BTC/USDT', 'ETH/USDT', 'XRP/USDT', 'LTC/USDT', 'ADA/USDT', 'SOL/USDT', 'DOGE/USDT']
context = {
"project_name": "New Style",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
"current_time": now,
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
'active_symbol': symbol,
'signal': signal,
'available_symbols': available_symbols,
}
return render(request, "core/index.html", context)

BIN
db.sqlite3 Normal file

Binary file not shown.

View File

@ -1,3 +1,4 @@
Django==5.2.7
mysqlclient==2.2.7
python-dotenv==1.1.1
ccxt
pandas

0
signals/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
signals/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
signals/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SignalsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'signals'

91
signals/engine.py Normal file
View File

@ -0,0 +1,91 @@
import ccxt
import pandas as pd
def get_ohlcv(symbol, timeframe):
exchange = ccxt.kraken()
# Symbol mapping for Kraken
symbol_map = {
'BTC/USDT': 'BTC/USD',
'ETH/USDT': 'ETH/USD',
'XRP/USDT': 'XRP/USD',
'LTC/USDT': 'LTC/USD',
'ADA/USDT': 'ADA/USD',
'SOL/USDT': 'SOL/USD',
'DOGE/USDT': 'DOGE/USD',
}
try:
# First, try the original symbol
ohlcv = exchange.fetch_ohlcv(symbol, timeframe)
except ccxt.BadSymbol:
# If the symbol is not found, try the mapped symbol
mapped_symbol = symbol_map.get(symbol)
if mapped_symbol:
try:
ohlcv = exchange.fetch_ohlcv(mapped_symbol, timeframe)
except ccxt.ExchangeError as e:
raise Exception(f"Could not fetch data for {symbol} or {mapped_symbol} from Kraken: {e})")
else:
raise Exception(f"Symbol {symbol} not found on Kraken and no mapping available.")
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'

View File

@ -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')),
],
),
]

View File

36
signals/models.py Normal file
View File

@ -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}"

3
signals/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
signals/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = 'signals'
urlpatterns = [
path('signal/<str:symbol>/', views.get_signal, name='get_signal'),
]

6
signals/views.py Normal file
View File

@ -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})