Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is ctx.stop_trailing for? #24

Closed
none2003 opened this issue Jun 2, 2023 · 6 comments
Closed

What is ctx.stop_trailing for? #24

none2003 opened this issue Jun 2, 2023 · 6 comments

Comments

@none2003
Copy link

none2003 commented Jun 2, 2023

Hi Ed,

After trying the Trailing Stop function in example (https://www.pybroker.com/en/latest/notebooks/8.%20Applying%20Stops.html), it was natural for me to want to implement an ATR Trailing Stop.

I saw an attribute "stop_trailing" in the ExecContext, and the documentation describes it as "Sets a trailing stop loss on a new pybroker.portfolio.Entry, where value is measured in points from entry price.", I'm a little confused here, in the case of long, will it stop when it is below the "highest market price" by a certain margin (percent/amount), like "stop_trailing_pct", or when it is below the "entry price/buy price" by a certain margin (percent/amount)?

I tried stop_trailing in following code, however, it seems it doesn't work, there is no trade been created, am I missing anything here?

def buy_with_trailing_stop_loss_and_profit(ctx):
    if not ctx.long_pos():
        ctx.buy_shares = ctx.calc_target_shares(1)
    elif ctx.long_pos():
        ctx.cancel_stops(ctx.symbol)
        # I guess stop_trailing setting a point where below the highest market price, so I put a 2 times of ATR here.
        # BTW, I used talib caculated ATR
        ctx.stop_trailing = ctx.indicator('atr_100')[-1] * 2

the result.metrics_df:
name value
0 trade_count 0.00
1 initial_market_value 100000000.00
2 end_market_value 385076252.89
3 total_pnl 0.00
4 unrealized_pnl 285076252.89
5 total_return_pct 0.00
6 total_profit 0.00
7 total_loss 0.00
8 total_fees 0.00
9 max_drawdown -429843907.78
10 max_drawdown_pct -72.30
11 win_rate 0.00
12 loss_rate 0.00
13 winning_trades 0.00
14 losing_trades 0.00
15 avg_pnl 0.00
16 avg_return_pct 0.00
17 avg_trade_bars 0.00
18 avg_profit 0.00
19 avg_profit_pct 0.00
20 avg_winning_trade_bars 0.00
21 avg_loss 0.00
22 avg_loss_pct 0.00
23 avg_losing_trade_bars 0.00
24 largest_win 0.00
25 largest_win_pct 0.00
26 largest_win_bars 0.00
27 largest_loss 0.00
28 largest_loss_pct 0.00
29 largest_loss_bars 0.00
30 max_wins 0.00
31 max_losses 0.00
32 sharpe 0.01
33 profit_factor 1.03
34 ulcer_index 4.87
35 upi 0.01
36 equity_r2 0.44
37 std_error 112812652.60

@edtechre
Copy link
Owner

edtechre commented Jun 2, 2023

Hi @none2003,

The field stop_trailing will behave just like stop_trailing_pct, except the units are set in absolute price points.

The reason the code isn't working is that the stop is created when the entry is created (i.e., ctx.buy_shares) and cannot be updated afterwards because each stop only applies to its associated entry.

To support your case of dynamically updating the stop, I can add support for setting a Callable that would return the points amount based on the current context (e.g., ATR).

In the meantime, you can determine if a stop is hit by checking ctx.long_pos() and ctx.high. Additionally, you can then update and save the stop threshold in ctx.session to achieve the trailing behavior.

@edtechre
Copy link
Owner

edtechre commented Jun 3, 2023

After considering this further, I have come to the conclusion that introducing support for dynamic stop values using Callables would make the API confusing. The desired behavior can already be achieved using the execution function. Because stop thresholds are intended to be static, I have implemented checks to prevent your example from failing silently. Instead, it will now throw an error.

To achieve what you are after, you can reference this code:

import pybroker as pyb
import talib

def exec_fn(ctx):
    if ctx.long_pos():
        stop = ctx.session.setdefault(
            "stop",
            ctx.high[-1] - ctx.indicator("atr_100")[-1] * 2
        )
        if ctx.low[-1] <= stop:
            ctx.sell_all_shares()
            del ctx.session["stop"]
        else:
            ctx.session["stop"] = max(
                stop,
                ctx.high[-1] - ctx.indicator("atr_100")[-1] * 2, 
            )
    else:
        ctx.buy_shares = ctx.calc_target_shares(1)
    
atr_100 = pyb.indicator("atr_100", lambda data: talib.ATR(data.high, data.low, data.close, timeperiod=100))
strategy.clear_executions()
strategy.add_execution(exec_fn, ['TSLA'], indicators=atr_100)
result = strategy.backtest(warmup=100)

Let me know if you need anything else!

@edtechre edtechre closed this as completed Jun 3, 2023
@none2003
Copy link
Author

none2003 commented Jun 3, 2023

Hi @edtechre

I tried your code, but it still doesn't work, no trade happens.
So I made some changes to your code based on my understanding of ATR Trailing Stop.
I list code below, in case anyone needs it.

def exec_fn(ctx: ExecContext):
    if ctx.long_pos():
        stop = ctx.session.setdefault(
            "stop",
            ctx.close[-1] - ctx.indicator("atr_100")[-1] * 2
        )
        stop = max(
            stop,
            ctx.close[-1] - ctx.indicator("atr_100")[-1] * 2,
        )
        if ctx.low[-1] <= stop:
            ctx.sell_all_shares()
            del ctx.session["stop"]
        else:
            ctx.session["stop"] = stop
    elif ctx.short_pos():
        stop = ctx.session.setdefault(
            "stop",
            ctx.close[-1] + ctx.indicator("atr_100")[-1] * 2
        )
        stop = min(
            stop,
            ctx.close[-1] + ctx.indicator("atr_100")[-1] * 2,
        )
        if ctx.high[-1] >= stop:
            ctx.cover_all_shares()
            del ctx.session["stop"]
        else:
            ctx.session["stop"] = stop
    else:
        # some buy/long or sell/short code here
        pass

@edtechre
Copy link
Owner

edtechre commented Jun 3, 2023

Whether a trade is made is going to depend on the data you're using for the backtest. My example uses the daily high price instead of close for updating the stop, and it placed trades for my dataset. But glad you have something working, thanks for sharing!

@none2003
Copy link
Author

After considering this further, I have come to the conclusion that introducing support for dynamic stop values using Callables would make the API confusing. The desired behavior can already be achieved using the execution function. Because stop thresholds are intended to be static, I have implemented checks to prevent your example from failing silently. Instead, it will now throw an error.

To achieve what you are after, you can reference this code:

import pybroker as pyb
import talib

def exec_fn(ctx):
    if ctx.long_pos():
        stop = ctx.session.setdefault(
            "stop",
            ctx.high[-1] - ctx.indicator("atr_100")[-1] * 2
        )
        if ctx.low[-1] <= stop:
            ctx.sell_all_shares()
            del ctx.session["stop"]
        else:
            ctx.session["stop"] = max(
                stop,
                ctx.high[-1] - ctx.indicator("atr_100")[-1] * 2, 
            )
    else:
        ctx.buy_shares = ctx.calc_target_shares(1)
    
atr_100 = pyb.indicator("atr_100", lambda data: talib.ATR(data.high, data.low, data.close, timeperiod=100))
strategy.clear_executions()
strategy.add_execution(exec_fn, ['TSLA'], indicators=atr_100)
result = strategy.backtest(warmup=100)

Let me know if you need anything else!

@edtechre,

I have a question about the timing of the trailing stop trigger in pyb. If ctx.buy_shares is executed along with ctx.stop_trailing_pct.

My understanding is that pyb calculates a stop loss level for each bar, so question 1, this stop loss level is calculated based on what price minus stop_trailing_pct? high[-1]?

Once the price of a bar falls below this stop level, then the stop loss will be triggered, so question 2, which of the ohlc specifically falls below the stop level and triggers the stop loss?

Question 3, after the stop is triggered, is the timing for the order to be fulfilled at the moment it is triggered, or at the next bar? This is related to the code you provided above, in your code, the trailing stop order will be fulfilled at the next bar, rather than at the moment stop loss was trigged.

Question 4, if it is at the next bar, which of the ohlc will be the price to fulfill tailing stop order?

@edtechre
Copy link
Owner

edtechre commented May 24, 2024

Hi @none2003,

My understanding is that pyb calculates a stop loss level for each bar, so question 1, this stop loss level is calculated based on what price minus stop_trailing_pct? high[-1]?

Yes, high for long positions, and low for short positions.

Once the price of a bar falls below this stop level, then the stop loss will be triggered, so question 2, which of the ohlc specifically falls below the stop level and triggers the stop loss?

Stop losses on long positions will check if the stop has fallen below the current bar's low price, high for short positions.

Question 3, after the stop is triggered, is the timing for the order to be fulfilled at the moment it is triggered, or at the next bar? This is related to the code you provided above, in your code, the trailing stop order will be fulfilled at the next bar, rather than at the moment stop loss was trigged.

Using the built-in stops, the stop will be filled on the current bar when it is triggered, and will be filled at the stop value. See the _trigger_stop method in portfolio.py: https://github.com/edtechre/pybroker/blob/master/src/pybroker/portfolio.py#L1171

You could simulate the same fill price by setting ctx.sell_fill_price.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants