diff --git a/zipline/finance/performance.py b/zipline/finance/performance.py index 5c930715..66528fea 100644 --- a/zipline/finance/performance.py +++ b/zipline/finance/performance.py @@ -72,6 +72,7 @@ class PerformanceTracker(): self.todays_performance.calculate_performance() def handle_market_close(self): + qutil.LOGGER.debug("###########market close###############") self.market_open = self.market_open + self.calendar_day while not self.trading_environment.is_trading_day(self.market_open): if self.market_open > self.trading_environment.trading_days[-1]: @@ -98,12 +99,13 @@ class PerformanceTracker(): ###################################################################################################### #roll over positions to current day. + self.todays_performance.calculate_performance() self.todays_performance = PerformancePeriod( self.market_open, self.market_close, self.todays_performance.positions, self.todays_performance.ending_value, - self.capital_base + self.todays_performance.ending_cash ) def handle_simulation_end(self): @@ -133,14 +135,18 @@ class Position(): def update(self, txn): if(self.sid != txn.sid): - raise NameError('attempt to update position with transaction in different sid') + raise NameError('updating position with txn for a different sid') #throw exception if(self.amount + txn.amount == 0): #we're covering a short or closing a position self.cost_basis = 0.0 self.amount = 0 else: - self.cost_basis = (self.cost_basis*self.amount + (txn.amount*txn.price))/(self.amount + txn.amount) + prev_cost = self.cost_basis*self.amount + txn_cost = txn.amount*txn.price + total_cost = prev_cost + txn_cost + total_shares = self.amount + txn.amount + self.cost_basis = total_cost/total_shares self.amount = self.amount + txn.amount def currentValue(self): @@ -148,35 +154,40 @@ class Position(): def __repr__(self): - return "sid: {sid}, amount: {amount}, cost_basis: {cost_basis}, last_sale: {last_sale}".format( - sid=self.sid, amount=self.amount, cost_basis=self.cost_basis, last_sale=self.last_sale) + template = "sid: {sid}, amount: {amount}, cost_basis: {cost_basis}, \ + last_sale: {last_sale}" + return template.format( + sid=self.sid, + amount=self.amount, + cost_basis=self.cost_basis, + last_sale=self.last_sale + ) class PerformancePeriod(): - def __init__(self, period_start, period_end, initial_positions, initial_value, capital_base = None): + def __init__(self, initial_positions, starting_value, starting_cash): self.ending_value = 0.0 self.period_capital_used = 0.0 - self.period_start = period_start - self.period_end = period_end self.positions = initial_positions #sid => position object - self.starting_value = initial_value - if(capital_base != None): - self.capital_base = capital_base - else: - self.capital_base = 0 + self.starting_value = starting_value + #cash balance at start of period + self.starting_cash = starting_cash + self.ending_cash = starting_cash def calculate_performance(self): self.ending_value = self.calculate_positions_value() - self.pnl = (self.ending_value - self.starting_value) - self.period_capital_used - if(self.capital_base != 0): - self.returns = self.pnl / self.starting_value + + total_at_start = self.starting_cash + self.starting_value + self.ending_cash = self.starting_cash + self.period_capital_used + total_at_end = self.ending_cash + self.ending_value + + self.pnl = total_at_end - total_at_start + if(total_at_start != 0): + self.returns = self.pnl / total_at_start else: self.returns = 0.0 def execute_transaction(self, txn): - if(txn.dt > self.period_end): - raise Exception("transaction dated {dt} attempted for period ending {ending}". - format(dt=txn.dt, ending=self.period_end)) if(not self.positions.has_key(txn.sid)): self.positions[txn.sid] = Position(txn.sid) self.positions[txn.sid].update(txn) @@ -195,5 +206,4 @@ class PerformancePeriod(): self.positions[event.sid].last_date = event.dt - \ No newline at end of file diff --git a/zipline/finance/risk.py b/zipline/finance/risk.py index 3607ec1d..c865aaa6 100644 --- a/zipline/finance/risk.py +++ b/zipline/finance/risk.py @@ -17,16 +17,18 @@ class daily_return(): return str(self.date) + " - " + str(self.returns) class RiskMetrics(): - def __init__(self, start_date, end_date, returns, benchmark_returns, treasury_curves, trading_calendar): + def __init__(self, start_date, end_date, returns, trading_environment): """ :param treasury_curves: {datetime in utc -> {duration label -> interest rate}} """ - self.treasury_curves = treasury_curves + self.treasury_curves = trading_environment.treasury_curves self.start_date = start_date self.end_date = end_date - self.trading_calendar = trading_calendar + self.trading_environment = trading_environment self.algorithm_period_returns, self.algorithm_returns = self.calculate_period_returns(returns) + benchmark_returns = [x for x in self.trading_environment.benchmark_returns if x.date >= returns[0].date and x.date <= returns[-1].date] + self.benchmark_period_returns, self.benchmark_returns = self.calculate_period_returns(benchmark_returns) if(len(self.benchmark_returns) != len(self.algorithm_returns)): raise Exception("Mismatch between benchmark_returns ({bm_count}) and algorithm_returns ({algo_count}) in range {start} : {end}".format( @@ -53,7 +55,7 @@ class RiskMetrics(): return '\n'.join(statements) def calculate_period_returns(self, daily_returns): - returns = [x.returns for x in daily_returns if x.date >= self.start_date and x.date <= self.end_date and self.trading_calendar.is_trading_day(x.date)] + returns = [x.returns for x in daily_returns if x.date >= self.start_date and x.date <= self.end_date and self.trading_environment.is_trading_day(x.date)] #qutil.LOGGER.debug("using {count} daily returns out of {total}".format(count=len(returns),total=len(daily_returns))) period_returns = 1.0 for r in returns: @@ -165,18 +167,13 @@ class RiskMetrics(): class RiskReport(): - def __init__(self, algorithm_returns, benchmark_returns, treasury_curves, trading_calendar): + def __init__(self, algorithm_returns, benchmark_returns, treasury_curves, trading_environment): """algorithm_returns needs to be a list of daily_return objects sorted in date ascending order""" self.algorithm_returns = algorithm_returns - self.bm_returns = [x for x in benchmark_returns if x.date >= self.algorithm_returns[0].date and x.date <= self.algorithm_returns[-1].date] self.treasury_curves = treasury_curves - self.trading_calendar = trading_calendar + self.trading_environment = trading_environment - qutil.LOGGER.debug("#### {start} thru {end} with {count} trading_days of {total} possible".format(start=self.algorithm_returns[0].date, - end=self.algorithm_returns[-1].date, - count=len(self.bm_returns), - total=len(benchmark_returns))) #calculate month ends self.month_periods = self.periodsInRange(1, self.algorithm_returns[0].date, self.algorithm_returns[-1].date) @@ -206,9 +203,7 @@ class RiskReport(): start_date=cur_start, end_date=cur_end, returns=self.algorithm_returns, - benchmark_returns=self.bm_returns, - treasury_curves=self.treasury_curves, - trading_calendar=self.trading_calendar + trading_environment=self.trading_environment ) ends.append(cur_period_metrics) @@ -244,6 +239,7 @@ class TradingEnvironment(object): self.trading_days = [] self.trading_day_map = {} self.treasury_curves = treasury_curves + self.benchmark_returns = benchmark_returns for bm in benchmark_returns: self.trading_days.append(bm.date) self.trading_day_map[bm.date] = bm diff --git a/zipline/test/factory.py b/zipline/test/factory.py index 68ed322b..4728bb45 100644 --- a/zipline/test/factory.py +++ b/zipline/test/factory.py @@ -23,14 +23,14 @@ def load_market_data(): def create_trade(sid, price, amount, datetime): - row = { + row = zp.namedict({ 'source_id' : "test_factory", 'type' : zp.DATASOURCE_TYPE.TRADE, 'sid' : sid, 'dt' : datetime, 'price' : price, 'volume' : amount - } + }) return row def create_trade_history(sid, prices, amounts, start_time, interval, trading_calendar): @@ -50,19 +50,23 @@ def create_trade_history(sid, prices, amounts, start_time, interval, trading_cal return trades -def createTxn(sid, price, amount, datetime, btrid=None): - txn = Transaction(sid=sid, amount=amount, dt = datetime, - price=price, transaction_cost=-1*price*amount) +def create_txn(sid, price, amount, datetime, btrid=None): + txn = zp.namedict({ + 'sid':sid, + 'amount':amount, + 'dt':datetime, + 'price':price, + }) return txn -def create_transaction_history(sid, priceList, amtList, startTime, interval, trading_calendar): +def create_txn_history(sid, priceList, amtList, startTime, interval, trading_calendar): txns = [] current = startTime for price, amount in zip(priceList, amtList): if trading_calendar.is_trading_day(current): - txns.append(createTxn(sid, price, amount, current)) + txns.append(create_txn(sid, price, amount, current)) current = current + interval else: diff --git a/zipline/test/test_performance.py b/zipline/test/test_performance.py new file mode 100644 index 00000000..1efed392 --- /dev/null +++ b/zipline/test/test_performance.py @@ -0,0 +1,526 @@ +import unittest +import copy +import random +import datetime + +import zipline.test.factory as factory +import zipline.util as qutil +import zipline.protocol as zp +import zipline.finance.performance as perf +import zipline.finance.risk as risk +class PerformanceTestCase(unittest.TestCase): + + def setUp(self): + qutil.configure_logging() + self.benchmark_returns, self.treasury_curves = \ + factory.load_market_data() + + self.trading_environment = risk.TradingEnvironment( + self.benchmark_returns, + self.treasury_curves + ) + + self.onesec = datetime.timedelta(seconds=1) + self.oneday = datetime.timedelta(days=1) + self.tradingday = datetime.timedelta(hours=6, minutes=30) + random_index = random.randint( + 0, + len(self.trading_environment.trading_days) + ) + + self.dt = self.trading_environment.trading_days[random_index] + + def tearDown(self): + pass + + def test_long_position(self): + """ + verify that the performance period calculates properly for a \ +single buy transaction + """ + #post some trades in the market + trades = factory.create_trade_history( + 1, + [10,10,10,11], + [100,100,100,100], + self.dt, + self.onesec, + self.trading_environment + ) + + txn = factory.create_txn(1,10.0,100,self.dt + self.onesec) + pp = perf.PerformancePeriod({}, 0.0, 1000.0) + + pp.execute_transaction(txn) + for trade in trades: + pp.update_last_sale(trade) + + pp.calculate_performance() + + self.assertEqual( + pp.period_capital_used, + -1 * txn.price * txn.amount, + "capital used should be equal to the opposite of the transaction \ + cost of sole txn in test" + ) + + self.assertEqual(len(pp.positions),1,"should be just one position") + + self.assertEqual( + pp.positions[1].sid, + txn.sid, + "position should be in security with id 1") + + self.assertEqual( + pp.positions[1].amount, + txn.amount, + "should have a position of {sharecount} shares".format( + sharecount=txn.amount + ) + ) + + self.assertEqual( + pp.positions[1].cost_basis, + txn.price, + "should have a cost basis of 10" + ) + + self.assertEqual( + pp.positions[1].last_sale, + trades[-1]['price'], + "last sale should be same as last trade. \ + expected {exp} actual {act}".format( + exp=trades[-1]['price'], + act=pp.positions[1].last_sale + ) + ) + + self.assertEqual( + pp.ending_value, + 1100, + "ending value should be price of last trade times number of \ + shares in position" + ) + + self.assertEqual(pp.pnl, 100, "gain of 1 on 100 shares should be 100") + + def test_short_position(self): + """verify that the performance period calculates properly for a \ +single short-sale transaction""" + trades_1 = factory.create_trade_history( + 1, + [10,10,10,11], + [100,100,100,100], + self.dt, + self.onesec, + self.trading_environment + ) + + txn = factory.create_txn(1, 10.0, -100, self.dt + self.onesec) + pp = perf.PerformancePeriod({}, 0.0, 1000.0) + + pp.execute_transaction(txn) + for trade in trades_1: + pp.update_last_sale(trade) + + pp.calculate_performance() + + self.assertEqual( + pp.period_capital_used, + -1 * txn.price * txn.amount, + "capital used should be equal to the opposite of the transaction\ + cost of sole txn in test" + ) + + self.assertEqual( + len(pp.positions), + 1, + "should be just one position") + + self.assertEqual( + pp.positions[1].sid, + txn.sid, + "position should be in security from the transaction" + ) + + self.assertEqual( + pp.positions[1].amount, + -100, + "should have a position of -100 shares" + ) + + self.assertEqual( + pp.positions[1].cost_basis, + txn.price, + "should have a cost basis of 10" + ) + + self.assertEqual( + pp.positions[1].last_sale, + trades_1[-1]['price'], + "last sale should be price of last trade" + ) + + self.assertEqual( + pp.ending_value, + -1100, + "ending value should be price of last trade times number of \ + shares in position" + ) + + self.assertEqual(pp.pnl,-100,"gain of 1 on 100 shares should be 100") + + #simulate additional trades, and ensure that the position value + #reflects the new price + trades_2 = factory.create_trade_history( + 1, + [10,9], + [100,100], + trades_1[-1]['dt'] + self.onesec, + self.onesec, + self.trading_environment + ) + + #simulate a rollover to a new period + pp2 = perf.PerformancePeriod( + pp.positions, + pp.ending_value, + pp.ending_cash + ) + + for trade in trades_2: + pp2.update_last_sale(trade) + + pp2.calculate_performance() + + self.assertEqual( + pp2.period_capital_used, + 0, + "capital used should be zero, there were no transactions in \ + performance period" + ) + + self.assertEqual( + len(pp2.positions), + 1, + "should be just one position" + ) + + self.assertEqual( + pp2.positions[1].sid, + txn.sid, + "position should be in security from the transaction" + ) + + self.assertEqual( + pp2.positions[1].amount, + -100, + "should have a position of -100 shares" + ) + + self.assertEqual( + pp2.positions[1].cost_basis, + txn.price, + "should have a cost basis of 10" + ) + + self.assertEqual( + pp2.positions[1].last_sale, + trades_2[-1].price, + "last sale should be price of last trade" + ) + + self.assertEqual( + pp2.ending_value, + -900, + "ending value should be price of last trade times number of \ + shares in position") + + self.assertEqual( + pp2.pnl, + 200, + "drop of 2 on -100 shares should be 200" + ) + + #now run a performance period encompassing the entire trade sample. + ppTotal = perf.PerformancePeriod({}, 0.0, 1000.0) + + for trade in trades_1: + ppTotal.update_last_sale(trade) + + ppTotal.execute_transaction(txn) + + for trade in trades_2: + ppTotal.update_last_sale(trade) + + ppTotal.calculate_performance() + + self.assertEqual( + ppTotal.period_capital_used, + -1 * txn.price * txn.amount, + "capital used should be equal to the opposite of the transaction \ +cost of sole txn in test" + ) + + self.assertEqual( + len(ppTotal.positions), + 1, + "should be just one position" + ) + self.assertEqual( + ppTotal.positions[1].sid, + txn.sid, + "position should be in security from the transaction" + ) + + self.assertEqual( + ppTotal.positions[1].amount, + -100, + "should have a position of -100 shares" + ) + + self.assertEqual( + ppTotal.positions[1].cost_basis, + txn.price, + "should have a cost basis of 10" + ) + + self.assertEqual( + ppTotal.positions[1].last_sale, + trades_2[-1].price, + "last sale should be price of last trade" + ) + + self.assertEqual( + ppTotal.ending_value, + -900, + "ending value should be price of last trade times number of \ + shares in position") + + self.assertEqual( + ppTotal.pnl, + 100, + "drop of 1 on -100 shares should be 100" + ) + + def test_covering_short(self): + """verify performance where short is bought and covered, and shares \ +trade after cover""" + + trades = factory.create_trade_history( + 1, + [10,10,10,11,9,8,7,8,9,10], + [100,100,100,100,100,100,100,100,100,100], + self.dt, + self.onesec, + self.trading_environment + ) + + short_txn = factory.create_txn( + 1, + 10.0, + -100, + self.dt + self.onesec + ) + + cover_txn = factory.create_txn(1,7.0,100,self.dt + self.onesec * 6) + pp = perf.PerformancePeriod({}, 0.0, 1000.0) + + pp.execute_transaction(short_txn) + pp.execute_transaction(cover_txn) + + for trade in trades: + pp.update_last_sale(trade) + + pp.calculate_performance() + + short_txn_cost = short_txn.price * short_txn.amount + cover_txn_cost = cover_txn.price * cover_txn.amount + + self.assertEqual( + pp.period_capital_used, + -1 * short_txn_cost - cover_txn_cost, + "capital used should be equal to the net transaction costs" + ) + + self.assertEqual( + len(pp.positions), + 1, + "should be just one position" + ) + + self.assertEqual( + pp.positions[1].sid, + short_txn.sid, + "position should be in security from the transaction" + ) + + self.assertEqual( + pp.positions[1].amount, + 0, + "should have a position of -100 shares" + ) + + self.assertEqual( + pp.positions[1].cost_basis, + 0, + "a covered position should have a cost basis of 0" + ) + + self.assertEqual( + pp.positions[1].last_sale, + trades[-1].price, + "last sale should be price of last trade" + ) + + self.assertEqual( + pp.ending_value, + 0, + "ending value should be price of last trade times number of \ +shares in position" + ) + + self.assertEqual( + pp.pnl, + 300, + "gain of 1 on 100 shares should be 300" + ) + + def test_cost_basis_calc(self): + trades = factory.create_trade_history( + 1, + [10,11,11,12], + [100,100,100,100], + self.dt, + self.onesec, + self.trading_environment + ) + + transactions = factory.create_txn_history( + 1, + [10,11,11,12], + [100,100,100,100], + self.dt, + self.onesec, + self.trading_environment + ) + + pp = perf.PerformancePeriod({}, 0.0, 1000.0) + + for txn in transactions: + pp.execute_transaction(txn) + + for trade in trades: + pp.update_last_sale(trade) + + pp.calculate_performance() + + self.assertEqual( + pp.positions[1].last_sale, + trades[-1].price, + "should have a last sale of 12, got {val}".format( + val=pp.positions[1].last_sale + ) + ) + + self.assertEqual( + pp.positions[1].cost_basis, + 11, + "should have a cost basis of 11" + ) + + self.assertEqual( + pp.pnl, + 400 + ) + + saleTxn = factory.create_txn( + 1, + 10.0, + -100, + self.dt + self.onesec * 4) + + down_tick = factory.create_trade( + 1, + 10.0, + 100, + trades[-1].dt + self.onesec) + + pp2 = perf.PerformancePeriod( + copy.deepcopy(pp.positions), + pp.ending_value, + pp.ending_cash + ) + + pp2.execute_transaction(saleTxn) + pp2.update_last_sale(down_tick) + + pp2.calculate_performance() + self.assertEqual( + pp2.positions[1].last_sale, + 10, + "should have a last sale of 10, was {val}".format(val=pp2.positions[1].last_sale) + ) + + self.assertEqual( + round(pp2.positions[1].cost_basis,2), + 11.33, + "should have a cost basis of 11.33" + ) + + #print "second period pnl is {pnl}".format(pnl=pp2.pnl) + self.assertEqual(pp2.pnl, -800, "this period goes from +400 to -400") + + pp3 = perf.PerformancePeriod({}, 0.0, 1000.0) + + transactions.append(saleTxn) + for txn in transactions: + pp3.execute_transaction(txn) + + trades.append(down_tick) + for trade in trades: + pp3.update_last_sale(trade) + + pp3.calculate_performance() + self.assertEqual( + pp3.positions[1].last_sale, + 10, + "should have a last sale of 10" + ) + + self.assertEqual( + round(pp3.positions[1].cost_basis,2), + 11.33, + "should have a cost basis of 11.33" + ) + + self.assertEqual( + pp3.pnl, + -400, + "should be -400 for all trades and transactions in period" + ) + + + def dtest_daily_performance_calc(self): + hostedAlgo = factories.createAlgo("workingAlgo.py") + btRecord = BackTestRun(duration_unit="Days",duration_count=5,capital_base=25000000) + bt = BackTest(hostedAlgo,btRecord) + start = bt.periodStart + end = bt.periodEnd + #print "{start} to {end}".format(start=start, end=end) + + trades = factories.createTradeHistory(1,[10,11,12,11],[100,100,100,100],start, self.oneday) + #createTransaction(self, sid, amount, price, dt, order_id) + bt.createTransaction(1, 100, 10.0, trades[0].dt + 30*self.onesec, None) + curPeriod = start + bt.positions = {} + dailyPeriods = [] + bt.initialValue = 0.0 + while (bt.mktClose) <= bt.periodEnd: + bt.updatePerformance() + dailyPeriods.append(bt.curPeriod) + bt.nextMarketDay() + + self.assertEqual(dailyPeriods[0].pnl,0,"the first day's performance should be zero") + self.assertEqual(dailyPeriods[1].pnl,100,"the second day's pnl should be 100 but was {pnl}".format(pnl=dailyPeriods[1].pnl)) + \ No newline at end of file