From 01403c7df4f21d964cfb1981af1eee71783f27a0 Mon Sep 17 00:00:00 2001 From: Jongheon Kim Date: Sat, 4 Oct 2025 13:50:46 +0900 Subject: [PATCH] Initial commit: Django quantitative strategy executor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 11 + .gitignore | 87 ++++++++ CLAUDE.md | 66 ++++++ executor/__init__.py | 0 executor/asgi.py | 16 ++ executor/settings.py | 124 +++++++++++ executor/urls.py | 23 ++ executor/wsgi.py | 16 ++ manage.py | 22 ++ pyproject.toml | 8 + strategies/__init__.py | 0 strategies/admin.py | 3 + strategies/apps.py | 6 + strategies/base.py | 110 ++++++++++ strategies/implementations.py | 202 ++++++++++++++++++ strategies/migrations/0001_initial.py | 61 ++++++ ...02_remove_strategyversion_code_and_more.py | 22 ++ strategies/migrations/__init__.py | 0 strategies/models.py | 81 +++++++ strategies/tests.py | 3 + strategies/urls.py | 9 + strategies/views.py | 161 ++++++++++++++ 22 files changed, 1031 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 executor/__init__.py create mode 100644 executor/asgi.py create mode 100644 executor/settings.py create mode 100644 executor/urls.py create mode 100644 executor/wsgi.py create mode 100755 manage.py create mode 100644 pyproject.toml create mode 100644 strategies/__init__.py create mode 100644 strategies/admin.py create mode 100644 strategies/apps.py create mode 100644 strategies/base.py create mode 100644 strategies/implementations.py create mode 100644 strategies/migrations/0001_initial.py create mode 100644 strategies/migrations/0002_remove_strategyversion_code_and_more.py create mode 100644 strategies/migrations/__init__.py create mode 100644 strategies/models.py create mode 100644 strategies/tests.py create mode 100644 strategies/urls.py create mode 100644 strategies/views.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..390caf7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(python manage.py:*)", + "Bash(git init:*)", + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..055ff96 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..618fe86 --- /dev/null +++ b/CLAUDE.md @@ -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 ` +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 \ No newline at end of file diff --git a/executor/__init__.py b/executor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/executor/asgi.py b/executor/asgi.py new file mode 100644 index 0000000..482b085 --- /dev/null +++ b/executor/asgi.py @@ -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() diff --git a/executor/settings.py b/executor/settings.py new file mode 100644 index 0000000..68fb2ac --- /dev/null +++ b/executor/settings.py @@ -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' diff --git a/executor/urls.py b/executor/urls.py new file mode 100644 index 0000000..f8448d7 --- /dev/null +++ b/executor/urls.py @@ -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')), +] diff --git a/executor/wsgi.py b/executor/wsgi.py new file mode 100644 index 0000000..7d29d68 --- /dev/null +++ b/executor/wsgi.py @@ -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() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..6a74c73 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', '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() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..29ad820 --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/strategies/__init__.py b/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/strategies/admin.py b/strategies/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/strategies/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/strategies/apps.py b/strategies/apps.py new file mode 100644 index 0000000..6a711b2 --- /dev/null +++ b/strategies/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StrategiesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'strategies' diff --git a/strategies/base.py b/strategies/base.py new file mode 100644 index 0000000..0ea474a --- /dev/null +++ b/strategies/base.py @@ -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) \ No newline at end of file diff --git a/strategies/implementations.py b/strategies/implementations.py new file mode 100644 index 0000000..10b95ed --- /dev/null +++ b/strategies/implementations.py @@ -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) + } \ No newline at end of file diff --git a/strategies/migrations/0001_initial.py b/strategies/migrations/0001_initial.py new file mode 100644 index 0000000..d1589e7 --- /dev/null +++ b/strategies/migrations/0001_initial.py @@ -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'], + }, + ), + ] diff --git a/strategies/migrations/0002_remove_strategyversion_code_and_more.py b/strategies/migrations/0002_remove_strategyversion_code_and_more.py new file mode 100644 index 0000000..d88923b --- /dev/null +++ b/strategies/migrations/0002_remove_strategyversion_code_and_more.py @@ -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), + ), + ] diff --git a/strategies/migrations/__init__.py b/strategies/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/strategies/models.py b/strategies/models.py new file mode 100644 index 0000000..d004d9d --- /dev/null +++ b/strategies/models.py @@ -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'] diff --git a/strategies/tests.py b/strategies/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/strategies/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/strategies/urls.py b/strategies/urls.py new file mode 100644 index 0000000..da749da --- /dev/null +++ b/strategies/urls.py @@ -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//', views.execution_status, name='execution_status'), +] \ No newline at end of file diff --git a/strategies/views.py b/strategies/views.py new file mode 100644 index 0000000..5c2e74f --- /dev/null +++ b/strategies/views.py @@ -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 + })