Merge branch 'feature/fetch-public-trades-cached' into feature/fetch-public-trades
This commit is contained in:
@@ -21,6 +21,7 @@ This guide walks you through utilizing public trade data for advanced orderflow
|
|||||||
|
|
||||||
2. **Configure Orderflow Processing:** Define your desired settings for orderflow processing within the orderflow section of config.json. Here, you can adjust factors like:
|
2. **Configure Orderflow Processing:** Define your desired settings for orderflow processing within the orderflow section of config.json. Here, you can adjust factors like:
|
||||||
|
|
||||||
|
- `cache_size`: How many previous orderflow candles are saved into cache instead of calculated every new candle
|
||||||
- `max_candles`: Filter how many candles get processed from the tail
|
- `max_candles`: Filter how many candles get processed from the tail
|
||||||
- `scale`: This controls the price bin size for the footprint chart.
|
- `scale`: This controls the price bin size for the footprint chart.
|
||||||
- `stacked_imbalance_range`: Defines the minimum consecutive imbalanced price levels required for consideration.
|
- `stacked_imbalance_range`: Defines the minimum consecutive imbalanced price levels required for consideration.
|
||||||
@@ -29,6 +30,7 @@ This guide walks you through utilizing public trade data for advanced orderflow
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"orderflow": {
|
"orderflow": {
|
||||||
|
"cache_size": 1000,
|
||||||
"max_candles": 1500,
|
"max_candles": 1500,
|
||||||
"scale": 0.5,
|
"scale": 0.5,
|
||||||
"stacked_imbalance_range": 3, // needs at least this amount of imbalance next to each other
|
"stacked_imbalance_range": 3, // needs at least this amount of imbalance next to each other
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ CONF_SCHEMA = {
|
|||||||
"orderflow": {
|
"orderflow": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"cache_size": {"type": "number", "minimum": 1, "default": 1000},
|
||||||
"max_candles": {"type": "number", "minimum": 1, "default": 1500},
|
"max_candles": {"type": "number", "minimum": 1, "default": 1500},
|
||||||
"scale": {"type": "number", "minimum": 0.0},
|
"scale": {"type": "number", "minimum": 0.0},
|
||||||
"stacked_imbalance_range": {"type": "number", "minimum": 0},
|
"stacked_imbalance_range": {"type": "number", "minimum": 0},
|
||||||
|
|||||||
@@ -4,16 +4,20 @@ Functions to convert orderflow data from public_trades
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS, Config
|
from freqtrade.constants import DEFAULT_ORDERFLOW_COLUMNS
|
||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Global cache dictionary
|
||||||
|
cached_grouped_trades: OrderedDict[pd.Timestamp, pd.DataFrame] = OrderedDict()
|
||||||
|
|
||||||
|
|
||||||
def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
|
def _init_dataframe_with_trades_columns(dataframe: pd.DataFrame):
|
||||||
"""
|
"""
|
||||||
@@ -56,17 +60,16 @@ def _calculate_ohlcv_candle_start_and_end(df: pd.DataFrame, timeframe: str):
|
|||||||
df.drop(columns=["datetime"], inplace=True)
|
df.drop(columns=["datetime"], inplace=True)
|
||||||
|
|
||||||
|
|
||||||
def populate_dataframe_with_trades(
|
def populate_dataframe_with_trades(config, dataframe, trades):
|
||||||
config: Config, dataframe: pd.DataFrame, trades: pd.DataFrame
|
|
||||||
) -> pd.DataFrame:
|
|
||||||
"""
|
"""
|
||||||
Populates a dataframe with trades
|
Populates a dataframe with trades
|
||||||
:param dataframe: Dataframe to populate
|
:param dataframe: Dataframe to populate
|
||||||
:param trades: Trades to populate with
|
:param trades: Trades to populate with
|
||||||
:return: Dataframe with trades populated
|
:return: Dataframe with trades populated
|
||||||
"""
|
"""
|
||||||
config_orderflow = config["orderflow"]
|
|
||||||
timeframe = config["timeframe"]
|
timeframe = config["timeframe"]
|
||||||
|
config_orderflow = config["orderflow"]
|
||||||
|
cache_size = config_orderflow["cache_size"]
|
||||||
|
|
||||||
# create columns for trades
|
# create columns for trades
|
||||||
_init_dataframe_with_trades_columns(dataframe)
|
_init_dataframe_with_trades_columns(dataframe)
|
||||||
@@ -92,14 +95,13 @@ def populate_dataframe_with_trades(
|
|||||||
stacked_imbalances_bid_series = pd.Series(index=dataframe.index, dtype=object)
|
stacked_imbalances_bid_series = pd.Series(index=dataframe.index, dtype=object)
|
||||||
stacked_imbalances_ask_series = pd.Series(index=dataframe.index, dtype=object)
|
stacked_imbalances_ask_series = pd.Series(index=dataframe.index, dtype=object)
|
||||||
|
|
||||||
|
trades_grouped_by_candle_start = trades.groupby("candle_start", group_keys=False)
|
||||||
for candle_start, trades_grouped_df in trades_grouped_by_candle_start:
|
for candle_start, trades_grouped_df in trades_grouped_by_candle_start:
|
||||||
is_between = candle_start == dataframe["date"]
|
is_between = candle_start == dataframe["date"]
|
||||||
if is_between.any():
|
if is_between.any():
|
||||||
from freqtrade.exchange import timeframe_to_next_date
|
from freqtrade.exchange import timeframe_to_next_date
|
||||||
|
|
||||||
candle_next = timeframe_to_next_date(timeframe, candle_start)
|
candle_next = timeframe_to_next_date(timeframe, candle_start)
|
||||||
# skip if there are no trades at next candle
|
|
||||||
# because that this candle isn't finished yet
|
|
||||||
if candle_next not in trades_grouped_by_candle_start.groups:
|
if candle_next not in trades_grouped_by_candle_start.groups:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"candle at {candle_start} with {len(trades_grouped_df)} trades "
|
f"candle at {candle_start} with {len(trades_grouped_df)} trades "
|
||||||
@@ -109,6 +111,17 @@ def populate_dataframe_with_trades(
|
|||||||
indices = dataframe.index[is_between].tolist()
|
indices = dataframe.index[is_between].tolist()
|
||||||
# Add trades to each candle
|
# Add trades to each candle
|
||||||
trades_series.loc[indices] = [trades_grouped_df] * len(indices)
|
trades_series.loc[indices] = [trades_grouped_df] * len(indices)
|
||||||
|
# Use caching mechanism
|
||||||
|
if (candle_start, candle_next) in cached_grouped_trades:
|
||||||
|
cache_entry = cached_grouped_trades[(candle_start, candle_next)]
|
||||||
|
# dataframe.loc[is_between] = cache_entry # doesn't take, so we need workaround:
|
||||||
|
# Create a dictionary of the column values to be assigned
|
||||||
|
update_dict = {c: cache_entry[c].iat[0] for c in cache_entry.columns}
|
||||||
|
# Assign the values using the update_dict
|
||||||
|
dataframe.loc[is_between, update_dict.keys()] = pd.DataFrame(
|
||||||
|
[update_dict], index=dataframe.loc[is_between].index
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# Calculate orderflow for each candle
|
# Calculate orderflow for each candle
|
||||||
orderflow = trades_to_volumeprofile_with_total_delta_bid_ask(
|
orderflow = trades_to_volumeprofile_with_total_delta_bid_ask(
|
||||||
@@ -136,14 +149,11 @@ def populate_dataframe_with_trades(
|
|||||||
] * len(indices)
|
] * len(indices)
|
||||||
|
|
||||||
bid = np.where(
|
bid = np.where(
|
||||||
trades_grouped_df["side"].str.contains("sell"),
|
trades_grouped_df["side"].str.contains("sell"), trades_grouped_df["amount"], 0
|
||||||
trades_grouped_df["amount"],
|
|
||||||
0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ask = np.where(
|
ask = np.where(
|
||||||
trades_grouped_df["side"].str.contains("buy"),
|
trades_grouped_df["side"].str.contains("buy"), trades_grouped_df["amount"], 0
|
||||||
trades_grouped_df["amount"],
|
|
||||||
0,
|
|
||||||
)
|
)
|
||||||
deltas_per_trade = ask - bid
|
deltas_per_trade = ask - bid
|
||||||
min_delta = deltas_per_trade.cumsum().min()
|
min_delta = deltas_per_trade.cumsum().min()
|
||||||
@@ -157,6 +167,15 @@ def populate_dataframe_with_trades(
|
|||||||
dataframe.loc[indices, "ask"] - dataframe.loc[indices, "bid"]
|
dataframe.loc[indices, "ask"] - dataframe.loc[indices, "bid"]
|
||||||
)
|
)
|
||||||
dataframe.loc[indices, "total_trades"] = len(trades_grouped_df)
|
dataframe.loc[indices, "total_trades"] = len(trades_grouped_df)
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
cached_grouped_trades[(candle_start, candle_next)] = dataframe.loc[
|
||||||
|
is_between
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Maintain cache size
|
||||||
|
if len(cached_grouped_trades) > cache_size:
|
||||||
|
cached_grouped_trades.popitem(last=False)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Found NO candles for trades starting with {candle_start}")
|
logger.debug(f"Found NO candles for trades starting with {candle_start}")
|
||||||
logger.debug(f"trades.groups_keys in {time.time() - start_time} seconds")
|
logger.debug(f"trades.groups_keys in {time.time() - start_time} seconds")
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
@@ -47,6 +49,15 @@ def public_trades_list_simple(testdatadir):
|
|||||||
return read_csv(testdatadir / "orderflow/public_trades_list_simple_example.csv").copy()
|
return read_csv(testdatadir / "orderflow/public_trades_list_simple_example.csv").copy()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def reset_cache(request):
|
||||||
|
import freqtrade.data.converter.orderflow as orderflow
|
||||||
|
|
||||||
|
global orderflow # noqa F811
|
||||||
|
orderflow.cached_grouped_trades = OrderedDict()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
def test_public_trades_columns_before_change(
|
def test_public_trades_columns_before_change(
|
||||||
populate_dataframe_with_trades_dataframe, populate_dataframe_with_trades_trades
|
populate_dataframe_with_trades_dataframe, populate_dataframe_with_trades_trades
|
||||||
):
|
):
|
||||||
@@ -71,7 +82,7 @@ def test_public_trades_columns_before_change(
|
|||||||
|
|
||||||
|
|
||||||
def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
|
def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
|
||||||
populate_dataframe_with_trades_dataframe, populate_dataframe_with_trades_trades
|
reset_cache, populate_dataframe_with_trades_dataframe, populate_dataframe_with_trades_trades
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Tests the `populate_dataframe_with_trades` function's order flow calculation.
|
Tests the `populate_dataframe_with_trades` function's order flow calculation.
|
||||||
@@ -90,6 +101,7 @@ def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
|
|||||||
config = {
|
config = {
|
||||||
"timeframe": "5m",
|
"timeframe": "5m",
|
||||||
"orderflow": {
|
"orderflow": {
|
||||||
|
"cache_size": 1000,
|
||||||
"max_candles": 1500,
|
"max_candles": 1500,
|
||||||
"scale": 0.005,
|
"scale": 0.005,
|
||||||
"imbalance_volume": 0,
|
"imbalance_volume": 0,
|
||||||
@@ -169,7 +181,7 @@ def test_public_trades_mock_populate_dataframe_with_trades__check_orderflow(
|
|||||||
|
|
||||||
|
|
||||||
def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
|
def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
|
||||||
populate_dataframe_with_trades_dataframe, populate_dataframe_with_trades_trades
|
reset_cache, populate_dataframe_with_trades_dataframe, populate_dataframe_with_trades_trades
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Tests the `populate_dataframe_with_trades` function's handling of trades,
|
Tests the `populate_dataframe_with_trades` function's handling of trades,
|
||||||
@@ -201,6 +213,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
|
|||||||
config = {
|
config = {
|
||||||
"timeframe": "5m",
|
"timeframe": "5m",
|
||||||
"orderflow": {
|
"orderflow": {
|
||||||
|
"cache_size": 1000,
|
||||||
"max_candles": 1500,
|
"max_candles": 1500,
|
||||||
"scale": 0.5,
|
"scale": 0.5,
|
||||||
"imbalance_volume": 0,
|
"imbalance_volume": 0,
|
||||||
@@ -243,7 +256,7 @@ def test_public_trades_trades_mock_populate_dataframe_with_trades__check_trades(
|
|||||||
assert 169.442 == row["ask"]
|
assert 169.442 == row["ask"]
|
||||||
|
|
||||||
# Assert the number of trades
|
# Assert the number of trades
|
||||||
assert 151 == len(row.trades)
|
assert 151 == len(row["trades"])
|
||||||
|
|
||||||
# Assert specific details of the first trade
|
# Assert specific details of the first trade
|
||||||
t = row["trades"].iloc[0]
|
t = row["trades"].iloc[0]
|
||||||
@@ -367,6 +380,7 @@ def test_public_trades_config_max_trades(
|
|||||||
orderflow_config = {
|
orderflow_config = {
|
||||||
"timeframe": "5m",
|
"timeframe": "5m",
|
||||||
"orderflow": {
|
"orderflow": {
|
||||||
|
"cache_size": 1000,
|
||||||
"max_candles": 1,
|
"max_candles": 1,
|
||||||
"scale": 0.005,
|
"scale": 0.005,
|
||||||
"imbalance_volume": 0,
|
"imbalance_volume": 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user