Files
bn-pc/tests/test_position_closer_flow.py
yhydev 48d31cd1d0 移除 Redis、精简配置,新增 Docker 支持
- position_closer: 去掉 Redis 依赖,平仓条件仅名义+未实现盈亏
- requirements: 移除 redis
- settings.toml: 仅保留实际使用的配置项
- 新增 Dockerfile(仅安装依赖)、docker-compose(挂载代码与配置)
- 新增 .dockerignore、.gitignore(含 nohup.log)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 10:06:30 +08:00

217 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
除平仓外的流程单元测试拉持仓、解析精度、dry_run 不下单。
使用 mock 替代真实 Binance/Redis验证 get_positions、get_symbol_precision、
place_close_order(dry_run=True) 不调用 futures_create_order。
"""
import sys
from decimal import Decimal
from pathlib import Path
from unittest.mock import AsyncMock
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from position_closer import (
filter_nonzero_positions,
get_min_unrealized_profit_in_window,
get_positions,
get_symbol_precision,
get_total_unrealized_profit,
parse_exchange_info_to_precisions,
place_close_order,
save_unrealized_profit_history,
)
# ----- get_total_unrealized_profit账户总未实现盈亏 -----
@pytest.mark.asyncio
async def test_get_total_unrealized_profit_sums_all_positions() -> None:
raw_positions = [
{"symbol": "BTCUSDT", "unrealizedProfit": "10.5"},
{"symbol": "ETHUSDT", "unrealizedProfit": "-2"},
{"symbol": "XRPUSDT", "unrealizedProfit": "0.5"},
]
client = AsyncMock()
client.futures_account = AsyncMock(return_value={"positions": raw_positions})
result = await get_total_unrealized_profit(client)
assert result == pytest.approx(10.5 - 2 + 0.5)
assert client.futures_account.await_count == 1
# ----- get_positions拉取持仓并过滤为零 -----
@pytest.mark.asyncio
async def test_get_positions_returns_only_nonzero() -> None:
raw_positions = [
{"symbol": "BTCUSDT", "positionAmt": "0.1", "notional": "5000"},
{"symbol": "ETHUSDT", "positionAmt": "0", "notional": "0"},
{"symbol": "XRPUSDT", "positionAmt": "-1", "notional": "2000"},
]
client = AsyncMock()
client.futures_account = AsyncMock(return_value={"positions": raw_positions})
result = await get_positions(client)
assert result == filter_nonzero_positions(raw_positions)
assert len(result) == 2
assert client.futures_account.await_count == 1
@pytest.mark.asyncio
async def test_get_positions_empty_account() -> None:
client = AsyncMock()
client.futures_account = AsyncMock(return_value={"positions": []})
result = await get_positions(client)
assert result == []
assert client.futures_account.await_count == 1
# ----- get_symbol_precision拉取并解析精度 -----
@pytest.mark.asyncio
async def test_get_symbol_precision_parses_exchange_info() -> None:
info = {
"symbols": [
{
"symbol": "BTCUSDT",
"filters": [
{"filterType": "LOT_SIZE", "stepSize": "0.001"},
{"filterType": "PRICE_FILTER", "tickSize": "0.1"},
],
}
]
}
client = AsyncMock()
client.futures_exchange_info = AsyncMock(return_value=info)
result = await get_symbol_precision(client)
assert result == parse_exchange_info_to_precisions(info)
assert result["BTCUSDT"] == {"lot_size": "0.001", "price_filter": "0.1"}
assert client.futures_exchange_info.await_count == 1
# ----- place_close_order(dry_run=True):不调用下单 API -----
@pytest.mark.asyncio
async def test_place_close_order_dry_run_does_not_call_futures_create_order() -> None:
client = AsyncMock()
client.futures_create_order = AsyncMock()
precisions = {"BTCUSDT": {"lot_size": "0.001", "price_filter": "0.1"}}
await place_close_order(
client,
symbol="BTCUSDT",
position_side="LONG",
position_amt=0.1,
current_price=Decimal("50000"),
precisions=precisions,
reduce_only=True,
dry_run=True,
)
client.futures_create_order.assert_not_called()
@pytest.mark.asyncio
async def test_place_close_order_not_dry_run_calls_futures_create_order() -> None:
client = AsyncMock()
client.futures_create_order = AsyncMock(return_value={"orderId": 123})
precisions = {"BTCUSDT": {"lot_size": "0.001", "price_filter": "0.1"}}
await place_close_order(
client,
symbol="BTCUSDT",
position_side="LONG",
position_amt=0.1,
current_price=Decimal("50000"),
precisions=precisions,
reduce_only=True,
dry_run=False,
)
client.futures_create_order.assert_called_once()
call_kw = client.futures_create_order.call_args[1]
assert call_kw["symbol"] == "BTCUSDT"
assert call_kw["side"] == "SELL"
assert call_kw["quantity"] == "0.1"
assert "price" in call_kw
# ----- save_unrealized_profit_history / get_min_unrealized_profit_in_window -----
@pytest.mark.asyncio
async def test_save_unrealized_profit_history_calls_zadd_and_zremrangebyscore() -> None:
from unittest.mock import MagicMock, patch
mock_redis = MagicMock()
mock_redis.zadd = AsyncMock()
mock_redis.zremrangebyscore = AsyncMock()
mock_redis.aclose = AsyncMock()
with patch("redis.asyncio.Redis") as MockRedis:
MockRedis.from_url.return_value = mock_redis
await save_unrealized_profit_history(
"redis://localhost/0",
"test:history",
12.5,
300,
)
mock_redis.zadd.assert_called_once()
call_args = mock_redis.zadd.call_args[0]
assert call_args[0] == "test:history"
assert isinstance(call_args[1], dict)
member = list(call_args[1].keys())[0]
assert ":12.5" in member
mock_redis.zremrangebyscore.assert_called_once()
mock_redis.aclose.assert_called_once()
@pytest.mark.asyncio
async def test_get_min_unrealized_profit_in_window_returns_min() -> None:
from unittest.mock import MagicMock, patch
mock_redis = MagicMock()
mock_redis.zrangebyscore = AsyncMock(
return_value=["1000.0:5.0", "1001.0:3.0", "1002.0:7.0"]
)
mock_redis.aclose = AsyncMock()
with patch("redis.asyncio.Redis") as MockRedis:
MockRedis.from_url.return_value = mock_redis
result = await get_min_unrealized_profit_in_window(
"redis://localhost/0",
"test:history",
300,
)
assert result == 3.0
mock_redis.zrangebyscore.assert_called_once()
mock_redis.aclose.assert_called_once()
@pytest.mark.asyncio
async def test_get_min_unrealized_profit_in_window_empty_returns_none() -> None:
from unittest.mock import MagicMock, patch
mock_redis = MagicMock()
mock_redis.zrangebyscore = AsyncMock(return_value=[])
mock_redis.aclose = AsyncMock()
with patch("redis.asyncio.Redis") as MockRedis:
MockRedis.from_url.return_value = mock_redis
result = await get_min_unrealized_profit_in_window(
"redis://localhost/0",
"test:history",
300,
)
assert result is None