Easy way to calculate Profit per trade in python

I’m requesting some help with being able to calculate the profit per trade in Python. I’m trying to do it with the info in get_activities() but it seems to be more complicated than I was expecting. Anybody have some code?

try php its less of a headach

Do you mean like — the avg trade ? Or simply the profit? I would look at portfolio / position object, I have a trailstop class that tracks unrealized PNL using the api.list_positions() object, and looping through the positions.
Ex:
avgTrade = sum([pos.realized_pl for pos in api.list_positions()]) / len(api.list_positions())
Written out —
totalPNL = 0
for pos in api.list_positions():
realPNL = pos.realized_pl
totalPNL += realPNL

n_positions = len(api.list_positions)
avg = totalPNL/n_positions

Could also take portfolio realized pl, divide it by orders that match the time period your measuring, etc. Lots of ways to do this.

Thank you. That’s not quite what I was looking for. I was looking for an easy way to list how much profit was made on each individual trade . For example:

I bought MSFT for $100 6/1, sold MSFT for $105 6/5 for $5.00 profit.
I bought AAPL for $90 6/1, sold AAPL for $100 6/5 for $10.00 profit. etc

It seems you have to loop through the api.get_activities() in order to first figure out how much you had sold it for, then loop through again to see what the initial investment was and return the difference. I’ll post what I came up with so far.

1 Like

This is what I came up with so far.

def get_profit_per_stock():
    activities = api.get_activities()
    buys = []
    sells = []
    pnl_list = []

    for each in activities:
        symbol = each.symbol

        # Create list of all the buys
        if each.symbol == symbol and each.side == 'buy':
            cash_spent = float(each.price) * float(each.qty)
            buys.append((each.symbol, cash_spent))

        # Create list of sells
        if each.symbol == symbol and each.side == 'sell':
            cash_taken = float(each.price) * float(each.qty)
            sells.append((each.symbol, cash_taken))

        # In case there are partial fills in the buys list this will merge them
        d = {x: 0 for x, _ in buys}
        for name, num in buys: d[name] += num
        merged_buys = list(map(tuple, d.items()))

        # In case there are partial fills in the sells list this will merge them
        e = {x: 0 for x, _ in sells}
        for name, num in sells: e[name] += num
        merged_sells = list(map(tuple, e.items()))

        # This is a list of tuples listing (symbol, total money spent on the buys, total money spent on the sells)
        transaction_list = [x + y[1:] for x in merged_sells for y in merged_buys if x[0] == y[0]]

        # This simply gives you the Profit/Loss for each symbol in the transaction list
        for each in transaction_list:
            symbol = each[0]
            pnl = round(float(each[1]) - float(each[2]), 2)
            pnl_list.append([symbol, pnl])

        return pnl_list

Oh — didn’t realize you meant like — past orders.
Sorry about that.
I still think you could loop through orders, and match symbols / orders that way — or still possibly use the unrealized_pl tags.
It’s not something I’ve done tho – I’ll give it a quick try.

Your method looks like it would work, (don’t quite follow the partials aspect ? Why would they be different, wouldn’t it just be the position qty same as others?) —
It just seems like there’s got to be an easier way to do this using the orders or positions objects.
Problem is the positions clear after their closed — so my thoughts were may be easier to approximate this while they are open and easy to calculate.

I can’t find much on the get_activities() call / return looking through documentation, happy to take a look if you send a link though.

Another option would be to just basically save the unrealized_pl right before closing it, that’s something I’ve done in the past (not in Alpaca) — either by subtracting mid from avgPrice or calling a sort of pnl builtin. I think I would likely just loop through the positions and mark down last pnl before closing it…
like if you close at end of day, just run it at 2:58 or 2:59 and save them in a dictionary.

My example was from a trailing stop, so right when it triggered, I would just save the last PNL value as realized – but this is more of an approximation.

now = dt.datetime.now()
if now.hour == 14 and now.minute >= 55:
    pnl_by_symbol = defaultdict(float)
    for pos in api.list_positions():
        pnl_by_symbol[pos.symbol] = pos.unrealized_pl
        #close position logic...

or could use the order object, and get a bit messy with matching orders hopefully by time, qty, opp sides, etc. Would be easier if all exits were stop orders, btw – don’t know if that’s an option.

`#Confirm status is what you want + date is restrictive to relevant orders
 orders = self.api.list_orders(status='filled')`

 for ord in orders:
     sym = ord.symbol
     qty = ord.qty
     avgPrice = ord.filled_avg_price
     match = [order for order if order.symbol == sym and order.qty = qty and order.side != ord.side]
     #Match is closing order
     if match.filled_at > ord.filled_at:
          pnl = (match.filled_avg_price - ord.filled_avg_price) * qty
     else:
         pnl = (ord.filled_avg_price - match.filled_avg_price) * qty
     pnl_dict[ord.symbol] = pnl
     #Hoping you only have 1 position per symbol per day -- if not would sort by time, match qty.

