brighter-trading/tests/test_paper_margin_broker_in...

902 lines
31 KiB
Python

"""
Unit tests for PaperMarginBroker increase_position functionality.
Phase 1b of Margin Position Editor Plan.
"""
import pytest
from datetime import datetime, timezone, timedelta
from src.brokers.paper_margin_broker import PaperMarginBroker, MarginPosition
@pytest.fixture
def broker():
"""Create a paper margin broker with a mock price provider."""
prices = {'BTC/USDT': 60000.0, 'ETH/USDT': 3000.0}
def price_provider(symbol):
return prices.get(symbol, 0.0)
broker = PaperMarginBroker(
price_provider=price_provider,
initial_balance=10000.0
)
return broker
@pytest.fixture
def broker_with_long_position(broker):
"""Create broker with an existing long position."""
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=60000.0
)
return broker
@pytest.fixture
def broker_with_short_position(broker):
"""Create broker with an existing short position."""
broker.open_position(
symbol='BTC/USDT',
side='short',
collateral=1000.0,
leverage=5.0,
current_price=60000.0
)
return broker
class TestIncreasePositionCollateralFirst:
"""Tests for increase_position using collateral + leverage inputs."""
def test_increase_long_collateral_first(self, broker_with_long_position):
"""Increase long position using collateral + leverage inputs."""
broker = broker_with_long_position
position_before = broker.get_position('BTC/USDT')
initial_size = position_before.size
initial_collateral = position_before.collateral
initial_balance = broker.get_balance()
# Increase with 500 USDT at 3x leverage
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
# Size should increase
assert position.size > initial_size
expected_add_size = (500.0 * 3.0) / 60000.0 # 0.025 BTC
assert abs(position.size - (initial_size + expected_add_size)) < 1e-9
# Collateral should increase
assert position.collateral == initial_collateral + 500.0
# Balance should decrease
assert broker.get_balance() == initial_balance - 500.0
def test_increase_short_collateral_first(self, broker_with_short_position):
"""Increase short position using collateral + leverage inputs."""
broker = broker_with_short_position
position_before = broker.get_position('BTC/USDT')
initial_size = position_before.size
initial_collateral = position_before.collateral
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
# Size should increase (short position)
assert position.size > initial_size
assert position.side == 'short'
# Collateral should increase
assert position.collateral == initial_collateral + 500.0
class TestIncreasePositionSizeFirst:
"""Tests for increase_position using direct size input."""
def test_increase_long_size_first(self, broker_with_long_position):
"""Increase long position using direct size input."""
broker = broker_with_long_position
position_before = broker.get_position('BTC/USDT')
initial_size = position_before.size
initial_collateral = position_before.collateral
# Increase by 0.05 BTC directly
position = broker.increase_position(
symbol='BTC/USDT',
additional_size=0.05,
current_price=60000.0
)
# Size should increase by exactly 0.05
assert abs(position.size - (initial_size + 0.05)) < 1e-9
# Collateral should increase (calculated based on existing leverage)
assert position.collateral > initial_collateral
def test_increase_short_size_first(self, broker_with_short_position):
"""Increase short position using direct size input."""
broker = broker_with_short_position
position_before = broker.get_position('BTC/USDT')
initial_size = position_before.size
position = broker.increase_position(
symbol='BTC/USDT',
additional_size=0.03,
current_price=60000.0
)
assert abs(position.size - (initial_size + 0.03)) < 1e-9
assert position.side == 'short'
class TestWeightedAverageEntry:
"""Tests for weighted average entry price calculation."""
def test_increase_weighted_average_entry(self, broker):
"""Entry price is correctly weighted across increases."""
# Open at 60000
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=3.0,
current_price=60000.0
)
position = broker.get_position('BTC/USDT')
initial_size = position.size # 3000/60000 = 0.05 BTC
initial_entry = position.entry_price
# Simulate price change (update price provider)
broker._current_prices['BTC/USDT'] = 65000.0
# Increase at new price
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=1000.0,
execution_leverage=3.0,
current_price=65000.0
)
add_size = (1000.0 * 3.0) / 65000.0 # ~0.0462 BTC
# Weighted average = (old_value + new_value) / total_size
expected_avg = (initial_size * 60000.0 + add_size * 65000.0) / (initial_size + add_size)
assert abs(position.entry_price - expected_avg) < 0.01
def test_multiple_increases_weighted_average(self, broker):
"""Multiple increases maintain correct weighted average."""
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=2.0,
current_price=50000.0
)
# First increase at 55000
broker._current_prices['BTC/USDT'] = 55000.0
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=2.0,
current_price=55000.0
)
# Second increase at 60000
broker._current_prices['BTC/USDT'] = 60000.0
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=2.0,
current_price=60000.0
)
# Calculate expected weighted average
# First: 2000/50000 = 0.04 BTC at 50000
# Second: 1000/55000 = ~0.0182 BTC at 55000
# Third: 1000/60000 = ~0.0167 BTC at 60000
# Total value = 2000 + 1000 + 1000 = 4000
# Total size = 0.04 + 0.0182 + 0.0167 = ~0.0749
# Avg entry = 4000 / 0.0749 = ~53,404
assert position.entry_price > 50000
assert position.entry_price < 60000
class TestInterestAccrual:
"""Tests for interest accrual before merging."""
def test_increase_accrues_interest_before_merge(self, broker_with_long_position):
"""Interest is locked in before position is merged."""
broker = broker_with_long_position
position = broker.get_position('BTC/USDT')
# Manually set some accrued interest
position.interest_accrued = 5.0
interest_before = position.interest_accrued
# Increase position
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
# Interest should still be there (not reset)
assert position.interest_accrued >= interest_before
class TestLiquidationPriceRecalculation:
"""Tests for liquidation price recalculation."""
def test_increase_recalculates_liquidation_price(self, broker_with_long_position):
"""Liquidation price reflects new effective leverage."""
broker = broker_with_long_position
position_before = broker.get_position('BTC/USDT')
liq_before = position_before.liquidation_price
# Adding more collateral at lower leverage should improve liq price
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=2000.0, # Large collateral
execution_leverage=2.0, # Low leverage
current_price=60000.0
)
# For a long, lower effective leverage = lower liquidation price (safer)
# The overall effective leverage should decrease
assert position.liquidation_price < liq_before
class TestValidationErrors:
"""Tests for validation and error handling."""
def test_increase_rejects_missing_position(self, broker):
"""Fails when no position exists for symbol."""
with pytest.raises(ValueError, match="No position for"):
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0
)
def test_increase_rejects_insufficient_balance(self, broker_with_long_position):
"""Fails cleanly when balance cannot cover collateral."""
broker = broker_with_long_position
# Try to add more collateral than available balance
with pytest.raises(ValueError, match="Insufficient balance"):
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=50000.0, # Way more than balance
execution_leverage=3.0
)
def test_increase_rejects_excessive_leverage(self, broker_with_long_position):
"""Fails when resulting leverage exceeds max."""
broker = broker_with_long_position
with pytest.raises(ValueError, match="exceeds max"):
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=20.0 # Exceeds 10x max
)
def test_increase_rejects_invalid_inputs_both_modes(self, broker_with_long_position):
"""Cannot provide both collateral+leverage AND size."""
broker = broker_with_long_position
with pytest.raises(ValueError, match="not both"):
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
additional_size=0.05 # Both modes provided
)
def test_increase_rejects_no_inputs(self, broker_with_long_position):
"""Must provide either mode's inputs."""
broker = broker_with_long_position
with pytest.raises(ValueError, match="Must provide"):
broker.increase_position(
symbol='BTC/USDT'
# No inputs provided
)
def test_increase_rejects_negative_collateral(self, broker_with_long_position):
"""Negative collateral is rejected."""
broker = broker_with_long_position
with pytest.raises(ValueError, match="must be positive"):
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=-100.0,
execution_leverage=3.0
)
def test_increase_rejects_negative_size(self, broker_with_long_position):
"""Negative size is rejected."""
broker = broker_with_long_position
with pytest.raises(ValueError, match="must be positive"):
broker.increase_position(
symbol='BTC/USDT',
additional_size=-0.05
)
def test_increase_rejects_low_leverage(self, broker_with_long_position):
"""Leverage below 1x is rejected."""
broker = broker_with_long_position
with pytest.raises(ValueError, match="at least 1x"):
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=0.5
)
class TestFullLifecycle:
"""Integration tests for full position lifecycle."""
def test_multiple_increases_then_close(self, broker):
"""Full lifecycle: open, increase twice, close. P/L correct."""
# Open position at 50000
broker._current_prices['BTC/USDT'] = 50000.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=2.0,
current_price=50000.0
)
initial_balance = broker.get_balance()
# First increase at 52000
broker._current_prices['BTC/USDT'] = 52000.0
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=2.0,
current_price=52000.0
)
# Second increase at 54000
broker._current_prices['BTC/USDT'] = 54000.0
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=2.0,
current_price=54000.0
)
total_collateral = position.collateral
total_size = position.size
avg_entry = position.entry_price
# Close at 58000 (profit)
broker._current_prices['BTC/USDT'] = 58000.0
result = broker.close_position('BTC/USDT', current_price=58000.0)
assert result.success
assert result.realized_pnl > 0 # Should have profit
# Balance should reflect collateral returned + P/L
final_balance = broker.get_balance()
assert final_balance > initial_balance # Made profit
def test_increase_then_partial_reduce(self, broker):
"""Increase position then partially reduce."""
broker._current_prices['BTC/USDT'] = 60000.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=3.0,
current_price=60000.0
)
# Increase
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
position = broker.get_position('BTC/USDT')
size_after_increase = position.size
# Partial reduce
reduce_amount = size_after_increase * 0.3
position = broker.reduce_position(
symbol='BTC/USDT',
reduce_size=reduce_amount,
current_price=60000.0
)
assert position is not None
assert position.size < size_after_increase
assert abs(position.size - (size_after_increase - reduce_amount)) < 1e-9
class TestPreviewIncrease:
"""Tests for preview_increase method."""
def test_preview_matches_execution(self, broker_with_long_position):
"""Preview values should match what execution produces."""
broker = broker_with_long_position
# Get preview
preview = broker.preview_increase(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
assert preview['valid']
projected = preview['projected']
# Execute the same operation
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
# Values should match
assert abs(position.size - projected['total_size']) < 1e-9
assert abs(position.entry_price - projected['average_entry']) < 0.01
assert abs(position.collateral - projected['total_collateral']) < 0.01
def test_preview_invalid_returns_errors(self, broker_with_long_position):
"""Preview with invalid inputs returns errors."""
broker = broker_with_long_position
preview = broker.preview_increase(
symbol='BTC/USDT',
additional_collateral=50000.0, # More than balance
execution_leverage=3.0
)
assert not preview['valid']
assert len(preview['errors']) > 0
assert 'Insufficient balance' in preview['errors'][0]
def test_preview_warnings_for_high_leverage(self, broker_with_long_position):
"""Preview warns about high leverage."""
broker = broker_with_long_position
preview = broker.preview_increase(
symbol='BTC/USDT',
additional_collateral=100.0, # Small collateral
execution_leverage=8.0, # High leverage
current_price=60000.0
)
assert preview['valid']
assert len(preview['warnings']) > 0
assert any('5x' in w for w in preview['warnings'])
class TestRemoveMargin:
"""Tests for remove_margin functionality."""
def test_remove_margin_success(self, broker_with_long_position):
"""Can remove margin when position is healthy."""
broker = broker_with_long_position
position = broker.get_position('BTC/USDT')
initial_collateral = position.collateral
initial_balance = broker.get_balance()
# Remove small amount of margin
position = broker.remove_margin('BTC/USDT', amount=100.0)
assert position.collateral == initial_collateral - 100.0
assert broker.get_balance() == initial_balance + 100.0
def test_remove_margin_rejects_too_much(self, broker_with_long_position):
"""Cannot remove margin that would breach min margin."""
broker = broker_with_long_position
position = broker.get_position('BTC/USDT')
# Try to remove almost all collateral
with pytest.raises(ValueError, match="max withdrawable"):
broker.remove_margin('BTC/USDT', amount=position.collateral * 0.95)
def test_preview_remove_margin(self, broker_with_long_position):
"""Preview remove margin shows correct projected values."""
broker = broker_with_long_position
preview = broker.preview_remove_margin('BTC/USDT', amount=100.0)
assert preview['valid']
assert preview['projected']['withdrawn_amount'] == 100.0
assert preview['projected']['max_withdrawable'] > 0
class TestPositionHistory:
"""Tests for position history tracking."""
def test_increase_recorded_in_history(self, broker_with_long_position):
"""Increase operation is recorded in position history."""
broker = broker_with_long_position
history_before = len(broker.get_position_history())
broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
history_after = broker.get_position_history()
assert len(history_after) == history_before + 1
last_entry = history_after[-1]
assert last_entry['type'] == 'increase'
assert last_entry['symbol'] == 'BTC/USDT'
assert last_entry['size_delta'] > 0
assert last_entry['collateral_delta'] == 500.0
assert 'before' in last_entry
assert 'after' in last_entry
class TestRemoveMarginHealthCheck:
"""Tests for remove_margin health-based validation (codex issue #1)."""
def test_remove_margin_rejects_when_underwater(self, broker):
"""
Remove_margin must reject withdrawal when position is underwater.
Regression test for codex finding: position opened at 100, marked to 85,
should not allow withdrawal that would cause immediate liquidation.
"""
# Open 5x long at price 100
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
# Price drops to 85 - position is now in loss
broker._current_prices['BTC/USDT'] = 85.0
broker.update() # Update position with new price
position = broker.get_position('BTC/USDT')
# Position should be unhealthy - margin_ratio should be low
assert position.margin_ratio < 100, f"Expected low margin ratio, got {position.margin_ratio}"
# Attempting to withdraw significant collateral should fail
with pytest.raises(ValueError, match="max withdrawable"):
broker.remove_margin('BTC/USDT', amount=400.0)
def test_remove_margin_max_withdrawable_reflects_current_health(self, broker):
"""Max withdrawable should be based on current price, not entry price."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=3.0,
current_price=100.0
)
# Price drops significantly
broker._current_prices['BTC/USDT'] = 75.0
broker.update()
position = broker.get_position('BTC/USDT')
# Preview should show very limited withdrawal capacity
preview = broker.preview_remove_margin('BTC/USDT', amount=500.0)
# Should be invalid - can't withdraw that much when position is underwater
assert not preview['valid'], "Should not allow withdrawal when position is in significant loss"
def test_remove_margin_allowed_when_profitable(self, broker):
"""Remove_margin should allow withdrawal when position is profitable."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=2.0, # Conservative leverage
current_price=100.0
)
# Price rises - position is now profitable
broker._current_prices['BTC/USDT'] = 120.0
broker.update()
position = broker.get_position('BTC/USDT')
assert position.margin_ratio > 100, "Position should be healthy when profitable"
# Should be able to withdraw some excess
preview = broker.preview_remove_margin('BTC/USDT', amount=100.0)
assert preview['valid'], f"Should allow modest withdrawal when profitable: {preview.get('errors')}"
def test_preview_remove_margin_accounts_for_elapsed_interest(self, broker):
"""Preview remove_margin should accrue interest the same way execution does."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
position = broker.get_position('BTC/USDT')
position.last_interest_at = datetime.now(timezone.utc) - timedelta(hours=24)
preview = broker.preview_remove_margin('BTC/USDT', amount=495.0)
assert not preview['valid']
with pytest.raises(ValueError, match="max withdrawable"):
broker.remove_margin('BTC/USDT', amount=495.0)
class TestPreviewMarginRatioAccuracy:
"""Tests for preview margin ratio matching execution (codex issue #2)."""
def test_preview_increase_margin_ratio_matches_execution(self, broker_with_long_position):
"""Preview margin_ratio should match what execution produces."""
broker = broker_with_long_position
# Get preview
preview = broker.preview_increase(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
assert preview['valid']
projected_ratio = preview['projected']['margin_ratio']
# Execute
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
# Margin ratios should be close (allow small rounding differences)
assert abs(position.margin_ratio - projected_ratio) < 1.0, \
f"Preview margin_ratio {projected_ratio} doesn't match execution {position.margin_ratio}"
def test_preview_add_margin_ratio_matches_execution(self, broker_with_long_position):
"""Preview add_margin ratio should match execution."""
broker = broker_with_long_position
preview = broker.preview_add_margin('BTC/USDT', amount=500.0)
assert preview['valid']
projected_ratio = preview['projected']['margin_ratio']
position = broker.add_margin('BTC/USDT', amount=500.0)
assert abs(position.margin_ratio - projected_ratio) < 1.0, \
f"Preview margin_ratio {projected_ratio} doesn't match execution {position.margin_ratio}"
def test_preview_increase_margin_ratio_matches_execution_after_elapsed_interest(self, broker):
"""Preview increase should account for accrued interest before execution."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
position = broker.get_position('BTC/USDT')
position.last_interest_at = datetime.now(timezone.utc) - timedelta(hours=24)
preview = broker.preview_increase(
symbol='BTC/USDT',
additional_collateral=100.0,
execution_leverage=2.0,
current_price=100.0
)
assert preview['valid']
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=100.0,
execution_leverage=2.0,
current_price=100.0
)
assert abs(position.margin_ratio - preview['projected']['margin_ratio']) < 0.1
def test_preview_add_margin_ratio_matches_execution_after_elapsed_interest(self, broker):
"""Preview add_margin should match execution after accruing elapsed interest."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
position = broker.get_position('BTC/USDT')
position.last_interest_at = datetime.now(timezone.utc) - timedelta(hours=24)
preview = broker.preview_add_margin('BTC/USDT', amount=100.0)
assert preview['valid']
position = broker.add_margin('BTC/USDT', amount=100.0)
assert abs(position.margin_ratio - preview['projected']['margin_ratio']) < 0.1
def test_preview_reduce_realized_pnl_matches_execution_after_elapsed_interest(self, broker):
"""Preview reduce should use accrued interest in realized P/L calculations."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
position = broker.get_position('BTC/USDT')
position.last_interest_at = datetime.now(timezone.utc) - timedelta(hours=24)
reduce_size = position.size * 0.2
preview = broker.preview_reduce(
symbol='BTC/USDT',
reduce_size=reduce_size,
current_price=100.0
)
assert preview['valid']
balance_before = broker.get_balance()
broker.reduce_position(
symbol='BTC/USDT',
reduce_size=reduce_size,
current_price=100.0
)
actual_returned = broker.get_balance() - balance_before
assert preview['projected']['interest_paid'] > 0
assert abs(actual_returned - preview['projected']['returned_to_balance']) < 0.1
class TestExecutionReturnsCurrentFields:
"""Tests for execution returning current market fields (codex issue #3)."""
def test_increase_returns_updated_margin_ratio(self, broker_with_long_position):
"""increase_position should return position with current margin_ratio."""
broker = broker_with_long_position
position = broker.increase_position(
symbol='BTC/USDT',
additional_collateral=500.0,
execution_leverage=3.0,
current_price=60000.0
)
# margin_ratio should be freshly calculated, not stale
assert position.margin_ratio > 0
assert position.current_price == 60000.0
# unrealized_pnl should reflect current price
assert position.unrealized_pnl is not None
def test_add_margin_returns_updated_margin_ratio(self, broker_with_long_position):
"""add_margin should return position with current margin_ratio."""
broker = broker_with_long_position
position = broker.add_margin('BTC/USDT', amount=500.0)
assert position.margin_ratio > 0
assert position.current_price > 0
def test_remove_margin_returns_updated_margin_ratio(self, broker):
"""remove_margin should return position with current margin_ratio."""
broker._current_prices['BTC/USDT'] = 60000.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=2000.0, # Large collateral for safe withdrawal
leverage=2.0,
current_price=60000.0
)
# Make position profitable so we can withdraw
broker._current_prices['BTC/USDT'] = 65000.0
broker.update()
position = broker.remove_margin('BTC/USDT', amount=100.0)
assert position.margin_ratio > 0
assert position.current_price == 65000.0
def test_add_margin_returns_accrued_market_fields(self, broker):
"""add_margin should return values that already include elapsed interest."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
position = broker.get_position('BTC/USDT')
position.last_interest_at = datetime.now(timezone.utc) - timedelta(hours=24)
position = broker.add_margin('BTC/USDT', amount=100.0)
returned_margin_ratio = position.margin_ratio
returned_liquidation_price = position.liquidation_price
returned_interest = position.interest_accrued
assert returned_interest > 0
broker.update()
refreshed = broker.get_position('BTC/USDT')
assert abs(refreshed.margin_ratio - returned_margin_ratio) < 0.1
assert abs(refreshed.liquidation_price - returned_liquidation_price) < 0.1
def test_reduce_returns_updated_market_fields_after_elapsed_interest(self, broker):
"""reduce_position should return fresh market fields after partial close."""
broker._current_prices['BTC/USDT'] = 100.0
broker.open_position(
symbol='BTC/USDT',
side='long',
collateral=1000.0,
leverage=5.0,
current_price=100.0
)
position = broker.get_position('BTC/USDT')
position.last_interest_at = datetime.now(timezone.utc) - timedelta(hours=24)
reduced = broker.reduce_position(
symbol='BTC/USDT',
reduce_size=position.size * 0.2,
current_price=100.0
)
returned_margin_ratio = reduced.margin_ratio
returned_unrealized_pnl = reduced.unrealized_pnl
broker.update()
refreshed = broker.get_position('BTC/USDT')
assert abs(refreshed.margin_ratio - returned_margin_ratio) < 0.1
assert abs(refreshed.unrealized_pnl - returned_unrealized_pnl) < 0.1