 # 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()
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)

# 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

# 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 == y]

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

return pnl_list``````

Oh — didn’t realize you meant like — past orders.
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
dfProfit['profit'] = ''

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

for index, row in dfSel.iterrows():
# show data row

# 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

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:
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(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('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)

# Filter out any stocks where the buy and sell quantities don't sum to 0

# Finally, group by stock and sum the net_trade amounts
``````

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
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’.