tradingmachine.orders.underlying_peg
¶
The underlying_peg synthetic order type: a resting limit whose price moves by delta times another instrument's move, such as an option bid following the index.
This is pegged-to-stock or delta-pegged, the Atlas's G6. The order is placed at the template's price, and UBI then keeps its price at that price plus delta times the watched instrument's move since it was placed, rounded to the tick, kept between lowest_price and highest_price, and changed only once it has moved at least step_ticks. The order's own book is never read, which matters on a far strike where one order moves the premium. The template must be a limit order with a price, and changing the price yourself restarts the peg from your price and the watched instrument's price at that moment. UBI keeps the watched price the peg measures from as watched_start in the part's pricing_memory, which the order's parent shows. lowest_price and highest_price must each be a whole number of ticks, or UBI refuses the order with HTTP 400.
Typical usage example:
order = underlying_peg.UnderlyingPegOrder( nifty_call, transaction_type="buy", product="nrml", order_type="limit", quantity=75, price=120.0, watch_instrument=nifty, delta=0.5, step_ticks=4, dry_run=True, ) answer = order.place()
UnderlyingPegOrder
¶
UnderlyingPegOrder(instrument, *, transaction_type, product, order_type, quantity, watch_instrument, delta, 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, lowest_price=None, highest_price=None, step_ticks=None)
Bases: SyntheticOrder
A resting limit whose price moves by delta times another instrument's move, such as an option bid following the index.
This is pegged-to-stock or delta-pegged, the Atlas's G6. The order is placed at the template's price, and UBI then keeps its price at that price plus delta times the watched instrument's move since it was placed, rounded to the tick, kept between lowest_price and highest_price, and changed only once it has moved at least step_ticks. The order's own book is never read, which matters on a far strike where one order moves the premium. The template must be a limit order with a price, and changing the price yourself restarts the peg from your price and the watched instrument's price at that moment. UBI keeps the watched price the peg measures from as watched_start in the part's pricing_memory, which the order's parent shows. lowest_price and highest_price must each be a whole number of ticks, or UBI refuses the order with HTTP 400.
The order template's attributes are described on SyntheticOrder.
Attributes:
| Name | Type | Description |
|---|---|---|
watch_instrument |
The instruments.Instrument whose price the order follows, usually the underlying, and not the traded instrument itself. |
|
delta |
The float number of rupees the order's price moves per rupee the watched instrument moves, negative for a put. |
|
lowest_price |
The float lowest price in rupees the order is moved to, above zero, or None for no floor. |
|
highest_price |
The float highest price in rupees the order is moved to, above zero and not below |
|
step_ticks |
The int smallest move in ticks worth a modification, at least 1, or None to let UBI use 1. |
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 |
watch_instrument
|
Instrument
|
The instruments.Instrument whose price the order follows, usually the underlying, and not the traded instrument itself. |
required |
delta
|
float
|
The float number of rupees the order's price moves per rupee the watched instrument moves, negative for a put. |
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
|
lowest_price
|
float | None
|
The float lowest price in rupees the order is moved to, above zero, or None for no floor. |
None
|
highest_price
|
float | None
|
The float highest price in rupees the order is moved to, above zero and not below |
None
|
step_ticks
|
int | None
|
The int smallest move in ticks worth a modification, at least 1, or None to let UBI use 1. |
None
|
Source code in src/tradingmachine/orders/underlying_peg.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. |
Source code in src/tradingmachine/orders/underlying_peg.py
Example programs¶
Rest a buy 5% below the market whose price follows another share's moves¶
Run it with .venv/bin/python examples/orders/underlying_peg/underlying_peg_order/bid_following_another_share.py.
"""Rest a buy 5% below the market whose price follows another share's moves.
The program places an underlying peg buying one Vodafone Idea share 5% below the market. UBI then moves the bid by 0.3 rupees for every rupee Yes Bank moves from where it stood when the order was placed, in steps of at least two ticks, and never above 3% below the market at the start, so the bid cannot be chased up to the price. The program prints the parent and its order at the broker and cancels it.
Typical usage example:
.venv/bin/python examples/orders/underlying_peg/underlying_peg_order/bid_following_another_share.py
"""
import time
from tradingmachine.accounts import account
from tradingmachine.assets import equities
from tradingmachine.assets import instruments
from tradingmachine.orders import underlying_peg
from tradingmachine.unified_broker_interface import exceptions
class BidFollowingAnotherShare:
"""A Vodafone Idea bid pegged to Yes Bank's moves, kept at least 3% below the market.
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, which the program buys.
second_share: The tradingmachine.assets.equities.Equity for Yes Bank on the NSE, whose moves the bid follows.
order: The tradingmachine.orders.underlying_peg.UnderlyingPegOrder the program places, or None before run() builds it.
"""
def __init__(self):
"""Looks up the share the program trades and the share its price follows.
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.second_share = equities.Equity(exchange="nse", symbol="YESBANK")
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) -> underlying_peg.UnderlyingPegOrder:
"""Builds a buy 5% below the market that moves 0.3 rupees per rupee of Yes Bank, 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.underlying_peg.UnderlyingPegOrder, not yet placed.
Raises:
ValueError: UBI has no last price for a share.
"""
return underlying_peg.UnderlyingPegOrder(
self.share,
transaction_type="buy",
product="mis",
order_type="limit",
quantity=1,
price=self.price_from_market(self.share, -5),
watch_instrument=self.second_share,
delta=0.3,
highest_price=self.price_from_market(self.share, -3),
step_ticks=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 print_broker_orders(self) -> None:
"""Prints the broker orders the order has placed so far.
Returns:
None.
Raises:
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI could not read the order book.
"""
broker_orders = self.order.orders
if broker_orders is None:
print("No broker order has been placed yet.")
return
for row in broker_orders.to_dict("records"):
print(
f" order {row.get('order_id')}: {row.get('transaction_type')} "
f"{row.get('quantity')} at {row.get('price')}, {row.get('status')}"
)
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:
"""Places the peg, prints the parent and its order at the broker, then 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()
self.print_broker_orders()
finally:
self.cancel_order()
if __name__ == "__main__":
BidFollowingAnotherShare().run()
Preview and rest a sell 5% above the market whose price follows the Nifty 50¶
Run it with .venv/bin/python examples/orders/underlying_peg/underlying_peg_order/offer_following_the_index.py.
"""Preview and rest a sell 5% above the market whose price follows the Nifty 50.
The program previews, then places, an underlying peg selling one Vodafone Idea share 5% above the market. UBI then moves the offer by 0.0004 rupees for every point the Nifty 50 moves from where it stood when the order was placed, and never below 3% above the market at the start, so the offer cannot be chased down to the price. The program prints the parent and its order at the broker and cancels it.
Typical usage example:
.venv/bin/python examples/orders/underlying_peg/underlying_peg_order/offer_following_the_index.py
"""
import time
from tradingmachine.accounts import account
from tradingmachine.assets import equities
from tradingmachine.assets import instruments
from tradingmachine.orders import underlying_peg
from tradingmachine.unified_broker_interface import exceptions
class OfferFollowingTheIndex:
"""A Vodafone Idea offer pegged to the Nifty 50's moves, kept at least 3% above the market.
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, which the program sells.
index: The tradingmachine.assets.equities.EquityIndex for the Nifty 50 on the NSE, whose moves the offer follows.
order: The tradingmachine.orders.underlying_peg.UnderlyingPegOrder the program places, or None before run() builds it.
"""
def __init__(self):
"""Looks up the share the program trades and the index its price follows.
Raises:
tradingmachine.assets.exceptions.InstrumentError: The share or the index could not be found in UBI.
"""
self.trading_account = account.Account()
self.share = equities.Equity(exchange="nse", symbol="IDEA")
self.index = equities.EquityIndex(exchange="nse", symbol="NIFTY")
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) -> underlying_peg.UnderlyingPegOrder:
"""Builds a sell 5% above the market that moves 0.0004 rupees per index point, floored 3% above 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.underlying_peg.UnderlyingPegOrder, not yet placed.
Raises:
ValueError: UBI has no last price for a share.
"""
return underlying_peg.UnderlyingPegOrder(
self.share,
transaction_type="sell",
product="mis",
order_type="limit",
quantity=1,
price=self.price_from_market(self.share, 5),
watch_instrument=self.index,
delta=0.0004,
lowest_price=self.price_from_market(self.share, 3),
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 print_broker_orders(self) -> None:
"""Prints the broker orders the order has placed so far.
Returns:
None.
Raises:
tradingmachine.unified_broker_interface.exceptions.UnifiedBrokerInterfaceError: UBI could not read the order book.
"""
broker_orders = self.order.orders
if broker_orders is None:
print("No broker order has been placed yet.")
return
for row in broker_orders.to_dict("records"):
print(
f" order {row.get('order_id')}: {row.get('transaction_type')} "
f"{row.get('quantity')} at {row.get('price')}, {row.get('status')}"
)
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 peg, places it, prints the parent and its order at the broker, then 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()
self.print_broker_orders()
finally:
self.cancel_order()
if __name__ == "__main__":
OfferFollowingTheIndex().run()