Getting Transactions for Pyfolio from Moonshot Backtest Results?

Is there an easy way to get transactions from a Moonshot backtest that can be fed into Pyfolio to power the pf.create_round_trip_tear_sheet(returns, positions, transactions) method? I don’t see anything in the docs, nor the code, but thought I’d ask just in case.

If not, what’s the best way to go about generating these entry/exit transactions from the backtest results? At a minimum, it looks like we’d need to reference prices used within the backtest along with AbsExposure and detecting when positions go from 0 to something, and then from something to 0, to capture the round trip transactions.

Any tips is appreciated!

Here’s the docs on transactions from the Pyfolio code context:

    transactions : pd.DataFrame, optional
    Executed trade volumes and fill prices.

    - One row per trade.
    - Trades on different names that occur at the
      same time will have identical indicies.
    - Example:
      ::

        index                  amount   price    symbol
        2004-01-09 12:18:01    483      324.12   'AAPL'
        2004-01-09 12:18:01    122      83.10    'MSFT'
        2004-01-13 14:12:23    -75      340.43   'AAPL'

Just wanted to report back that I came up with a somewhat reasonable solution, but hoping there’s a better/simpler way; let me know!

See code below in case it helps others.

First, execute a moonshot backtest using Python and store results in a dataframe:

from quantrocket.moonshot import backtest, read_moonshot_csv

backtest_results = 'backtest_results.csv'
backtest("big-gap", nlv={'USD':50000},
         start_date="2018-01-03", end_date="2018-01-06",
         filepath_or_buffer=backtest_results,
         details=True, no_cache=True)
backtest_results = read_moonshot_csv(backtest_results)

Extract transactions
Note that I added prices and quantities to the backtest results dataframe within the strategy code.

prior_signal = backtest_results.loc['Signal'].shift(1)
current_signal = backtest_results.loc['Signal']
entries = (current_signal != 0) & (prior_signal == 0) # going from no signal to signal i.e. an entry

# Exits still need to be implemented below, but can grab them like this
exits = (current_signal == 0) & (prior_signal != 0) # going from signal to no signal i.e. an exit

Aggregate values for each column

backtest_datetime = backtest_results.loc['Quantity'][entries].dropna(how='all', axis='columns').dropna(how='all').unstack().unstack().unstack().unstack().unstack().dropna().index.map(lambda x : pd.to_datetime(' '.join( (str(x[0].date()), str(x[1])) )))
backtest_sid = backtest_results.loc['Quantity'][entries].dropna(how='all', axis='columns').dropna(how='all').unstack().unstack().unstack().unstack().unstack().dropna().index.get_level_values(2)
backtest_quantity = backtest_results.loc['Quantity'][entries].dropna(how='all', axis='columns').dropna(how='all').unstack().unstack().unstack().unstack().unstack().dropna()
backtest_price = backtest_results.loc['Price'][entries].dropna(how='all', axis='columns').dropna(how='all').unstack().unstack().unstack().unstack().unstack().dropna()
backtest_cost_basis = backtest_results.loc['MarketValue'][entries].dropna(how='all', axis='columns').dropna(how='all').unstack().unstack().unstack().unstack().unstack().dropna()

Construct transactions dataframe

transactions = pd.DataFrame(index=backtest_datetime.tz_localize('UTC'))
transactions['amount'] = backtest_quantity.values
transactions['price'] = backtest_price.values
transactions['symbol'] = backtest_sid.values

Lastly, using Pyfolio for generate a round trip tearsheet

from moonchart.utils import intraday_to_daily
from pyfolio.quantrocket_utils import pad_initial

results = backtest_results

if "Time" in results.index.names:
    results = intraday_to_daily(results)

# pandas DatetimeIndexes are serialized with UTC offsets, and pandas
# parses them back to UTC but doesn't set the tz; pyfolio needs tz-aware
if not results.index.get_level_values("Date").tz:
    results = results.tz_localize("UTC", level="Date")

returns = results.loc["Return"].sum(axis=1)
positions = results.loc["NetExposure"]
positions["cash"] = 1 - positions.sum(axis=1)

returns.name = "returns"
returns = pad_initial(returns)


# Generate tearsheet
warnings.filterwarnings('ignore')
pf.create_round_trip_tear_sheet(returns, positions=positions, transactions=transactions)

Perhaps all the unstacks could be simplified, but yes, you have to do something like this.

Ok cool, just wanted to make sure. I ended up cleaning up my code a bit, and created some helper methods to streamline things, and it’s working nicely with Pyfolio. Thanks Brian!