tradingmachine.orders.closing_price
¶
The closing_price synthetic order type: an order sliced by volume through the half hour the day's closing price is computed from.
This is market-on-close or limit-on-close, the Atlas's G2. NSE and BSE compute an equity's closing price as the volume weighted average of the trades from 15:00 to 15:30, so this is a volume weighted order spread across that window, which is the nearest there is for futures, options and intraday orders; only the cash segment's post-closing session fills at the closing price exactly, and it takes only delivery orders. The length is worked out from the window, so there is no over_minutes. An order sent before the window answers HTTP 202 with an outcome of armed and sends its first slice when the window opens, one sent inside the window sends its first slice at once and spreads the rest over what is left, and one sent after 15:30 is refused with HTTP 400. The window follows the instrument's exchange trading calendar. Slices are shared out in whole lots, and a slice that comes to nothing, as it does when the order has fewer lots than slices, is skipped and the schedule moves on.
Typical usage example:
order = closing_price.ClosingPriceOrder( share, transaction_type="buy", product="cnc", order_type="limit", quantity=600, price=1000.0, slices=6, dry_run=True, ) answer = order.place()
ClosingPriceOrder
¶
ClosingPriceOrder(instrument, *, transaction_type, product, order_type, quantity, price=None, trigger_price=None, validity=None, disclosed_quantity=None, after_market=False, tag=None, price_reference=None, quantity_reference=None, closes_position=False, reduce_only=False, hold_limits=None, dry_run=False, slices=None, window_start=None, volume_profile=None)
Bases: SyntheticOrder
An order sliced by volume through the half hour the day's closing price is computed from.
This is market-on-close or limit-on-close, the Atlas's G2. NSE and BSE compute an equity's closing price as the volume weighted average of the trades from 15:00 to 15:30, so this is a volume weighted order spread across that window, which is the nearest there is for futures, options and intraday orders; only the cash segment's post-closing session fills at the closing price exactly, and it takes only delivery orders. The length is worked out from the window, so there is no over_minutes. An order sent before the window answers HTTP 202 with an outcome of armed and sends its first slice when the window opens, one sent inside the window sends its first slice at once and spreads the rest over what is left, and one sent after 15:30 is refused with HTTP 400. The window follows the instrument's exchange trading calendar. Slices are shared out in whole lots, and a slice that comes to nothing, as it does when the order has fewer lots than slices, is skipped and the schedule moves on.
The order template's attributes are described on SyntheticOrder.
Attributes:
| Name | Type | Description |
|---|---|---|
slices |
The int number of slices, from 2 to 60, or None to let UBI use 6, one every five minutes across the default window. A quantity of fewer lots than this is accepted, and the empty slices are skipped. |
|
window_start |
The str time the window opens, as |
|
volume_profile |
The list of float relative weights, one per half hour from the open, none negative and adding up to more than zero, or None to let UBI use its own. |
Initialises the order template and this type's own settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument
|
TradeableInstrument
|
The instruments.TradeableInstrument to place the order in. |
required |
transaction_type
|
str
|
The str side of the order, |
required |
product
|
str
|
The str product, |
required |
order_type
|
str
|
The str kind of order, |
required |
quantity
|
int | None
|
The int quantity in underlying units, not lots, or None when a quantity reference supplies it. |
required |
price
|
float | None
|
The float limit price in rupees, or None for an order type that takes no price or when a price reference supplies it. |
None
|
trigger_price
|
float | None
|
The float trigger price in rupees of the order itself, or None for an order type that takes no trigger. |
None
|
validity
|
str | None
|
The str validity, |
None
|
disclosed_quantity
|
int | None
|
The int quantity to show on the exchange, or None to disclose the whole order. |
None
|
after_market
|
bool
|
A bool that is True to send the order as an after-market order. |
False
|
tag
|
str | None
|
A str of up to twenty letters and digits to label the order with, or None. |
None
|
price_reference
|
dict | None
|
A dict describing the price for UBI to work out, such as |
None
|
quantity_reference
|
dict | None
|
A dict describing the quantity for UBI to work out, such as |
None
|
closes_position
|
bool
|
A bool that is True when every order this type sends closes a position, so it may use the share of a broker's daily order cap kept for exits. |
False
|
reduce_only
|
bool
|
A bool that is True to have UBI refuse, with HTTP 409, any leg that is not on the closing side of the net position held when it is sent or is bigger than that position. |
False
|
hold_limits
|
bool | None
|
A bool that is True to have UBI hold each order that would rest at the broker at a fixed limit price until the other side of the book reaches it, False to send them as they come, or None to let UBI use the type's default. |
None
|
dry_run
|
bool
|
A bool that is True to have UBI check the order and answer with the |
False
|
slices
|
int | None
|
The int number of slices, from 2 to 60, or None to let UBI use 6, one every five minutes across the default window. A quantity of fewer lots than this is accepted, and the empty slices are skipped. |
None
|
window_start
|
str | None
|
The str time the window opens, as |
None
|
volume_profile
|
list[float] | None
|
The list of float relative weights, one per half hour from the open, none negative and adding up to more than zero, or None to let UBI use its own. |
None
|
Source code in src/tradingmachine/orders/closing_price.py
synthetic_fields
¶
Gives this type's own settings, the fields of the synthetic object besides type.
Returns:
| Type | Description |
|---|---|
dict
|
A dict of UBI field names to values, where a value of None means the field is left out. |
Examples:
Print the settings of an order sliced ten times through the closing half hour:
from tradingmachine.assets import equities
from tradingmachine.orders import closing_price
share = equities.Equity(exchange="nse", symbol="IDEA")
order = closing_price.ClosingPriceOrder(
share,
transaction_type="buy",
product="cnc",
order_type="limit",
quantity=10,
price=13.0,
slices=10,
)
print(order.synthetic_fields())
Show a window that opens at 14:45 with a volume profile weighted towards the close:
from tradingmachine.assets import equities
from tradingmachine.orders import closing_price
share = equities.Equity(exchange="nse", symbol="IDEA")
order = closing_price.ClosingPriceOrder(
share,
transaction_type="buy",
product="cnc",
order_type="limit",
quantity=12,
price=13.0,
slices=6,
window_start="14:45",
volume_profile=[
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
3.0,
],
)
print(order.synthetic)
Source code in src/tradingmachine/orders/closing_price.py
Example programs¶
Preview and schedule a sell through a later closing window with its own volume profile¶
Run it with .venv/bin/python examples/orders/closing_price/closing_price_order/late_window_with_volume_profile.py.
"""Preview and schedule a sell through a later closing window with its own volume profile.
The program previews, then schedules, a sell of three Vodafone Idea shares 3% above the market in three slices through a window opening at 15:15, weighting the last half hour of the day three times as heavily as the rest. It prints the parent and cancels it.
Typical usage example:
.venv/bin/python examples/orders/closing_price/closing_price_order/late_window_with_volume_profile.py
"""
import time
from tradingmachine.accounts import account
from tradingmachine.assets import equities
from tradingmachine.assets import instruments
from tradingmachine.orders import closing_price
from tradingmachine.unified_broker_interface import exceptions
class ProfiledClosingSell:
"""A sell spread through a shortened closing window with a caller's volume profile.
Attributes:
trading_account: The tradingmachine.accounts.account.Account, used to read the engine's answer when it comes late.
share: The tradingmachine.assets.equities.Equity for Vodafone Idea on the NSE.
order: The tradingmachine.orders.closing_price.ClosingPriceOrder the program places, or None before run() builds it.
"""
def __init__(self):
"""Looks up the shares the program trades.
Raises:
tradingmachine.assets.exceptions.InstrumentError: A share could not be found in UBI.
"""
self.trading_account = account.Account()
self.share = equities.Equity(exchange="nse", symbol="IDEA")
self.order = None
def price_from_market(
self,
instrument: instruments.TradeableInstrument,
percent: float,
) -> float:
"""Gives a price a percentage away from an instrument's last price, rounded to its tick size.
Args:
instrument: The tradingmachine.assets.instruments.TradeableInstrument to price.
percent: The float percentage to move from the last price, negative for a price below the market.
Returns:
The float price in rupees.
Raises:
ValueError: UBI has no last price for the instrument.
"""
last_price = instrument.last_price
if last_price is None:
raise ValueError(f"UBI has no last price for {instrument!r}")
tick_size = 0.05
if instrument.tick_size is not None:
tick_size = float(instrument.tick_size)
ticks = round(last_price * (1 + percent / 100) / tick_size)
return round(ticks * tick_size, 2)
def build_order(self, dry_run: bool) -> closing_price.ClosingPriceOrder:
"""Builds three slices from 15:15 with a profile weighted towards the close.
Args:
dry_run: A bool that is True to build the order as a dry run, which UBI only checks and prices.
Returns:
The tradingmachine.orders.closing_price.ClosingPriceOrder, not yet placed.
Raises:
ValueError: UBI has no last price for a share.
"""
volume_profile = []
for half_hour in range(12):
volume_profile.append(1.0)
volume_profile.append(3.0)
return closing_price.ClosingPriceOrder(
self.share,
transaction_type="sell",
product="mis",
order_type="limit",
quantity=3,
price=self.price_from_market(self.share, 3),
slices=3,
window_start="15:15",
volume_profile=volume_profile,
dry_run=dry_run,
)
def print_parent(self) -> None:
"""Prints the state the order engine holds the order in and each leg it has.
Returns:
None.
Raises:
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI could not read the parent.
"""
parent = self.order.parent
print(f"Parent state: {parent['state']}")
for leg in parent["legs"]:
print(
f" leg {leg.get('role')}: {leg.get('transaction_type')} "
f"{leg.get('quantity')} at {leg.get('price')} "
f"trigger {leg.get('trigger_price')}, {leg.get('state')}"
)
def place_order(self) -> dict:
"""Places the order, sending it again when a broker refuses it, and reading the engine's stored answer when the engine answers too late, so that the order can always be cancelled.
Returns:
The dict answer of the placement, whose `parent_id` is also kept on the order.
Raises:
tradingmachine.unified_broker_interface.exceptions.OrderRejectedError: Brokers refused the order three times.
tradingmachine.unified_broker_interface.exceptions.OrderOutcomeUnknownError: The engine's answer could not be read within thirty seconds.
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused the order or could not be reached.
"""
for attempt in range(3):
try:
return self.order.place()
except exceptions.OrderRejectedError as error:
if attempt == 2:
raise
print(f"A broker refused the order, so it is sent again: {error}")
time.sleep(1)
except exceptions.OrderOutcomeUnknownError as error:
return self.read_late_answer(error)
return {}
def read_late_answer(
self,
error: exceptions.OrderOutcomeUnknownError,
) -> dict:
"""Reads the order engine's stored answer to a placement it answered too late, and keeps its parent id.
Args:
error: The tradingmachine.unified_broker_interface.exceptions.OrderOutcomeUnknownError the placement raised.
Returns:
The dict answer the placement would have given.
Raises:
tradingmachine.unified_broker_interface.exceptions.OrderOutcomeUnknownError: The engine's answer could not be read within thirty seconds.
"""
intent_id = error.detail.get("intent_id")
print(f"The engine answered late, so intent {intent_id} is read.")
for attempt in range(15):
time.sleep(2)
try:
stored = self.trading_account.intent(intent_id)
except exceptions.NotFoundError:
continue
answer = stored["response"]
self.order.parent_id = answer.get("parent_id")
return answer
raise error
def cancel_order(self) -> None:
"""Cancels the order in the order engine, with every leg it still has at a broker, and prints the result.
A broker can refuse or be slow to answer one leg's cancel, which leaves the parent `cancelling`, and the engine itself can answer late, so the cancel is sent again until the parent is `cancelled`, up to five times.
Returns:
None.
Raises:
RuntimeError: The parent was still not cancelled after five attempts, so a leg may still be live.
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused the cancel or could not be reached.
"""
if self.order is None or self.order.parent_id is None:
print("No order was placed, so there is nothing to cancel.")
return
for attempt in range(5):
try:
answer = self.order.cancel()
except exceptions.ConflictError:
print(f"The parent has already finished: {self.order.parent['state']}")
return
except exceptions.OrderOutcomeUnknownError:
print("The engine answered the cancel late, so it is sent again.")
time.sleep(3)
continue
print(
f"Cancelled: state {answer['state']}, "
f"{len(answer['cancelled_legs'])} legs cancelled"
)
for leg in answer["cancelled_legs"]:
print(
f" {leg.get('broker')} {leg.get('order_id')}: {leg.get('outcome')}"
)
if answer["state"] == "cancelled":
return
time.sleep(3)
raise RuntimeError(
f"Parent {self.order.parent_id} is still not cancelled, so check it by hand"
)
def run(self) -> None:
"""Previews the order, schedules it, prints the parent and cancels it.
Returns:
None.
Raises:
ValueError: UBI has no last price for a share.
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused a request or could not be reached.
"""
preview = self.build_order(dry_run=True).place()
print("A dry run, which sends and records nothing, says UBI would send:")
print(preview.get("request", preview))
self.order = self.build_order(dry_run=False)
answer = self.place_order()
print(
f"Placed a {self.order.SYNTHETIC_TYPE} order: outcome "
f"{answer.get('outcome')}, parent {self.order.parent_id}"
)
try:
time.sleep(2)
self.print_parent()
finally:
self.cancel_order()
if __name__ == "__main__":
ProfiledClosingSell().run()
Schedule a buy sliced through the closing half hour, then cancel it¶
Run it with .venv/bin/python examples/orders/closing_price/closing_price_order/schedule_buy_into_close.py.
"""Schedule a buy sliced through the closing half hour, then cancel it.
The program schedules two shares of Vodafone Idea to be bought in two slices through the 15:00 to 15:30 window the closing price is computed from, each capped 3% below the market. Sent before 15:00 it only waits, so the program prints the scheduled parent and cancels it.
Typical usage example:
.venv/bin/python examples/orders/closing_price/closing_price_order/schedule_buy_into_close.py
"""
import time
from tradingmachine.accounts import account
from tradingmachine.assets import equities
from tradingmachine.assets import instruments
from tradingmachine.orders import closing_price
from tradingmachine.unified_broker_interface import exceptions
class ClosingWindowBuy:
"""A buy spread through the window the day's closing price comes from.
Attributes:
trading_account: The tradingmachine.accounts.account.Account, used to read the engine's answer when it comes late.
share: The tradingmachine.assets.equities.Equity for Vodafone Idea on the NSE.
order: The tradingmachine.orders.closing_price.ClosingPriceOrder the program places, or None before run() builds it.
"""
def __init__(self):
"""Looks up the shares the program trades.
Raises:
tradingmachine.assets.exceptions.InstrumentError: A share could not be found in UBI.
"""
self.trading_account = account.Account()
self.share = equities.Equity(exchange="nse", symbol="IDEA")
self.order = None
def price_from_market(
self,
instrument: instruments.TradeableInstrument,
percent: float,
) -> float:
"""Gives a price a percentage away from an instrument's last price, rounded to its tick size.
Args:
instrument: The tradingmachine.assets.instruments.TradeableInstrument to price.
percent: The float percentage to move from the last price, negative for a price below the market.
Returns:
The float price in rupees.
Raises:
ValueError: UBI has no last price for the instrument.
"""
last_price = instrument.last_price
if last_price is None:
raise ValueError(f"UBI has no last price for {instrument!r}")
tick_size = 0.05
if instrument.tick_size is not None:
tick_size = float(instrument.tick_size)
ticks = round(last_price * (1 + percent / 100) / tick_size)
return round(ticks * tick_size, 2)
def build_order(self, dry_run: bool) -> closing_price.ClosingPriceOrder:
"""Builds two slices of one share through the closing window, capped 3% below the market.
Args:
dry_run: A bool that is True to build the order as a dry run, which UBI only checks and prices.
Returns:
The tradingmachine.orders.closing_price.ClosingPriceOrder, not yet placed.
Raises:
ValueError: UBI has no last price for a share.
"""
return closing_price.ClosingPriceOrder(
self.share,
transaction_type="buy",
product="mis",
order_type="limit",
quantity=2,
price=self.price_from_market(self.share, -3),
slices=2,
dry_run=dry_run,
)
def print_parent(self) -> None:
"""Prints the state the order engine holds the order in and each leg it has.
Returns:
None.
Raises:
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI could not read the parent.
"""
parent = self.order.parent
print(f"Parent state: {parent['state']}")
for leg in parent["legs"]:
print(
f" leg {leg.get('role')}: {leg.get('transaction_type')} "
f"{leg.get('quantity')} at {leg.get('price')} "
f"trigger {leg.get('trigger_price')}, {leg.get('state')}"
)
def place_order(self) -> dict:
"""Places the order, sending it again when a broker refuses it, and reading the engine's stored answer when the engine answers too late, so that the order can always be cancelled.
Returns:
The dict answer of the placement, whose `parent_id` is also kept on the order.
Raises:
tradingmachine.unified_broker_interface.exceptions.OrderRejectedError: Brokers refused the order three times.
tradingmachine.unified_broker_interface.exceptions.OrderOutcomeUnknownError: The engine's answer could not be read within thirty seconds.
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused the order or could not be reached.
"""
for attempt in range(3):
try:
return self.order.place()
except exceptions.OrderRejectedError as error:
if attempt == 2:
raise
print(f"A broker refused the order, so it is sent again: {error}")
time.sleep(1)
except exceptions.OrderOutcomeUnknownError as error:
return self.read_late_answer(error)
return {}
def read_late_answer(
self,
error: exceptions.OrderOutcomeUnknownError,
) -> dict:
"""Reads the order engine's stored answer to a placement it answered too late, and keeps its parent id.
Args:
error: The tradingmachine.unified_broker_interface.exceptions.OrderOutcomeUnknownError the placement raised.
Returns:
The dict answer the placement would have given.
Raises:
tradingmachine.unified_broker_interface.exceptions.OrderOutcomeUnknownError: The engine's answer could not be read within thirty seconds.
"""
intent_id = error.detail.get("intent_id")
print(f"The engine answered late, so intent {intent_id} is read.")
for attempt in range(15):
time.sleep(2)
try:
stored = self.trading_account.intent(intent_id)
except exceptions.NotFoundError:
continue
answer = stored["response"]
self.order.parent_id = answer.get("parent_id")
return answer
raise error
def cancel_order(self) -> None:
"""Cancels the order in the order engine, with every leg it still has at a broker, and prints the result.
A broker can refuse or be slow to answer one leg's cancel, which leaves the parent `cancelling`, and the engine itself can answer late, so the cancel is sent again until the parent is `cancelled`, up to five times.
Returns:
None.
Raises:
RuntimeError: The parent was still not cancelled after five attempts, so a leg may still be live.
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused the cancel or could not be reached.
"""
if self.order is None or self.order.parent_id is None:
print("No order was placed, so there is nothing to cancel.")
return
for attempt in range(5):
try:
answer = self.order.cancel()
except exceptions.ConflictError:
print(f"The parent has already finished: {self.order.parent['state']}")
return
except exceptions.OrderOutcomeUnknownError:
print("The engine answered the cancel late, so it is sent again.")
time.sleep(3)
continue
print(
f"Cancelled: state {answer['state']}, "
f"{len(answer['cancelled_legs'])} legs cancelled"
)
for leg in answer["cancelled_legs"]:
print(
f" {leg.get('broker')} {leg.get('order_id')}: {leg.get('outcome')}"
)
if answer["state"] == "cancelled":
return
time.sleep(3)
raise RuntimeError(
f"Parent {self.order.parent_id} is still not cancelled, so check it by hand"
)
def run(self) -> None:
"""Schedules the order, prints the parent and cancels it.
Returns:
None.
Raises:
ValueError: UBI has no last price for a share.
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI refused a request or could not be reached.
"""
self.order = self.build_order(dry_run=False)
answer = self.place_order()
print(
f"Placed a {self.order.SYNTHETIC_TYPE} order: outcome "
f"{answer.get('outcome')}, parent {self.order.parent_id}"
)
try:
time.sleep(2)
self.print_parent()
finally:
self.cancel_order()
if __name__ == "__main__":
ClosingWindowBuy().run()