업데이트
This commit is contained in:
245
src/tests/test_order.py
Normal file
245
src/tests/test_order.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""Unit tests for order.py - order placement and validation."""
|
||||
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from src.common import MIN_KRW_ORDER
|
||||
from src.order import (
|
||||
adjust_price_to_tick_size,
|
||||
place_buy_order_upbit,
|
||||
place_sell_order_upbit,
|
||||
)
|
||||
|
||||
|
||||
class TestAdjustPriceToTickSize:
|
||||
"""Test price adjustment to Upbit tick size."""
|
||||
|
||||
def test_adjust_price_with_valid_price(self):
|
||||
"""Test normal price adjustment."""
|
||||
with patch("src.order.pyupbit.get_tick_size", return_value=1000):
|
||||
result = adjust_price_to_tick_size(50000000)
|
||||
assert result > 0
|
||||
assert result % 1000 == 0
|
||||
|
||||
def test_adjust_price_returns_original_on_error(self):
|
||||
"""Test fallback to original price on API error."""
|
||||
with patch("src.order.pyupbit.get_tick_size", side_effect=Exception("API error")):
|
||||
result = adjust_price_to_tick_size(50000000)
|
||||
assert result == 50000000
|
||||
|
||||
|
||||
class TestPlaceBuyOrderValidation:
|
||||
"""Test buy order validation (dry-run mode)."""
|
||||
|
||||
def test_buy_order_dry_run(self):
|
||||
"""Test dry-run buy order simulation."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = True
|
||||
cfg.config = {}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=50000000):
|
||||
result = place_buy_order_upbit("KRW-BTC", 100000, cfg)
|
||||
|
||||
assert result["status"] == "simulated"
|
||||
assert result["market"] == "KRW-BTC"
|
||||
assert result["amount_krw"] == 100000
|
||||
|
||||
def test_buy_order_below_min_amount(self):
|
||||
"""Test buy order rejected for amount below minimum."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = True
|
||||
cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=50000000):
|
||||
# Try to buy with amount below minimum
|
||||
result = place_buy_order_upbit("KRW-BTC", MIN_KRW_ORDER - 1000, cfg)
|
||||
|
||||
assert result["status"] == "skipped_too_small"
|
||||
assert result["reason"] == "min_order_value"
|
||||
|
||||
def test_buy_order_zero_price(self):
|
||||
"""Test buy order rejected when current price is 0 or invalid."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = True
|
||||
cfg.config = {}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=0):
|
||||
result = place_buy_order_upbit("KRW-BTC", 100000, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert "error" in result
|
||||
|
||||
def test_buy_order_no_api_key(self):
|
||||
"""Test buy order fails gracefully without API keys."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = None
|
||||
cfg.upbit_secret_key = None
|
||||
cfg.config = {}
|
||||
|
||||
result = place_buy_order_upbit("KRW-BTC", 100000, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert "error" in result
|
||||
|
||||
|
||||
class TestPlaceSellOrderValidation:
|
||||
"""Test sell order validation (dry-run mode)."""
|
||||
|
||||
def test_sell_order_dry_run(self):
|
||||
"""Test dry-run sell order simulation."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = True
|
||||
|
||||
result = place_sell_order_upbit("KRW-BTC", 0.01, cfg)
|
||||
|
||||
assert result["status"] == "simulated"
|
||||
assert result["market"] == "KRW-BTC"
|
||||
assert result["amount"] == 0.01
|
||||
|
||||
def test_sell_order_invalid_amount(self):
|
||||
"""Test sell order rejected for invalid amount."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}}
|
||||
|
||||
result = place_sell_order_upbit("KRW-BTC", 0, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["error"] == "invalid_amount"
|
||||
|
||||
def test_sell_order_below_min_value(self):
|
||||
"""Test sell order rejected when estimated value below minimum."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}}
|
||||
|
||||
# Current price very low so amount * price < min_order_value
|
||||
with patch("src.holdings.get_current_price", return_value=100):
|
||||
# 0.001 BTC * 100 KRW = 0.1 KRW < 5000 KRW minimum
|
||||
result = place_sell_order_upbit("KRW-BTC", 0.001, cfg)
|
||||
|
||||
assert result["status"] == "skipped_too_small"
|
||||
assert result["reason"] == "min_order_value"
|
||||
|
||||
def test_sell_order_price_unavailable(self):
|
||||
"""Test sell order fails when current price unavailable."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=0):
|
||||
result = place_sell_order_upbit("KRW-BTC", 0.01, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["error"] == "price_unavailable"
|
||||
|
||||
def test_sell_order_no_api_key(self):
|
||||
"""Test sell order fails gracefully without API keys."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = None
|
||||
cfg.upbit_secret_key = None
|
||||
cfg.config = {}
|
||||
|
||||
result = place_sell_order_upbit("KRW-BTC", 0.01, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert "error" in result
|
||||
|
||||
|
||||
class TestBuyOrderResponseValidation:
|
||||
"""Test buy order API response validation."""
|
||||
|
||||
def test_response_type_validation(self):
|
||||
"""Test validation of response type (must be dict)."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=50000000):
|
||||
with patch("src.order.pyupbit.Upbit") as mock_upbit_class:
|
||||
mock_upbit = MagicMock()
|
||||
mock_upbit_class.return_value = mock_upbit
|
||||
# Invalid response: string instead of dict
|
||||
mock_upbit.buy_limit_order.return_value = "invalid_response"
|
||||
|
||||
with patch("src.order.adjust_price_to_tick_size", return_value=50000000):
|
||||
result = place_buy_order_upbit("KRW-BTC", 100000, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["error"] == "invalid_response_type"
|
||||
|
||||
def test_response_uuid_validation(self):
|
||||
"""Test validation of uuid in response."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER, "buy_price_slippage_pct": 1.0}}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=50000000):
|
||||
with patch("src.order.pyupbit.Upbit") as mock_upbit_class:
|
||||
mock_upbit = MagicMock()
|
||||
mock_upbit_class.return_value = mock_upbit
|
||||
# Response without uuid (Upbit error format)
|
||||
mock_upbit.buy_limit_order.return_value = {
|
||||
"error": {"name": "insufficient_funds", "message": "잔액 부족"}
|
||||
}
|
||||
|
||||
with patch("src.order.adjust_price_to_tick_size", return_value=50000000):
|
||||
result = place_buy_order_upbit("KRW-BTC", 100000, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["error"] == "order_rejected"
|
||||
|
||||
|
||||
class TestSellOrderResponseValidation:
|
||||
"""Test sell order API response validation."""
|
||||
|
||||
def test_sell_response_uuid_missing(self):
|
||||
"""Test sell order fails when uuid missing from response."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {"auto_trade": {"min_order_value_krw": MIN_KRW_ORDER}}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=50000000):
|
||||
with patch("src.order.pyupbit.Upbit") as mock_upbit_class:
|
||||
mock_upbit = MagicMock()
|
||||
mock_upbit_class.return_value = mock_upbit
|
||||
# Missing uuid
|
||||
mock_upbit.sell_market_order.return_value = {"market": "KRW-BTC"}
|
||||
|
||||
result = place_sell_order_upbit("KRW-BTC", 0.01, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["error"] == "order_rejected"
|
||||
|
||||
def test_sell_response_type_invalid(self):
|
||||
"""Test sell order fails with invalid response type."""
|
||||
cfg = Mock()
|
||||
cfg.dry_run = False
|
||||
cfg.upbit_access_key = "key"
|
||||
cfg.upbit_secret_key = "secret"
|
||||
cfg.config = {}
|
||||
|
||||
with patch("src.holdings.get_current_price", return_value=50000000):
|
||||
with patch("src.order.pyupbit.Upbit") as mock_upbit_class:
|
||||
mock_upbit = MagicMock()
|
||||
mock_upbit_class.return_value = mock_upbit
|
||||
# Invalid response
|
||||
mock_upbit.sell_market_order.return_value = "not_a_dict"
|
||||
|
||||
result = place_sell_order_upbit("KRW-BTC", 0.01, cfg)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["error"] == "invalid_response_type"
|
||||
Reference in New Issue
Block a user