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

Question about shares and pnl number in "result.trades" #80

Closed
none2003 opened this issue Dec 1, 2023 · 8 comments
Closed

Question about shares and pnl number in "result.trades" #80

none2003 opened this issue Dec 1, 2023 · 8 comments

Comments

@none2003
Copy link

none2003 commented Dec 1, 2023

Hi @edtechre,

I noticed a weirdness about some number in result.trades.

Steps to reproduce:

  1. I made the following settings:
config = StrategyConfig(
    initial_cash=100000000,
    fee_mode=FeeMode.ORDER_PERCENT,
    fee_amount=0.0,
    bars_per_year=252
)
  1. During trading:
# to buy
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.buy_fill_price = PriceType.OPEN

# to sell
ctx.sell_all_shares()
ctx.sell_fill_price = PriceType.OPEN
  1. backtesting...

The first two rows of "result.trades" look like this:
image
The first shares value is 112694 rather than 100000000 / 887.064(entry price) = 112731.

Personally, I think 112731 is right.

And even shares value of 112694 were correct, shouldn't the pnl be 112694 * 1012.578(exit price) - 100000000 = 14111465.13?

Ideally, I think pnl should be 112731 * 1012.578 - 100000000 = 14148930.52

I did some digging in source code, and I found this:

    def calc_target_shares(
        self,
        target_size: float,
        price: Optional[float] = None,
        cash: Optional[float] = None,
    ) -> int:
        price = self.close[-1] if price is None else price
        return super().calc_target_shares(target_size, price, cash)

It seems the execution price when buying is the price of last bar, even "ctx.buy_fill_price = PriceType.OPEN" has been set. Is this intentional?

I apologize for not having enough time to find the code that calculating pnl.

@edtechre
Copy link
Owner

edtechre commented Dec 5, 2023

It seems the execution price when buying is the price of last bar, even "ctx.buy_fill_price = PriceType.OPEN" has been set. Is this intentional?

Yes, this is intentional since calc_target_shares is generic and can be used to calculate buy_shares or sell_shares. You will need to pass in the previous day's open price using self.open[-1] if you intend to use the open price.

@edtechre edtechre closed this as completed Dec 5, 2023
@none2003
Copy link
Author

none2003 commented Dec 5, 2023

Hi @edtechre,

Well, my intention is:

I want to buy or sell a security tomorrow at the open price, once I found a calculated signal based on today's close price appears.

For example, on 2023/12/5, I'm backtesting a strategy on a security data from 2008/1/1 to 2008/3/1, now I found close price is cross up MA20 on 2008/1/10, and I want to buy this security at the open price of very next day (2008/1/11).

How would I do that?

And what's the point show the shares/pnl which doesn't match with the entry/exit price in the result.trades?

There is another question:
If I have to pass the self.open[-1] to calc_target_shares manually, what's purpose of the setting in doc: ctx.buy_fill_price = PriceType.OPEN and ctx.buy_fill_price = PriceType.OPEN?

@edtechre edtechre reopened this Dec 10, 2023
@edtechre
Copy link
Owner

Hi @none2003,

Sorry that I did not previously address your concerns. I misunderstood your original posts and it seems there are two different issues here:

The first shares value is 112694 rather than 100000000 / 887.064(entry price) = 112731.

Personally, I think 112731 is right.

And even shares value of 112694 were correct, shouldn't the pnl be 112694 * 1012.578(exit price) - 100000000 = 14111465.13

I believe the discrepancy is due to rounding. My suspicion is that the entry price is not 887.064 but is some some other floating point value. Note that the values in result.trades are rounded up to the nearest cent before they are displayed.

If you can email me your data with the symbols anonymized, I can take a look and verify this.

I want to buy or sell a security tomorrow at the open price, once I found a calculated signal based on today's close price appears.

This is done by setting ctx.buy_fill_price = PriceType.OPEN.

If I have to pass the self.open[-1] to calc_target_shares manually, what's purpose of the setting in doc: ctx.buy_fill_price = PriceType.OPEN and ctx.buy_fill_price = PriceType.OPEN?

They serve different purposes, calc_target_shares is a helper function for calculating the number of shares to buy or sell. Passing in self.open[-1] will calculate the number of shares based on the last completed bar's open price. Setting ctx.buy_fill_price = PriceType.OPEN will set the fill price to the open price of the next bar.

It sounds like you want to calculate the number of shares (i.e. 20% of your portfolio) to a symbol using the next bar's open price. But this is not supported because it would introduce lookahead bias into your strategy, and would likely be unrealistic to implement in a real strategy anyway due to slippage.

@none2003
Copy link
Author

issue#80.zip

I uploaded an example notebook. This strategy is simple:

  • if no long position and close price > ema_20, then buy ^SPX with all the cash at next bar's open price;
  • if long position exists and close price < ema_20, then sell all the long position at next bar's open price.

The very first buy order is filled with open price 2645.10 of 2017-12-01, this is consistent with the settings (ctx.buy_fill_price = PriceType.OPEN). However, the "shares" number is 37770, I think it should be 100000000 / 2645.10009765625 = 37805.

The following sell order is filled with open price 2741.06 of 2018-02-05, this is also consistent with the settings (ctx.sell_fill_price = PriceType.OPEN). Let's assume share number 37770 were right, the pnl should be 37770 * 2741.06005859375 - 100000000 = 3529838.4131. And if using share number 37805, that will be 37805 * 2741.06005859375 - 100000000 = 3625775.5151. But in result.order, it gives 3624407.72, largely differs with both numbers above given.

@edtechre
Copy link
Owner

Thank you for providing a notebook. However, I will need data to be able to debug.

Let's assume share number 37770 were right, the pnl should be 37770 * 2741.06005859375 - 100000000

I am not sure where 100000000 comes from. The PnL is not calculated using your initial cash, it is calculated as the PnL from entry to exit:

entry_amount = shares * entry_price
exit_amount = shares * exit_price
entry_pnl = exit_amount - entry_amount

If config.enable_fractional_shares was not set to true, then not all of your initial cash (100000000) will be invested since only whole shares will be traded.

@none2003
Copy link
Author

Please download and check the notebook, I used Yfinance to get "^SPX" data live inside notebook, like pyb example notebooks.

@edtechre
Copy link
Owner

The calculations there look correct to me and is consistent with my explanation of calc_target_shares and buy_fill_price above.

ctx.buy_shares = ctx.calc_target_shares(1)

By default, this uses the close price of the last completed bar. It is never the fill price of the next bar, because that would introduce lookahead bias. You can't calculate the number of shares to buy based on a future price you haven't seen yet.

For the first trade, close price = 2647.580078125
100000000 // 2647.580078125 = 37770 shares

Then the actual open price is 2645.100098. The exit price is 2741.060059.
PnL = (2741.060059 - 2645.100098) * 37770 = ~3624408e6

This is consistent with the dataframe before the values are quantized by rounding up to the nearest cent.

@none2003
Copy link
Author

OK, I see. Thank you for the detailed explanation.

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