Skip to content
Open
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
153 changes: 97 additions & 56 deletions flumine/execution/simulatedexecution.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
import requests
from typing import Optional

from .baseexecution import BaseExecution
from .. import config
from .baseexecution import BaseExecution
from ..clients.clients import VenueType
from ..order.order import OrderStatus
from ..order.orderpackage import BaseOrderPackage, OrderPackageType


Expand Down Expand Up @@ -33,30 +34,47 @@ def execute_place(
self, order_package, http_session: Optional[requests.Session]
) -> None:
if order_package.client.paper_trade:
time.sleep(order_package.bet_delay + config.place_latency)
# todo PASSIVE matching
time.sleep(order_package.simulated_latency_plus_delay)
market = self.flumine.markets.markets[order_package.market_id]
for order, instruction in zip(order_package, order_package.place_instructions):
market_book = market.market_book

# calc current elapsed seconds
elapsed_seconds = order_package.elapsed_seconds
if elapsed_seconds < order_package.simulated_latency_plus_delay:
# check if PASSIVE execution available
if (
market_book.market_definition.bet_delay_models is None
or "PASSIVE" not in market_book.market_definition.bet_delay_models
):
return

for order, instruction in zip(
order_package.orders_pending, order_package.place_instructions
):
with order.trade:
self._bet_id += 1
simulated_response = order.simulated.place(
order_package, market.market_book, instruction, self._bet_id
order_package, market_book, instruction, self._bet_id
)
if simulated_response.status == "DELAY": # PENDING delay
self._bet_id -= 1
continue
self._order_logger(
order, simulated_response, order_package.package_type
)
if simulated_response.status == "SUCCESS":
order.executable()
elif simulated_response.status == "FAILURE":
order.execution_complete()

# update transaction counts
order_package.client.add_transaction(len(order_package))
# update transaction counts
order_package.client.add_transaction(1)

def execute_cancel(
self, order_package, http_session: Optional[requests.Session]
) -> None:
if order_package.client.paper_trade:
time.sleep(config.cancel_latency)
time.sleep(order_package.simulated_latency_plus_delay)
market = self.flumine.markets.markets[order_package.market_id]
failed_transaction_count = 0
for order in order_package:
Expand All @@ -82,7 +100,7 @@ def execute_update(
self, order_package, http_session: Optional[requests.Session]
) -> None:
if order_package.client.paper_trade:
time.sleep(config.update_latency)
time.sleep(order_package.simulated_latency_plus_delay)
market = self.flumine.markets.markets[order_package.market_id]
failed_transaction_count = 0
for order, instruction in zip(order_package, order_package.update_instructions):
Expand All @@ -106,59 +124,82 @@ def execute_update(
def execute_replace(
self, order_package, http_session: Optional[requests.Session]
) -> None:
if (
order_package.client.paper_trade
): # todo should the cancel happen without a delay?
time.sleep(order_package.bet_delay + config.replace_latency)
if order_package.client.paper_trade:
# todo the cancel happens without a bet delay!
# todo PASSIVE matching
time.sleep(order_package.simulated_latency_plus_delay)
market = self.flumine.markets.markets[order_package.market_id]
market_book = market.market_book

# calc current elapsed seconds
elapsed_seconds = order_package.elapsed_seconds
if elapsed_seconds < config.cancel_latency:
return

failed_transaction_count = 0
for order, instruction in zip(
order_package, order_package.replace_instructions
):
for order in order_package.orders_pending:
with order.trade:
# cancel current order
err = False
cancel_instruction_report = order.simulated.cancel(market.market_book)
if cancel_instruction_report.status == "SUCCESS":
order.execution_complete()
elif cancel_instruction_report.status == "FAILURE":
order.executable()
failed_transaction_count += 1
err = True
self._order_logger(
order,
cancel_instruction_report,
OrderPackageType.CANCEL,
)
if err:
continue

# place new order
self._bet_id += 1
replacement_order = order.trade.create_order_replacement(
order,
instruction.get("newPrice"),
cancel_instruction_report.size_cancelled,
order_package.date_time_created,
)
place_instruction_report = replacement_order.simulated.place(
order_package, market.market_book, instruction, self._bet_id
)
if place_instruction_report.status == "SUCCESS":
if order.status == OrderStatus.REPLACING:
# we can cancel after the latency (no bet delay)
instruction = order.create_replace_instruction()
# cancel current order
err = False
cancel_instruction_report = order.simulated.cancel(market_book)
if cancel_instruction_report.status == "SUCCESS":
order.execution_complete()
elif cancel_instruction_report.status == "FAILURE":
order.executable()
failed_transaction_count += 1
err = True
self._order_logger(
replacement_order,
place_instruction_report,
order_package.package_type,
order,
cancel_instruction_report,
OrderPackageType.CANCEL,
)
# add to blotter
market.place_order(
replacement_order, execute=False, client=order.client
if err:
continue

# create the replacement order
replacement_order = order.trade.create_order_replacement(
order,
instruction.get("newPrice"),
cancel_instruction_report.size_cancelled,
order_package.date_time_created,
)
replacement_order.executable()
elif place_instruction_report.status == "FAILURE":
order.executable()
replacement_order.placing()
order_package._orders.append(replacement_order)

elif order.status == OrderStatus.PENDING:
# we can re'place' after the bet delay latency or PASSIVE
if elapsed_seconds < config.place_latency + order_package.bet_delay:
# check if PASSIVE execution available
if (
market_book.market_definition.bet_delay_models is None
or "PASSIVE"
not in market_book.market_definition.bet_delay_models
):
return
instruction = order.create_place_instruction()
self._bet_id += 1
place_instruction_report = order.simulated.place(
order_package, market_book, instruction, self._bet_id
)
if place_instruction_report.status == "DELAY": # PENDING delay
self._bet_id -= 1
continue
if place_instruction_report.status == "SUCCESS":
self._order_logger(
order,
place_instruction_report,
order_package.package_type,
)
# add to blotter
market.place_order(order, execute=False, client=order.client)
order.executable()
elif place_instruction_report.status == "FAILURE":
order.execution_complete()
# update transaction counts
order_package.client.add_transaction(1)

# update transaction counts
order_package.client.add_transaction(len(order_package))
if failed_transaction_count:
order_package.client.add_transaction(failed_transaction_count, failed=True)
2 changes: 1 addition & 1 deletion flumine/markets/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __call__(self, market) -> None:

market.context["simulated"] = market_analytics
# process simulated orders
if market.blotter.active:
if market.blotter.active and market.market_book.status == "OPEN":
self._process_simulated_orders(market, market_analytics)

def remove_market(self, market) -> None:
Expand Down
28 changes: 25 additions & 3 deletions flumine/order/orderpackage.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def __init__(
# following used for simulated execution
self.processed = False
self.bet_delay = bet_delay
self.simulated_delay = self.calc_simulated_delay()
self.simulated_latency = self.calc_simulated_latency()
self.simulated_latency_plus_delay = self.calc_simulated_latency_delay()

def retry(self):
if self._retry and self._retry_count < self._max_retries:
Expand All @@ -71,7 +72,19 @@ def reset_orders(self, complete: bool = False) -> None:
else:
order.executable()

def calc_simulated_delay(self) -> float:
def calc_simulated_latency(self) -> float:
if self.client.execution.VENUE == VenueType.SIMULATED:
if self.package_type == OrderPackageType.PLACE:
return config.place_latency
elif self.package_type == OrderPackageType.CANCEL:
return config.cancel_latency
elif self.package_type == OrderPackageType.UPDATE:
return config.update_latency
elif self.package_type == OrderPackageType.REPLACE:
return config.replace_latency
return -1

def calc_simulated_latency_delay(self) -> float:
if self.client.execution.VENUE == VenueType.SIMULATED:
if self.package_type == OrderPackageType.PLACE:
return config.place_latency + self.bet_delay
Expand All @@ -81,6 +94,7 @@ def calc_simulated_delay(self) -> float:
return config.update_latency
elif self.package_type == OrderPackageType.REPLACE:
return config.replace_latency + self.bet_delay
return -1

@property
def place_instructions(self) -> list:
Expand All @@ -106,6 +120,14 @@ def order_limit(cls, package_type: OrderPackageType) -> int:
def orders(self) -> list:
return [o for o in self._orders if o.status != OrderStatus.VIOLATION]

@property
def orders_pending(self) -> list:
return [
o
for o in self._orders
if o.status in [OrderStatus.PENDING, OrderStatus.REPLACING]
]

@property
def retry_count(self) -> int:
return self._retry_count
Expand Down Expand Up @@ -149,7 +171,7 @@ class BetfairOrderPackage(BaseOrderPackage):

@property
def place_instructions(self) -> list:
return [order.create_place_instruction() for order in self]
return [order.create_place_instruction() for order in self.orders_pending]

@property
def cancel_instructions(self) -> list:
Expand Down
5 changes: 3 additions & 2 deletions flumine/order/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ def __init__(
def _update_status(self, status: TradeStatus) -> None:
self.status_log.append(status)
self.status = status
if logger.isEnabledFor(logging.INFO):
logger.info("Trade status update: %s" % self.status.value, extra=self.info)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Trade status update: %s" % self.status.value, extra=self.info)
if self.complete:
self.complete_trade()

Expand All @@ -71,6 +71,7 @@ def complete_trade(self) -> None:
self.market_id, self.selection_id, self.handicap
)
runner_context.reset(self.id)
logger.info("Trade completed: %s" % self.status.value, extra=self.info)

@property
def complete(self) -> bool:
Expand Down
14 changes: 14 additions & 0 deletions flumine/simulation/simulatedorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ def place(
raise NotImplementedError(
"Simulated betTargetSize placement not implemented"
)
# calc current delay
elapsed_seconds = order_package.elapsed_seconds
bet_delay_complete = False
if elapsed_seconds > order_package.simulated_latency_plus_delay:
bet_delay_complete = True

# Validate that min_fill_size <= size for fill or kill orders
is_fill_or_kill_order = time_in_force == "FILL_OR_KILL"
if is_fill_or_kill_order and min_fill_size > size:
Expand All @@ -138,6 +144,10 @@ def place(
)
if self.order.side == "BACK":
available_to_back = get_price(runner.ex.available_to_back, 0) or 1.01
if bet_delay_complete is False and price <= available_to_back:
# order would be 'aggressive'
return SimulatedPlaceResponse(status="DELAY")

if (
not order_package.client.best_price_execution
and available_to_back > price
Expand Down Expand Up @@ -184,6 +194,10 @@ def place(
available = runner.ex.available_to_lay
else:
available_to_lay = get_price(runner.ex.available_to_lay, 0) or 1000
if bet_delay_complete is False and price >= available_to_lay:
# order would be 'aggressive'
return SimulatedPlaceResponse(status="DELAY")

if (
not order_package.client.best_price_execution
and available_to_lay < price
Expand Down
7 changes: 4 additions & 3 deletions flumine/simulation/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def _process_market_books(self, event: events.MarketBookEvent) -> None:
)

def process_order_package(self, order_package) -> None:
# place in pending list (wait for latency+delay)
# place in pending list (wait for latency)
self.handler_queue.append(order_package)

def _process_simulated_orders(self, market) -> None:
Expand Down Expand Up @@ -187,10 +187,11 @@ def _check_pending_packages(self, market_id: str) -> None:
for order_package in self.handler_queue:
if (
order_package.market_id == market_id
and order_package.elapsed_seconds > order_package.simulated_delay
and order_package.elapsed_seconds > order_package.simulated_latency
):
order_package.client.execution.handler(order_package)
processed.append(order_package)
if len(order_package.orders_pending) == 0:
processed.append(order_package)
for p in processed:
self.handler_queue.remove(p)

Expand Down
Loading
Loading