Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/advanced-orderflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ dataframe["delta"] # Difference between ask and bid volume.
dataframe["min_delta"] # Minimum delta within the candle
dataframe["max_delta"] # Maximum delta within the candle
dataframe["total_trades"] # Total number of trades
dataframe["stacked_imbalances_bid"] # Price level of stacked bid imbalance
dataframe["stacked_imbalances_ask"] # Price level of stacked ask imbalance
dataframe["stacked_imbalances_bid"] # List of price levels of stacked bid imbalance range beginnings
dataframe["stacked_imbalances_ask"] # List of price levels of stacked ask imbalance range beginnings
```

You can access these columns in your strategy code for further analysis. Here's an example:
Expand Down
48 changes: 19 additions & 29 deletions freqtrade/data/converter/orderflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,12 @@ def populate_dataframe_with_trades(
dataframe.at[index, "imbalances"] = imbalances.to_dict(orient="index")

stacked_imbalance_range = config_orderflow["stacked_imbalance_range"]
dataframe.at[index, "stacked_imbalances_bid"] = stacked_imbalance_bid(
imbalances, stacked_imbalance_range=stacked_imbalance_range
dataframe.at[index, "stacked_imbalances_bid"] = stacked_imbalance(
imbalances, label="bid", stacked_imbalance_range=stacked_imbalance_range
)

dataframe.at[index, "stacked_imbalances_ask"] = stacked_imbalance_ask(
imbalances, stacked_imbalance_range=stacked_imbalance_range
dataframe.at[index, "stacked_imbalances_ask"] = stacked_imbalance(
imbalances, label="ask", stacked_imbalance_range=stacked_imbalance_range
)

bid = np.where(
Expand Down Expand Up @@ -256,34 +256,24 @@ def trades_orderflow_to_imbalances(df: pd.DataFrame, imbalance_ratio: int, imbal
return dataframe


def stacked_imbalance(
df: pd.DataFrame, label: str, stacked_imbalance_range: int, should_reverse: bool
):
def stacked_imbalance(df: pd.DataFrame, label: str, stacked_imbalance_range: int):
"""
y * (y.groupby((y != y.shift()).cumsum()).cumcount() + 1)
https://stackoverflow.com/questions/27626542/counting-consecutive-positive-values-in-python-pandas-array
"""
imbalance = df[f"{label}_imbalance"]
int_series = pd.Series(np.where(imbalance, 1, 0))
stacked = int_series * (
int_series.groupby((int_series != int_series.shift()).cumsum()).cumcount() + 1
)

max_stacked_imbalance_idx = stacked.index[stacked >= stacked_imbalance_range]
stacked_imbalance_price = np.nan
if not max_stacked_imbalance_idx.empty:
idx = (
max_stacked_imbalance_idx[0]
if not should_reverse
else np.flipud(max_stacked_imbalance_idx)[0]
)
stacked_imbalance_price = imbalance.index[idx]
return stacked_imbalance_price


def stacked_imbalance_ask(df: pd.DataFrame, stacked_imbalance_range: int):
return stacked_imbalance(df, "ask", stacked_imbalance_range, should_reverse=True)


def stacked_imbalance_bid(df: pd.DataFrame, stacked_imbalance_range: int):
return stacked_imbalance(df, "bid", stacked_imbalance_range, should_reverse=False)
# Group consecutive True values and get their counts
groups = (int_series != int_series.shift()).cumsum()
counts = int_series.groupby(groups).cumsum()

# Find indices where count meets or exceeds the range requirement
valid_indices = counts[counts >= stacked_imbalance_range].index

stacked_imbalance_prices = []
if not valid_indices.empty:
# Get all prices from valid indices from beginning of the range
stacked_imbalance_prices = [
imbalance.index.values[idx - (stacked_imbalance_range - 1)] for idx in valid_indices
]
return stacked_imbalance_prices
2 changes: 1 addition & 1 deletion freqtrade/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ def _rpc_status_table(
active_order_side = ".".join(
"*" if (o.get("is_open") and o.get("ft_is_entry")) else "**"
for o in orders
if o.get("is_open")
if o.get("is_open") and o.get("ft_order_side") != "stoploss"
)

# Direction string for non-spot
Expand Down
51 changes: 43 additions & 8 deletions tests/data/test_converter_orderflow.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import numpy as np
import pandas as pd
import pytest

from freqtrade.constants import DEFAULT_TRADES_COLUMNS
from freqtrade.data.converter import populate_dataframe_with_trades
from freqtrade.data.converter.orderflow import (
ORDERFLOW_ADDED_COLUMNS,
stacked_imbalance,
timeframe_to_DateOffset,
trades_to_volumeprofile_with_total_delta_bid_ask,
)
Expand Down Expand Up @@ -185,24 +185,24 @@ def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
assert results["max_delta"] == 17.298

# Assert that stacked imbalances are NaN (not applicable in this test)
assert np.isnan(results["stacked_imbalances_bid"])
assert np.isnan(results["stacked_imbalances_ask"])
assert results["stacked_imbalances_bid"] == []
assert results["stacked_imbalances_ask"] == []

# Repeat assertions for the third from last row
results = df.iloc[-2]
assert pytest.approx(results["delta"]) == -20.862
assert pytest.approx(results["min_delta"]) == -54.559999
assert 82.842 == results["max_delta"]
assert 234.99 == results["stacked_imbalances_bid"]
assert 234.96 == results["stacked_imbalances_ask"]
assert results["stacked_imbalances_bid"] == [234.97]
assert results["stacked_imbalances_ask"] == [234.94]

# Repeat assertions for the last row
results = df.iloc[-1]
assert pytest.approx(results["delta"]) == -49.302
assert results["min_delta"] == -70.222
assert pytest.approx(results["max_delta"]) == 11.213
assert np.isnan(results["stacked_imbalances_bid"])
assert np.isnan(results["stacked_imbalances_ask"])
assert results["stacked_imbalances_bid"] == []
assert results["stacked_imbalances_ask"] == []


def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
Expand Down Expand Up @@ -358,7 +358,8 @@ def test_public_trades_binned_big_sample_list(public_trades_list):
assert 197.512 == df["bid_amount"].iloc[0] # total bid amount
assert 88.98 == df["ask_amount"].iloc[0] # total ask amount
assert 26 == df["ask"].iloc[0] # ask price
assert -108.532 == pytest.approx(df["delta"].iloc[0]) # delta (bid amount - ask amount)
# delta (bid amount - ask amount)
assert -108.532 == pytest.approx(df["delta"].iloc[0])

assert 3 == df["bid"].iloc[-1] # bid price
assert 50.659 == df["bid_amount"].iloc[-1] # total bid amount
Expand Down Expand Up @@ -567,6 +568,40 @@ def test_analyze_with_orderflow(
assert isinstance(lastval_of2, dict)


def test_stacked_imbalances_multiple_prices():
"""Test that stacked imbalances correctly returns multiple price levels when present"""
# Test with empty result
df_no_stacks = pd.DataFrame(
{
"bid_imbalance": [False, False, True, False],
"ask_imbalance": [False, True, False, False],
},
index=[234.95, 234.96, 234.97, 234.98],
)
no_stacks = stacked_imbalance(df_no_stacks, "bid", stacked_imbalance_range=2)
assert no_stacks == []

# Create a sample DataFrame with known imbalances
df = pd.DataFrame(
{
"bid_imbalance": [True, True, True, False, False, True, True, False, True],
"ask_imbalance": [False, False, True, True, True, False, False, True, True],
},
index=[234.95, 234.96, 234.97, 234.98, 234.99, 235.00, 235.01, 235.02, 235.03],
)
# Test bid imbalances (should return prices in ascending order)
bid_prices = stacked_imbalance(df, "bid", stacked_imbalance_range=2)
assert bid_prices == [234.95, 234.96, 235.00]

# Test ask imbalances (should return prices in descending order)
ask_prices = stacked_imbalance(df, "ask", stacked_imbalance_range=2)
assert ask_prices == [234.97, 234.98, 235.02]

# Test with higher stacked_imbalance_range
bid_prices_higher = stacked_imbalance(df, "bid", stacked_imbalance_range=3)
assert bid_prices_higher == [234.95]


def test_timeframe_to_DateOffset():
assert timeframe_to_DateOffset("1s") == pd.DateOffset(seconds=1)
assert timeframe_to_DateOffset("1m") == pd.DateOffset(minutes=1)
Expand Down
Loading