248 lines
9.2 KiB
Python
248 lines
9.2 KiB
Python
"""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"] == 99950.0
|
|
|
|
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):
|
|
with patch("src.common.krw_budget_manager.allocate", return_value=(True, 100000, "tok")):
|
|
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):
|
|
with patch("src.common.krw_budget_manager.allocate", return_value=(True, 100000, "tok")):
|
|
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"
|