163 lines
6.0 KiB
Python
163 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Created on Wed Feb 5 21:56:42 2020
|
|
|
|
@author: cpan
|
|
"""
|
|
|
|
"""MC2-P1: Market simulator."""
|
|
|
|
import pandas as pd
|
|
import numpy as np
|
|
# import datetime as dt
|
|
# import os
|
|
from util import get_data, plot_data
|
|
|
|
def normalize_data(df):
|
|
return df/df.iloc[0,:]
|
|
|
|
def fill_missing_values(df_data):
|
|
'''First fill forward and then fill backward'''
|
|
df_data.fillna(method="ffill", inplace=True)
|
|
df_data.fillna(method="bfill", inplace=True)
|
|
|
|
def get_orders(orders_file):
|
|
if isinstance(orders_file, pd.DataFrame):
|
|
# orders_df = orders_file.set_index('Date')
|
|
orders_df = orders_file
|
|
else:
|
|
orders_df = pd.read_csv(orders_file, index_col = 'Date', parse_dates = True,
|
|
na_values = ['nan'])
|
|
orders_df = orders_df.dropna()
|
|
orders_df = orders_df.sort_index()
|
|
return orders_df
|
|
|
|
def compute_daily_returns(df):
|
|
daily_returns = df.copy()
|
|
daily_returns[1:] = (df[1:] / df[:-1].values) - 1
|
|
daily_returns.iloc[0, :] = 0
|
|
return daily_returns
|
|
|
|
def compute_portfolio_stats(price, allocs=[0.1,0.2,0,3,0.4], rfr=0.0, sf=252.0):
|
|
norm_price = normalize_data(price)
|
|
norm_positions_val = norm_price * allocs
|
|
if len(norm_positions_val.columns) == 1:
|
|
norm_portfolio_val = norm_positions_val
|
|
else:
|
|
norm_portfolio_val = norm_positions_val.sum(axis=1).to_frame('PORTFOLIO')
|
|
cr = norm_portfolio_val.iloc[-1] / norm_portfolio_val.iloc[0] -1
|
|
daily_returns = compute_daily_returns(norm_portfolio_val)
|
|
daily_returns = daily_returns[1:] # remove first row (all zeros)
|
|
adr = daily_returns.mean()
|
|
sddr = daily_returns.std()
|
|
sr = np.sqrt(sf) * (adr - rfr)/sddr
|
|
return cr, adr, sddr, sr
|
|
|
|
def plot_against_SPY(df):
|
|
df_temp = df.copy()
|
|
if 'SPY' not in df_temp.columns:
|
|
df_SPY = get_data(['SPY'], pd.to_datetime(df_temp.index.values))
|
|
df_temp['SPY'] = df_SPY.values
|
|
else:
|
|
df_SPY = df_temp['SPY']
|
|
df_temp = normalize_data(df_temp)
|
|
plot_data(df_temp)
|
|
return df_SPY
|
|
|
|
def compute_portvals(orders_file = "./orders/orders.csv", start_val = 1000000,
|
|
commission=9.95, impact=0.005):
|
|
#read in order data
|
|
orders_df = get_orders(orders_file)
|
|
#scan symbols
|
|
symbols = list(set(orders_df['Symbol'].values))
|
|
#get date range
|
|
dates = pd.date_range(orders_df.index.values[0], orders_df.index.values[-1])
|
|
#read in prices
|
|
prices = get_data(symbols, dates)
|
|
# fill_missing_values(prices) # included in get_data() function
|
|
prices = prices[symbols]
|
|
#add an extra column 'Cash' and initialize it to all ones
|
|
prices['Cash'] = np.ones(prices.shape[0])
|
|
|
|
#duplicate price df into a units df, intialize it to all zeros
|
|
positions=prices*0.0
|
|
#initialize cash position with starting value
|
|
positions.loc[positions.index.values[0],['Cash']]=start_val
|
|
|
|
#adjust positions to show how stock units and cash are changing over time with orders
|
|
for index, row in orders_df.iterrows():
|
|
stock_sym = row['Symbol']
|
|
order_price = prices.loc[index, stock_sym]
|
|
order_shrs = row['Shares']
|
|
|
|
if row['Order'].upper() == 'BUY':
|
|
if positions.loc[index, 'Cash'] < order_shrs*order_price +\
|
|
commission + order_shrs*order_price*impact:
|
|
# print('Not enough cash to excute the order:\n', row)
|
|
pass
|
|
else:
|
|
#update positions on transaction days
|
|
positions.loc[index, stock_sym] += order_shrs
|
|
positions.loc[index, "Cash"] -= order_shrs*order_price
|
|
#deduct commission
|
|
positions.loc[index,"Cash"] -= commission
|
|
#impact = no. of orders in transaction * price of each share * impact.
|
|
positions.loc[index,"Cash"] -= order_shrs*order_price*impact
|
|
elif row['Order'].upper() == 'SELL':
|
|
if positions.loc[index, stock_sym] < order_shrs:
|
|
# print('Not enough shares to sell to fill the order:\n', row)
|
|
pass
|
|
else:
|
|
positions.loc[index, stock_sym] -= order_shrs
|
|
positions.loc[index, "Cash"] += order_shrs*order_price
|
|
#deduct commission
|
|
positions.loc[index,"Cash"] -= commission
|
|
#impact = no. of orders in transaction * price of each share * impact.
|
|
positions.loc[index,"Cash"] -= order_shrs*order_price*impact
|
|
|
|
# propagate positions beyond transaction days
|
|
start_row = positions.index.get_loc(index) + 1
|
|
positions.iloc[start_row:, :] = positions.iloc[start_row-1].values
|
|
|
|
#calculate port_vals
|
|
port_vals=prices*positions
|
|
port_vals.insert(0, 'Portfolio', port_vals.sum(axis=1))
|
|
|
|
return port_vals
|
|
|
|
def test_code():
|
|
of = "./orders/orders-05.csv"
|
|
sv = 1000000
|
|
|
|
# Process orders
|
|
portvals = compute_portvals(orders_file = of, start_val = sv)
|
|
if isinstance(portvals, pd.DataFrame):
|
|
portvals = portvals[portvals.columns[0]].to_frame() # just get the first column
|
|
else:
|
|
print("warning, code did not return a DataFrame")
|
|
|
|
# Get portfolio stats
|
|
start_date = pd.to_datetime(portvals.index.values[0])
|
|
end_date = pd.to_datetime(portvals.index.values[-1])
|
|
price_SPY = plot_against_SPY(portvals)
|
|
#portfolio stats calculated similar to assess_portfolio
|
|
rfr=0
|
|
sf=252
|
|
|
|
cr, adr, sddr, sr = compute_portfolio_stats(portvals, [1.0], rfr, sf)
|
|
crSP,adrSP,sddrSP,srSP = compute_portfolio_stats(price_SPY, [1.0], rfr, sf)
|
|
# Compare portfolio against $SPX
|
|
print("\nDate Range: {} to {}".format(start_date.date(), end_date.date()))
|
|
print()
|
|
print("Sharpe Ratio: {}, {}".format(sr, srSP))
|
|
print()
|
|
print("Cumulative Return: {}, {}".format(cr, crSP))
|
|
print()
|
|
print("Standard Deviation: {}, {}".format(sddr, sddrSP))
|
|
print()
|
|
print("Average Daily Return: {}, {}".format(adr, adrSP))
|
|
print()
|
|
print("Final Portfolio Value: {:.2f}".format(portvals['Portfolio'].iloc[-1]))
|
|
|
|
if __name__ == "__main__":
|
|
test_code() |