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