qstrader
qstrader copied to clipboard
FillEvent doesn't have correct timestamp/price
Hello,
Reading the code I was pretty sure FillEvent
doesn't have correct timestamp/price
In fact a strategy generate a SignalEvent
which is sized, and risk measured so we have an OrderEvent (market order) in events queue.
There is pretty no chance that this order will be executed at exactly same price than what your strategy "saw" when generating this SignalEvent
.
To my understanding, order should be executed at next price, next timestamp.
Here is an example strategy to show this behavior:
import click
from qstrader import settings
from qstrader.compat import queue
from qstrader.price_parser import PriceParser
from qstrader.price_handler.historic_csv_tick import HistoricCSVTickPriceHandler
from qstrader.position_sizer.fixed import FixedPositionSizer
from qstrader.risk_manager.example import ExampleRiskManager
from qstrader.portfolio_handler import PortfolioHandler
from qstrader.compliance.example import ExampleCompliance
from qstrader.execution_handler.ib_simulated import IBSimulatedExecutionHandler
from qstrader.statistics.simple import SimpleStatistics
from qstrader.trading_session.backtest import Backtest
from qstrader.strategy.base import AbstractStrategy
from qstrader.event import (SignalEvent, EventType)
class ExampleStrategy(AbstractStrategy):
"""
A testing strategy that alternates between buying and selling
a ticker on every 5th tick. This has the effect of continuously
"crossing the spread" and so will be loss-making strategy.
It is used to test that the backtester/live trading system is
behaving as expected.
"""
def __init__(self, tickers, events_queue):
self.tickers = tickers
self.events_queue = events_queue
self.ticks = 0
self.invested = False
def calculate_signals(self, event):
ticker = self.tickers[0]
if event.type == EventType.TICK and event.ticker == ticker:
print(event)
if event.time.second == 5:
if not self.invested:
signal = SignalEvent(ticker, "BOT")
print(signal)
self.events_queue.put(signal)
self.invested = True
def run(config, testing, tickers, filename):
# Set up variables needed for backtest
events_queue = queue.Queue()
csv_dir = config.CSV_DATA_DIR
initial_equity = PriceParser.parse(500000.00)
# Use Historic CSV Price Handler
price_handler = HistoricCSVTickPriceHandler(
csv_dir, events_queue, tickers
)
# Use the Example Strategy
strategy = ExampleStrategy(tickers, events_queue)
# Use an example Position Sizer
position_sizer = FixedPositionSizer()
# Use an example Risk Manager
risk_manager = ExampleRiskManager()
# Use the default Portfolio Handler
portfolio_handler = PortfolioHandler(
initial_equity, events_queue, price_handler,
position_sizer, risk_manager
)
# Use the ExampleCompliance component
compliance = ExampleCompliance(config)
# Use a simulated IB Execution Handler
execution_handler = IBSimulatedExecutionHandler(
events_queue, price_handler, compliance
)
# Use the default Statistics
statistics = SimpleStatistics(config, portfolio_handler)
# Set up the backtest
backtest = Backtest(
price_handler, strategy,
portfolio_handler, execution_handler,
position_sizer, risk_manager,
statistics, initial_equity
)
results = backtest.simulate_trading(testing=testing)
statistics.save(filename)
return results
@click.command()
@click.option('--config', default=settings.DEFAULT_CONFIG_FILENAME, help='Config filename')
@click.option('--testing/--no-testing', default=False, help='Enable testing mode')
@click.option('--tickers', default='GOOG', help='Tickers (use comma)')
@click.option('--filename', default='', help='Pickle (.pkl) statistics filename')
def main(config, testing, tickers, filename):
tickers = tickers.split(",")
config = settings.from_file(config, testing)
run(config, testing, tickers, filename)
if __name__ == "__main__":
main()
and execution_handler/ib_simulated.py
was modified according:
def execute_order(self, event):
...
print(fill_event.timestamp)
print(fill_event.price)
self.events_queue.put(fill_event)
Here is data/GOOG.csv
Ticker,Time,Bid,Ask
GOOG,01.02.2016 00:00:01.358,683.56000,683.58000
GOOG,01.02.2016 00:00:02.544,683.55998,683.58002
GOOG,01.02.2016 00:00:03.765,683.55999,683.58001
GOOG,01.02.2016 00:00:05.215,683.56001,683.57999
GOOG,01.02.2016 00:00:06.509,683.56002,683.57998
GOOG,01.02.2016 00:00:07.964,683.55999,683.58001
GOOG,01.02.2016 00:00:09.369,683.56000,683.58000
GOOG,01.02.2016 00:00:10.823,683.56001,683.57999
GOOG,01.02.2016 00:00:12.221,683.56000,683.58000
GOOG,01.02.2016 00:00:13.546,683.56000,683.58000
and strategy output
$ python examples/strategy_tick_backtest.py --testing
Running Backtest...
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:01.358000, Bid: 6835599999, Ask: 6835800000
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:02.544000, Bid: 6835599800, Ask: 6835800200
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:03.765000, Bid: 6835599900, Ask: 6835800100
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:05.215000, Bid: 6835600099, Ask: 6835799900
<qstrader.event.SignalEvent object at 0x119678320>
2016-02-01 00:00:05.215000
6835799900
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:06.509000, Bid: 6835600200, Ask: 6835799800
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:07.964000, Bid: 6835599900, Ask: 6835800100
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:09.369000, Bid: 6835599999, Ask: 6835800000
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:10.823000, Bid: 6835600099, Ask: 6835799900
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:12.221000, Bid: 6835599999, Ask: 6835800000
Type: EventType.TICK, Ticker: GOOG, Time: 2016-02-01 00:00:13.546000, Bid: 6835599999, Ask: 6835800000
---------------------------------
Backtest complete.
Sharpe Ratio: -4.7863
Max Drawdown: 2.0
Max Drawdown Pct: 0.0004
Save results to 'out/statistics_2016-07-31_142810.pkl'
We clearly see that order is executed at 2016-02-01 00:00:05.215000 with price 6835600099 In fact, it should be executed (or not) only at 00:00:06.509000 with price 6835600200
Why I'm saying "or not"... because you should be able to set a maximum allowed slippage.
If execution price is too far from expected price, order won't be executed.
What is your opinion ?
Do you have any idea to "fix" it ?
Maybe we should make an other kind of execution handler with such behavior ?
Kind regards
Hi Femtotrader, Not sure if you still monitor this - I was having the same question.
One thing I could think of is to use a sort of backtest-loop-counter and prevent the fill event from being executed in the same loop iteration. So basically, the fill event would stay in the queue until the next iteration (with the next executable price). Do you think this is viable?
The need for a kind of limit-order still exists even with this fix (I guess that's where you're going with the "or not" part).
Dirk
Hi @DirkFR ,
This issue doesn't deals with limit-order (but as you said previously "the need for a kind of limit-order still exists")
To be more clear about the "or not" part of my question you might have a look at http://www.metatrader5.com/en/terminal/help/performing_deals and https://www.mql5.com/en/docs/constants/environment_state/marketinfoconstants#enum_symbol_trade_execution and see differences between "Market execution" and "Instant execution" (with Deviation)
I think an other queue (belonging to execution handler) is necessary (maybe a priority queue) to perform Market execution or instant execution.
Current behaviour of this backtester looks like "Request Execution Mode".
femto