Initial commit: Django quantitative strategy executor
- Django 5.2.7 project with Python 3.13+ - Quant strategy management system with version control - Strategy implementations using registry pattern - API endpoints for strategy listing and execution - Sample strategy implementations (MovingAverage, RSI, BollingerBand) - Async strategy execution with status tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python manage.py:*)",
|
||||
"Bash(git init:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
87
.gitignore
vendored
Normal file
87
.gitignore
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media/
|
||||
staticfiles/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# UV
|
||||
uv.lock
|
||||
66
CLAUDE.md
Normal file
66
CLAUDE.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a Django 5.2.7 project named "executor" using Python 3.13+ with a minimal configuration. It's a fresh Django project with only the default Django applications installed and no custom apps yet.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Environment Setup
|
||||
```bash
|
||||
# Activate virtual environment (if using .venv)
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Django Commands
|
||||
```bash
|
||||
# Run development server
|
||||
python manage.py runserver
|
||||
|
||||
# Database migrations
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
|
||||
# Create superuser
|
||||
python manage.py createsuperuser
|
||||
|
||||
# Django shell
|
||||
python manage.py shell
|
||||
|
||||
# Run tests
|
||||
python manage.py test
|
||||
|
||||
# Collect static files
|
||||
python manage.py collectstatic
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- **executor/**: Main Django project directory containing settings, URLs, and WSGI/ASGI configuration
|
||||
- **templates/**: Global template directory (currently empty)
|
||||
- **db.sqlite3**: SQLite database file
|
||||
- **manage.py**: Django management script
|
||||
- **pyproject.toml**: Project dependencies managed by uv
|
||||
- **uv.lock**: Dependency lock file
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- Uses SQLite as the default database
|
||||
- Templates directory is configured at the project root level
|
||||
- Standard Django project layout with no custom applications yet
|
||||
- Secret key is currently set to Django's insecure default (should be changed for production)
|
||||
- Debug mode is enabled (development configuration)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
This appears to be a new Django project. When adding new functionality:
|
||||
1. Create Django apps using `python manage.py startapp <app_name>`
|
||||
2. Add new apps to `INSTALLED_APPS` in `executor/settings.py`
|
||||
3. Create models, views, and URL patterns within the apps
|
||||
4. Run migrations after model changes
|
||||
5. Add templates to either app-specific template directories or the global `templates/` directory
|
||||
0
executor/__init__.py
Normal file
0
executor/__init__.py
Normal file
16
executor/asgi.py
Normal file
16
executor/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for executor 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/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'executor.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
124
executor/settings.py
Normal file
124
executor/settings.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Django settings for executor project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-ma+k#^%b4oe-(bm7y=kswcbbm-i+d=#fft7t_tl0sk0$_4asop'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'strategies',
|
||||
]
|
||||
|
||||
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 = 'executor.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates']
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'executor.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'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
|
||||
# https://docs.djangoproject.com/en/5.2/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/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
23
executor/urls.py
Normal file
23
executor/urls.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
URL configuration for executor project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/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
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('strategies.urls')),
|
||||
]
|
||||
16
executor/wsgi.py
Normal file
16
executor/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for executor 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/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'executor.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
manage.py
Executable file
22
manage.py
Executable file
@@ -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', 'executor.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()
|
||||
8
pyproject.toml
Normal file
8
pyproject.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[project]
|
||||
name = "executor"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"django>=5.2.7",
|
||||
]
|
||||
0
strategies/__init__.py
Normal file
0
strategies/__init__.py
Normal file
3
strategies/admin.py
Normal file
3
strategies/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
strategies/apps.py
Normal file
6
strategies/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StrategiesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'strategies'
|
||||
110
strategies/base.py
Normal file
110
strategies/base.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
|
||||
|
||||
class BaseQuantStrategy(ABC):
|
||||
"""
|
||||
퀀트 전략의 기본 클래스
|
||||
모든 전략 구현체는 이 클래스를 상속받아야 합니다.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""전략 이름"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def description(self) -> str:
|
||||
"""전략 설명"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def version(self) -> str:
|
||||
"""전략 버전"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def default_parameters(self) -> Dict[str, Any]:
|
||||
"""기본 파라미터"""
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
전략을 실행합니다.
|
||||
|
||||
Args:
|
||||
parameters: 실행 파라미터
|
||||
|
||||
Returns:
|
||||
실행 결과
|
||||
"""
|
||||
pass
|
||||
|
||||
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
파라미터 유효성 검사
|
||||
|
||||
Args:
|
||||
parameters: 검사할 파라미터
|
||||
|
||||
Returns:
|
||||
유효성 검사 결과
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class StrategyRegistry:
|
||||
"""전략 구현체 레지스트리"""
|
||||
|
||||
_strategies = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, strategy_class: BaseQuantStrategy):
|
||||
"""전략 구현체를 레지스트리에 등록"""
|
||||
strategy_instance = strategy_class()
|
||||
key = f"{strategy_instance.name}:{strategy_instance.version}"
|
||||
cls._strategies[key] = strategy_class
|
||||
return strategy_class
|
||||
|
||||
@classmethod
|
||||
def get_strategy(cls, name: str, version: str) -> BaseQuantStrategy:
|
||||
"""전략 구현체를 가져옵니다"""
|
||||
key = f"{name}:{version}"
|
||||
strategy_class = cls._strategies.get(key)
|
||||
if strategy_class:
|
||||
return strategy_class()
|
||||
raise ValueError(f"Strategy {key} not found in registry")
|
||||
|
||||
@classmethod
|
||||
def list_strategies(cls) -> Dict[str, Dict[str, Any]]:
|
||||
"""등록된 모든 전략 목록을 반환합니다"""
|
||||
strategies = {}
|
||||
for key, strategy_class in cls._strategies.items():
|
||||
strategy_instance = strategy_class()
|
||||
name = strategy_instance.name
|
||||
if name not in strategies:
|
||||
strategies[name] = {
|
||||
'name': name,
|
||||
'description': strategy_instance.description,
|
||||
'versions': []
|
||||
}
|
||||
strategies[name]['versions'].append({
|
||||
'version': strategy_instance.version,
|
||||
'default_parameters': strategy_instance.default_parameters
|
||||
})
|
||||
return strategies
|
||||
|
||||
@classmethod
|
||||
def get_strategy_key(cls, name: str, version: str) -> str:
|
||||
"""전략 키를 생성합니다"""
|
||||
return f"{name}:{version}"
|
||||
|
||||
|
||||
def strategy(cls):
|
||||
"""전략 구현체를 자동으로 레지스트리에 등록하는 데코레이터"""
|
||||
return StrategyRegistry.register(cls)
|
||||
202
strategies/implementations.py
Normal file
202
strategies/implementations.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from typing import Dict, Any
|
||||
import time
|
||||
import random
|
||||
import math
|
||||
from .base import BaseQuantStrategy, strategy
|
||||
|
||||
|
||||
@strategy
|
||||
class MovingAverageCrossover(BaseQuantStrategy):
|
||||
"""이동평균선 교차 전략"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "MovingAverageCrossover"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "단기 이동평균선이 장기 이동평균선을 상향 돌파할 때 매수, 하향 돌파할 때 매도하는 전략"
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
return "1.0.0"
|
||||
|
||||
@property
|
||||
def default_parameters(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"short_window": 20,
|
||||
"long_window": 50,
|
||||
"initial_capital": 100000,
|
||||
"position_size": 0.1
|
||||
}
|
||||
|
||||
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
|
||||
required_params = ["short_window", "long_window", "initial_capital"]
|
||||
for param in required_params:
|
||||
if param not in parameters:
|
||||
return False
|
||||
|
||||
if parameters["short_window"] >= parameters["long_window"]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
if parameters is None:
|
||||
parameters = self.default_parameters
|
||||
|
||||
if not self.validate_parameters(parameters):
|
||||
raise ValueError("Invalid parameters")
|
||||
|
||||
# 시뮬레이션 실행
|
||||
time.sleep(1) # 실행 시간 시뮬레이션
|
||||
|
||||
# 모의 결과 생성
|
||||
profit_rate = random.uniform(-0.15, 0.25)
|
||||
trades_count = random.randint(15, 60)
|
||||
win_rate = random.uniform(0.45, 0.75)
|
||||
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"version": self.version,
|
||||
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
|
||||
"profit_rate": round(profit_rate * 100, 2),
|
||||
"trades_executed": trades_count,
|
||||
"win_rate": round(win_rate, 3),
|
||||
"execution_time": "1.2s",
|
||||
"parameters_used": parameters,
|
||||
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2)
|
||||
}
|
||||
|
||||
|
||||
@strategy
|
||||
class RSIMeanReversion(BaseQuantStrategy):
|
||||
"""RSI 평균회귀 전략"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "RSIMeanReversion"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "RSI 지표를 이용한 평균회귀 전략. RSI가 과매수/과매도 구간에서 반대 방향으로 거래"
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
return "1.0.0"
|
||||
|
||||
@property
|
||||
def default_parameters(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"rsi_period": 14,
|
||||
"oversold_threshold": 30,
|
||||
"overbought_threshold": 70,
|
||||
"initial_capital": 100000,
|
||||
"position_size": 0.05
|
||||
}
|
||||
|
||||
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
|
||||
required_params = ["rsi_period", "oversold_threshold", "overbought_threshold", "initial_capital"]
|
||||
for param in required_params:
|
||||
if param not in parameters:
|
||||
return False
|
||||
|
||||
if not (0 < parameters["oversold_threshold"] < parameters["overbought_threshold"] < 100):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
if parameters is None:
|
||||
parameters = self.default_parameters
|
||||
|
||||
if not self.validate_parameters(parameters):
|
||||
raise ValueError("Invalid parameters")
|
||||
|
||||
# 시뮬레이션 실행
|
||||
time.sleep(1.5) # 실행 시간 시뮬레이션
|
||||
|
||||
# 모의 결과 생성
|
||||
profit_rate = random.uniform(-0.10, 0.18)
|
||||
trades_count = random.randint(25, 80)
|
||||
win_rate = random.uniform(0.40, 0.65)
|
||||
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"version": self.version,
|
||||
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
|
||||
"profit_rate": round(profit_rate * 100, 2),
|
||||
"trades_executed": trades_count,
|
||||
"win_rate": round(win_rate, 3),
|
||||
"execution_time": "1.5s",
|
||||
"parameters_used": parameters,
|
||||
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2),
|
||||
"max_drawdown": round(random.uniform(0.05, 0.20), 3)
|
||||
}
|
||||
|
||||
|
||||
@strategy
|
||||
class BollingerBandBreakout(BaseQuantStrategy):
|
||||
"""볼린저 밴드 돌파 전략"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "BollingerBandBreakout"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "볼린저 밴드 상한선 돌파시 매수, 하한선 돌파시 매도하는 돌파 전략"
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
return "2.0.0"
|
||||
|
||||
@property
|
||||
def default_parameters(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"period": 20,
|
||||
"std_dev": 2.0,
|
||||
"initial_capital": 100000,
|
||||
"position_size": 0.08,
|
||||
"stop_loss": 0.05
|
||||
}
|
||||
|
||||
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
|
||||
required_params = ["period", "std_dev", "initial_capital"]
|
||||
for param in required_params:
|
||||
if param not in parameters:
|
||||
return False
|
||||
|
||||
if parameters["std_dev"] <= 0 or parameters["period"] <= 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
if parameters is None:
|
||||
parameters = self.default_parameters
|
||||
|
||||
if not self.validate_parameters(parameters):
|
||||
raise ValueError("Invalid parameters")
|
||||
|
||||
# 시뮬레이션 실행
|
||||
time.sleep(2) # 실행 시간 시뮬레이션
|
||||
|
||||
# 모의 결과 생성
|
||||
profit_rate = random.uniform(-0.20, 0.30)
|
||||
trades_count = random.randint(10, 40)
|
||||
win_rate = random.uniform(0.35, 0.70)
|
||||
|
||||
return {
|
||||
"strategy": self.name,
|
||||
"version": self.version,
|
||||
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
|
||||
"profit_rate": round(profit_rate * 100, 2),
|
||||
"trades_executed": trades_count,
|
||||
"win_rate": round(win_rate, 3),
|
||||
"execution_time": "2.0s",
|
||||
"parameters_used": parameters,
|
||||
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2),
|
||||
"sharpe_ratio": round(random.uniform(0.5, 2.5), 2),
|
||||
"volatility": round(random.uniform(0.15, 0.35), 3)
|
||||
}
|
||||
61
strategies/migrations/0001_initial.py
Normal file
61
strategies/migrations/0001_initial.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-04 04:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QuantStrategy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Quant Strategies',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StrategyVersion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('version', models.CharField(max_length=20)),
|
||||
('code', models.TextField()),
|
||||
('parameters', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_current', models.BooleanField(default=False)),
|
||||
('strategy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='strategies.quantstrategy')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('strategy', 'version')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StrategyExecution',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('execution_parameters', models.JSONField(blank=True, default=dict)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)),
|
||||
('started_at', models.DateTimeField(auto_now_add=True)),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('result', models.JSONField(blank=True, null=True)),
|
||||
('error_message', models.TextField(blank=True)),
|
||||
('strategy_version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='strategies.strategyversion')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-started_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-04 04:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('strategies', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='strategyversion',
|
||||
name='code',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategyversion',
|
||||
name='implementation_key',
|
||||
field=models.CharField(default='', help_text='전략 구현체의 레지스트리 키', max_length=200),
|
||||
),
|
||||
]
|
||||
0
strategies/migrations/__init__.py
Normal file
0
strategies/migrations/__init__.py
Normal file
81
strategies/models.py
Normal file
81
strategies/models.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
import json
|
||||
from .base import StrategyRegistry
|
||||
|
||||
|
||||
class QuantStrategy(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Quant Strategies"
|
||||
|
||||
|
||||
class StrategyVersion(models.Model):
|
||||
strategy = models.ForeignKey(QuantStrategy, on_delete=models.CASCADE, related_name='versions')
|
||||
version = models.CharField(max_length=20)
|
||||
implementation_key = models.CharField(max_length=200, default="", help_text="전략 구현체의 레지스트리 키")
|
||||
parameters = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_current = models.BooleanField(default=False)
|
||||
|
||||
def clean(self):
|
||||
if self.is_current:
|
||||
StrategyVersion.objects.filter(
|
||||
strategy=self.strategy,
|
||||
is_current=True
|
||||
).exclude(pk=self.pk).update(is_current=False)
|
||||
|
||||
if not self.implementation_key:
|
||||
self.implementation_key = StrategyRegistry.get_strategy_key(
|
||||
self.strategy.name, self.version
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_strategy_implementation(self):
|
||||
"""전략 구현체 인스턴스를 반환합니다"""
|
||||
try:
|
||||
name, version = self.implementation_key.split(':')
|
||||
return StrategyRegistry.get_strategy(name, version)
|
||||
except ValueError:
|
||||
raise ValidationError(f"Invalid implementation key: {self.implementation_key}")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.strategy.name} v{self.version}"
|
||||
|
||||
class Meta:
|
||||
unique_together = ['strategy', 'version']
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
class StrategyExecution(models.Model):
|
||||
EXECUTION_STATUS = [
|
||||
('pending', 'Pending'),
|
||||
('running', 'Running'),
|
||||
('completed', 'Completed'),
|
||||
('failed', 'Failed'),
|
||||
]
|
||||
|
||||
strategy_version = models.ForeignKey(StrategyVersion, on_delete=models.CASCADE)
|
||||
execution_parameters = models.JSONField(default=dict, blank=True)
|
||||
status = models.CharField(max_length=20, choices=EXECUTION_STATUS, default='pending')
|
||||
started_at = models.DateTimeField(auto_now_add=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
result = models.JSONField(null=True, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Execution of {self.strategy_version} - {self.status}"
|
||||
|
||||
class Meta:
|
||||
ordering = ['-started_at']
|
||||
3
strategies/tests.py
Normal file
3
strategies/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
9
strategies/urls.py
Normal file
9
strategies/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('strategies/', views.list_strategies, name='list_strategies'),
|
||||
path('strategies/execute/', views.execute_strategy, name='execute_strategy'),
|
||||
path('strategies/implementations/', views.list_available_implementations, name='list_available_implementations'),
|
||||
path('executions/<int:execution_id>/', views.execution_status, name='execution_status'),
|
||||
]
|
||||
161
strategies/views.py
Normal file
161
strategies/views.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.utils import timezone
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
|
||||
from .models import QuantStrategy, StrategyVersion, StrategyExecution
|
||||
from .base import StrategyRegistry
|
||||
from . import implementations # 구현체들을 로드하여 레지스트리에 등록
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def list_strategies(request):
|
||||
# 레지스트리에서 사용 가능한 전략들을 가져옴
|
||||
available_strategies = StrategyRegistry.list_strategies()
|
||||
|
||||
# DB에서 관리되는 전략들을 가져옴
|
||||
strategies = QuantStrategy.objects.filter(is_active=True).prefetch_related('versions')
|
||||
|
||||
strategy_list = []
|
||||
for strategy in strategies:
|
||||
current_version = strategy.versions.filter(is_current=True).first()
|
||||
strategy_data = {
|
||||
'id': strategy.id,
|
||||
'name': strategy.name,
|
||||
'description': strategy.description,
|
||||
'created_at': strategy.created_at.isoformat(),
|
||||
'current_version': {
|
||||
'id': current_version.id,
|
||||
'version': current_version.version,
|
||||
'implementation_key': current_version.implementation_key,
|
||||
'parameters': current_version.parameters,
|
||||
'created_at': current_version.created_at.isoformat()
|
||||
} if current_version else None,
|
||||
'total_versions': strategy.versions.count(),
|
||||
'available_implementations': available_strategies.get(strategy.name, {}).get('versions', [])
|
||||
}
|
||||
strategy_list.append(strategy_data)
|
||||
|
||||
return JsonResponse({
|
||||
'strategies': strategy_list,
|
||||
'total_count': len(strategy_list),
|
||||
'available_implementations': available_strategies
|
||||
})
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def execute_strategy(request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
strategy_name = data.get('strategy_name')
|
||||
version = data.get('version')
|
||||
execution_parameters = data.get('parameters', {})
|
||||
|
||||
if not strategy_name:
|
||||
return JsonResponse({
|
||||
'error': 'strategy_name is required'
|
||||
}, status=400)
|
||||
|
||||
strategy = get_object_or_404(QuantStrategy, name=strategy_name, is_active=True)
|
||||
|
||||
if version:
|
||||
strategy_version = get_object_or_404(
|
||||
StrategyVersion,
|
||||
strategy=strategy,
|
||||
version=version
|
||||
)
|
||||
else:
|
||||
strategy_version = strategy.versions.filter(is_current=True).first()
|
||||
if not strategy_version:
|
||||
return JsonResponse({
|
||||
'error': 'No current version found for this strategy'
|
||||
}, status=404)
|
||||
|
||||
execution = StrategyExecution.objects.create(
|
||||
strategy_version=strategy_version,
|
||||
execution_parameters=execution_parameters,
|
||||
status='pending'
|
||||
)
|
||||
|
||||
def run_strategy():
|
||||
try:
|
||||
execution.status = 'running'
|
||||
execution.save()
|
||||
|
||||
# 전략 구현체 인스턴스를 가져와서 실행
|
||||
strategy_impl = strategy_version.get_strategy_implementation()
|
||||
|
||||
# 기본 파라미터와 실행 파라미터를 병합
|
||||
merged_parameters = strategy_impl.default_parameters.copy()
|
||||
merged_parameters.update(execution_parameters)
|
||||
|
||||
# 전략 실행
|
||||
result = strategy_impl.execute(merged_parameters)
|
||||
|
||||
execution.status = 'completed'
|
||||
execution.result = result
|
||||
execution.completed_at = timezone.now()
|
||||
execution.save()
|
||||
|
||||
except Exception as e:
|
||||
execution.status = 'failed'
|
||||
execution.error_message = str(e)
|
||||
execution.completed_at = timezone.now()
|
||||
execution.save()
|
||||
|
||||
thread = threading.Thread(target=run_strategy)
|
||||
thread.start()
|
||||
|
||||
return JsonResponse({
|
||||
'execution_id': execution.id,
|
||||
'status': 'pending',
|
||||
'message': 'Strategy execution started'
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'error': 'Invalid JSON in request body'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def execution_status(request, execution_id):
|
||||
execution = get_object_or_404(StrategyExecution, id=execution_id)
|
||||
|
||||
response_data = {
|
||||
'execution_id': execution.id,
|
||||
'strategy': execution.strategy_version.strategy.name,
|
||||
'version': execution.strategy_version.version,
|
||||
'status': execution.status,
|
||||
'started_at': execution.started_at.isoformat(),
|
||||
'execution_parameters': execution.execution_parameters
|
||||
}
|
||||
|
||||
if execution.completed_at:
|
||||
response_data['completed_at'] = execution.completed_at.isoformat()
|
||||
|
||||
if execution.status == 'completed' and execution.result:
|
||||
response_data['result'] = execution.result
|
||||
|
||||
if execution.status == 'failed' and execution.error_message:
|
||||
response_data['error_message'] = execution.error_message
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def list_available_implementations(request):
|
||||
"""레지스트리에 등록된 모든 전략 구현체 목록을 반환"""
|
||||
available_strategies = StrategyRegistry.list_strategies()
|
||||
return JsonResponse({
|
||||
'available_implementations': available_strategies
|
||||
})
|
||||
Reference in New Issue
Block a user