Compare commits

...

5 Commits

Author SHA1 Message Date
21in7
301457ce57 chore: remove unused risk_per_trade references
Made-with: Cursor
2026-03-01 20:39:26 +09:00
21in7
ab580b18af feat: apply dynamic margin ratio in bot position sizing
Made-with: Cursor
2026-03-01 20:39:07 +09:00
21in7
795689ac49 feat: replace risk_per_trade with margin_ratio in calculate_quantity
Made-with: Cursor
2026-03-01 20:38:18 +09:00
21in7
fe9690698a feat: add get_dynamic_margin_ratio to RiskManager
Made-with: Cursor
2026-03-01 20:37:46 +09:00
21in7
95abac53a8 feat: add dynamic margin ratio config params
Made-with: Cursor
2026-03-01 20:37:04 +09:00
10 changed files with 618 additions and 28 deletions

View File

@@ -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` 조정으로 감소 속도 제어 가능

View File

@@ -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 기준)

View File

@@ -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심볼) ==="

View File

@@ -85,9 +85,11 @@ class TradingBot:
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
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 = Indicators(df).get_atr_stop(df, signal, price)
notional = quantity * price
@@ -165,6 +167,9 @@ class TradingBot:
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,

View File

@@ -11,17 +11,21 @@ class Config:
api_secret: str = ""
symbol: str = "XRPUSDT"
leverage: int = 10
risk_per_trade: float = 0.02
max_positions: int = 3
stop_loss_pct: float = 0.015 # 1.5%
take_profit_pct: float = 0.045 # 4.5% (3:1 RR)
trailing_stop_pct: float = 0.01 # 1%
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.risk_per_trade = float(os.getenv("RISK_PER_TRADE", "0.02"))
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"))

View File

@@ -15,14 +15,12 @@ class BinanceFuturesClient:
MIN_NOTIONAL = 5.0 # 바이낸스 선물 최소 명목금액 (USDT)
def calculate_quantity(self, balance: float, price: float, leverage: int) -> float:
"""리스크 기반 포지션 크기 계산 (최소 명목금액 $5 보장)"""
risk_amount = balance * self.config.risk_per_trade
notional = risk_amount * leverage
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
# XRP는 소수점 1자리, 단 최소 명목금액 충족 여부 재확인
qty_rounded = round(quantity, 1)
if qty_rounded * price < self.MIN_NOTIONAL:
qty_rounded = round(self.MIN_NOTIONAL / price + 0.05, 1)

View File

@@ -34,3 +34,14 @@ class RiskManager:
"""매일 자정 초기화"""
self.daily_pnl = 0.0
logger.info("일일 PnL 초기화")
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))

View File

@@ -6,16 +6,16 @@ from src.config import Config
def test_config_loads_symbol():
os.environ["SYMBOL"] = "XRPUSDT"
os.environ["LEVERAGE"] = "10"
os.environ["RISK_PER_TRADE"] = "0.02"
cfg = Config()
assert cfg.symbol == "XRPUSDT"
assert cfg.leverage == 10
assert cfg.risk_per_trade == 0.02
def test_config_notion_keys():
os.environ["NOTION_TOKEN"] = "secret_test"
os.environ["NOTION_DATABASE_ID"] = "db_test_id"
def test_config_dynamic_margin_params():
os.environ["MARGIN_MAX_RATIO"] = "0.50"
os.environ["MARGIN_MIN_RATIO"] = "0.20"
os.environ["MARGIN_DECAY_RATE"] = "0.0006"
cfg = Config()
assert cfg.notion_token == "secret_test"
assert cfg.notion_database_id == "db_test_id"
assert cfg.margin_max_ratio == 0.50
assert cfg.margin_min_ratio == 0.20
assert cfg.margin_decay_rate == 0.0006

View File

@@ -12,11 +12,19 @@ def config():
"BINANCE_API_SECRET": "test_secret",
"SYMBOL": "XRPUSDT",
"LEVERAGE": "10",
"RISK_PER_TRADE": "0.02",
})
return Config()
@pytest.fixture
def client():
config = Config()
config.leverage = 10
c = BinanceFuturesClient.__new__(BinanceFuturesClient)
c.config = config
return c
@pytest.mark.asyncio
async def test_set_leverage(config):
with patch("src.exchange.Client") as MockClient:
@@ -28,11 +36,21 @@ async def test_set_leverage(config):
assert result is not None
def test_calculate_quantity(config):
with patch("src.exchange.Client") as MockClient:
MockClient.return_value = MagicMock()
client = BinanceFuturesClient(config)
# 잔고 1000 USDT, 리스크 2%, 레버리지 10, 가격 0.5
qty = client.calculate_quantity(balance=1000.0, price=0.5, leverage=10)
# 1000 * 0.02 * 10 / 0.5 = 400
assert qty == pytest.approx(400.0, rel=0.01)
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

View File

@@ -11,7 +11,6 @@ def config():
"BINANCE_API_SECRET": "s",
"SYMBOL": "XRPUSDT",
"LEVERAGE": "10",
"RISK_PER_TRADE": "0.02",
})
return Config()
@@ -34,3 +33,51 @@ def test_position_size_capped(config):
rm = RiskManager(config, max_daily_loss_pct=0.05)
rm.open_positions = ["pos1", "pos2", "pos3"]
assert rm.can_open_new_position() is False
# --- 동적 증거금 비율 테스트 ---
@pytest.fixture
def dynamic_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(dynamic_config):
r = RiskManager(dynamic_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)