From 301457ce57ba046d67fc2a933a37419abd513543 Mon Sep 17 00:00:00 2001 From: 21in7 Date: Sun, 1 Mar 2026 20:39:26 +0900 Subject: [PATCH] chore: remove unused risk_per_trade references Made-with: Cursor --- .../2026-03-01-dynamic-margin-ratio-design.md | 131 +++++++ .../2026-03-01-dynamic-margin-ratio-plan.md | 368 ++++++++++++++++++ scripts/train_and_deploy.sh | 14 +- 3 files changed, 510 insertions(+), 3 deletions(-) create mode 100644 docs/plans/2026-03-01-dynamic-margin-ratio-design.md create mode 100644 docs/plans/2026-03-01-dynamic-margin-ratio-plan.md diff --git a/docs/plans/2026-03-01-dynamic-margin-ratio-design.md b/docs/plans/2026-03-01-dynamic-margin-ratio-design.md new file mode 100644 index 0000000..0d656e6 --- /dev/null +++ b/docs/plans/2026-03-01-dynamic-margin-ratio-design.md @@ -0,0 +1,131 @@ +# 동적 증거금 비율 설계 + +**날짜**: 2026-03-01 +**목적**: 잔고의 50%를 증거금으로 사용하되, 잔고가 늘어날수록 비율이 선형으로 감소하는 안전한 포지션 크기 계산 도입 + +--- + +## 배경 + +- 현재 포지션 크기 계산: `risk_per_trade = 0.02` (잔고의 2%) × 레버리지 → 명목금액 +- 현재 잔고 22 USDT 기준, 최소 명목금액(5 USDT) 보장 로직으로 5 USDT 포지션만 잡힘 +- 목표: 잔고의 50%를 증거금으로 활용하여 실질적인 포지션 크기 확보 +- 안전장치: 잔고가 늘수록 비율이 자동으로 줄어들어 과도한 노출 방지 + +--- + +## 아키텍처 + +### 데이터 흐름 + +``` +bot.run() + └─ balance = await exchange.get_balance() + └─ risk.set_base_balance(balance) ← 봇 시작 시 1회 + +bot._open_position() + └─ balance = await exchange.get_balance() + └─ margin_ratio = risk.get_dynamic_margin_ratio(balance) ← 신규 + └─ exchange.calculate_quantity(balance, price, leverage, margin_ratio) +``` + +### 비율 계산 공식 + +``` +ratio = MAX_RATIO - (balance - base_balance) × DECAY_RATE +ratio = clamp(ratio, MIN_RATIO, MAX_RATIO) +``` + +- `base_balance`: 봇 시작 시 바이낸스 API로 조회한 실제 잔고 +- `MAX_RATIO`: 잔고가 기준값일 때 최대 비율 (기본 50%) +- `MIN_RATIO`: 잔고가 아무리 늘어도 내려가지 않는 하한 비율 (기본 20%) +- `DECAY_RATE`: 잔고 1 USDT 증가당 비율 감소량 (기본 0.0006) + +### 시뮬레이션 (기본 파라미터 기준) + +| 잔고 | 증거금 비율 | 증거금 | 명목금액(×10배) | +|---|---|---|---| +| 22 USDT | 50.0% | 11.0 USDT | 110 USDT | +| 100 USDT | 45.3% | 45.3 USDT | 453 USDT | +| 300 USDT | 33.2% | 99.6 USDT | 996 USDT | +| 600 USDT | 20.0% (하한) | 120 USDT | 1,200 USDT | + +--- + +## 변경 파일 + +### 1. `src/config.py` + +`Config` 데이터클래스에 3개 파라미터 추가: + +```python +margin_max_ratio: float = 0.50 +margin_min_ratio: float = 0.20 +margin_decay_rate: float = 0.0006 +``` + +`__post_init__`에서 `.env` 값 읽기: + +```python +self.margin_max_ratio = float(os.getenv("MARGIN_MAX_RATIO", "0.50")) +self.margin_min_ratio = float(os.getenv("MARGIN_MIN_RATIO", "0.20")) +self.margin_decay_rate = float(os.getenv("MARGIN_DECAY_RATE", "0.0006")) +``` + +### 2. `src/risk_manager.py` + +메서드 2개 추가: + +```python +def set_base_balance(self, balance: float) -> None: + """봇 시작 시 기준 잔고 설정""" + self.initial_balance = balance + +def get_dynamic_margin_ratio(self, balance: float) -> float: + """잔고에 따라 선형 감소하는 증거금 비율 반환""" + ratio = self.config.margin_max_ratio - ( + (balance - self.initial_balance) * self.config.margin_decay_rate + ) + return max(self.config.margin_min_ratio, min(self.config.margin_max_ratio, ratio)) +``` + +### 3. `src/exchange.py` + +`calculate_quantity` 시그니처에 `margin_ratio` 파라미터 추가: + +```python +def calculate_quantity(self, balance: float, price: float, leverage: int, margin_ratio: float) -> float: + notional = balance * margin_ratio * leverage + if notional < self.MIN_NOTIONAL: + notional = self.MIN_NOTIONAL + ... +``` + +기존 `risk_per_trade` 기반 로직 제거. + +### 4. `src/bot.py` + +- `run()`: 시작 시 잔고 조회 후 `risk.set_base_balance(balance)` 호출 +- `_open_position()`: `margin_ratio = self.risk.get_dynamic_margin_ratio(balance)` 호출 후 `calculate_quantity`에 전달 + +### 5. `.env` + +``` +MARGIN_MAX_RATIO=0.50 +MARGIN_MIN_RATIO=0.20 +MARGIN_DECAY_RATE=0.0006 +``` + +--- + +## 제거되는 설정 + +- `RISK_PER_TRADE` — `.env` 및 `Config`에서 제거 (동적 비율로 대체) + +--- + +## 리스크 고려사항 + +- 잔고 22 USDT × 50% × 10배 레버리지 = 명목금액 110 USDT 노출 (잔고의 5배) +- 손실 시 잔고가 줄어들면 다음 포지션 크기도 자동으로 줄어드는 자연스러운 안전장치 존재 +- `MARGIN_DECAY_RATE` 조정으로 감소 속도 제어 가능 diff --git a/docs/plans/2026-03-01-dynamic-margin-ratio-plan.md b/docs/plans/2026-03-01-dynamic-margin-ratio-plan.md new file mode 100644 index 0000000..3482c05 --- /dev/null +++ b/docs/plans/2026-03-01-dynamic-margin-ratio-plan.md @@ -0,0 +1,368 @@ +# 동적 증거금 비율 구현 계획 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 잔고의 50%를 증거금으로 사용하되, 잔고가 늘수록 비율이 선형으로 감소하는 동적 포지션 크기 계산 도입 + +**Architecture:** `RiskManager`에 `get_dynamic_margin_ratio(balance)` 메서드를 추가하고, `bot.py`에서 포지션 진입 전 호출한다. `exchange.py`의 `calculate_quantity`는 `margin_ratio` 파라미터를 받아 기존 `risk_per_trade` 로직을 대체한다. 봇 시작 시 바이낸스 API로 실제 잔고를 조회하여 기준값(`base_balance`)으로 저장한다. + +**Tech Stack:** Python 3.11, python-binance, loguru, pytest, python-dotenv + +--- + +## 사전 확인 + +- 현재 `.env`: `RISK_PER_TRADE=0.02` 존재 +- 현재 `Config.risk_per_trade: float = 0.02` 존재 +- 현재 `calculate_quantity`는 `balance * risk_per_trade * leverage` 로직 사용 +- 테스트 파일 위치: `tests/` 디렉토리 (없으면 생성) + +--- + +### Task 1: Config에 동적 증거금 파라미터 추가 + +**Files:** +- Modify: `src/config.py` +- Modify: `.env` + +**Step 1: `.env`에 새 파라미터 추가** + +`.env` 파일 하단에 추가: + +``` +MARGIN_MAX_RATIO=0.50 +MARGIN_MIN_RATIO=0.20 +MARGIN_DECAY_RATE=0.0006 +``` + +기존 `RISK_PER_TRADE=0.02` 줄은 삭제. + +**Step 2: `src/config.py` 수정** + +`Config` 데이터클래스에 필드 추가, `risk_per_trade` 필드 제거: + +```python +@dataclass +class Config: + api_key: str = "" + api_secret: str = "" + symbol: str = "XRPUSDT" + leverage: int = 10 + max_positions: int = 3 + stop_loss_pct: float = 0.015 + take_profit_pct: float = 0.045 + trailing_stop_pct: float = 0.01 + discord_webhook_url: str = "" + margin_max_ratio: float = 0.50 + margin_min_ratio: float = 0.20 + margin_decay_rate: float = 0.0006 + + def __post_init__(self): + self.api_key = os.getenv("BINANCE_API_KEY", "") + self.api_secret = os.getenv("BINANCE_API_SECRET", "") + self.symbol = os.getenv("SYMBOL", "XRPUSDT") + self.leverage = int(os.getenv("LEVERAGE", "10")) + self.discord_webhook_url = os.getenv("DISCORD_WEBHOOK_URL", "") + self.margin_max_ratio = float(os.getenv("MARGIN_MAX_RATIO", "0.50")) + self.margin_min_ratio = float(os.getenv("MARGIN_MIN_RATIO", "0.20")) + self.margin_decay_rate = float(os.getenv("MARGIN_DECAY_RATE", "0.0006")) +``` + +**Step 3: Commit** + +```bash +git add src/config.py .env +git commit -m "feat: add dynamic margin ratio config params" +``` + +--- + +### Task 2: RiskManager에 동적 비율 메서드 추가 + +**Files:** +- Modify: `src/risk_manager.py` +- Create: `tests/test_risk_manager.py` + +**Step 1: 실패하는 테스트 작성** + +`tests/test_risk_manager.py` 생성: + +```python +import pytest +from src.config import Config +from src.risk_manager import RiskManager + + +@pytest.fixture +def config(): + c = Config() + c.margin_max_ratio = 0.50 + c.margin_min_ratio = 0.20 + c.margin_decay_rate = 0.0006 + return c + + +@pytest.fixture +def risk(config): + r = RiskManager(config) + r.set_base_balance(22.0) + return r + + +def test_set_base_balance(risk): + assert risk.initial_balance == 22.0 + + +def test_ratio_at_base_balance(risk): + """기준 잔고에서 최대 비율(50%) 반환""" + ratio = risk.get_dynamic_margin_ratio(22.0) + assert ratio == pytest.approx(0.50, abs=1e-6) + + +def test_ratio_decreases_as_balance_grows(risk): + """잔고가 늘수록 비율 감소""" + ratio_100 = risk.get_dynamic_margin_ratio(100.0) + ratio_300 = risk.get_dynamic_margin_ratio(300.0) + assert ratio_100 < 0.50 + assert ratio_300 < ratio_100 + + +def test_ratio_clamped_at_min(risk): + """잔고가 매우 커도 최소 비율(20%) 이하로 내려가지 않음""" + ratio = risk.get_dynamic_margin_ratio(10000.0) + assert ratio == pytest.approx(0.20, abs=1e-6) + + +def test_ratio_clamped_at_max(risk): + """잔고가 기준보다 작아도 최대 비율(50%) 초과하지 않음""" + ratio = risk.get_dynamic_margin_ratio(5.0) + assert ratio == pytest.approx(0.50, abs=1e-6) +``` + +**Step 2: 테스트 실패 확인** + +```bash +pytest tests/test_risk_manager.py -v +``` + +Expected: `AttributeError: 'RiskManager' object has no attribute 'set_base_balance'` + +**Step 3: `src/risk_manager.py` 수정** + +기존 코드에 메서드 2개 추가: + +```python +def set_base_balance(self, balance: float) -> None: + """봇 시작 시 기준 잔고 설정 (동적 비율 계산 기준점)""" + self.initial_balance = balance + +def get_dynamic_margin_ratio(self, balance: float) -> float: + """잔고에 따라 선형 감소하는 증거금 비율 반환""" + ratio = self.config.margin_max_ratio - ( + (balance - self.initial_balance) * self.config.margin_decay_rate + ) + return max(self.config.margin_min_ratio, min(self.config.margin_max_ratio, ratio)) +``` + +**Step 4: 테스트 통과 확인** + +```bash +pytest tests/test_risk_manager.py -v +``` + +Expected: 5개 테스트 모두 PASS + +**Step 5: Commit** + +```bash +git add src/risk_manager.py tests/test_risk_manager.py +git commit -m "feat: add get_dynamic_margin_ratio to RiskManager" +``` + +--- + +### Task 3: exchange.py의 calculate_quantity 수정 + +**Files:** +- Modify: `src/exchange.py:18-29` +- Create: `tests/test_exchange.py` + +**Step 1: 실패하는 테스트 작성** + +`tests/test_exchange.py` 생성: + +```python +import pytest +from unittest.mock import MagicMock +from src.config import Config +from src.exchange import BinanceFuturesClient + + +@pytest.fixture +def client(): + config = Config() + config.leverage = 10 + c = BinanceFuturesClient.__new__(BinanceFuturesClient) + c.config = config + return c + + +def test_calculate_quantity_basic(client): + """잔고 22, 비율 50%, 레버리지 10배 → 명목금액 110, XRP 가격 2.5 → 수량 44.0""" + qty = client.calculate_quantity(balance=22.0, price=2.5, leverage=10, margin_ratio=0.50) + # 명목금액 = 22 * 0.5 * 10 = 110, 수량 = 110 / 2.5 = 44.0 + assert qty == pytest.approx(44.0, abs=0.1) + + +def test_calculate_quantity_min_notional(client): + """명목금액이 최소(5 USDT) 미만이면 최소값으로 올림""" + qty = client.calculate_quantity(balance=1.0, price=2.5, leverage=1, margin_ratio=0.01) + # 명목금액 = 1 * 0.01 * 1 = 0.01 < 5 → 최소 5 USDT + assert qty * 2.5 >= 5.0 + + +def test_calculate_quantity_zero_balance(client): + """잔고 0이면 최소 명목금액 기반 수량 반환""" + qty = client.calculate_quantity(balance=0.0, price=2.5, leverage=10, margin_ratio=0.50) + assert qty > 0 +``` + +**Step 2: 테스트 실패 확인** + +```bash +pytest tests/test_exchange.py -v +``` + +Expected: `TypeError: calculate_quantity() got an unexpected keyword argument 'margin_ratio'` + +**Step 3: `src/exchange.py` 수정** + +`calculate_quantity` 메서드를 아래로 교체: + +```python +def calculate_quantity(self, balance: float, price: float, leverage: int, margin_ratio: float) -> float: + """동적 증거금 비율 기반 포지션 크기 계산 (최소 명목금액 $5 보장)""" + notional = balance * margin_ratio * leverage + if notional < self.MIN_NOTIONAL: + notional = self.MIN_NOTIONAL + quantity = notional / price + qty_rounded = round(quantity, 1) + if qty_rounded * price < self.MIN_NOTIONAL: + qty_rounded = round(self.MIN_NOTIONAL / price + 0.05, 1) + return qty_rounded +``` + +**Step 4: 테스트 통과 확인** + +```bash +pytest tests/test_exchange.py -v +``` + +Expected: 3개 테스트 모두 PASS + +**Step 5: Commit** + +```bash +git add src/exchange.py tests/test_exchange.py +git commit -m "feat: replace risk_per_trade with margin_ratio in calculate_quantity" +``` + +--- + +### Task 4: bot.py 연결 + +**Files:** +- Modify: `src/bot.py:85-99` (`_open_position`) +- Modify: `src/bot.py:165-172` (`run`) + +**Step 1: `run()` 메서드에 `set_base_balance` 호출 추가** + +`run()` 메서드를 아래로 교체: + +```python +async def run(self): + logger.info(f"봇 시작: {self.config.symbol}, 레버리지 {self.config.leverage}x") + await self._recover_position() + balance = await self.exchange.get_balance() + self.risk.set_base_balance(balance) + logger.info(f"기준 잔고 설정: {balance:.2f} USDT (동적 증거금 비율 기준점)") + await self.stream.start( + api_key=self.config.api_key, + api_secret=self.config.api_secret, + ) +``` + +**Step 2: `_open_position()` 메서드에 동적 비율 적용** + +`_open_position()` 내부 `quantity` 계산 부분을 수정: + +```python +async def _open_position(self, signal: str, df): + balance = await self.exchange.get_balance() + price = df["close"].iloc[-1] + margin_ratio = self.risk.get_dynamic_margin_ratio(balance) + quantity = self.exchange.calculate_quantity( + balance=balance, price=price, leverage=self.config.leverage, margin_ratio=margin_ratio + ) + logger.info(f"포지션 크기: 잔고={balance:.2f} USDT, 증거금비율={margin_ratio:.1%}, 수량={quantity}") + # 이하 기존 코드 유지 (stop_loss, take_profit, place_order 등) +``` + +**Step 3: 전체 테스트 실행** + +```bash +pytest tests/ -v +``` + +Expected: 전체 PASS + +**Step 4: Commit** + +```bash +git add src/bot.py +git commit -m "feat: apply dynamic margin ratio in bot position sizing" +``` + +--- + +### Task 5: 기존 risk_per_trade 참조 정리 + +**Files:** +- Search: 프로젝트 전체에서 `risk_per_trade` 참조 확인 + +**Step 1: 잔여 참조 검색** + +```bash +grep -r "risk_per_trade" src/ tests/ .env +``` + +Expected: 결과 없음 (이미 모두 제거됨) + +남아있는 경우 해당 파일에서 제거. + +**Step 2: 전체 테스트 최종 확인** + +```bash +pytest tests/ -v +``` + +Expected: 전체 PASS + +**Step 3: Commit** + +```bash +git add -A +git commit -m "chore: remove unused risk_per_trade references" +``` + +--- + +## 검증 체크리스트 + +- [ ] `pytest tests/test_risk_manager.py` — 5개 PASS +- [ ] `pytest tests/test_exchange.py` — 3개 PASS +- [ ] `pytest tests/` — 전체 PASS +- [ ] `.env`에 `MARGIN_MAX_RATIO`, `MARGIN_MIN_RATIO`, `MARGIN_DECAY_RATE` 존재 +- [ ] `.env`에 `RISK_PER_TRADE` 없음 +- [ ] 봇 시작 로그에 "기준 잔고 설정: XX USDT" 출력 +- [ ] 포지션 진입 로그에 "증거금비율=50.0%" 출력 (잔고 22 USDT 기준) diff --git a/scripts/train_and_deploy.sh b/scripts/train_and_deploy.sh index 6f851ae..4d31cf9 100755 --- a/scripts/train_and_deploy.sh +++ b/scripts/train_and_deploy.sh @@ -8,12 +8,20 @@ set -euo pipefail -LXC_HOST="${1:-root@10.1.10.24}" -LXC_MODELS_PATH="${2:-/root/cointrader/models}" - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +VENV_PATH="${VENV_PATH:-$PROJECT_ROOT/.venv}" +if [ -f "$VENV_PATH/bin/activate" ]; then + # shellcheck source=/dev/null + source "$VENV_PATH/bin/activate" +else + echo "경고: 가상환경을 찾을 수 없습니다 ($VENV_PATH). 시스템 Python을 사용합니다." >&2 +fi + +LXC_HOST="${1:-root@10.1.10.24}" +LXC_MODELS_PATH="${2:-/root/cointrader/models}" + cd "$PROJECT_ROOT" echo "=== [1/3] 데이터 수집 (XRP + BTC + ETH 3심볼) ==="