test: add test for balance_distribution_over_time
This commit is contained in:
@@ -20,6 +20,7 @@ from freqtrade.data.btanalysis import (
|
||||
load_trades,
|
||||
load_trades_from_db,
|
||||
)
|
||||
from freqtrade.data.btanalysis.trade_parallelism import balance_distribution_over_time
|
||||
from freqtrade.data.history import load_data, load_pair_history
|
||||
from freqtrade.data.metrics import (
|
||||
calculate_cagr,
|
||||
@@ -649,3 +650,189 @@ def test_load_file_from_zip(tmp_path):
|
||||
|
||||
with pytest.raises(ValueError, match=r"File .* not found in zip.*"):
|
||||
load_file_from_zip(zip_file, "testfile55.txt")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_balance_distribution_over_time(is_short):
|
||||
"""
|
||||
Test balance_distribution_over_time for both long and short trades.
|
||||
"""
|
||||
# Create a minimal trades DataFrame with 4 trades over time
|
||||
# Base dates for trades
|
||||
start_date = dt_utc(2023, 1, 1)
|
||||
base_date = start_date + timedelta(hours=15)
|
||||
stake_currency = "USDT"
|
||||
start_balance = 1000.0
|
||||
fee = 0.001 # 0.1% fee
|
||||
|
||||
# Create trades spanning different time periods
|
||||
trades_data = {
|
||||
"pair": ["BTC/USDT", "ETH/USDT", "XRP/USDT", "LTC/USDT"],
|
||||
"stake_amount": [100.0, 150.0, 80.0, 120.0],
|
||||
"open_date": [
|
||||
base_date,
|
||||
base_date + timedelta(hours=2),
|
||||
base_date + timedelta(hours=5),
|
||||
base_date + timedelta(hours=8),
|
||||
],
|
||||
"close_date": [
|
||||
base_date + timedelta(hours=3),
|
||||
base_date + timedelta(hours=6),
|
||||
base_date + timedelta(hours=9),
|
||||
base_date + timedelta(hours=12),
|
||||
],
|
||||
"open_rate": [40000.0, 2000.0, 0.5, 100.0],
|
||||
"close_rate": [41000.0, 2100.0, 0.52, 105.0],
|
||||
"fee_open": [fee, fee, fee, fee],
|
||||
"fee_close": [fee, fee, fee, fee],
|
||||
"is_short": [is_short, is_short, is_short, is_short],
|
||||
"leverage": [1.0, 1.0, 1.0, 1.0],
|
||||
"orders": [
|
||||
# Trade 1: BTC/USDT - entry at 40000, exit at 41000
|
||||
[
|
||||
{
|
||||
"amount": 0.0025, # 100 / 40000
|
||||
"filled": 0.0025,
|
||||
"safe_price": 40000.0,
|
||||
"ft_order_side": "sell" if is_short else "buy",
|
||||
"order_filled_timestamp": int(base_date.timestamp() * 1000),
|
||||
"ft_is_entry": True,
|
||||
},
|
||||
{
|
||||
"amount": 0.0025,
|
||||
"filled": 0.0025,
|
||||
"safe_price": 41000.0,
|
||||
"ft_order_side": "buy" if is_short else "sell",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=3)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": False,
|
||||
},
|
||||
],
|
||||
# Trade 2: ETH/USDT - entry at 2000, exit at 2100
|
||||
[
|
||||
{
|
||||
"amount": 0.075, # 150 / 2000
|
||||
"filled": 0.075,
|
||||
"safe_price": 2000.0,
|
||||
"ft_order_side": "sell" if is_short else "buy",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=2)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": True,
|
||||
},
|
||||
{
|
||||
"amount": 0.075,
|
||||
"filled": 0.075,
|
||||
"safe_price": 2100.0,
|
||||
"ft_order_side": "buy" if is_short else "sell",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=6)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": False,
|
||||
},
|
||||
],
|
||||
# Trade 3: XRP/USDT - entry at 0.5, exit at 0.52
|
||||
[
|
||||
{
|
||||
"amount": 160.0, # 80 / 0.5
|
||||
"filled": 160.0,
|
||||
"safe_price": 0.5,
|
||||
"ft_order_side": "sell" if is_short else "buy",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=5)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": True,
|
||||
},
|
||||
{
|
||||
"amount": 160.0,
|
||||
"filled": 160.0,
|
||||
"safe_price": 0.52,
|
||||
"ft_order_side": "buy" if is_short else "sell",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=9)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": False,
|
||||
},
|
||||
],
|
||||
# Trade 4: LTC/USDT - entry at 100, exit at 105
|
||||
[
|
||||
{
|
||||
"amount": 1.2, # 120 / 100
|
||||
"filled": 1.2,
|
||||
"safe_price": 100.0,
|
||||
"ft_order_side": "sell" if is_short else "buy",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=8)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": True,
|
||||
},
|
||||
{
|
||||
"amount": 1.2,
|
||||
"filled": 1.2,
|
||||
"safe_price": 105.0,
|
||||
"ft_order_side": "buy" if is_short else "sell",
|
||||
"order_filled_timestamp": int(
|
||||
(base_date + timedelta(hours=12)).timestamp() * 1000
|
||||
),
|
||||
"ft_is_entry": False,
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
trades_df = DataFrame(trades_data)
|
||||
pairlist = ["BTC/USDT", "ETH/USDT", "XRP/USDT", "LTC/USDT"]
|
||||
|
||||
min_date = start_date
|
||||
max_date = start_date + timedelta(hours=35)
|
||||
|
||||
result = balance_distribution_over_time(
|
||||
trades=trades_df,
|
||||
min_date=min_date,
|
||||
max_date=max_date,
|
||||
timeframe="1h",
|
||||
stake_currency=stake_currency,
|
||||
start_balance=start_balance,
|
||||
pairlist=pairlist,
|
||||
)
|
||||
|
||||
# Verify basic structure
|
||||
assert isinstance(result, DataFrame)
|
||||
assert stake_currency in result.columns
|
||||
for pair in pairlist:
|
||||
assert pair in result.columns
|
||||
|
||||
# Verify the index is a DatetimeIndex
|
||||
assert isinstance(result.index, Timestamp.__class__.__bases__[0])
|
||||
|
||||
# Verify we have entries over the full time period (36h)
|
||||
assert len(result) == 36
|
||||
|
||||
# First trade opens 15h after the start date
|
||||
assert result.iloc[0][stake_currency] == 1000
|
||||
expected_first_balance = start_balance - (100.0 + 100.0 * fee)
|
||||
assert result.iloc[15][stake_currency] == pytest.approx(expected_first_balance)
|
||||
|
||||
# Check that pair columns have non-zero values during trade periods
|
||||
# Trade 1 (BTC/USDT) is open from hour 15 to hour 18
|
||||
# At hour 16, BTC/USDT should have position
|
||||
btc_during_trade = result.loc[base_date + timedelta(hours=1), "BTC/USDT"]
|
||||
assert btc_during_trade > 0, "Trade should have positive position during open period"
|
||||
|
||||
# After Trade 1 closes at hour 3, BTC/USDT position should be 0
|
||||
btc_after_close = result.loc[base_date + timedelta(hours=4) :, "BTC/USDT"]
|
||||
assert all(btc_after_close == 0), "Position should be 0 after trade closes"
|
||||
|
||||
# Final stake currency should reflect all trades' cash flows minus fees
|
||||
# The function tracks cash flow: entries subtract stake, exits add stake
|
||||
# Both long and short use the same formula based on order prices
|
||||
final_balance = result.iloc[-1][stake_currency]
|
||||
|
||||
# Verify the balance changed (trades had effect)
|
||||
assert final_balance != start_balance, "Balance should change after trading"
|
||||
|
||||
# Since all exit prices > entry prices, exits return more cash than entries spent
|
||||
# This means final balance > start balance for both long and short trades
|
||||
# (the function tracks cash flow, not P&L from long/short perspective)
|
||||
assert final_balance > start_balance, "Exit prices > entry prices should increase balance"
|
||||
|
||||
Reference in New Issue
Block a user