mhallsmoore / qstrader

QuantStart.com - QSTrader backtesting simulation engine.
https://www.quantstart.com/qstrader/
MIT License
2.9k stars 854 forks source link

FillEvent doesn't have correct timestamp/price #114

Closed femtotrader closed 7 months ago

femtotrader commented 8 years ago

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

DirkFR commented 7 years ago

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

femtotrader commented 7 years ago

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