Skip to main content

Introduction

Strategies are specialized scripts that share many capabilities with indicators but are designed for building trading systems. Strategies allow you to place, modify, and cancel orders, as well as analyze performance metrics. Currently, strategies cannot be used for live trading, but they can be run in testing mode. Testing mode simulates trades on historical and real-time bars, allowing you to evaluate your strategies in both backtesting and forward-testing scenarios. The current version has the following limitations, which will be implemented later:
  • Strategies cannot be used for live trading.
  • Strategies can trade only one instrument at a time. Like indicators, strategies can request additional market data via calc_on for analysis purposes, but trading operations are limited to the primary instrument on which the strategy is running.

First strategy

Strategies overview To help you get started quickly, let’s walk through a simple strategy. Here is a strategy that flips its position based on a simple condition:
# indie:lang_version = 5
from indie import strategy, param
from indie.strategies import order_side


@strategy('InSide Bar Strategy', overlay_main_pane=True, initial_capital=4200000.0)
@param.float('order_size', default=10.0, title='Order size', min=0.1, max=100.0)
@param.str('order_size_unit', default='% of cash', title='Order size unit', options=['% of cash', 'quantity'])
def Main(self, order_size, order_size_unit):
    desired_pos_size = order_size if order_size_unit == 'quantity' else self.trading.cash * order_size / 100 / self.close[0]
    if self.high[0] < self.high[1] and self.low[0] > self.low[1]:
        pos_size = self.trading.position.size
        if self.close[0] > self.open[0] and pos_size <= 0:
            # reverse previous position if exists or open a new one
            self.trading.place_order(order_side.BUY, size=desired_pos_size + abs(pos_size)).submit()
        elif self.close[0] < self.open[0] and pos_size >= 0:
            self.trading.place_order(order_side.SELL, size=desired_pos_size + abs(pos_size)).submit()

The structure of strategy code follows the same pattern as indicator code:
  • Indie language version
  • package imports
  • Main function or Main class — the entry point of every strategy
However, there are several important differences:
  • the @strategy decorator is used instead of the @indicator decorator
  • self inherits from MainStrategyContext rather than from MainContext, as indicators do
  • strategies have access to the self.trading property, which provides an interface for order and position management
Let’s take a closer look at what is happening in this example. The Main function is called on every candle (and every price tick for real-time data) within the data range used to run the strategy. As with indicators, this function can return values to be rendered on the chart. But in strategies, the Main function can also place orders and make trading decisions. The self parameter in the Main function is an instance of MainStrategyContext. It represents the context of the instrument on which the strategy is running. It provides access to OHLCV values (e.g. self.close) and allows the strategy to place orders for that instrument. Our Main function is decorated with the mandatory @strategy decorator. Decorators (names that begin with the @ character) provide metadata about strategies to the TakeProfit runtime. In addition to the settings provided by the @indicator decorator, the @strategy decorator also allows you to configure various strategy execution parameters. The same parameters can be configured via the UI. In our example, we configure only the initial capital parameter and leave all other settings at their default values. For more information about the available settings, see the Strategy params section. Please note that we need to import certain symbols such as strategy, because they are not part of the built-in Indie symbols. Many strategy-related symbols are located in the indie.strategies package. You can view the entire contents of the package in the library reference. Notice that in this example, all trading operations are performed through the self.trading property. Alternatively, placed orders can also be managed using methods of the Order class, which will be demonstrated later. The self.trading property provides two main groups of functions: Order management methods implement the builder pattern and are used like this:
# Place order:
order = (
    self.trading.place_order(side=order_side.BUY, size=1.0).
    limit(price=120000).
    stop(price=119000.0).
    take_profit(stop=135000, limit=134000).
    stop_loss(stop=115000).
    submit()
)

# Update order:
self.trading.amend_order(order.id).stop_loss(114000).submit()

# Cancel order:
self.trading.cancel_order(order.id)
Note that cancel_order does not require an explicit submit call, as it has no settings. More detailed information about these methods can be found on the Orders page and in the library reference.

A more advanced example

Now we’re ready to look at a slightly more advanced example that creates orders and saves them to variables for later updating the order parameters:
# indie:lang_version = 5
from indie import strategy, param, MainStrategyContext, Optional
from indie.algorithms import Highest, Lowest
from indie.strategies import order_side as o_sd, order_status as o_st, Order


def is_not_active(order: Optional[Order]) -> bool:
    return order is None or order.value().status in [o_st.FILLED, o_st.CANCELED, o_st.REJECTED, o_st.PARTIALLY_FILLED_CLOSED]


def is_updatable(order: Optional[Order]) -> bool:
    return order is not None and order.value().status in [o_st.CREATED, o_st.PLACED, o_st.PENDING_PLACED, o_st.PARTIALLY_FILLED]


@strategy('Price Channel Strategy', overlay_main_pane=True)
@param.int('length', default=20, title='Lookback window length', min=1)
@param.float('order_size', default=10.0, title='Order size', min=0.1, max=100.0)
@param.str('order_size_unit', default='% of cash', title='Order size unit', options=['% of cash', 'quantity'])
class Main(MainStrategyContext):
    def __init__(self):
        self.long_order: Optional[Order] = None
        self.short_order: Optional[Order] = None

    def calc(self, length, order_size, order_size_unit):
        highest = Highest.new(self.high, length)
        lowest = Lowest.new(self.low, length)

        if self.bar_index > length:
            entry_size = order_size if order_size_unit == 'quantity' else self.trading.cash * order_size / 100 / self.close[0]
            self.entry(o_sd.BUY, entry_size, highest[0])
            self.entry(o_sd.SELL, entry_size, lowest[0])

    def entry(self, side: o_sd, size: float, stop_price: float) -> None:
        order = self.long_order if side == o_sd.BUY else self.short_order

        pos_size = self.trading.position.size
        # reverse previous position if exists or open a new one
        order_size = size + abs(pos_size)
        if pos_size == 0 or pos_size > 0 and side == o_sd.SELL or pos_size < 0 and side == o_sd.BUY:
            # create new order or update existing one
            if is_not_active(order):
                order = (
                    self.trading.place_order(side=side, size=order_size).
                    stop(stop_price).
                    submit()
                )
            elif is_updatable(order):
                (
                    self.trading.amend_order(order.value().id).
                    size(order_size).
                    stop(stop_price).
                    submit()
                )
        elif is_updatable(order):
            # if desired position is already achieved, cancel existing order
            self.trading.cancel_order(order.value().id)
            order = None

        if side == o_sd.BUY:
            self.long_order = order
        else:
            self.short_order = order

What’s new in this example?
  • Main is a class rather than a function. For strategies, it must inherit from MainStrategyContext instead of MainContext, which is used in indicators.
  • The self.trading.place_order(...).submit() method returns an Order object, which you can store to track its status or cancel/update it later. We store order objects to the self.long_order and self.short_order variables for later use.
  • Methods for updating or canceling orders have two forms: self.trading.amend_order(...)/cancel_order(...) and order.update(...)/cancel().
  • In this example, we use stop orders. You can read more about order types in the Orders section.
Please note that working with Optional is different in Python and in Indie. In Indie, Optional is an actual wrapper class, and you need to call its value() method to retrieve the underlying value. For more information, see the library reference page. You can find more examples in the Built-in Strategies section.

Next steps