Sorry – don’t know if this is helpful, but this is how I would approach it – just seems easier to me than what you are attempting, but maybe I’m missing the point.

Hi,
I have written a function to generate a trade report with profits with every trade.
I used PANDAS and my orders were created with bracket and oto classes.

Please give the following a try and let me know, it would be good to refine this and detect any bugs.

Thanks.

import pandas as pd
from tabulate import tabulate
import pytz
import datetime as dt
from datetime import date

# pass the API and prevDays (0 for today, 1 since yesterday...)
def report2(api, prevDays):
    #
    # get all closed orders and import them into a dataframe
    #
    orderTotal = 500
    today = dt.date.today() - dt.timedelta(days=prevDays)
    today = dt.datetime.combine(today, dt.datetime.min.time())
    today = today.strftime("%Y-%m-%dT%H:%M:%SZ")
    #print(today)
    orders = api.list_orders(status='all', limit=orderTotal, after=today)
    if not orders:
        return
    dfOrders = pd.DataFrame()
    for o in orders:
        # convert dot notation to dict
        d = vars(o)
        # import dict into dataframe
        df = pd.DataFrame.from_dict(d, orient='index')
        # append to dataframe
        dfOrders = dfOrders.append(df, ignore_index=True)

    # select filled orders with buy or sell
    dfSel = dfOrders
    # choose a subset (use .copy() as we are slicing and to avoid warning)
    dfSel = dfSel[['submitted_at', 'filled_at', 'symbol', 'filled_qty', 'side', 'type', 'filled_avg_price', 'status']].copy()

    # convert filled_at to date
    dfSel['submitted_at'] = pd.to_datetime(dfSel['submitted_at'], format="%Y-%m-%d %H:%M:%S")
    dfSel['filled_at']    = pd.to_datetime(dfSel['filled_at'], format="%Y-%m-%d %H:%M:%S")
    # convert to our timezone
    dfSel['submitted_at'] = dfSel['submitted_at'].dt.tz_convert('America/New_York')
    dfSel['filled_at']    = dfSel['filled_at'].dt.tz_convert('America/New_York')
    # remove millis
    dfSel['submitted_at'] = dfSel['submitted_at'].dt.strftime("%Y-%m-%d %H:%M:%S")
    dfSel['filled_at']    = dfSel['filled_at'].dt.strftime("%Y-%m-%d %H:%M:%S")

    # Sort: https://kanoki.org/2020/01/28/sort-pandas-dataframe-and-series/
    # need to sort in order to perform the proper calculations
    # sort based on the following sequence of types: market then limit, then stop_limit
    dfSel['type'] = pd.Categorical(dfSel['type'], categories=["market", "limit", "stop_limit"])
    # sort first based on symbol, then type as per the list above, then submitted date
    dfSel.sort_values(by=['symbol', 'submitted_at', 'type'], inplace=True, ascending=True)

    # reset index
    dfSel.reset_index(drop=True, inplace=True)
    # drop the 'side' column
    # dfProfit = dfSel.drop('side', 1)
    dfProfit = dfSel
    # add empty 'profit' column
    dfProfit['profit'] = ''

    totalProfit = 0.0
    profitCnt   = 0
    lossCnt     = 0
    slCnt       = 0
    ptCnt       = 0
    trCnt       = 0
    qty         = 0
    profit      = 0
    sign        = {'buy': -1, 'sell': 1}

    # show header row
    #print(tabulate(dfSel[:0], headers='keys', tablefmt='simple', showindex=False))

    for index, row in dfSel.iterrows():
        # show data row
        #print(index, tabulate(dfSel[index:index+1], headers='', tablefmt='plain'))

        # conditions:
        # - buy/sell have the same symbol
        # - a trade is considered if no new/held orders are still open
        # - once qty is 0 a complete trade is confirmed and profit calculated
        # - a filled_avg_price is not None

        if index > 0:
            if dfSel['symbol'][index - 1] != dfSel['symbol'][index]:
                qty    = 0
                profit = 0

        if dfSel['status'][index] == 'held':
            continue
        if dfSel['status'][index] == 'new':
            continue
        if dfSel['filled_avg_price'][index] is None:
            continue
        if dfSel['filled_avg_price'][index] == '':
            continue
        if dfSel['filled_avg_price'][index] == 'None':
            continue

        #print(index, tabulate(dfSel[index:index+1], headers='', tablefmt='plain'))

        side      = dfSel['side'][index]
        filledQty = int(dfSel['filled_qty'][index]) * sign[side]
        qty       = qty + filledQty
        price     = float(dfSel['filled_avg_price'][index])
        pl        = filledQty * price
        profit    = profit + pl

        #print(f"{dfSel['symbol'][index]}: qty {filledQty} price {price} profit {profit:.3f}")

        if qty==0:
            # complete trade
            trCnt = trCnt + 1
            # put the profit in its column
            #dfProfit['profit'][index] = profit
            dfProfit.loc[index, 'profit'] = round(profit, 2)
            totalProfit = totalProfit + profit
            if profit >= 0:
                profitCnt = profitCnt + 1
                if dfSel['type'][index] == 'limit':
                    ptCnt = ptCnt + 1
            else:
                lossCnt = lossCnt + 1
                if dfSel['type'][index] == 'stop_limit':
                    slCnt = slCnt + 1
            profit = 0

    # append the total
    dfProfit.loc["Total", "profit"] = round(totalProfit, 2)  # dfProfit.profit.sum()

    # print profit report
    print(tabulate(dfProfit, headers='keys', tablefmt='simple', showindex=True, floatfmt=".2f"))
    #print(dfProfit.to_string())

    totalCnt = profitCnt + lossCnt
    if totalCnt > 0:
        ratio = profitCnt / totalCnt * 100.0
        print('\nProfits:', profitCnt)
        print('Losses :'  , lossCnt)
        print('Ratio  :'  , f'{ratio:.2f}%')
        print('Trades :'  , trCnt)
        print('Stops  :'  , slCnt)
        print('Targets:'  , ptCnt)
