tradingmachine.orders.strategy_stop
¶
The strategy_stop synthetic order type: a basket whose every leg is closed when the whole strategy's profit or loss crosses a line.
It places its candidates the way a basket does, then marks every leg to its last traded price on each tick and closes every leg when the total falls to loss_limit or below or rises to profit_target or above. Give at least one of the two. A quote marked stale marks nothing, so the stop waits for a live one. When the stop acts, the basket's orders still resting are cancelled, and whatever they fill before the cancel lands is closed too, so the parent completes only once nothing of the basket rests and every close has finished. A close the broker refuses is not sent again, and the parent ends failed, because that leg is left open. UBI never resolves references for this type. The first candidate's instrument anchors the request.
Typical usage example:
order = strategy_stop.StrategyStopOrder( candidates=[ order_candidate.OrderCandidate(reliance, price=1450.0), order_candidate.OrderCandidate(infosys, price=1500.0), ], transaction_type="buy", product="cnc", order_type="limit", quantity=1, loss_limit=-5000.0, dry_run=True, ) answer = order.place()
StrategyStopOrder
¶
StrategyStopOrder(*, candidates, transaction_type, product, order_type, quantity, price=None, trigger_price=None, validity=None, after_market=False, tag=None, closes_position=False, reduce_only=False, hold_limits=None, dry_run=False, loss_limit=None, profit_target=None, hedge_benefit=False)
Bases: SyntheticOrder
A basket whose every leg is closed when the whole strategy's profit or loss crosses a line.
It places its candidates the way a basket does, then marks every leg to its last traded price on each tick and closes every leg when the total falls to loss_limit or below or rises to profit_target or above. Give at least one of the two. A quote marked stale marks nothing, so the stop waits for a live one. When the stop acts, the basket's orders still resting are cancelled, and whatever they fill before the cancel lands is closed too, so the parent completes only once nothing of the basket rests and every close has finished. A close the broker refuses is not sent again, and the parent ends failed, because that leg is left open. UBI never resolves references for this type. The first candidate's instrument anchors the request.
The order template's attributes are described on SyntheticOrder, where instrument is the first candidate's.
Attributes:
| Name | Type | Description |
|---|---|---|
candidates |
The list of order_candidate.OrderCandidate, one per instrument. |
|
hedge_benefit |
A bool that is True to have UBI price options and futures on one underlying and expiry together, as a hedged whole, when it checks that the broker can afford the legs. |
|
loss_limit |
The float loss in rupees for the whole strategy at or below which every leg is closed, below zero, or None. |
|
profit_target |
The float profit in rupees for the whole strategy at or above which every leg is closed, above zero, or None. |
Initialises the candidates, the template they default to and this type's own settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
list[OrderCandidate]
|
The list of order_candidate.OrderCandidate, from 1 to 25, each on a different instrument. |
required |
transaction_type
|
str
|
The str default side for every candidate, |
required |
product
|
str
|
The str default product for every candidate, |
required |
order_type
|
str
|
The str default kind of order for every candidate, |
required |
quantity
|
int
|
The int default quantity in underlying units for every candidate. |
required |
price
|
float | None
|
The float default limit price in rupees for every candidate, or None. It is carried into a candidate that sets |
None
|
trigger_price
|
float | None
|
The float default trigger price in rupees for every candidate, or None. |
None
|
validity
|
str | None
|
The str default validity, |
None
|
after_market
|
bool
|
A bool that is True to send the orders as after-market orders. |
False
|
tag
|
str | None
|
A str default label of up to twenty letters and digits, or None. |
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
|
loss_limit
|
float | None
|
The float loss in rupees for the whole strategy at or below which every leg is closed, below zero, or None. |
None
|
profit_target
|
float | None
|
The float profit in rupees for the whole strategy at or above which every leg is closed, above zero, or None. |
None
|
hedge_benefit
|
bool
|
A bool that is True to have UBI price options and futures on one underlying and expiry together, as a hedged whole, when it checks that the broker can afford the legs, rather than adding every leg's margin up. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
No candidate was given, so there is no instrument to anchor the request. |
Source code in src/tradingmachine/orders/strategy_stop.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, holding the candidates as UBI reads them, where a value of None means the field is left out. |
Source code in src/tradingmachine/orders/strategy_stop.py
Example programs¶
Preview and rest a bid and an offer on two shares, with hedge benefit asked for, closed together on a combined loss¶
Run it with .venv/bin/python examples/orders/strategy_stop/strategy_stop_order/preview_hedged_bid_and_offer_with_hedge_benefit.py.
"""Preview and rest a bid and an offer on two shares, with hedge benefit asked for, closed together on a combined loss.
The program previews, then places, a strategy stop with `hedge_benefit` set, which asks UBI to price options and futures on one underlying and expiry together when it checks that the broker can afford the legs; for two shares it changes nothing, but it shows where the setting goes. The strategy rests a buy of one Vodafone Idea share 3% below the market and a sell of one Yes Bank share 3% above it. If either filled, UBI would close every filled leg once the total lost 40 rupees. Neither order fills in the seconds the strategy lives; the program prints the parent and the resting orders and cancels it.
Typical usage example:
.venv/bin/python examples/orders/strategy_stop/strategy_stop_order/preview_hedged_bid_and_offer_with_hedge_benefit.py
"""
import time
from tradingmachine.accounts import account
from tradingmachine.assets import equities
from tradingmachine.assets import instruments
from tradingmachine.orders import order_candidate
from tradingmachine.orders import strategy_stop
from tradingmachine.unified_broker_interface import exceptions
class HedgedBidAndOfferWithHedgeBenefit:
"""A bid on one share and an offer on another, closed as a whole on a combined loss, with hedged margin asked for.
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.
second_share: The tradingmachine.assets.equities.Equity for Yes Bank on the NSE.
order: The tradingmachine.orders.strategy_stop.StrategyStopOrder 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.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) -> strategy_stop.StrategyStopOrder:
"""Builds a bid 3% below one share's market and an offer 3% above the other's, closed together at a loss of 40 rupees.
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.strategy_stop.StrategyStopOrder, not yet placed.
Raises:
ValueError: UBI has no last price for a share.
"""
return strategy_stop.StrategyStopOrder(
candidates=[
order_candidate.OrderCandidate(
self.share,
price=self.price_from_market(self.share, -3),
),
order_candidate.OrderCandidate(
self.second_share,
transaction_type="sell",
price=self.price_from_market(self.second_share, 3),
),
],
transaction_type="buy",
product="mis",
order_type="limit",
quantity=1,
price=self.price_from_market(self.share, -3),
loss_limit=-40.0,
hedge_benefit=True,
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 strategy stop, places it, prints the parent and its orders 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__":
HedgedBidAndOfferWithHedgeBenefit().run()
Rest bids for two shares that would all be closed together if the pair lost or made a set amount¶
Run it with .venv/bin/python examples/orders/strategy_stop/strategy_stop_order/two_share_bids_with_loss_limit.py.
"""Rest bids for two shares that would all be closed together if the pair lost or made a set amount.
The program places a strategy stop that rests a buy of one Vodafone Idea share and one Yes Bank share, each 3% below its own market. If either filled, UBI would add up the filled legs' profit and loss on every tick and close every leg once the total fell to a loss of 50 rupees or rose to a profit of 50 rupees. Neither bid fills in the seconds the order lives; the program prints the parent and the resting bids and cancels it.
Typical usage example:
.venv/bin/python examples/orders/strategy_stop/strategy_stop_order/two_share_bids_with_loss_limit.py
"""
import time
from tradingmachine.accounts import account
from tradingmachine.assets import equities
from tradingmachine.assets import instruments
from tradingmachine.orders import order_candidate
from tradingmachine.orders import strategy_stop
from tradingmachine.unified_broker_interface import exceptions
class TwoShareBidsWithLossLimit:
"""A basket of bids for two cheap shares, closed as a whole on a combined loss or profit.
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.
second_share: The tradingmachine.assets.equities.Equity for Yes Bank on the NSE.
order: The tradingmachine.orders.strategy_stop.StrategyStopOrder 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.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) -> strategy_stop.StrategyStopOrder:
"""Builds bids 3% below each share's market, closed together at a loss of 50 rupees or a profit of 50 rupees.
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.strategy_stop.StrategyStopOrder, not yet placed.
Raises:
ValueError: UBI has no last price for a share.
"""
return strategy_stop.StrategyStopOrder(
candidates=[
order_candidate.OrderCandidate(
self.share,
price=self.price_from_market(self.share, -3),
),
order_candidate.OrderCandidate(
self.second_share,
price=self.price_from_market(self.second_share, -3),
),
],
transaction_type="buy",
product="mis",
order_type="limit",
quantity=1,
price=self.price_from_market(self.share, -3),
loss_limit=-50.0,
profit_target=50.0,
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 strategy stop, prints the parent and its bids 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__":
TwoShareBidsWithLossLimit().run()