Files
AutoCoinTrader2/src/tests/test_order.py

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"