3 Likes

Thats exactly what I was looking for thank you! Looks good so far.

1 Like

@nad, dude, gimme your venmo so i can send you a few bucks. this function helped me out A LOT.

how does this work when orders exceed the 500 limit that api.list_orders can return?

I don’t need any money, just glad to help. We use a lot of open source software, a small contribution is my payback.

1 Like

I fixed some bugs and some minor enhancements, when I have some time I will upload an update.

1 Like

Here’s another way to get just net profit over a given set of trades using pandas.

# Get a list of filled orders. 
# Can also limit the results by date if desired.
activities = api.get_activities(activity_types='FILL')

# Turn the activities list into a dataframe for easier manipulation
activities_df = pd.DataFrame([activity._raw for activity in activities])

# The qty and price fields are strings. 
# Convert them to numeric values for calculations.
activities_df[['price', 'qty']] = activities_df[['price', 'qty']].apply(pd.to_numeric)

# Add columns for net_qty and net_trade. 
activities_df['net_qty'] = np.where(activities_df.side=='buy', activities_df.qty, -activities_df.qty)
activities_df['net_trade'] = -activities_df.net_qty * activities_df.price

# Filter out any stocks where the buy and sell quantities don't sum to 0
net_zero_trades = activities_df.groupby('symbol').filter(lambda trades: sum(trades.net_qty) == 0)

# Finally, group by stock and sum the net_trade amounts 
profit_per_symbol = net_zero_trades.groupby('symbol').net_trade.sum()

The result profit_per_symbol is a pandas series with the stock symbol as the index and the associated net profit. It looks like this

symbol
AAPL      1.08
GDX     -82.62
RING   -169.22
SPY     -64.00
Name: net_trade, dtype: float64

But, there are a few caveats…

First, it sums the net profit for each stock over the entire timeframe requested in get_activities. For example, if one bought IBM then sold it, then bought and sold it again, it gives the net profit across all 4 transactions. It does not break it into individual ‘round trips’.

Second, it only calculates profit for stocks where the net qty of shares is 0. For example, if one bought 10 shares of IBM and sold only 5 shares, it excludes IBM and doesn’t calculate a net profit. This behavior will also cause it to get confused if there’s a stock split.

Hi Nad, I have no experience with coding but is there another way to video tutorial how to use your code? Please I am trying to see my P/L in alpaca and so far it is impossible. Thanks

Nice clean code that works assuming api.get_activities(…) does indeed do what the name of the method claims. I have trading activity going back (according to get_history) to March but can only retrieve 2 days of activity in April. What am I doing wrong?

I have traded hundreds of symbols but your code only reports activity for about 13 of them. All of my trades are net_qty == 0 so that shouldn’t be a problem. The code also shows a net profit which is true for the two days it is reporting on but I certainly don’t have a net profit since inception. I must be overlooking something.