├── odin ├── handlers │ ├── __init__.py │ ├── position_handler │ │ ├── __init__.py │ │ ├── position │ │ │ ├── __init__.py │ │ │ ├── pending_position.py │ │ │ └── filled_position.py │ │ ├── templates │ │ │ ├── __init__.py │ │ │ └── suggested_proportion_position_handler.py │ │ └── abstract_position_handler.py │ ├── fund_handler │ │ ├── __init__.py │ │ └── fund_handler.py │ ├── portfolio_handler │ │ ├── __init__.py │ │ └── portfolio_handler.py │ ├── data_handler │ │ ├── __init__.py │ │ ├── database │ │ │ ├── __init__.py │ │ │ ├── interactive_brokers_data_handler.py │ │ │ ├── database_data_handler.py │ │ │ └── abstract_database_data_handler.py │ │ ├── price_handler │ │ │ ├── __init__.py │ │ │ ├── abstract_price_handler.py │ │ │ ├── database_price_handler.py │ │ │ └── interactive_brokers_price_handler.py │ │ └── abstract_data_handler.py │ ├── symbol_handler │ │ ├── __init__.py │ │ ├── fixed_symbol_handler.py │ │ ├── dollar_volume_symbol_handler.py │ │ └── abstract_symbol_handler.py │ └── execution_handler │ │ ├── __init__.py │ │ ├── abstract_execution_handler.py │ │ ├── simulated_execution_handler.py │ │ └── interactive_brokers_execution_handler.py ├── __init__.py ├── strategy │ ├── __init__.py │ ├── templates │ │ ├── __init__.py │ │ └── buy_and_hold_strategy.py │ ├── indicators │ │ ├── __init__.py │ │ ├── moving_average.py │ │ └── williams.py │ └── abstract_strategy.py ├── utilities │ ├── finance │ │ ├── __init__.py │ │ ├── modern_portfolio_theory │ │ │ ├── __init__.py │ │ │ ├── black_litterman.py │ │ │ └── markowitz.py │ │ └── indices.py │ ├── mixins │ │ ├── __init__.py │ │ ├── equity_mixin.py │ │ ├── strategy_mixins │ │ │ ├── __init__.py │ │ │ ├── priority_mixins.py │ │ │ ├── features_mixins.py │ │ │ ├── transaction_mixins.py │ │ │ ├── direction_mixins.py │ │ │ └── proportion_mixins.py │ │ └── contract_mixin.py │ ├── __init__.py │ ├── params │ │ ├── price_fields.py │ │ ├── direction_types.py │ │ ├── odin_enum.py │ │ ├── __init__.py │ │ ├── io_params.py │ │ ├── verbosity.py │ │ ├── actions.py │ │ ├── trade_types.py │ │ ├── event_types.py │ │ ├── priorities.py │ │ └── interactive_brokers.py │ ├── fund_actions.py │ ├── odin_init.py │ └── compute_days_elapsed.py ├── fund │ ├── __init__.py │ ├── simulated_fund.py │ └── fund.py ├── portfolio │ ├── components │ │ ├── __init__.py │ │ ├── portfolio_state.py │ │ └── portfolio_history.py │ ├── __init__.py │ ├── simulated_portfolio.py │ ├── interactive_brokers_portfolio.py │ └── abstract_portfolio.py ├── events │ ├── __init__.py │ ├── event_types │ │ ├── fund_event.py │ │ ├── __init__.py │ │ ├── market_event.py │ │ ├── management_event.py │ │ ├── rebalance_event.py │ │ ├── portfolio_event.py │ │ ├── event.py │ │ ├── signal_event.py │ │ ├── order_event.py │ │ └── fill_event.py │ └── events_queue.py ├── metrics │ ├── __init__.py │ ├── compute_sharpe_ratio.py │ ├── performance_summary.py │ ├── compute_drawdowns.py │ └── visualizer.py ├── tests │ ├── test_odin_securities.py │ ├── test_params.py │ ├── test_finance_utilities.py │ ├── test_odin_init.py │ ├── test_price_handler.py │ ├── test_metrics.py │ ├── test_symbol_handler.py │ ├── test_portfolio.py │ ├── test_execution_handler.py │ └── test_position.py └── requirements.txt ├── examples ├── scripts │ ├── live_buy_sell │ │ ├── main.py │ │ ├── settings.py │ │ ├── fund.py │ │ ├── strategy.py │ │ └── handlers.py │ ├── live_buy_and_hold │ │ ├── main.py │ │ ├── settings.py │ │ ├── fund.py │ │ └── handlers.py │ └── README.md └── notebooks │ ├── buy_and_hold │ ├── settings.py │ ├── fund.py │ └── handlers.py │ ├── etf_cointegration │ ├── fund.py │ ├── settings.py │ ├── handlers.py │ └── strategy.py │ ├── moving_average_crossover │ ├── fund.py │ ├── settings.py │ ├── strategy.py │ └── handlers.py │ └── rebalance_etfs │ ├── settings.py │ ├── fund.py │ ├── strategy.py │ └── handlers.py ├── setup.py ├── LICENSE ├── CONTRIBUTING.md └── README.md /odin/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /odin/__init__.py: -------------------------------------------------------------------------------- 1 | from .fund import Fund, SimulatedFund 2 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_sell/main.py: -------------------------------------------------------------------------------- 1 | import fund 2 | fund.fund.trade() 3 | -------------------------------------------------------------------------------- /odin/strategy/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract_strategy import AbstractStrategy 2 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_and_hold/main.py: -------------------------------------------------------------------------------- 1 | import fund 2 | fund.fund.trade() 3 | -------------------------------------------------------------------------------- /odin/handlers/fund_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .fund_handler import FundHandler 2 | -------------------------------------------------------------------------------- /odin/utilities/finance/__init__.py: -------------------------------------------------------------------------------- 1 | from .indices import Indices, untradeable_assets 2 | -------------------------------------------------------------------------------- /odin/strategy/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from .buy_and_hold_strategy import BuyAndHoldStrategy 2 | -------------------------------------------------------------------------------- /odin/fund/__init__.py: -------------------------------------------------------------------------------- 1 | from .fund import Fund 2 | from .simulated_fund import SimulatedFund 3 | -------------------------------------------------------------------------------- /odin/handlers/portfolio_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .portfolio_handler import PortfolioHandler 2 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .database import DatabaseDataHandler, InteractiveBrokersDataHandler 2 | -------------------------------------------------------------------------------- /odin/strategy/indicators/__init__.py: -------------------------------------------------------------------------------- 1 | from .moving_average import MovingAverage 2 | from .williams import Williams 3 | -------------------------------------------------------------------------------- /odin/utilities/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .equity_mixin import EquityMixin 2 | from .contract_mixin import ContractMixin 3 | -------------------------------------------------------------------------------- /odin/portfolio/components/__init__.py: -------------------------------------------------------------------------------- 1 | from .portfolio_history import PortfolioHistory 2 | from .portfolio_state import PortfolioState 3 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/position/__init__.py: -------------------------------------------------------------------------------- 1 | from .filled_position import FilledPosition 2 | from .pending_position import PendingPosition 3 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/templates/__init__.py: -------------------------------------------------------------------------------- 1 | from .suggested_proportion_position_handler import ( 2 | SuggestedProportionPositionHandler 3 | ) 4 | -------------------------------------------------------------------------------- /odin/portfolio/__init__.py: -------------------------------------------------------------------------------- 1 | from .simulated_portfolio import SimulatedPortfolio 2 | from .interactive_brokers_portfolio import InteractiveBrokersPortfolio 3 | -------------------------------------------------------------------------------- /odin/utilities/finance/modern_portfolio_theory/__init__.py: -------------------------------------------------------------------------------- 1 | from .markowitz import solve_markowitz 2 | from .black_litterman import solve_black_litterman 3 | -------------------------------------------------------------------------------- /odin/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | from .odin_init import odin_init 2 | from .compute_days_elapsed import compute_days_elapsed 3 | from .fund_actions import period_dict 4 | -------------------------------------------------------------------------------- /odin/handlers/symbol_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .fixed_symbol_handler import FixedSymbolHandler 2 | from .dollar_volume_symbol_handler import DollarVolumeSymbolHandler 3 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/database/__init__.py: -------------------------------------------------------------------------------- 1 | from .database_data_handler import DatabaseDataHandler 2 | from .interactive_brokers_data_handler import InteractiveBrokersDataHandler 3 | -------------------------------------------------------------------------------- /odin/handlers/execution_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .simulated_execution_handler import ( 2 | SimulatedExecutionHandler 3 | ) 4 | from .interactive_brokers_execution_handler import * 5 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/price_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from .database_price_handler import DatabasePriceHandler 2 | from .interactive_brokers_price_handler import InteractiveBrokersPriceHandler 3 | -------------------------------------------------------------------------------- /odin/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_types import ( 2 | MarketEvent, SignalEvent, OrderEvent, FillEvent, RebalanceEvent, 3 | ManagementEvent, 4 | ) 5 | from .events_queue import EventsQueue 6 | -------------------------------------------------------------------------------- /odin/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | from .compute_drawdowns import compute_drawdowns 2 | from .compute_sharpe_ratio import compute_sharpe_ratio 3 | from .performance_summary import performance_summary 4 | from .visualizer import Visualizer 5 | -------------------------------------------------------------------------------- /odin/utilities/params/price_fields.py: -------------------------------------------------------------------------------- 1 | from .odin_enum import OdinEnum 2 | 3 | 4 | class PriceFields(OdinEnum): 5 | current_price = "current_price" 6 | sim_high_price = "sim_high_price" 7 | sim_low_price = "sim_low_price" 8 | -------------------------------------------------------------------------------- /odin/events/event_types/fund_event.py: -------------------------------------------------------------------------------- 1 | from .event import Event 2 | 3 | 4 | class FundEvent(Event): 5 | """Fund Event Class 6 | This class is to capture all universal properties of events impacting the 7 | whole fund. 8 | """ 9 | 10 | -------------------------------------------------------------------------------- /odin/events/event_types/__init__.py: -------------------------------------------------------------------------------- 1 | from .market_event import MarketEvent 2 | from .signal_event import SignalEvent 3 | from .order_event import OrderEvent 4 | from .fill_event import FillEvent 5 | from .rebalance_event import RebalanceEvent 6 | from .management_event import ManagementEvent 7 | -------------------------------------------------------------------------------- /odin/utilities/params/direction_types.py: -------------------------------------------------------------------------------- 1 | from .odin_enum import OdinEnum 2 | 3 | 4 | class Directions(OdinEnum): 5 | """Direction Type Declaration Module 6 | 7 | Trades may either be long or short in Odin. 8 | """ 9 | long_dir = "LONG" 10 | short_dir = "SHORT" 11 | 12 | -------------------------------------------------------------------------------- /odin/utilities/params/odin_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OdinEnum(Enum): 5 | """Enumeration Class for OdinEnum 6 | 7 | This enumeration class instructs all enumeration objects inheriting from it 8 | to show their value when they are requested to be printed to the standard 9 | output. 10 | """ 11 | def __str__(self): 12 | return self.value 13 | -------------------------------------------------------------------------------- /odin/utilities/mixins/equity_mixin.py: -------------------------------------------------------------------------------- 1 | class EquityMixin(object): 2 | """Equity Mixin Class""" 3 | 4 | @property 5 | def equity(self): 6 | """Computes the total equity of the portfolio. This is simply the sum of 7 | the free capital available to the portfolio and the current market value 8 | of each position in the portfolio. 9 | """ 10 | pos = self.filled_positions.values() 11 | return self.capital + sum([p.relative_value for p in pos]) 12 | -------------------------------------------------------------------------------- /odin/utilities/params/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_types import Events 2 | from .direction_types import Directions 3 | from .trade_types import TradeTypes 4 | from .interactive_brokers import InteractiveBrokers as IB 5 | from .interactive_brokers import ib_commission, ib_silent_errors 6 | from .actions import Actions, action_dict 7 | from .priorities import priority_dict 8 | from .io_params import IOFiles 9 | from .verbosity import Verbosities, verbosity_dict 10 | from .price_fields import PriceFields 11 | -------------------------------------------------------------------------------- /odin/utilities/mixins/strategy_mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .features_mixins import DefaultFeaturesMixin 2 | from .priority_mixins import DefaultPriorityMixin 3 | from .direction_mixins import ( 4 | LongStrategyMixin, 5 | ShortStrategyMixin 6 | ) 7 | from .proportion_mixins import ( 8 | EqualBuyProportionMixin, 9 | TotalSellProportionMixin 10 | ) 11 | from .transaction_mixins import ( 12 | AlwaysBuyIndicatorMixin, 13 | NeverSellIndicatorMixin, 14 | NeverExitIndicatorMixin, 15 | ) 16 | -------------------------------------------------------------------------------- /examples/scripts/README.md: -------------------------------------------------------------------------------- 1 | # Trading Scripts in Odin 2 | 3 | These scripts demonstrate how Odin can interface with Interactive Brokers to place algorithmic trades. **If you did not properly configure Odin or if you use your live trading account, be aware that real trades will be executed.** If you decide to try these scripts, it is recommended that you do so by logging into your paper trading session of TWS. 4 | 5 | Please be cautious when executing these demo scripts and please ensure that you know exactly what you are doing. 6 | -------------------------------------------------------------------------------- /odin/events/event_types/market_event.py: -------------------------------------------------------------------------------- 1 | from .event import Event 2 | from ...utilities.params import Events 3 | 4 | class MarketEvent(Event): 5 | """Market Event Class 6 | 7 | Handles the event of receiving a new market update with corresponding bars. 8 | 9 | Parameters 10 | ---------- 11 | datetime: Refer to base class documentation. 12 | """ 13 | def __init__(self, datetime): 14 | """Initialize parameters of the market event object.""" 15 | super(MarketEvent, self).__init__(Events.market, datetime) 16 | -------------------------------------------------------------------------------- /odin/utilities/fund_actions.py: -------------------------------------------------------------------------------- 1 | """Funds in Odin may need to periodically rebalance themselves or have 2 | management fees subtracted from their total equity. Rebalancing can be performed 3 | at a number of different time resolutions including weekly, monthly, quarterly, 4 | and annually. Odin will determine if the corresponding number of trading days 5 | have elapsed and then enact a rebalancing or management event if required by the 6 | trading strategy. 7 | """ 8 | period_dict = { 9 | "weekly": 7, 10 | "monthly": 21, 11 | "quarterly": 63, 12 | "annually": 252 13 | } 14 | -------------------------------------------------------------------------------- /odin/utilities/mixins/contract_mixin.py: -------------------------------------------------------------------------------- 1 | from ib.ext.Contract import Contract 2 | 3 | 4 | class ContractMixin(object): 5 | """Contract Mixin Class""" 6 | 7 | def create_contract(self, symbol): 8 | """Create an Interactive Brokers contract object. This specifies the 9 | equity type, the exchange, the currency, and the symbol to trade. 10 | """ 11 | c = Contract() 12 | c.m_symbol = symbol 13 | c.m_secType = "STK" 14 | c.m_exchange = "SMART" 15 | c.m_primaryExch = "SMART" 16 | c.m_currency = "USD" 17 | return c 18 | -------------------------------------------------------------------------------- /odin/events/event_types/management_event.py: -------------------------------------------------------------------------------- 1 | from .fund_event import FundEvent 2 | from ...utilities.params import Events 3 | 4 | 5 | class ManagementEvent(FundEvent): 6 | """Fund Management Event Class 7 | 8 | Portfolio managers take a fee from the fund at the conclusion of the year. 9 | Typically, this is some percentage of the assets-under-management (AUM) and 10 | another percentage of the returns. 11 | """ 12 | def __init__(self, datetime): 13 | """Initialize parameters of the management event object.""" 14 | super(ManagementEvent, self).__init__(Events.management, datetime) 15 | 16 | -------------------------------------------------------------------------------- /odin/utilities/mixins/strategy_mixins/priority_mixins.py: -------------------------------------------------------------------------------- 1 | from ....strategy import AbstractStrategy 2 | 3 | 4 | class DefaultPriorityMixin(AbstractStrategy): 5 | """Default Priority Mixin Class 6 | 7 | This class simply retrieves the symbols that are available at the conclusion 8 | of the previous day and returns them in the same order that they are in the 9 | bars panel object. 10 | """ 11 | def generate_priority(self, feats): 12 | """Implementation of abstract base class method.""" 13 | return self.portfolio.data_handler.bars.ix[ 14 | "adj_price_close", -1, : 15 | ].dropna().index 16 | -------------------------------------------------------------------------------- /odin/utilities/mixins/strategy_mixins/features_mixins.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from ....strategy import AbstractStrategy 3 | 4 | 5 | class DefaultFeaturesMixin(AbstractStrategy): 6 | """Default Features Mixin Class 7 | 8 | This just creates an empty dataframe containing as the index the symbols 9 | available on each day of trading and no columns. 10 | """ 11 | def generate_features(self): 12 | """Implementation of abstract base class method.""" 13 | symbols = self.portfolio.data_handler.bars.ix[ 14 | "adj_price_close", -1, : 15 | ].dropna().index 16 | return pd.DataFrame(index=symbols) 17 | -------------------------------------------------------------------------------- /examples/notebooks/buy_and_hold/settings.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | from odin.utilities import params 4 | 5 | 6 | # Start date and end date of the time series. 7 | start_date = dt.datetime(2006, 1, 1) 8 | end_date = dt.datetime(2016, 12, 30) 9 | # Only trade the S&P 500 ETF. 10 | symbols = ["SPY"] 11 | 12 | # Start trading will $100,000 in capital initially. 13 | init_capital = 100000.0 14 | # Only hold a single position. 15 | maximum_capacity = 1 16 | 17 | # Number of prior trading days to download at the start of the time series. 18 | n_init = 10 19 | # Set an identifier for the portfolio. 20 | pid = "buy_and_hold" 21 | fid = "fund" 22 | 23 | # Set verbosity level. 24 | verbosity = 1 25 | -------------------------------------------------------------------------------- /odin/events/event_types/rebalance_event.py: -------------------------------------------------------------------------------- 1 | from .fund_event import FundEvent 2 | from ...utilities.params import Events 3 | 4 | 5 | class RebalanceEvent(FundEvent): 6 | """Rebalance Fund Event Class 7 | 8 | Odin will periodically ensure that the capital investment in long and short 9 | portfolios of a fund remains consistent with some predetermined weighting. 10 | For many cases, this will be full dollar-neutrality, corresponding to an 11 | equal split of equity. This event triggers rebalancing. 12 | """ 13 | def __init__(self, datetime): 14 | """Initialize parameters of the rebalance event object.""" 15 | super(RebalanceEvent, self).__init__(Events.rebalance, datetime) 16 | 17 | -------------------------------------------------------------------------------- /odin/tests/test_odin_securities.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | import numpy as np 4 | import unittest 5 | from odin_securities.queries import gets 6 | from odin_securities.vendors import quandl, yahoo_finance 7 | 8 | 9 | class OdinSecuritiesTest(unittest.TestCase): 10 | def test_get_actions(self): 11 | start = dt.datetime(2013, 1, 1) 12 | end = dt.datetime(2016, 1, 1) 13 | a = gets.actions(start, end) 14 | 15 | def test_valid_symbols(self): 16 | symbol = "^^^^" 17 | self.assertFalse(yahoo_finance.check_valid_symbol(symbol)) 18 | self.assertFalse(quandl.check_valid_symbol(symbol)) 19 | 20 | if __name__ == "__main__": 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/price_handler/abstract_price_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class AbstractPriceHandler(object): 5 | """Abstract Price Handler Class 6 | 7 | Price handler objects are responsible for retrieving either live or 8 | simulated price data for particular assets. This information is then used 9 | to make entry or exit decisions for positions, and to size out the quantity 10 | of shares to transact. 11 | """ 12 | __metaclass__ = ABCMeta 13 | 14 | @abstractmethod 15 | def request_prices(self, current_date, symbols): 16 | """Request prices for assets on the current date.""" 17 | raise NotImplementedError() 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /odin/utilities/params/io_params.py: -------------------------------------------------------------------------------- 1 | from .odin_enum import OdinEnum 2 | 3 | 4 | class IOFiles(OdinEnum): 5 | """Odin Input/Output Parameters 6 | 7 | This module contains constants for specifying universal file names and 8 | directories that Odin uses to store pertinent information about funds and 9 | portfolios on the local disk. This allows for persistence across multiple 10 | sessions. 11 | """ 12 | # Resource files when setting up a fund for trading. 13 | handlers_file = "handlers.py" 14 | settings_file = "settings.py" 15 | strategy_file = "strategy.py" 16 | fund_file = "fund.py" 17 | main_file = "main.py" 18 | # Date formatting string. 19 | date_format = "%Y-%m-%d %H:%M:%S" 20 | -------------------------------------------------------------------------------- /odin/utilities/params/verbosity.py: -------------------------------------------------------------------------------- 1 | """Event Verbosity Declaration Module 2 | 3 | This module declares the verbosity level at which certain informational 4 | statements will appear. The higher the verbosity level, the more information 5 | will be printed to the console as shares are transacted and market data is 6 | received. 7 | """ 8 | from .odin_enum import OdinEnum 9 | from .event_types import Events 10 | 11 | verbosity_dict = { 12 | # Market data events. 13 | Events.market: 3, 14 | # Portfolio-level events. 15 | Events.signal: 2, 16 | Events.order: 2, 17 | Events.fill: 1, 18 | # Fund-level events. 19 | Events.rebalance: 1, 20 | Events.management: 1, 21 | } 22 | 23 | class Verbosities(OdinEnum): 24 | portfolio = 3 25 | -------------------------------------------------------------------------------- /odin/utilities/finance/indices.py: -------------------------------------------------------------------------------- 1 | from ..params.odin_enum import OdinEnum 2 | 3 | 4 | class Indices(OdinEnum): 5 | """Odin Financial Indices 6 | 7 | This file contains the tickers of common stock market indices for use in 8 | trading strategies that may wish to invest idle capital in an index, to 9 | generate signals relative to an index, or to simply trade the index 10 | generally. 11 | """ 12 | # These are the actual indices and they are not tradeable. 13 | sp_100 = "^OEX" 14 | sp_500 = "^GSPC" 15 | # Exchange traded funds that track the indices and are tradeable. 16 | sp_100_etf = "OEF" 17 | sp_500_etf = "SPY" 18 | 19 | # Define a tuple of assets that cannot be bought. 20 | untradeable_assets = [Indices.sp_100.value, Indices.sp_500.value] 21 | -------------------------------------------------------------------------------- /odin/utilities/mixins/strategy_mixins/transaction_mixins.py: -------------------------------------------------------------------------------- 1 | from ....strategy import AbstractStrategy 2 | 3 | 4 | class AlwaysBuyIndicatorMixin(AbstractStrategy): 5 | """Always Buy Strategy Mixin""" 6 | def buy_indicator(self, feats): 7 | """Implementation of abstract base class method.""" 8 | return True 9 | 10 | 11 | class NeverSellIndicatorMixin(AbstractStrategy): 12 | """Never Sell Strategy Mixin""" 13 | def sell_indicator(self, feats): 14 | """Implementation of abstract base class method.""" 15 | return False 16 | 17 | 18 | class NeverExitIndicatorMixin(AbstractStrategy): 19 | """Never Exit Strategy Mixin""" 20 | def exit_indicator(self, feats): 21 | """Implementation of abstract base class method.""" 22 | return False 23 | -------------------------------------------------------------------------------- /examples/notebooks/buy_and_hold/fund.py: -------------------------------------------------------------------------------- 1 | from odin.portfolio import SimulatedPortfolio 2 | from odin.utilities import params 3 | from odin.handlers.fund_handler import FundHandler 4 | from odin.fund import SimulatedFund 5 | from odin.strategy.templates import BuyAndHoldStrategy 6 | import settings 7 | import handlers 8 | 9 | 10 | # Generate objects for the portfolios and strategies that the fund will trade. 11 | portfolios = [ 12 | SimulatedPortfolio(handlers.dh, handlers.posh, handlers.porth), 13 | ] 14 | strategies = [ 15 | BuyAndHoldStrategy(portfolios[0]), 16 | ] 17 | # Create the fund and fund handler objects. 18 | fh = FundHandler( 19 | handlers.events, strategies, settings.start_date, settings.fid 20 | ) 21 | fund = SimulatedFund(handlers.dh, handlers.eh, fh, settings.verbosity) 22 | 23 | -------------------------------------------------------------------------------- /odin/utilities/mixins/strategy_mixins/direction_mixins.py: -------------------------------------------------------------------------------- 1 | from ...params import Directions 2 | from ....strategy import AbstractStrategy 3 | 4 | 5 | class LongStrategyMixin(AbstractStrategy): 6 | """Long Strategy Mixin Class 7 | 8 | This class should be inherited by strategies that are long-only. 9 | """ 10 | def compute_direction(self, feats): 11 | """Implementation of abstract base class method.""" 12 | return Directions.long_dir 13 | 14 | 15 | class ShortStrategyMixin(AbstractStrategy): 16 | """Short Strategy Mixin Class 17 | 18 | This class should be inherited by strategies that are short-only. 19 | """ 20 | def compute_direction(self, feats): 21 | """Implementation of abstract base class method.""" 22 | return Directions.short_dir 23 | -------------------------------------------------------------------------------- /examples/notebooks/etf_cointegration/fund.py: -------------------------------------------------------------------------------- 1 | from odin.portfolio import SimulatedPortfolio 2 | from odin.utilities import params 3 | from odin.handlers.fund_handler import FundHandler 4 | from odin.fund import SimulatedFund 5 | from odin.strategy.templates import BuyAndHoldStrategy 6 | import strategy 7 | import settings 8 | import handlers 9 | 10 | 11 | # Generate objects for the portfolios and strategies that the fund will trade. 12 | portfolios = [ 13 | SimulatedPortfolio(handlers.dh, handlers.posh, handlers.porth), 14 | ] 15 | strategies = [ 16 | strategy.CointegratedETFStrategy(portfolios[0]), 17 | ] 18 | 19 | # Create the fund and fund handler objects. 20 | fh = FundHandler( 21 | handlers.events, strategies, settings.start_date, settings.fid 22 | ) 23 | fund = SimulatedFund(handlers.dh, handlers.eh, fh, settings.verbosity) 24 | 25 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_and_hold/settings.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | from odin.utilities import params 4 | from odin.utilities.finance import Indices 5 | 6 | 7 | # Only trade the S&P 500 ETF. 8 | symbols = [Indices.sp_500_etf.value] 9 | 10 | # Start trading will $100,000 in capital initially. 11 | init_capital = 100000.0 12 | # Only hold a single position. 13 | maximum_capacity = 1 14 | 15 | # Account and fund identifiers. 16 | pid = "buy_and_hold_example_portfolio" 17 | fid = "buy_and_hold_example_fund" 18 | 19 | # Number of prior trading days to download at the start of the time series. 20 | n_init = 10 21 | # Set an account for the portfolio. 22 | account = "" 23 | 24 | # The amount of time to wait before requesting more market data. 25 | delay = 10 26 | # Set verbosity level. 27 | verbosity = 3 28 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_sell/settings.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | from odin.utilities import params 4 | from odin.utilities.finance import Indices 5 | 6 | 7 | # Only trade the S&P 500 ETF. 8 | symbols = [Indices.sp_500_etf.value] 9 | 10 | # Start trading will $100,000 in capital initially. 11 | init_capital = 100000.0 12 | # Only hold a single position. 13 | maximum_capacity = 1 14 | 15 | # Account and fund identifiers. 16 | pid = "buy_and_sell_example_portfolio" 17 | fid = "buy_and_sell_example_fund" 18 | 19 | # Number of prior trading days to download at the start of the time series. 20 | n_init = 10 21 | # Set an account for the portfolio. 22 | account = "" 23 | 24 | # The amount of time to wait before requesting more market data. 25 | delay = 10 26 | # Set verbosity level. 27 | verbosity = 3 28 | -------------------------------------------------------------------------------- /examples/notebooks/moving_average_crossover/fund.py: -------------------------------------------------------------------------------- 1 | from odin.portfolio import SimulatedPortfolio 2 | from odin.utilities import params 3 | from odin.handlers.fund_handler import FundHandler 4 | from odin.fund import SimulatedFund 5 | from odin.strategy.templates import BuyAndHoldStrategy 6 | import strategy 7 | import settings 8 | import handlers 9 | 10 | 11 | # Generate objects for the portfolios and strategies that the fund will trade. 12 | portfolios = [ 13 | SimulatedPortfolio(handlers.dh, handlers.posh, handlers.porth), 14 | ] 15 | strategies = [ 16 | strategy.MovingAverageCrossoverStrategy(portfolios[0]), 17 | ] 18 | 19 | # Create the fund and fund handler objects. 20 | fh = FundHandler( 21 | handlers.events, strategies, settings.start_date, settings.fid 22 | ) 23 | fund = SimulatedFund(handlers.dh, handlers.eh, fh, settings.verbosity) 24 | 25 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/templates/suggested_proportion_position_handler.py: -------------------------------------------------------------------------------- 1 | from ..abstract_position_handler import AbstractPositionHandler 2 | 3 | 4 | class SuggestedProportionPositionHandler(AbstractPositionHandler): 5 | """Suggested Proportion Position Handler Class 6 | 7 | This class will (naively) utilize the suggested proportion of equity 8 | provided by the signal event object to allocate the portfolio's equity. 9 | """ 10 | def compute_buy_weights(self, signal_event, portfolio_handler): 11 | """Implementation of abstract base class method.""" 12 | return {signal_event.symbol: signal_event.suggested_proportion} 13 | 14 | def compute_sell_weights(self, signal_event, portfolio_handler): 15 | """Implementation of abstract base class method.""" 16 | return {signal_event.symbol: signal_event.suggested_proportion} 17 | -------------------------------------------------------------------------------- /examples/notebooks/etf_cointegration/settings.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | from odin.utilities import params 4 | from odin.utilities.finance import Indices 5 | 6 | 7 | # Start date and end date of the time series. 8 | start_date = dt.datetime(2012, 1, 1) 9 | end_date = dt.datetime(2017, 1, 1) 10 | # Trade ETFs. 11 | symbols = ["ARNC", "UNG"] 12 | 13 | # Start trading will $100,000 in capital initially. 14 | init_capital = 100000.0 15 | # Only hold a single position. 16 | maximum_capacity = 2 17 | 18 | # Number of prior trading days to download at the start of the time series. 19 | n_init = 500 20 | # Set an identifier for the portfolio. 21 | pid = "cointegrated_etf" 22 | fid = "fund" 23 | # Assume that transacting shares moves the price by five-hundredths of a basis 24 | # point. 25 | transaction_cost = 0.0005 26 | 27 | # Set verbosity level. 28 | verbosity = 1 29 | -------------------------------------------------------------------------------- /odin/tests/test_params.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from odin.utilities.params import Events, priority_dict, TradeTypes 3 | 4 | 5 | class ParamsTest(unittest.TestCase): 6 | def test_priorities(self): 7 | """Test to ensure that the relative importance of portfolio-level events 8 | are consistent. 9 | """ 10 | me = priority_dict[Events.market] 11 | se = priority_dict[Events.signal] 12 | oe = priority_dict[Events.order] 13 | fe = priority_dict[Events.fill] 14 | self.assertTrue(me > se[TradeTypes.buy_trade]) 15 | self.assertTrue(se[TradeTypes.buy_trade] > se[TradeTypes.sell_trade]) 16 | self.assertTrue(se[TradeTypes.sell_trade] > se[TradeTypes.exit_trade]) 17 | self.assertTrue(se[TradeTypes.exit_trade] > oe) 18 | self.assertTrue(oe > fe) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /odin/utilities/finance/modern_portfolio_theory/black_litterman.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from cvxopt import matrix 4 | from cvxopt.blas import dot 5 | from cvxopt.solvers import qp, options 6 | options["show_progress"] = False 7 | 8 | 9 | def solve_black_litterman(prices): 10 | """This method solves the Black-Litterman asset allocation problem. 11 | Generally viewed as an improvement over the Markowitz mean-variance 12 | portfolio allocation methodology, Black-Litterman takes a Bayesian approach 13 | to incorporating market views into the optimization problem. 14 | 15 | TODO: Figure out how Black-Litterman actually works. 16 | """ 17 | # Number of assets. 18 | pct = prices.pct_change().ix[1:].dropna(axis=1) 19 | n = pct.shape[1] 20 | 21 | # Compute the covariance of the historical percentage changes. 22 | S = pct.cov().values 23 | -------------------------------------------------------------------------------- /examples/notebooks/moving_average_crossover/settings.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | from odin.utilities import params 4 | from odin.utilities.finance import Indices 5 | 6 | 7 | # Start date and end date of the time series. 8 | start_date = dt.datetime(2006, 1, 1) 9 | end_date = dt.datetime(2017, 1, 1) 10 | # Trade ETFs. 11 | symbols = ["AAPL"] 12 | 13 | # Start trading will $100,000 in capital initially. 14 | init_capital = 100000.0 15 | # Only hold a single position. 16 | maximum_capacity = 1 17 | 18 | # Number of prior trading days to download at the start of the time series. 19 | n_init = 500 20 | # Set an identifier for the portfolio. 21 | pid = "moving_average_crossover" 22 | fid = "fund" 23 | # Assume that transacting shares moves the price by five-hundredths of a basis 24 | # point. 25 | transaction_cost = 0.0005 26 | 27 | # Set verbosity level. 28 | verbosity = 1 29 | -------------------------------------------------------------------------------- /odin/utilities/params/actions.py: -------------------------------------------------------------------------------- 1 | """Action Declaration Module 2 | 3 | Because Odin uses a notion of buying for entering either long or short 4 | positions, it is necessary to resolve combinations of directionality and trade 5 | type to specific actions. 6 | """ 7 | from .direction_types import Directions 8 | from .trade_types import TradeTypes 9 | from .odin_enum import OdinEnum 10 | 11 | 12 | class Actions(OdinEnum): 13 | buy = "BUY" 14 | sell = "SELL" 15 | 16 | action_dict = { 17 | (Directions.long_dir, TradeTypes.buy_trade): Actions.buy, 18 | (Directions.long_dir, TradeTypes.sell_trade): Actions.sell, 19 | (Directions.short_dir, TradeTypes.buy_trade): Actions.sell, 20 | (Directions.short_dir, TradeTypes.sell_trade): Actions.buy, 21 | (Directions.long_dir, TradeTypes.exit_trade): Actions.sell, 22 | (Directions.short_dir, TradeTypes.exit_trade): Actions.buy, 23 | } 24 | -------------------------------------------------------------------------------- /examples/notebooks/rebalance_etfs/settings.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import pandas as pd 3 | from odin.utilities import params 4 | from odin.utilities.finance import Indices 5 | 6 | 7 | # Start date and end date of the time series. 8 | start_date = dt.datetime(2006, 1, 1) 9 | end_date = dt.datetime(2016, 12, 30) 10 | # Trade ETFs. 11 | symbols = ["SPY", "AGG"] 12 | 13 | # Start trading will $100,000 in capital initially. 14 | init_capital = 100000.0 15 | # Only hold a single position. 16 | maximum_capacity = 2 17 | 18 | # Number of prior trading days to download at the start of the time series. 19 | n_init = 10 20 | # Set an identifier for the portfolio. 21 | pid = "weighted_etfs" 22 | pid_bench = "spyder_etf" 23 | fid = "fund" 24 | # Assume that transacting shares moves the price by five-hundredths of a basis 25 | # point. 26 | transaction_cost = 0.0005 27 | 28 | # Set verbosity level. 29 | verbosity = 1 30 | -------------------------------------------------------------------------------- /odin/tests/test_finance_utilities.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pandas as pd 3 | import datetime as dt 4 | from odin.handlers.data_handler import DatabaseDataHandler 5 | from odin.handlers.symbol_handler import FixedSymbolHandler 6 | from odin.events import EventsQueue 7 | from odin.utilities.finance.modern_portfolio_theory import ( 8 | solve_markowitz, solve_black_litterman 9 | ) 10 | 11 | 12 | class FinanceUtilitiesTest(unittest.TestCase): 13 | def test_markowitz(self): 14 | q = EventsQueue() 15 | symbols = ["GOOG", "MS", "AMZN", "GS"] 16 | sh = FixedSymbolHandler(symbols, []) 17 | start, end = dt.datetime(2011, 1, 1), dt.datetime(2017, 1, 1) 18 | dh = DatabaseDataHandler(q, sh, start, end, 252*5) 19 | dh.request_prices() 20 | prices = dh.bars["adj_price_close"] 21 | series = solve_markowitz(prices, 1.) 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /odin/utilities/params/trade_types.py: -------------------------------------------------------------------------------- 1 | """Trade Type Declaration Module 2 | 3 | Odin conceptualizes entering either a long or short position as a 'BUY' 4 | operation. This is done so that taking a short position can be regarded as 5 | setting aside equity to cover the cost of the initial position and that this is 6 | not fundamentally different from using equity to purchase shares in a long 7 | position. 8 | 9 | The different kinds of trade types are 'BUY', 'SELL', and 'EXIT'. The first two 10 | are self-explanatory. The third corresponds to the idea of completely 11 | liquidating a position (either long or short) buy selling shares (for longs) or 12 | buying shares (for shorts). A 'SELL' signal where the number of shares equals 13 | the total number of shares held is equivalent to an exit. 14 | """ 15 | from .odin_enum import OdinEnum 16 | 17 | 18 | class TradeTypes(OdinEnum): 19 | buy_trade = "BUY" 20 | sell_trade = "SELL" 21 | exit_trade = "EXIT" 22 | 23 | -------------------------------------------------------------------------------- /examples/notebooks/rebalance_etfs/fund.py: -------------------------------------------------------------------------------- 1 | from odin.portfolio import SimulatedPortfolio 2 | from odin.utilities import params 3 | from odin.handlers.fund_handler import FundHandler 4 | from odin.fund import SimulatedFund 5 | from odin.strategy.templates import BuyAndHoldStrategy 6 | import strategy 7 | import settings 8 | import handlers 9 | 10 | 11 | # Generate objects for the portfolios and strategies that the fund will trade. 12 | portfolios = [ 13 | SimulatedPortfolio(handlers.dh, handlers.posh, handlers.porth), 14 | SimulatedPortfolio(handlers.dh, handlers.posh_bench, handlers.porth_bench), 15 | ] 16 | strategies = [ 17 | strategy.RebalanceETFStrategy(portfolios[0]), 18 | strategy.BuyAndHoldSpyderStrategy(portfolios[1]), 19 | ] 20 | # Create the fund and fund handler objects. 21 | fh = FundHandler( 22 | handlers.events, strategies, settings.start_date, settings.fid 23 | ) 24 | fund = SimulatedFund(handlers.dh, handlers.eh, fh, settings.verbosity) 25 | 26 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/price_handler/database_price_handler.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from odin_securities.queries import gets 3 | from .abstract_price_handler import AbstractPriceHandler 4 | from ....utilities.params import PriceFields 5 | 6 | 7 | class DatabasePriceHandler(AbstractPriceHandler): 8 | """Database Price Handler Class 9 | 10 | The database price handler extracts the corresponding price data from the 11 | requested symbols stored in the Odin Securities master database. 12 | """ 13 | def request_prices(self, current_date, symbols): 14 | """Implementation of abstract base class method.""" 15 | prices = gets.prices(current_date, symbols=symbols) 16 | prices.drop(["adj_price_close", "adj_volume"], inplace=True) 17 | prices.items = [ 18 | PriceFields.current_price.value, 19 | PriceFields.sim_high_price.value, 20 | PriceFields.sim_low_price.value, 21 | ] 22 | return prices 23 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/position/pending_position.py: -------------------------------------------------------------------------------- 1 | from ....utilities.params import action_dict 2 | 3 | 4 | class PendingPosition(object): 5 | """Pending Position Class 6 | 7 | The pending position object represents a position that has been sent to the 8 | brokerage to be filled (and become a filled position), but which has not yet 9 | been completed. The pending position represents simply the ticker, the 10 | quantity of shares in the position, whether or not the strategy is long or 11 | short the stock, and an identifier for the portfolio. 12 | """ 13 | def __init__( 14 | self, symbol, direction, trade_type, portfolio_id 15 | ): 16 | """Initialize parameters of the pending position object.""" 17 | self.symbol = symbol 18 | self.direction = direction 19 | self.portfolio_id = portfolio_id 20 | self.trade_type = trade_type 21 | self.action = action_dict[(self.direction, self.trade_type)] 22 | -------------------------------------------------------------------------------- /odin/utilities/params/event_types.py: -------------------------------------------------------------------------------- 1 | from .odin_enum import OdinEnum 2 | 3 | 4 | class Events(OdinEnum): 5 | """Event Type Enumeration 6 | 7 | Odin trades by awaiting sequences of predetermined events and interpreting 8 | such events in order to make subsequent decisions. The documentation for 9 | each event type is contained in the events module, though string identifiers 10 | are stored in this file for global access. 11 | 12 | There are two kinds of indicators: The first operates at the portfolio level 13 | and is interpreted by a specific portfolio object; the second is for the 14 | larger level of funds, impacting all of the portfolio objects contained 15 | within a fund object. 16 | """ 17 | # New market data event. 18 | market = "MARKET" 19 | # Portfolio-level indicators. 20 | signal = "SIGNAL" 21 | order = "ORDER" 22 | fill = "FILL" 23 | # Fund-level events. 24 | rebalance = "REBALANCE" 25 | management = "MANAGEMENT" 26 | -------------------------------------------------------------------------------- /odin/strategy/templates/buy_and_hold_strategy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from ..abstract_strategy import AbstractStrategy 3 | from ...utilities.mixins.strategy_mixins import ( 4 | LongStrategyMixin, 5 | EqualBuyProportionMixin, 6 | DefaultPriorityMixin, 7 | DefaultFeaturesMixin, 8 | AlwaysBuyIndicatorMixin, 9 | NeverSellIndicatorMixin, 10 | NeverExitIndicatorMixin, 11 | ) 12 | 13 | 14 | class BuyAndHoldStrategy( 15 | LongStrategyMixin, 16 | EqualBuyProportionMixin, 17 | DefaultPriorityMixin, 18 | DefaultFeaturesMixin, 19 | AlwaysBuyIndicatorMixin, 20 | NeverSellIndicatorMixin, 21 | NeverExitIndicatorMixin, 22 | ): 23 | """Buy And Hold Strategy Class 24 | 25 | The buy and hold strategy is a passive approach to investing where a 26 | position is entered and then never exited. Nor are additional positions 27 | taken up. This is a strategy that is both very scalable and suitable for 28 | backtesting core functionalities. 29 | """ 30 | -------------------------------------------------------------------------------- /odin/strategy/indicators/moving_average.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class MovingAverage(object): 5 | """Moving Average Class 6 | 7 | Provides support for both simple moving averages and exponential moving 8 | averages 9 | """ 10 | def __init__(self, window): 11 | """Initialize parameters of the moving average object.""" 12 | self.window = window 13 | 14 | def simple_moving_average(self, series): 15 | """Implementation of a simple moving average.""" 16 | return series.ix[-self.window:].mean() 17 | 18 | def simple_z_score(self, series): 19 | """Implementation of a simple z-score.""" 20 | recent = series.ix[-self.window:] 21 | return (series.ix[-1] - recent.mean()) / recent.std() 22 | 23 | def exponential_moving_average(self, series, alpha): 24 | """Implementation of an exponential moving average.""" 25 | weights = np.array([(1 - alpha) ** i for i in range(self.window)]) 26 | return (series.ix[-self.window:] * weights).sum() 27 | -------------------------------------------------------------------------------- /odin/utilities/mixins/strategy_mixins/proportion_mixins.py: -------------------------------------------------------------------------------- 1 | from ....strategy import AbstractStrategy 2 | 3 | 4 | class EqualBuyProportionMixin(AbstractStrategy): 5 | """Equal Proportion Mixin Class 6 | 7 | This class should be inherited by strategies that intend to partition their 8 | equity equally among the assets that are purchased. 9 | """ 10 | def compute_buy_proportion(self, feats): 11 | """Implementation of abstract base class method.""" 12 | return 1.0 / self.portfolio.portfolio_handler.maximum_capacity 13 | 14 | 15 | class TotalSellProportionMixin(AbstractStrategy): 16 | """Total Sell Proportion Mixin Class 17 | 18 | This class will instruct the strategy to entirely liquidate a position when 19 | a sell signal is created. Note that, in general, a sell signal is distinct 20 | from an exit signal, though choosing the sell a position entirely makes the 21 | two equivalent. 22 | """ 23 | def compute_sell_proportion(self, feats): 24 | """Implementation of abstract base class method.""" 25 | return 1.0 26 | -------------------------------------------------------------------------------- /odin/tests/test_odin_init.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | from odin.utilities import odin_init 5 | from odin.utilities.params import IOFiles 6 | 7 | 8 | class OdinInitTest(unittest.TestCase): 9 | def test_odin_init(self): 10 | # Create a test file. 11 | path = "./test_portfolio_id/" 12 | main = path + IOFiles.main_file.value 13 | handlers = path + IOFiles.handlers_file.value 14 | settings = path + IOFiles.settings_file.value 15 | strategy = path + IOFiles.strategy_file.value 16 | fund = path + IOFiles.fund_file.value 17 | odin_init(path) 18 | # Assert that all of the requisite files exist. 19 | self.assertTrue(os.path.isdir(path)) 20 | self.assertTrue(os.path.isfile(main)) 21 | self.assertTrue(os.path.isfile(handlers)) 22 | self.assertTrue(os.path.isfile(settings)) 23 | self.assertTrue(os.path.isfile(strategy)) 24 | self.assertTrue(os.path.isfile(fund)) 25 | shutil.rmtree(path) 26 | 27 | 28 | if __name__ == "__main__": 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /odin/handlers/symbol_handler/fixed_symbol_handler.py: -------------------------------------------------------------------------------- 1 | from .abstract_symbol_handler import AbstractSymbolHandler 2 | 3 | 4 | class FixedSymbolHandler(AbstractSymbolHandler): 5 | """Fixed Symbol Handler Class 6 | 7 | The fixed symbol handler allows the user to easily specify a fixed set of 8 | tickers for which to query data. This is the simplest variety of symbol 9 | handler since it is invariant to the date parameter provided to the symbol 10 | selection method. 11 | 12 | Parameters 13 | ---------- 14 | symbols (optional): List of strings. 15 | An input specifying a particular list of tickers to which the dollar 16 | volume ordering should be restricted. 17 | """ 18 | def __init__(self, symbols, portfolio_handlers): 19 | """Initialize parameters of the fixed symbol handler object.""" 20 | super(FixedSymbolHandler, self).__init__(portfolio_handlers) 21 | self.symbols = symbols 22 | 23 | def select_symbols(self, date): 24 | """Implementation of abstract base class method.""" 25 | return self.append_positions(self.symbols) 26 | 27 | -------------------------------------------------------------------------------- /odin/tests/test_price_handler.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import unittest 3 | from odin.utilities.finance import Indices 4 | from odin.handlers.data_handler.price_handler import ( 5 | DatabasePriceHandler, InteractiveBrokersPriceHandler 6 | ) 7 | 8 | 9 | class PriceHandlerTest(unittest.TestCase): 10 | def test_database_price_handler(self): 11 | date = dt.datetime(2016, 12, 21) 12 | ph = DatabasePriceHandler() 13 | symbols = [Indices.sp_100_etf.value, Indices.sp_500_etf.value] 14 | prices = ph.request_prices(date, symbols) 15 | self.assertEqual(set(prices.minor_axis), set(symbols)) 16 | self.assertEqual(prices.major_axis[0], date) 17 | 18 | def test_interactive_brokers_price_handler(self): 19 | date = dt.datetime(2016, 12, 21) 20 | ph = InteractiveBrokersPriceHandler() 21 | symbols = [Indices.sp_100_etf.value, Indices.sp_500_etf.value] 22 | prices = ph.request_prices(date, symbols) 23 | self.assertEqual(set(prices.minor_axis), set(symbols)) 24 | self.assertEqual(prices.major_axis[0], date) 25 | 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /odin/utilities/params/priorities.py: -------------------------------------------------------------------------------- 1 | """Event Priority Declaration Module 2 | 3 | Events are prioritized according to their impact on the portfolio. Market events 4 | are regarded as least important and fill events as the most important. One notes 5 | that signal events have varying priority according to their trade type: Odin 6 | prioritizes selling over buying so that liquidity can be freed up before buying 7 | decisions are made; this allows more assets to be bought and enforces a more 8 | active utilization of capital. 9 | """ 10 | from .event_types import Events 11 | from .trade_types import TradeTypes 12 | 13 | 14 | priority_dict = { 15 | Events.market: 7, 16 | # Portfolio-level events. 17 | Events.signal: { 18 | TradeTypes.buy_trade: 6, 19 | TradeTypes.sell_trade: 5, 20 | TradeTypes.exit_trade: 4 21 | }, 22 | Events.order: 3, 23 | Events.fill: 2, 24 | # Fund-level events. Notice that management events have lower priority than 25 | # rebalancing events since presumably we should account for the transaction 26 | # fees associated with rebalancing before taking a cut. 27 | Events.rebalance: 8, 28 | Events.management: 9, 29 | } 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages, Command 3 | 4 | 5 | # Utility function to read the README file. 6 | # Used for the long_description. It's nice, because now 1) we have a top level 7 | # README file and 2) it's easier to type in the README file than to put a raw 8 | # string in below ... 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | 13 | class CleanCommand(Command): 14 | """Custom clean command to tidy up the project root.""" 15 | user_options = [] 16 | 17 | def initialize_options(self): 18 | pass 19 | 20 | def finalize_options(self): 21 | pass 22 | 23 | def run(self): 24 | os.system("rm -vrf ./build ./dist ./*.pyc ./*.egg-info") 25 | 26 | 27 | setup( 28 | name="odin", 29 | version="1.3", 30 | author="James Brofos", 31 | author_email="james@brofos.org", 32 | description="Algorithmic trading infrastructure in Python.", 33 | license="Copyright (c) James Brofos 2017", 34 | packages=find_packages(exclude="tests"), 35 | long_description=read("README.md"), 36 | cmdclass={ 37 | "clean": CleanCommand, 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 James Brofos 4 | Copyright (c) 2015-2016 Michael Halls-Moore 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /odin/events/event_types/portfolio_event.py: -------------------------------------------------------------------------------- 1 | from .event import Event 2 | 3 | 4 | class PortfolioEvent(Event): 5 | """Portfolio Event Class 6 | 7 | This class is to capture all universal properties of events impacting an 8 | individual portfolio. 9 | 10 | Parameters 11 | ---------- 12 | event_type, datetime: Refer to base class documentation. 13 | symbol: String. 14 | The ticker symbol about which the event is taking place. 15 | trade_type: String. 16 | Whether or not the security is being bought, sold, or completely exited. 17 | direction: String. 18 | A string indicator of whether we are long or short the security. 19 | portfolio_id: String. 20 | A unique identifier assigned to the portfolio. 21 | """ 22 | def __init__( 23 | self, symbol, trade_type, direction, event_type, datetime, 24 | portfolio_id 25 | ): 26 | """Initialize parameters of the portfolio event object.""" 27 | super(PortfolioEvent, self).__init__(event_type, datetime) 28 | self.symbol = symbol 29 | self.trade_type = trade_type 30 | self.direction = direction 31 | self.portfolio_id = portfolio_id 32 | 33 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/database/interactive_brokers_data_handler.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import numpy as np 3 | from ....events import MarketEvent 4 | from .abstract_database_data_handler import AbstractDatabaseDataHandler 5 | from ..price_handler import InteractiveBrokersPriceHandler 6 | 7 | 8 | class InteractiveBrokersDataHandler(AbstractDatabaseDataHandler): 9 | """Interactive Brokers Data Handler Class 10 | """ 11 | def __init__(self, events, symbol_handler, n_init): 12 | """Initialize parameters of the Interactive Brokers data handler object. 13 | """ 14 | price_handler = InteractiveBrokersPriceHandler() 15 | super(InteractiveBrokersDataHandler, self).__init__( 16 | events, symbol_handler, price_handler, n_init, dt.datetime.today() 17 | ) 18 | 19 | def request_prices(self): 20 | """Implementation of abstract base class method.""" 21 | selected = self.symbol_handler.select_symbols(self.current_date) 22 | self.current_date = dt.datetime.today() 23 | self.prices = self.price_handler.request_prices( 24 | self.current_date, selected 25 | ) 26 | self.events.put(MarketEvent(self.current_date)) 27 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_sell/fund.py: -------------------------------------------------------------------------------- 1 | from odin.portfolio import InteractiveBrokersPortfolio 2 | from odin.utilities import params 3 | from odin.handlers.fund_handler import FundHandler 4 | from odin.fund import Fund 5 | from odin_securities.queries import exists 6 | import handlers 7 | import settings 8 | import strategy 9 | 10 | # Generate objects for the portfolios and strategies that the fund will trade. 11 | portfolios = [ 12 | InteractiveBrokersPortfolio( 13 | handlers.dh, 14 | handlers.posh, 15 | handlers.porth, 16 | settings.account 17 | ), 18 | ] 19 | strategies = [ 20 | strategy.BuySellStrategy(portfolios[0]), 21 | ] 22 | 23 | # Create the fund and fund handler objects. 24 | if exists.fund(settings.fid): 25 | fh = FundHandler.from_database_fund( 26 | settings.fid, 27 | handlers.events, 28 | strategies 29 | ) 30 | else: 31 | fh = FundHandler( 32 | handlers.events, 33 | strategies, 34 | settings.start_date, 35 | settings.fid 36 | ) 37 | fh.to_database_fund() 38 | 39 | fund = Fund( 40 | handlers.dh, 41 | handlers.eh, 42 | fh, 43 | settings.delay, 44 | settings.verbosity 45 | ) 46 | 47 | -------------------------------------------------------------------------------- /odin/tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import unittest 4 | from odin.metrics import * 5 | 6 | 7 | class MetricsTest(unittest.TestCase): 8 | def test_compute_sharpe_ratio(self): 9 | """Check that the computation of the Sharpe ratio is functioning as 10 | expected. To achieve this, we manually compute the Sharpe ratio and 11 | compare it against the Sharpe ratio computed by the metrics module. 12 | """ 13 | returns = pd.Series([np.nan, 0.01, 0.02, -0.005]) 14 | sr_test = compute_sharpe_ratio(returns) 15 | sr_mean, sr_std = returns.mean(), np.sqrt( 16 | np.sum((returns - returns.mean())**2) / 2 17 | ) 18 | sr_valid = np.sqrt(252.) * sr_mean / sr_std 19 | self.assertEqual(sr_test, sr_valid) 20 | 21 | def test_compute_drawdowns(self): 22 | """Ensure that the computation of the drawdowns works as expected.""" 23 | equity = pd.Series([1., 2., 1.5, 1.75, 3.]) 24 | dd, dur = compute_drawdowns(equity, False) 25 | self.assertTrue((dur == [0, 0, 1, 2, 0]).all()) 26 | self.assertTrue((dd == [0, 0, 0.25, 0.125, 0]).all()) 27 | 28 | 29 | if __name__ == "__main__": 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_and_hold/fund.py: -------------------------------------------------------------------------------- 1 | from odin.portfolio import InteractiveBrokersPortfolio 2 | from odin.utilities import params 3 | from odin.handlers.fund_handler import FundHandler 4 | from odin.fund import Fund 5 | from odin.strategy.templates import BuyAndHoldStrategy 6 | from odin_securities.queries import exists 7 | import handlers 8 | import settings 9 | 10 | # Generate objects for the portfolios and strategies that the fund will trade. 11 | portfolios = [ 12 | InteractiveBrokersPortfolio( 13 | handlers.dh, 14 | handlers.posh, 15 | handlers.porth, 16 | settings.account 17 | ), 18 | ] 19 | strategies = [ 20 | BuyAndHoldStrategy( 21 | portfolios[0], params.Directions.long_dir 22 | ), 23 | ] 24 | 25 | # Create the fund and fund handler objects. 26 | if exists.fund(settings.fid): 27 | fh = FundHandler.from_database_fund( 28 | settings.fid, 29 | handlers.events, 30 | strategies 31 | ) 32 | else: 33 | fh = FundHandler( 34 | handlers.events, 35 | strategies, 36 | settings.start_date, 37 | settings.fid 38 | ) 39 | fh.to_database_fund() 40 | 41 | fund = Fund( 42 | handlers.dh, 43 | handlers.eh, 44 | fh, 45 | settings.delay, 46 | settings.verbosity 47 | ) 48 | 49 | -------------------------------------------------------------------------------- /odin/handlers/execution_handler/abstract_execution_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class AbstractExecutionHandler(object): 5 | """Abstract Execution Handler Class 6 | 7 | The execution handler abstract class handles the interaction between a set 8 | of order objects generated by a portfolio and the ultimate set of fill 9 | objects that actually occur in the market. 10 | 11 | The handlers can be used to subclass simulated brokerages or live 12 | brokerages, with identical interfaces. This allows strategies to be 13 | backtested in a very similar manner to the live trading engine. 14 | """ 15 | __metaclass__ = ABCMeta 16 | 17 | def __init__(self, events, is_live): 18 | """Initialize parameters of the abstract execution handler.""" 19 | self.events = events 20 | self.is_live = is_live 21 | 22 | @abstractmethod 23 | def execute_order(self, order_event): 24 | """Objects that implement the abstract execution handler must implement 25 | a method for submitting order events to the brokerage. These submitted 26 | orders must then be processed to become fill events that indicate that a 27 | position was properly entered or exited. 28 | """ 29 | raise NotImplementedError() 30 | -------------------------------------------------------------------------------- /odin/requirements.txt: -------------------------------------------------------------------------------- 1 | bleach==1.5.0 2 | cffi==1.9.1 3 | cryptography==1.7.1 4 | cvxopt==1.1.9 5 | cycler==0.10.0 6 | decorator==4.0.11 7 | entrypoints==0.2.2 8 | html5lib==0.9999999 9 | IbPy2==0.8.0 10 | idna==2.2 11 | inflection==0.3.1 12 | ipykernel==4.5.2 13 | ipython==5.1.0 14 | ipython-genutils==0.1.0 15 | ipywidgets==5.2.2 16 | Jinja2==2.9.4 17 | jsonschema==2.5.1 18 | jupyter==1.0.0 19 | jupyter-client==4.4.0 20 | jupyter-console==5.0.0 21 | jupyter-core==4.2.1 22 | MarkupSafe==0.23 23 | matplotlib==1.5.3 24 | mistune==0.7.3 25 | more-itertools==2.5.0 26 | nbconvert==5.0.0 27 | nbformat==4.2.0 28 | ndg-httpsclient==0.4.2 29 | nose2==0.6.5 30 | notebook==4.3.1 31 | numpy==1.11.3 32 | pandas==0.19.2 33 | pandas-datareader==0.2.1 34 | pandocfilters==1.4.1 35 | pexpect==4.2.1 36 | pickleshare==0.7.4 37 | prompt-toolkit==1.0.9 38 | psycopg2==2.6.2 39 | ptyprocess==0.5.1 40 | pyasn1==0.1.9 41 | pycparser==2.17 42 | Pygments==2.1.3 43 | pyOpenSSL==16.2.0 44 | pyparsing==2.1.10 45 | python-dateutil==2.6.0 46 | pytz==2016.10 47 | pyzmq==16.0.2 48 | qtconsole==4.2.1 49 | Quandl==3.0.1 50 | requests==2.12.4 51 | requests-file==1.4.1 52 | scipy==0.18.1 53 | seaborn==0.7.1 54 | simplegeneric==0.8.1 55 | simplejson==3.10.0 56 | six==1.10.0 57 | terminado==0.6 58 | testpath==0.3 59 | tornado==4.4.2 60 | traitlets==4.3.1 61 | wcwidth==0.1.7 62 | widgetsnbextension==1.2.6 63 | yahoo-finance==1.4.0 64 | -------------------------------------------------------------------------------- /examples/notebooks/rebalance_etfs/strategy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from odin.strategy import AbstractStrategy 3 | from odin.strategy.templates import BuyAndHoldStrategy 4 | from odin.utilities.mixins.strategy_mixins import ( 5 | LongStrategyMixin, 6 | TotalSellProportionMixin, 7 | AlwaysBuyIndicatorMixin, 8 | NeverSellIndicatorMixin, 9 | DefaultPriorityMixin, 10 | DefaultFeaturesMixin, 11 | ) 12 | 13 | 14 | class BuyAndHoldSpyderStrategy(BuyAndHoldStrategy): 15 | def buy_indicator(self, feats): 16 | return feats.name in ("SPY", ) 17 | 18 | 19 | class RebalanceETFStrategy( 20 | LongStrategyMixin, 21 | TotalSellProportionMixin, 22 | AlwaysBuyIndicatorMixin, 23 | NeverSellIndicatorMixin, 24 | DefaultPriorityMixin, 25 | DefaultFeaturesMixin, 26 | ): 27 | def compute_buy_proportion(self, feats): 28 | """Implementation of abstract base class method.""" 29 | if feats.name == "SPY": 30 | return 0.6 31 | elif feats.name == "AGG": 32 | return 0.4 33 | 34 | def exit_indicator(self, feats): 35 | """Implementation of abstract base class method.""" 36 | symbol = feats.name 37 | pos = self.portfolio.portfolio_handler.filled_positions[symbol] 38 | date = self.portfolio.data_handler.current_date 39 | return pos.compute_holding_period(date).days > 63 40 | -------------------------------------------------------------------------------- /examples/notebooks/moving_average_crossover/strategy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from odin.strategy.indicators import MovingAverage as MA 3 | from odin.utilities.mixins.strategy_mixins import ( 4 | LongStrategyMixin, 5 | EqualBuyProportionMixin, 6 | TotalSellProportionMixin, 7 | DefaultPriorityMixin, 8 | NeverSellIndicatorMixin 9 | ) 10 | 11 | 12 | class MovingAverageCrossoverStrategy( 13 | LongStrategyMixin, 14 | EqualBuyProportionMixin, 15 | TotalSellProportionMixin, 16 | DefaultPriorityMixin, 17 | NeverSellIndicatorMixin 18 | ): 19 | def buy_indicator(self, feats): 20 | """Implementation of abstract base class method.""" 21 | return ( 22 | feats.name == "AAPL" and 23 | feats["short_mavg"] > feats["long_mavg"] 24 | ) 25 | 26 | def exit_indicator(self, feats): 27 | """Implementation of abstract base class method.""" 28 | return ( 29 | feats["long_mavg"] > feats["short_mavg"] 30 | ) 31 | 32 | def generate_features(self): 33 | """Implementation of abstract base class method.""" 34 | series = self.portfolio.data_handler.bars["adj_price_close"] 35 | feats = pd.DataFrame(index=series.columns) 36 | feats["long_mavg"] = MA(200).simple_moving_average(series) 37 | feats["short_mavg"] = MA(50).simple_moving_average(series) 38 | return feats -------------------------------------------------------------------------------- /odin/metrics/compute_sharpe_ratio.py: -------------------------------------------------------------------------------- 1 | """Sharpe Ratio Module 2 | 3 | Create the Sharpe ratio for the strategy, based on a benchmark of zero (i.e. no 4 | risk-free rate information); this is coincidentally the proper risk-free rate to 5 | utilize for dollar-neutral strategies. The Sharpe ratio generally measures the 6 | returns of a strategy relative to its historical risk; the higher this ratio, 7 | the more heavily leveraged the strategy may be. 8 | 9 | The Sharpe ratio is assumed to be annualized to a yearly period since there are 10 | 252 trading days in a year. For higher or lower frequency strategies, this 11 | annualization constant may be augmented. 12 | """ 13 | import numpy as np 14 | 15 | 16 | def compute_sharpe_ratio(returns, periods=252.0): 17 | """Computes the Sharpe ratio based on a time-series of returns. 18 | 19 | Parameters 20 | ---------- 21 | returns: Pandas data frame. 22 | A Pandas data frame where the index is a time-series of dates 23 | corresponding to a historical period of performance of a trading 24 | algorithm. The values are the day-over-day percentage changes in equity. 25 | periods (Optional): Float. 26 | The annualization constant for computing the Sharpe ratio. By default, 27 | this corresponds to daily trades (because there are 252 trading sessions 28 | per year). 29 | """ 30 | return np.sqrt(periods) * returns.mean() / returns.std() 31 | -------------------------------------------------------------------------------- /odin/strategy/indicators/williams.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class Williams(object): 5 | """Williams %R Class 6 | 7 | Williams %R, or just %R, is a technical analysis oscillator showing the 8 | current closing price in relation to the high and low of the past N days 9 | (for a given N). It was developed by a publisher and promoter of trading 10 | materials, Larry Williams. Its purpose is to tell whether a stock or 11 | commodity market is trading near the high or the low, or somewhere in 12 | between, of its recent trading range. 13 | 14 | The oscillator is on a negative scale, from −100 (lowest) up to 0 (highest), 15 | obverse of the more common 0 to 100 scale found in many Technical Analysis 16 | oscillators. A value of −100 means the close today was the lowest low of the 17 | past N days, and 0 means today's close was the highest high of the past N 18 | days. 19 | 20 | Source: https://en.wikipedia.org/wiki/Williams_%25R 21 | """ 22 | def __init__(self, window): 23 | """Initialize parameters of the moving average object.""" 24 | self.window = window 25 | 26 | def percent_r(self, bars): 27 | """Implementation of Williams' %R.""" 28 | rec = bars.ix[:, -self.window:, :] 29 | high, low = rec["adj_price_high"].max(), rec["adj_price_low"].min() 30 | close = rec.ix["adj_price_close", -1, :] 31 | return (high - close) / (high - low) * -100.0 32 | 33 | -------------------------------------------------------------------------------- /odin/tests/test_symbol_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime as dt 3 | from odin.events import EventsQueue 4 | from odin.utilities.finance import Indices 5 | from odin.handlers.data_handler import DatabaseDataHandler 6 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 7 | from odin.handlers.symbol_handler import ( 8 | FixedSymbolHandler, DollarVolumeSymbolHandler 9 | ) 10 | 11 | 12 | class SymbolHandlerTest(unittest.TestCase): 13 | def test_fixed_symbol_handler(self): 14 | q = EventsQueue() 15 | start, end = dt.datetime(2015, 1, 2), dt.datetime(2015, 1, 9) 16 | symbols = [Indices.sp_100_etf.value, Indices.sp_500_etf.value] 17 | sh = FixedSymbolHandler(symbols, []) 18 | dh = DatabaseDataHandler(q, sh, start, end, 10) 19 | self.assertEqual(set(sh.select_symbols(dh.current_date)), set(symbols)) 20 | 21 | def test_dollar_volume_symbol_handler(self): 22 | q = EventsQueue() 23 | start, end = dt.datetime(2015, 1, 2), dt.datetime(2015, 1, 9) 24 | symbols = [Indices.sp_100_etf, Indices.sp_500_etf] 25 | sh = DollarVolumeSymbolHandler(1, [], None) 26 | dh = DatabaseDataHandler(q, sh, start, end, 10) 27 | sel = set([s for s in sh.select_symbols(dh.current_date)]) 28 | self.assertEqual( 29 | sel, set((Indices.sp_500_etf.value, )) 30 | ) 31 | 32 | 33 | if __name__ == "__main__": 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_sell/strategy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from odin.strategy import AbstractStrategy 3 | from odin.utilities.params import Directions 4 | 5 | 6 | class BuySellStrategy(AbstractStrategy): 7 | """Buy Sell Strategy Class 8 | 9 | The buy sell strategy is a stupid strategy that just buys and sells assets 10 | at every opportunity. This is just for testing functionality and isn't good 11 | for much else. 12 | """ 13 | def direction_indicator(self, feats): 14 | return Directions.long_dir 15 | 16 | def buy_indicator(self, feats): 17 | """Implementation of abstract base class method.""" 18 | return True 19 | 20 | def sell_indicator(self, feats): 21 | """Implementation of abstract base class method.""" 22 | return False 23 | 24 | def exit_indicator(self, feats): 25 | """Implementation of abstract base class method.""" 26 | return True 27 | 28 | def generate_features(self): 29 | """Implementation of abstract base class method.""" 30 | symbols = self.portfolio.data_handler.bars.ix[ 31 | "adj_price_open", -1, : 32 | ].dropna().index 33 | return pd.DataFrame(index=symbols) 34 | 35 | def generate_priority(self, feats): 36 | """Implementation of abstract base class method.""" 37 | return self.portfolio.data_handler.bars.ix[ 38 | "adj_price_open", -1, : 39 | ].dropna().index 40 | -------------------------------------------------------------------------------- /examples/notebooks/buy_and_hold/handlers.py: -------------------------------------------------------------------------------- 1 | from odin.events import EventsQueue 2 | from odin.handlers.portfolio_handler import PortfolioHandler 3 | from odin.handlers.position_handler.templates import ( 4 | SuggestedProportionPositionHandler 5 | ) 6 | from odin.handlers.execution_handler import SimulatedExecutionHandler 7 | from odin.handlers.symbol_handler import FixedSymbolHandler 8 | from odin.handlers.data_handler import DatabaseDataHandler 9 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 10 | import settings 11 | 12 | 13 | # Create a portfolio handler to manage transactions and keeping track of 14 | # capital. 15 | porth = PortfolioHandler( 16 | settings.maximum_capacity, settings.pid, settings.init_capital, 17 | settings.fid 18 | ) 19 | 20 | # Events queue for handling market data, signals, orders, and fills. 21 | events = EventsQueue() 22 | # Symbol handler will determine which symbols will be processed during trading. 23 | # In this example, we will just trade the S&P 500 ETF (SPY). 24 | sh = FixedSymbolHandler(settings.symbols, [porth]) 25 | 26 | # Set up a price handler and a data handler to provide data to the trading 27 | # system. 28 | dh = DatabaseDataHandler( 29 | events, sh, settings.start_date, settings.end_date, settings.n_init 30 | ) 31 | # Execution handler executes trades. 32 | eh = SimulatedExecutionHandler(dh) 33 | 34 | # Position handler to determine how much of an asset to purchase. 35 | posh = SuggestedProportionPositionHandler(dh) 36 | -------------------------------------------------------------------------------- /examples/notebooks/etf_cointegration/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from odin.events import EventsQueue 4 | from odin.handlers.portfolio_handler import PortfolioHandler 5 | from odin.handlers.position_handler.templates import ( 6 | SuggestedProportionPositionHandler 7 | ) 8 | from odin.handlers.execution_handler import SimulatedExecutionHandler 9 | from odin.handlers.symbol_handler import FixedSymbolHandler 10 | from odin.handlers.data_handler import DatabaseDataHandler 11 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 12 | import settings 13 | 14 | 15 | # Create a portfolio handler to manage transactions and keeping track of 16 | # capital. 17 | porth = PortfolioHandler( 18 | settings.maximum_capacity, settings.pid, settings.init_capital, 19 | settings.fid 20 | ) 21 | 22 | # Events queue for handling market data, signals, orders, and fills. 23 | events = EventsQueue() 24 | # Symbol handler will determine which symbols will be processed during trading. 25 | # In this example, we will just trade the S&P 500 ETF (SPY). 26 | sh = FixedSymbolHandler(settings.symbols, [porth]) 27 | 28 | # Set up a price handler and a data handler to provide data to the trading 29 | # system. 30 | dh = DatabaseDataHandler( 31 | events, sh, settings.start_date, settings.end_date, settings.n_init 32 | ) 33 | # Execution handler executes trades. 34 | eh = SimulatedExecutionHandler(dh, settings.transaction_cost) 35 | 36 | # Position handler to determine how much of an asset to purchase. 37 | posh = SuggestedProportionPositionHandler(dh) 38 | -------------------------------------------------------------------------------- /examples/notebooks/moving_average_crossover/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from odin.events import EventsQueue 4 | from odin.handlers.portfolio_handler import PortfolioHandler 5 | from odin.handlers.position_handler.templates import ( 6 | SuggestedProportionPositionHandler 7 | ) 8 | from odin.handlers.execution_handler import SimulatedExecutionHandler 9 | from odin.handlers.symbol_handler import FixedSymbolHandler 10 | from odin.handlers.data_handler import DatabaseDataHandler 11 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 12 | import settings 13 | 14 | 15 | # Create a portfolio handler to manage transactions and keeping track of 16 | # capital. 17 | porth = PortfolioHandler( 18 | settings.maximum_capacity, settings.pid, settings.init_capital, 19 | settings.fid 20 | ) 21 | 22 | # Events queue for handling market data, signals, orders, and fills. 23 | events = EventsQueue() 24 | # Symbol handler will determine which symbols will be processed during trading. 25 | # In this example, we will just trade the S&P 500 ETF (SPY). 26 | sh = FixedSymbolHandler(settings.symbols, [porth]) 27 | 28 | # Set up a price handler and a data handler to provide data to the trading 29 | # system. 30 | dh = DatabaseDataHandler( 31 | events, sh, settings.start_date, settings.end_date, settings.n_init 32 | ) 33 | # Execution handler executes trades. 34 | eh = SimulatedExecutionHandler(dh, settings.transaction_cost) 35 | 36 | # Position handler to determine how much of an asset to purchase. 37 | posh = SuggestedProportionPositionHandler(dh) 38 | -------------------------------------------------------------------------------- /odin/events/event_types/event.py: -------------------------------------------------------------------------------- 1 | from ...utilities.params import IOFiles 2 | 3 | 4 | class Event(object): 5 | """Event Class 6 | 7 | Odin processes trading information by handling events of different kinds. 8 | All of the events used by Odin are defined in this file. Excluding market 9 | events, there are two kinds of events: The first of these are events at the 10 | portfolio level, which handle generating trading signals, submitting order 11 | events, and processing fill events from the brokerage. The other kind of 12 | event pertains to the level of the fund: This events determine when the 13 | fund rebalances its long and short positions as well as when management 14 | fees are subtracted from the equity holdings of the fund. 15 | 16 | At the most abstract level, an event is described by its type and the date 17 | and time at which the event occurred. 18 | 19 | Parameters 20 | ---------- 21 | event_type: String. 22 | A string indicator of the type of event that is being represented. 23 | datetime: Datetime object. 24 | The date and time at which the event was created. 25 | """ 26 | def __init__(self, event_type, datetime): 27 | """Initialize parameters of the event object.""" 28 | self.datetime = datetime 29 | self.event_type = event_type 30 | 31 | def __str__(self): 32 | """String representation of the market event object.""" 33 | return "{}\t{}".format( 34 | self.event_type, 35 | self.datetime.strftime(IOFiles.date_format.value) 36 | ) 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Odin 2 | 3 | Odin is constantly being worked on and improved by its developers. We would be happy to receive pull requests if you have a feature or a bug fix that you want to see included in the main software. Feel free to open a pull request. 4 | 5 | Please refer to Odin's issues page for a (partial) list of improvements and planned features that we are aiming on incorporating into Odin. 6 | 7 | ## Slack 8 | 9 | Odin uses a Slack channel for chats between developers. The slack page can be found at [https://odin-global.slack.com/](https://odin-global.slack.com/). Please send james Brofos an email if you'd like to request to be added to the Slack channel. 10 | 11 | ## Requesting a Feature 12 | 13 | If you have a feature in mind that you'd like to see incorporated into Odin, please open a Github issue to discuss the enhancement. Ideally, your issue should detail the desired syntax and demonstrate how the functionality integrates elegantly with the rest of Odin's codebase. 14 | 15 | After there is agreement that your enhancement is a good one, feel free to start writing an implementation in code. Be sure to write thorough tests for your new feature to ensure that it works as expected, even in border cases! You can put your test code in the `./odin/tests/` directory. 16 | 17 | ## Examples 18 | 19 | We are always happy to see how you might have used Odin in your own backtesting or trading. :) Moreover, if you have an example that you'd like to add which illustrates how to implement a common strategy in Odin, please consider adding it to the `./examples/` directory. 20 | 21 | -------------------------------------------------------------------------------- /odin/events/event_types/signal_event.py: -------------------------------------------------------------------------------- 1 | from .portfolio_event import PortfolioEvent 2 | from ...utilities.params import Events, IOFiles 3 | 4 | 5 | class SignalEvent(PortfolioEvent): 6 | """Signal Event Class 7 | 8 | Handles the event of sending a Signal from a Strategy object. This is 9 | received by a Portfolio object and acted upon. 10 | """ 11 | def __init__( 12 | self, 13 | symbol, 14 | suggested_proportion, 15 | trade_type, direction, 16 | datetime, 17 | portfolio_id 18 | ): 19 | """Initialize parameters of the signal event object.""" 20 | super(SignalEvent, self).__init__( 21 | symbol, trade_type, direction, Events.signal, 22 | datetime, portfolio_id 23 | ) 24 | # N.B.: When the trade type of the signal indicates a buy, the suggested 25 | # proportion is the fraction of the portfolio's equity which will be 26 | # allocated to this position. On the other hand, when the trade type is 27 | # a sell, the proportion corresponds to the fraction of the existing 28 | # number of shares that should be liquidated. 29 | self.suggested_proportion = suggested_proportion 30 | 31 | def __str__(self): 32 | """String representation of the signal event object.""" 33 | return "{}\t{}\t{}\t{}\t{}".format( 34 | self.event_type, 35 | self.datetime.strftime(IOFiles.date_format.value), 36 | self.symbol, 37 | self.direction, 38 | self.trade_type 39 | ) 40 | -------------------------------------------------------------------------------- /odin/metrics/performance_summary.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from .compute_drawdowns import compute_drawdowns 3 | from .compute_sharpe_ratio import compute_sharpe_ratio 4 | 5 | 6 | def performance_summary(history, portfolio_id): 7 | """This function computes common performance metrics for a time-series of 8 | portfolio equity states. For instance, the function will compute the Sharpe 9 | ratio, the maximum drawdown, the drawdown duration, the annualized returns 10 | and the average number of positions held at each moment in the time-series. 11 | 12 | Parameters 13 | ---------- 14 | history: A portfolio history object. 15 | The portfolio history object containing the equity and positional 16 | information for a time-series corresponding to the period of performance 17 | of a trading algorithm. 18 | portfolio_id: String. 19 | A unique identifier assigned to the portfolio. 20 | """ 21 | equity = history.equity 22 | n = len(equity) 23 | m = pd.DataFrame(index=[portfolio_id]) 24 | m.ix[portfolio_id, "total equity"] = equity.ix[-1] 25 | m.ix[portfolio_id, "max equity"] = equity.max() 26 | m.ix[portfolio_id, "max drawdown"], m.ix[portfolio_id, "max duration"] = ( 27 | compute_drawdowns(equity) 28 | ) 29 | m.ix[portfolio_id, "sharpe ratio"] = ( 30 | compute_sharpe_ratio(history.returns) 31 | ) 32 | m.ix[portfolio_id, "avg positions"] = history.n_positions.mean() 33 | m.ix[portfolio_id, "annualized returns"] = ( 34 | (1. + history.returns).prod() ** (252. / n) 35 | ) 36 | 37 | return m 38 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/abstract_data_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class AbstractDataHandler(object): 5 | """Abstract Data Handler Class 6 | 7 | The data handler is an abstract base class providing an interface for all 8 | subsequent (inherited) data handlers (both live and historic). 9 | 10 | The goal of a (derived) data handler object is to output a generated set of 11 | bars for each symbol requested. This will replicate how a live strategy 12 | would function as current market data would be sent 'down the pipe'. Thus a 13 | historic and live system will be treated identically by the rest of the 14 | backtesting suite. 15 | """ 16 | __metaclass__ = ABCMeta 17 | 18 | def __init__(self, events, symbol_handler, price_handler): 19 | """Initialize parameters of the abstract data handler object.""" 20 | self.events = events 21 | self.symbol_handler = symbol_handler 22 | self.price_handler = price_handler 23 | self.continue_trading = True 24 | 25 | @abstractmethod 26 | def update(self): 27 | """Objects that implement the data handler abstract base class must 28 | implement a method for obtaining new bars from the data source. This 29 | method places the most recently available bars onto a data structure for 30 | access by methods and objects requiring access to the underlying 31 | financial data. 32 | """ 33 | raise NotImplementedError() 34 | 35 | @abstractmethod 36 | def request_prices(self): 37 | """Request the current price of assets.""" 38 | raise NotImplementedError() 39 | 40 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_sell/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from odin.events import EventsQueue 4 | from odin.handlers.portfolio_handler import PortfolioHandler 5 | from odin.handlers.position_handler import EqualEquityPositionHandler 6 | from odin.handlers.execution_handler import InteractiveBrokersExecutionHandler 7 | from odin.handlers.symbol_handler import FixedSymbolHandler 8 | from odin.handlers.data_handler import InteractiveBrokersDataHandler 9 | from odin.handlers.data_handler.price_handler import ( 10 | InteractiveBrokersPriceHandler 11 | ) 12 | from odin_securities.queries import exists 13 | import settings 14 | 15 | 16 | # Create a portfolio handler to manage transactions and keeping track of 17 | # capital. 18 | if exists.portfolio(settings.pid): 19 | porth = PortfolioHandler.from_database_portfolio(settings.pid) 20 | else: 21 | porth = PortfolioHandler( 22 | settings.maximum_capacity, settings.pid, settings.init_capital, 23 | settings.fid 24 | ) 25 | 26 | # Events queue for handling market data, signals, orders, and fills. 27 | events = EventsQueue() 28 | # Symbol handler will determine which symbols will be processed during trading. 29 | # In this example, we will just trade the S&P 500 ETF (SPY). 30 | sh = FixedSymbolHandler(settings.symbols, [porth]) 31 | 32 | # Set up a price handler and a data handler to provide data to the trading 33 | # system. 34 | dh = InteractiveBrokersDataHandler(events, sh, settings.n_init) 35 | # Execution handler executes trades. 36 | eh = InteractiveBrokersExecutionHandler(events) 37 | 38 | # Position handler to determine how much of an asset to purchase. 39 | posh = EqualEquityPositionHandler(settings.maximum_capacity, dh) 40 | -------------------------------------------------------------------------------- /examples/scripts/live_buy_and_hold/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from odin.events import EventsQueue 4 | from odin.handlers.portfolio_handler import PortfolioHandler 5 | from odin.handlers.position_handler import EqualEquityPositionHandler 6 | from odin.handlers.execution_handler import InteractiveBrokersExecutionHandler 7 | from odin.handlers.symbol_handler import FixedSymbolHandler 8 | from odin.handlers.data_handler import InteractiveBrokersDataHandler 9 | from odin.handlers.data_handler.price_handler import ( 10 | InteractiveBrokersPriceHandler 11 | ) 12 | from odin_securities.queries import exists 13 | import settings 14 | 15 | 16 | # Create a portfolio handler to manage transactions and keeping track of 17 | # capital. 18 | if exists.portfolio(settings.pid): 19 | porth = PortfolioHandler.from_database_portfolio(settings.pid) 20 | else: 21 | porth = PortfolioHandler( 22 | settings.maximum_capacity, settings.pid, settings.init_capital, 23 | settings.fid 24 | ) 25 | 26 | # Events queue for handling market data, signals, orders, and fills. 27 | events = EventsQueue() 28 | # Symbol handler will determine which symbols will be processed during trading. 29 | # In this example, we will just trade the S&P 500 ETF (SPY). 30 | sh = FixedSymbolHandler(settings.symbols, [porth]) 31 | 32 | # Set up a price handler and a data handler to provide data to the trading 33 | # system. 34 | dh = InteractiveBrokersDataHandler(events, sh, settings.n_init) 35 | # Execution handler executes trades. 36 | eh = InteractiveBrokersExecutionHandler(events) 37 | 38 | # Position handler to determine how much of an asset to purchase. 39 | posh = EqualEquityPositionHandler(settings.maximum_capacity, dh) 40 | -------------------------------------------------------------------------------- /odin/utilities/odin_init.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .params import IOFiles 3 | 4 | 5 | def odin_init(sname): 6 | """This function creates a directory with the necessary substructure for 7 | Odin to run a trading algorithm within it. Specifically, it creates a folder 8 | with the desired strategy name within the current directory. It then creates 9 | a subdirectory 'history' that contains relevant data on the portfolio; it 10 | also creates a file 'main.py' that is executed in order to perform trading. 11 | 12 | Usage 13 | ----- 14 | This code can be used from the command line as follows: 15 | python3 -c "from odin.utilities import odin_init ; odin_init('strat')" 16 | 17 | Parameters 18 | ---------- 19 | sname: String. 20 | A string giving an identifier to the directory that will house the 21 | implementation of the strategy and dependency files. 22 | """ 23 | path = "./" + sname + "/" 24 | main = path + IOFiles.main_file.value 25 | handlers = path + IOFiles.handlers_file.value 26 | settings = path + IOFiles.settings_file.value 27 | strategy = path + IOFiles.strategy_file.value 28 | fund = path + IOFiles.fund_file.value 29 | # Create files and directories. 30 | if not os.path.isdir(path): 31 | os.mkdir(path) 32 | if not os.path.isfile(main): 33 | open(main, "a").close() 34 | if not os.path.isfile(handlers): 35 | open(handlers, "a").close() 36 | if not os.path.isfile(settings): 37 | open(settings, "a").close() 38 | if not os.path.isfile(strategy): 39 | open(strategy, "a").close() 40 | if not os.path.isfile(fund): 41 | open(fund, "a").close() 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /odin/tests/test_portfolio.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime as dt 3 | from odin.handlers.symbol_handler import FixedSymbolHandler 4 | from odin.handlers.data_handler import DatabaseDataHandler 5 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 6 | from odin.events import EventsQueue 7 | from odin.handlers.portfolio_handler import PortfolioHandler 8 | from odin.utilities.finance import Indices 9 | 10 | 11 | class PortfolioTest(unittest.TestCase): 12 | def test_to_datebase_portfolio(self): 13 | q = EventsQueue() 14 | start, end = dt.datetime(2015, 1, 2), dt.datetime(2015, 1, 9) 15 | symbols = [Indices.sp_100_etf, Indices.sp_500_etf] 16 | sh = FixedSymbolHandler(symbols, []) 17 | dh = DatabaseDataHandler(q, sh, start, end, 10) 18 | max_cap = 1 19 | capital = 100000.0 20 | ph = PortfolioHandler( 21 | max_cap, "test_portfolio_id", capital, "test_fund_id" 22 | ) 23 | ph.to_database_portfolio() 24 | 25 | def test_from_database_portfolio(self): 26 | pid = "test_portfolio_id" 27 | q = EventsQueue() 28 | start, end = dt.datetime(2015, 1, 2), dt.datetime(2015, 1, 9) 29 | symbols = [Indices.sp_100_etf, Indices.sp_500_etf] 30 | sh = FixedSymbolHandler(symbols, []) 31 | dh = DatabaseDataHandler(q, sh, start, end, 10) 32 | ph = PortfolioHandler.from_database_portfolio(pid) 33 | self.assertEqual(ph.capital, 100000.0) 34 | self.assertEqual(ph.maximum_capacity, 1) 35 | self.assertEqual(ph.portfolio_id, pid) 36 | self.assertTrue("SPY" in ph.filled_positions) 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /examples/notebooks/rebalance_etfs/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from odin.events import EventsQueue 4 | from odin.handlers.portfolio_handler import PortfolioHandler 5 | from odin.handlers.position_handler.templates import ( 6 | SuggestedProportionPositionHandler 7 | ) 8 | from odin.handlers.execution_handler import SimulatedExecutionHandler 9 | from odin.handlers.symbol_handler import FixedSymbolHandler 10 | from odin.handlers.data_handler import DatabaseDataHandler 11 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 12 | import settings 13 | 14 | 15 | 16 | # Create a portfolio handler to manage transactions and keeping track of 17 | # capital. 18 | porth = PortfolioHandler( 19 | settings.maximum_capacity, settings.pid, settings.init_capital, 20 | settings.fid 21 | ) 22 | porth_bench = PortfolioHandler( 23 | 1, settings.pid_bench, settings.init_capital, settings.fid 24 | ) 25 | 26 | # Events queue for handling market data, signals, orders, and fills. 27 | events = EventsQueue() 28 | # Symbol handler will determine which symbols will be processed during trading. 29 | # In this example, we will just trade the S&P 500 ETF (SPY). 30 | sh = FixedSymbolHandler(settings.symbols, [porth]) 31 | 32 | # Set up a price handler and a data handler to provide data to the trading 33 | # system. 34 | dh = DatabaseDataHandler( 35 | events, sh, settings.start_date, settings.end_date, settings.n_init 36 | ) 37 | # Execution handler executes trades. 38 | eh = SimulatedExecutionHandler(dh, settings.transaction_cost) 39 | 40 | # Position handler to determine how much of an asset to purchase. 41 | posh = SuggestedProportionPositionHandler(dh) 42 | posh_bench = SuggestedProportionPositionHandler(dh) -------------------------------------------------------------------------------- /odin/utilities/params/interactive_brokers.py: -------------------------------------------------------------------------------- 1 | """Interactive Brokers Parameters 2 | 3 | This mdoule specifies parameters that are required to connect to the Interactive 4 | Brokers (IB) Trader Workstation. In particular, we specify here the port that 5 | listens for connections from the IB API. We also specify the global identifier 6 | for submitting trades to be executed and the identifier for retrieving 7 | information specific to a portfolio. 8 | 9 | This file contains privileged account information for both the live trading 10 | account and the paper trading account. These are required for submitting trades 11 | in their respective trading environments. 12 | """ 13 | from .odin_enum import OdinEnum 14 | 15 | 16 | class InteractiveBrokers(OdinEnum): 17 | # Client identifiers for connections to Interactive Brokers. 18 | portfolio_id = 101 19 | execution_handler_id = 102 20 | data_handler_id = 103 21 | # Interactive Brokers port connection. 22 | port = 7497 23 | 24 | 25 | def ib_commission(quantity, price): 26 | """Compute the commission charged by Interactive Brokers.""" 27 | c = 0.005 * quantity 28 | maximum = 0.005 * quantity * price 29 | minimum = 1.0 30 | 31 | if c < minimum: 32 | # The commission may not be less than one dollar. 33 | c = minimum 34 | elif c > maximum: 35 | # The commission may not exceed more than 0.005% of the total value of 36 | # the position. 37 | c = maximum 38 | 39 | return c 40 | 41 | 42 | # Define a list of error codes that are silent. 43 | ib_silent_errors = set([ 44 | # HMDS data farm connection is OK:ushmds. 45 | 2106, 46 | # Market data farm connection is OK:usfarm. 47 | 2104, 48 | ]) 49 | -------------------------------------------------------------------------------- /odin/events/event_types/order_event.py: -------------------------------------------------------------------------------- 1 | from .portfolio_event import PortfolioEvent 2 | from ...utilities.params import Events, IOFiles 3 | 4 | 5 | class OrderEvent(PortfolioEvent): 6 | """Order Event Class 7 | 8 | Handles the event of sending an Order to an execution system. The order 9 | contains a symbol (e.g. 'GOOG'), a type ('BUY', 'SELL', or 'EXIT'), 10 | quantity, and a direction (long or short). 11 | """ 12 | def __init__( 13 | self, symbol, quantity, trade_type, direction, datetime, 14 | portfolio_id 15 | ): 16 | """Initialize parameters of the order event object.""" 17 | super(OrderEvent, self).__init__( 18 | symbol, trade_type, direction, Events.order, datetime, 19 | portfolio_id 20 | ) 21 | self.quantity = quantity 22 | 23 | def __str__(self): 24 | """String representation of the order event object.""" 25 | return "{}\t{}\t{}\t{}\t{}\t{}".format( 26 | self.event_type, 27 | self.datetime.strftime(IOFiles.date_format.value), 28 | self.symbol, 29 | self.direction, 30 | self.trade_type, 31 | self.quantity 32 | ) 33 | 34 | @classmethod 35 | def from_signal_event(cls, signal_event, quantity): 36 | """Create an order event object from the corresponding information in a 37 | signal event object. The signal event includes all of the pertinent 38 | information except for the amount of the asset to purchase. 39 | """ 40 | return cls( 41 | signal_event.symbol, 42 | quantity, 43 | signal_event.trade_type, 44 | signal_event.direction, 45 | signal_event.datetime, 46 | signal_event.portfolio_id 47 | ) 48 | -------------------------------------------------------------------------------- /odin/handlers/symbol_handler/dollar_volume_symbol_handler.py: -------------------------------------------------------------------------------- 1 | from odin_securities.queries import gets 2 | from .abstract_symbol_handler import AbstractSymbolHandler 3 | from ...utilities.finance import Indices, untradeable_assets 4 | 5 | 6 | class DollarVolumeSymbolHandler(AbstractSymbolHandler): 7 | """Dollar Volume Symbol Handler Class 8 | 9 | The dollar volume symbol handler selects assets by ranking stocks according 10 | to the total dollar volume transacted during the previous trading session. 11 | The symbol handler ignores the S&P 100 and S&P 500 indices which have huge 12 | volume. 13 | """ 14 | def __init__(self, n, portfolio_handlers, symbols=None): 15 | """Initialize parameters of the dollar volume symbol handler object. 16 | 17 | Parameters 18 | ---------- 19 | n: Integer. 20 | The number of assets to take from the ranking according to dollar 21 | volume; i.e. the top stocks according to dollar volume during the 22 | previous trading session. 23 | symbols (optional): List of strings. 24 | An input specifying a particular list of tickers to which the dollar 25 | volume ordering should be restricted. 26 | """ 27 | super(DollarVolumeSymbolHandler, self).__init__(portfolio_handlers) 28 | self.n = n 29 | self.symbols = symbols 30 | 31 | def select_symbols(self, date): 32 | """Implementation of abstract base class method.""" 33 | bars = gets.prices(date, symbols=self.symbols) 34 | dv = bars.ix["adj_price_close", -1, :] * bars.ix["adj_volume", -1, :] 35 | rank = dv.sort_values(ascending=False).dropna() 36 | return self.append_positions( 37 | list(rank.index.drop(untradeable_assets, errors="ignore")[:self.n]) 38 | ) 39 | -------------------------------------------------------------------------------- /odin/tests/test_execution_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime as dt 3 | from odin.handlers.execution_handler import SimulatedExecutionHandler 4 | from odin.handlers.data_handler import DatabaseDataHandler 5 | from odin.handlers.symbol_handler import FixedSymbolHandler 6 | from odin.handlers.data_handler.price_handler import DatabasePriceHandler 7 | from odin.events import EventsQueue, MarketEvent, FillEvent, OrderEvent 8 | from odin.utilities.params import TradeTypes, Directions, PriceFields 9 | 10 | class ExecutionHandlerTest(unittest.TestCase): 11 | def test_simulated_execution_handler(self): 12 | """Ensures that the simulated execution handler is processing order 13 | events in the expected way. 14 | """ 15 | q = EventsQueue() 16 | symbol = "SPY" 17 | sh = FixedSymbolHandler([symbol], []) 18 | start, end = dt.datetime(2015, 1, 2), dt.datetime(2015, 1, 9) 19 | dh = DatabaseDataHandler(q, sh, start, end, 10) 20 | dh.request_prices() 21 | eh = SimulatedExecutionHandler(dh) 22 | quantity = 100 23 | o = OrderEvent( 24 | symbol, quantity, TradeTypes.buy_trade, Directions.long_dir, 25 | start, "long" 26 | ) 27 | eh.execute_order(o) 28 | dh.update() 29 | f = q.get() 30 | price_id = [ 31 | PriceFields.sim_low_price.value, PriceFields.sim_high_price.value 32 | ] 33 | price = dh.prices.ix[price_id, 0, symbol].mean() 34 | fill_cost = price * quantity * (1.0 + eh.transaction_cost) 35 | self.assertTrue(type(f) == FillEvent) 36 | self.assertTrue(type(q.get()) == MarketEvent) 37 | self.assertTrue(q.empty()) 38 | self.assertEqual(fill_cost, f.fill_cost) 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /odin/utilities/compute_days_elapsed.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | 4 | def compute_days_elapsed(date_entered, current_date): 5 | """Computes the number of weekdays (Mondays, Tuesdays, Wednesdays, 6 | Thursdays, and Fridays) that have elapsed between two dates. This is used by 7 | Odin to compute how long a position has been held in the portfolio. 8 | 9 | Parameters 10 | ---------- 11 | date_entered: Datetime object. 12 | The date of origination, which is used as a starting point for computing 13 | the number of business days elapsed. 14 | current_date: Datetime object. 15 | The end date of the time series, which marks the date at which the 16 | number of days elapsed ends. 17 | """ 18 | # Compute the weekday for the two provided dates. Zero is Monday and six is 19 | # Sunday. 20 | from_weekday = date_entered.weekday() 21 | to_weekday = current_date.weekday() 22 | # If start date is after Friday, modify it to Monday 23 | if from_weekday > 4: 24 | from_weekday = 0 25 | corr = 7 - date_entered.weekday() 26 | date_entered = date_entered + dt.timedelta(days=corr) 27 | 28 | # Compute the number of weekdays between the specified weekdays 29 | day_diff = to_weekday - from_weekday 30 | # Compute the number of whole weeks that have elapsed between the two dates. 31 | # We assume that there are five work days in each whole week. 32 | whole_weeks = ( 33 | ((current_date.date() - date_entered.date()).days - day_diff) / 7 34 | ) 35 | workdays_in_whole_weeks = whole_weeks * 5 36 | # Return the number of weekdays elapsed between the two dates. 37 | beginning_end_correction = ( 38 | min(day_diff, 5) - (max(current_date.weekday() - 4, 0) % 5) 39 | ) 40 | return max(workdays_in_whole_weeks + beginning_end_correction, 0) 41 | -------------------------------------------------------------------------------- /odin/handlers/symbol_handler/abstract_symbol_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class AbstractSymbolHandler(object): 5 | """Abstract Symbol Handler Class 6 | 7 | The symbol handler class is responsible for determining which stocks should 8 | have their prices retrieved for processing and potential trading during the 9 | next trading period. 10 | """ 11 | __metaclass__ = ABCMeta 12 | 13 | def __init__(self, portfolio_handlers): 14 | """Initialize parameters of the abstract symbol handler object.""" 15 | self.portfolio_handlers = portfolio_handlers 16 | 17 | @abstractmethod 18 | def select_symbols(self, date): 19 | """Select symbols to be processed during the next trading period. This 20 | function takes as input the price data from the current trading period, 21 | which can be used to generate rankings of stocks subsequently. 22 | """ 23 | raise NotImplementedError() 24 | 25 | def append_positions(self, selected): 26 | """If a fund (which consists of portfolios) holds positions, then it is 27 | critical that price and volume data be available for those positions. 28 | Otherwise, trading events may be missed. As such, this method ensures 29 | that all positions present in a fund's portfolios have price data 30 | requested. 31 | 32 | Parameters 33 | ---------- 34 | selected: List of symbols. 35 | A list of symbols that have been selected to have price and volume 36 | data queried from the database. This list will have the current 37 | positions of the fund appended to it. 38 | """ 39 | set_selected = set(selected) 40 | for p in self.portfolio_handlers: 41 | for pos in p.filled_positions: 42 | set_selected.add(pos) 43 | 44 | return list(set_selected) 45 | -------------------------------------------------------------------------------- /odin/metrics/compute_drawdowns.py: -------------------------------------------------------------------------------- 1 | """Drawdowns Module 2 | 3 | Calculate the largest peak-to-trough drawdown of the profit-and-loss curve as 4 | well as the duration of the drawdown. This function requires that the equity 5 | curve is represented by a pandas series object. Drawdowns measure simultaneously 6 | the maximum amount of capital that has been lost in the course of a strategy in 7 | addition to the amount of time elapsed since the strategy was able to recover 8 | from losses. As a rule of thumb, strategies with large (more than ten percent) 9 | or lengthy (more than four months) drawdowns will not have large Sharpe ratios. 10 | """ 11 | import pandas as pd 12 | 13 | 14 | def compute_drawdowns(equity_curve, return_max=True): 15 | """Computes the drawdown time-series for a provided equity curve. Returns 16 | both the drawdowns and their durations (or the maximums thereof). 17 | 18 | Parameters 19 | ---------- 20 | equity_curve: Pandas data frame. 21 | A Pandas data frame whose index is a time series of dates corresponding 22 | to trading days over the course of a historical time period. 23 | return_max (Optional): Boolean. 24 | A boolean indicator for whether or not the time series of the drawdown 25 | is returned or if only the maximums for the series should be returned. 26 | """ 27 | # Calculate the cumulative returns curve and set up the high water mark. 28 | # Then create the drawdown and duration series. 29 | hwm = equity_curve.cummax() 30 | drawdown = (hwm - equity_curve) / hwm 31 | eq_idx = equity_curve.index 32 | duration = pd.Series(index=eq_idx) 33 | 34 | # Loop over the index range. 35 | for date_ind in range(len(eq_idx)): 36 | duration.ix[date_ind] = ( 37 | 0 if drawdown.ix[date_ind] == 0 38 | else duration.ix[date_ind - 1] + 1 39 | ) 40 | 41 | if return_max: 42 | return drawdown.max(), duration.max() 43 | else: 44 | return drawdown, duration 45 | -------------------------------------------------------------------------------- /odin/events/event_types/fill_event.py: -------------------------------------------------------------------------------- 1 | from .portfolio_event import PortfolioEvent 2 | from ...utilities.params import Events, IOFiles 3 | 4 | 5 | class FillEvent(PortfolioEvent): 6 | """Fill Event Class 7 | 8 | Encapsulates the notion of a filled order, as returned from a brokerage. 9 | Stores the quantity of an instrument actually filled and at what price. In 10 | addition, stores the commission of the trade from the brokerage. 11 | """ 12 | def __init__( 13 | self, symbol, quantity, trade_type, direction, fill_cost, 14 | commission, datetime, portfolio_id, is_live 15 | ): 16 | """Initialize parameters of the fill event object.""" 17 | super(FillEvent, self).__init__( 18 | symbol, trade_type, direction, Events.fill, datetime, portfolio_id 19 | ) 20 | self.quantity = quantity 21 | self.fill_cost = fill_cost 22 | self.commission = commission 23 | self.price = self.fill_cost / self.quantity 24 | self.is_live = is_live 25 | 26 | def __str__(self): 27 | """String representation of the fill event object.""" 28 | return "{}\t{}\t{}\t{}\t{}\t{}\t{:0.2f}".format( 29 | self.event_type, 30 | self.datetime.strftime(IOFiles.date_format.value), 31 | self.symbol, 32 | self.direction, 33 | self.trade_type, 34 | self.quantity, 35 | self.fill_cost 36 | ) 37 | 38 | @classmethod 39 | def from_order_event(cls, order_event, fill_cost, commission, is_live): 40 | """Create a fill event object from the corresponding information in an 41 | order event object. 42 | """ 43 | return cls( 44 | order_event.symbol, 45 | order_event.quantity, 46 | order_event.trade_type, 47 | order_event.direction, 48 | fill_cost, 49 | commission, 50 | order_event.datetime, 51 | order_event.portfolio_id, 52 | is_live 53 | ) 54 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/database/database_data_handler.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import numpy as np 3 | from odin_securities.queries import gets 4 | from ....events import MarketEvent 5 | from .abstract_database_data_handler import AbstractDatabaseDataHandler 6 | from ..price_handler import DatabasePriceHandler 7 | 8 | class DatabaseDataHandler(AbstractDatabaseDataHandler): 9 | """Database Data Handler Class 10 | """ 11 | def __init__( 12 | self, events, symbol_handler, start_date, end_date, n_init 13 | ): 14 | """Initialize parameters of the database data handler object.""" 15 | price_handler = DatabasePriceHandler() 16 | # Determine which are valid trading days and set the start and end 17 | # period of the backtest. 18 | self.sessions = gets.standard_sessions( 19 | start_date, end_date 20 | )["datetime"] 21 | self.start_date = self.sessions.iloc[0] 22 | self.end_date = self.sessions.iloc[-1] 23 | self.yield_dates = self.__yield_dates() 24 | # Call the super method to initialize the remaining parameters. 25 | super(DatabaseDataHandler, self).__init__( 26 | events, symbol_handler, price_handler, n_init, self.start_date 27 | ) 28 | 29 | def __yield_dates(self): 30 | """Create an iterator over the dates contained in the historical 31 | download. 32 | """ 33 | for date in self.sessions: 34 | yield date 35 | 36 | def request_prices(self): 37 | """Implementation of abstract base class method.""" 38 | try: 39 | # TODO: Check if it is possible to reuse the old selected variable 40 | # from the update method. 41 | selected = self.symbol_handler.select_symbols(self.current_date) 42 | self.current_date = next(self.yield_dates) 43 | self.prices = self.price_handler.request_prices( 44 | self.current_date, selected 45 | ) 46 | except StopIteration: 47 | self.continue_trading = False 48 | else: 49 | self.events.put(MarketEvent(self.current_date)) 50 | -------------------------------------------------------------------------------- /examples/notebooks/etf_cointegration/strategy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from odin.strategy import AbstractStrategy 4 | from odin.strategy.templates import BuyAndHoldStrategy 5 | from odin.utilities.params import Directions 6 | from odin.utilities.mixins.strategy_mixins import ( 7 | EqualBuyProportionMixin, 8 | TotalSellProportionMixin, 9 | NeverSellIndicatorMixin, 10 | DefaultPriorityMixin 11 | ) 12 | 13 | 14 | class CointegratedETFStrategy( 15 | EqualBuyProportionMixin, 16 | TotalSellProportionMixin, 17 | NeverSellIndicatorMixin, 18 | DefaultPriorityMixin 19 | ): 20 | def compute_direction(self, feats): 21 | """Implementation of abstract base class method.""" 22 | if feats.name == "ARNC": 23 | if feats["z-score"] < -1.5: 24 | return Directions.long_dir 25 | elif feats["z-score"] > 1.5: 26 | return Directions.short_dir 27 | elif feats.name == "UNG": 28 | if feats["z-score"] < -1.5: 29 | return Directions.short_dir 30 | elif feats["z-score"] > 1.5: 31 | return Directions.long_dir 32 | 33 | def buy_indicator(self, feats): 34 | """Implementation of abstract base class method.""" 35 | return ( 36 | feats["z-score"] < -1.5 or 37 | feats["z-score"] > 1.5 38 | ) 39 | 40 | def exit_indicator(self, feats): 41 | """Implementation of abstract base class method.""" 42 | pos = self.portfolio.portfolio_handler.filled_positions["ARNC"] 43 | d = pos.direction 44 | return ( 45 | (feats["z-score"] > -0.5 and d == Directions.long_dir) or 46 | (feats["z-score"] < 0.5 and d == Directions.short_dir) 47 | ) 48 | 49 | def generate_features(self): 50 | """Implementation of abstract base class method.""" 51 | bars = self.portfolio.data_handler.bars.ix[:, -15:, :] 52 | prices = bars["adj_price_close"] 53 | weights = np.array([1.0, -1.]) 54 | feats = pd.DataFrame(index=bars.minor_axis) 55 | ts = prices.dot(weights) 56 | feats["z-score"] = (ts.ix[-1] - ts.mean()) / ts.std() 57 | return feats 58 | -------------------------------------------------------------------------------- /odin/fund/simulated_fund.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from .fund import Fund 3 | from .. import metrics 4 | 5 | 6 | class SimulatedFund(Fund): 7 | """Simulated Fund Class 8 | 9 | The simulated fund class makes it easier to compile all of the performance 10 | metrics for a collection of backtested portfolios. In particular, all of the 11 | metrics across the individual portfolios, as well as for the fund as a 12 | whole, are placed into a single pandas data frame. 13 | """ 14 | def __init__( 15 | self, data_handler, execution_handler, fund_handler, 16 | verbosity_level=0 17 | ): 18 | """Initialize parameters of the simulated fund object.""" 19 | super(SimulatedFund, self).__init__( 20 | data_handler, execution_handler, fund_handler, 0, verbosity_level 21 | ) 22 | 23 | def performance_summary(self): 24 | """Construct performance summaries for each of the strategies utilized 25 | in the simulated fund as well as the aggregation of the individual 26 | portfolios (i.e. the performance summary of the fund). This allows us to 27 | compare and contrast the drawdowns, the Sharpe ratios, and the returns 28 | for individual portfolios and the fund. 29 | """ 30 | # Initialize data frames for the metrics of the portfolios and the fund 31 | # and the equity and number of positions on a day-by-day basis. 32 | m = pd.DataFrame() 33 | self.history = pd.DataFrame( 34 | index=self.fund_handler.portfolios[0].history.states.keys(), 35 | columns=["equity", "n_positions"] 36 | ).fillna(0.0) 37 | 38 | # Append the performance summary of each portfolio. 39 | for p in self.fund_handler.portfolios: 40 | m = m.append(p.performance_summary()) 41 | self.history["equity"] += p.history.equity 42 | self.history["n_positions"] += p.history.n_positions 43 | 44 | # Compute the performance of the fund as a aggregation of the individual 45 | # portfolios. 46 | self.history["returns"] = self.history["equity"].pct_change() 47 | m = m.append(metrics.performance_summary( 48 | self.history, self.fund_handler.fund_id 49 | )) 50 | 51 | return m 52 | -------------------------------------------------------------------------------- /odin/events/events_queue.py: -------------------------------------------------------------------------------- 1 | from queue import PriorityQueue 2 | from ..utilities import params 3 | 4 | 5 | class EventsQueue(PriorityQueue): 6 | """Events Queue Class 7 | 8 | The events queue handles the order in which events are processed by Odin. 9 | Events are first processed according to their intrinsic priority and then 10 | according to the order in which the events were placed into the queue. This 11 | second means of prioritization is used to enforce the idea that, for 12 | example, signal events placed into the queue before other signal events are 13 | inherently more desirable than signal events placed into the queue later. 14 | """ 15 | def __init__(self): 16 | """Initialize parameters of the events queue.""" 17 | super(EventsQueue, self).__init__() 18 | self.count = 0 19 | 20 | def put(self, event): 21 | """Place an event into the events queue. An item is placed into the 22 | priority queue using the current count of value. This allows the order 23 | of events to be preserved within the priority queue without sacrificing 24 | the priority inherent to event types. 25 | 26 | Parameters 27 | ---------- 28 | event: An event object. 29 | The event object to be placed into the events queue. This can be one 30 | of a market, signal, order, fill, manage, or rebalance event. 31 | """ 32 | # Extract the priority for the event. We also differentiate between buy 33 | # and sell event types in the case of a signal event. 34 | p = params.priority_dict[event.event_type] 35 | if event.event_type == params.Events.signal: 36 | p = p[event.trade_type] 37 | 38 | super(EventsQueue, self).put((p, self.count, event)) 39 | self.count += 1 40 | 41 | def get(self, *args, **kwargs): 42 | """Retrieve an object from the events queue. An object is retrieved from 43 | the events queue and the count of objects of that type in the events 44 | queue is decremented. 45 | """ 46 | priority, count, event = super(EventsQueue, self).get(*args, **kwargs) 47 | return event 48 | 49 | def clear(self): 50 | """Remove all events from the events queue.""" 51 | while not self.empty(): 52 | self.get(False) 53 | 54 | -------------------------------------------------------------------------------- /odin/portfolio/components/portfolio_state.py: -------------------------------------------------------------------------------- 1 | from ...utilities.mixins import EquityMixin 2 | 3 | 4 | class PortfolioState(EquityMixin): 5 | """Portfolio State Class 6 | 7 | The portfolio state objects captures all the elements of the portfolio at a 8 | particular instance in time. In particular, it aggregates the current free 9 | capital of the portfolio as well as all of the positions that are presently 10 | held. 11 | 12 | The equity of the portfolio is computed directly from the specified capital 13 | and the current value of the positions. 14 | 15 | Parameters 16 | ---------- 17 | capital: Float. 18 | The amount of capital (measured in USD) presently held by the portfolio. 19 | filled_positions: Dictionary. 20 | A dictionary of filled position objects mapping symbols to the 21 | corresponding fill representation. 22 | maximum_capacity: Integer. 23 | An integer representing the maximum number of filled positions that can 24 | be held by a portfolio at any given time. 25 | symbols: List. 26 | A list of symbols for which the data handler associated with the 27 | portfolio streamed bar data. 28 | portfolio_id: String. 29 | A unique identifier assigned to the portfolio. 30 | """ 31 | def __init__( 32 | self, capital, filled_positions, maximum_capacity, portfolio_id 33 | ): 34 | """Initialize parameters of the portfolio state object.""" 35 | self.capital = capital 36 | self.filled_positions = filled_positions 37 | self.maximum_capacity = maximum_capacity 38 | self.portfolio_id = portfolio_id 39 | 40 | def __str__(self): 41 | """String representation of the portfolio state object. 42 | 43 | This representation is used generally for making a human-readable text 44 | of the status of the portfolio at a given time. 45 | """ 46 | buf = "-" * 46 + "\n" 47 | head = "\n" + "=" * (len(buf) - 1) + "\n" 48 | s = head + "Portfolio:\t" + self.portfolio_id + "\n" 49 | s += buf + "Equity:\t\t{:0.2f}\nCapital:\t{:0.2f}\n".format( 50 | self.equity, self.capital 51 | ) 52 | s += buf 53 | for pos in self.filled_positions.values(): 54 | s += str(pos) + "\n" 55 | s += buf 56 | 57 | return s 58 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/database/abstract_database_data_handler.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import numpy as np 3 | from abc import ABCMeta 4 | from odin_securities.queries import gets 5 | from ....events import MarketEvent 6 | from ..abstract_data_handler import AbstractDataHandler 7 | 8 | 9 | class AbstractDatabaseDataHandler(AbstractDataHandler): 10 | """Abstract Database Data Handler Class 11 | 12 | This abstract class integrates the data handler objects with Odin's 13 | securities database, allowing them to query price data. In particular it 14 | supports obtaining the initial set of bars and then subsequently updating 15 | those bars for assets specified by the symbol handler. 16 | """ 17 | __metaclass__ = ABCMeta 18 | 19 | def __init__( 20 | self, events, symbol_handler, price_handler, n_init, current_date 21 | ): 22 | """Initialize parameters of the abstract database data handler object. 23 | """ 24 | super(AbstractDatabaseDataHandler, self).__init__( 25 | events, symbol_handler, price_handler 26 | ) 27 | self.n_init = n_init 28 | self.current_date = current_date 29 | # Download initial bar data for the database data handler. This is 30 | # necessary to allow trading on the first day of sessions; otherwise, 31 | # we would not have data in time. 32 | self.__query_initial_bars() 33 | 34 | def __query_initial_bars(self): 35 | """Download an appropriate amount of initial data to allow the trading 36 | algorithm to begin at the beginning of the historical time-frame. This 37 | avoids needlessly having to wait for the algorithm to obtain a 'critical 38 | mass' of data before executing trades. 39 | """ 40 | end_date = self.current_date - dt.timedelta(days=1) 41 | start_date = end_date - dt.timedelta(days=self.n_init) 42 | self.bars = gets.prices(start_date, end_date) 43 | selected = self.symbol_handler.select_symbols(self.bars.major_axis[-1]) 44 | self.bars.drop( 45 | [s for s in self.bars.minor_axis if s not in selected], 46 | axis=2, inplace=True 47 | ) 48 | 49 | def update(self): 50 | """Implementation of abstract base class method.""" 51 | # N.B.: When update is called after all of the time period's events have 52 | # been processed, the bars are changed! 53 | selected = self.symbol_handler.select_symbols(self.current_date) 54 | self.bars = gets.prices( 55 | self.current_date - dt.timedelta(days=self.n_init), 56 | self.current_date, selected 57 | ) 58 | 59 | -------------------------------------------------------------------------------- /odin/handlers/execution_handler/simulated_execution_handler.py: -------------------------------------------------------------------------------- 1 | from .abstract_execution_handler import AbstractExecutionHandler 2 | from ...events import FillEvent 3 | from ...utilities import params 4 | 5 | 6 | class SimulatedExecutionHandler(AbstractExecutionHandler): 7 | """Simulated Execution Handler Class 8 | 9 | The simulated execution handler simply converts all order objects into their 10 | equivalent fill objects automatically without latency, slippage or 11 | fill-ratio issues. 12 | 13 | This allows a straightforward 'first go' test of any strategy, before 14 | implementation with a more sophisticated execution handler. The simulated 15 | execution handler incorporates both commission fees as well as transaction 16 | costs incurred by simply interacting with (buying or selling) an asset. The 17 | assumed commission for a one-directional trade with IB is $1.00. 18 | """ 19 | def __init__(self, data_handler, transaction_cost=0.0005): 20 | """Initialize parameters of the simulated execution handler object.""" 21 | super(SimulatedExecutionHandler, self).__init__( 22 | data_handler.events, False 23 | ) 24 | self.data_handler = data_handler 25 | self.transaction_cost = transaction_cost 26 | 27 | def execute_order(self, order_event): 28 | """Implementation of abstract base class method.""" 29 | # Construct quantities for the simulated fill event. 30 | symbol = order_event.symbol 31 | quantity = order_event.quantity 32 | action = params.action_dict[ 33 | (order_event.direction, order_event.trade_type) 34 | ] 35 | 36 | # Assume the fill price to be the average of the day's high and low and 37 | # incorporate transaction costs. If the action is to sell the asset, 38 | # then price moves down, otherwise for buying actions, the price is 39 | # driven upward. 40 | price_id = [ 41 | params.PriceFields.sim_low_price.value, 42 | params.PriceFields.sim_high_price.value 43 | ] 44 | fill_price = self.data_handler.prices.ix[price_id, 0, symbol].mean() 45 | fill_cost = fill_price * quantity 46 | # Change cost according to whether or not shares are bought or sold. 47 | # This is distinct from trade type. 48 | if action == params.Actions.sell: 49 | fill_cost *= (1. - self.transaction_cost) 50 | elif action == params.Actions.buy: 51 | fill_cost *= (1. + self.transaction_cost) 52 | 53 | # Place the fill event in the queue. 54 | commission = params.ib_commission(quantity, fill_price) 55 | fill_event = FillEvent.from_order_event( 56 | order_event, fill_cost, commission, self.is_live 57 | ) 58 | self.events.put(fill_event) 59 | -------------------------------------------------------------------------------- /odin/portfolio/components/portfolio_history.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from copy import deepcopy 3 | from collections import OrderedDict 4 | 5 | 6 | class PortfolioHistory(object): 7 | """Portfolio History Class 8 | 9 | The portfolio history class keeps track of historical portfolio states as in 10 | a time-series. This is achieved by copying the state of the portfolio on a 11 | known date into a dictionary where the keys are dates that have been ordered 12 | temporally. 13 | 14 | This allows us to iterate over the dates in a logical order as we track 15 | changes in portfolio equity, positions, and capital. 16 | 17 | Parameters 18 | ---------- 19 | portfolio_id: String. 20 | A unique identifier assigned to the portfolio. 21 | """ 22 | def __init__(self, portfolio_id): 23 | """Initialize parameters of the portfolio history object.""" 24 | self.portfolio_id = portfolio_id 25 | self.states = OrderedDict() 26 | 27 | def add_state(self, date, portfolio_state): 28 | """Update the dictionary of portfolio states ordered by dates by 29 | appending the latest entry. 30 | 31 | Parameters 32 | ---------- 33 | date: A datetime object. 34 | The date at which to record the current state of the portfolio. 35 | portfolio_state: Portfolio state object. 36 | A portfolio state object which will be archived as the state of the 37 | portfolio on the provided date. In order to avoid memory problems, a 38 | copy of the state is made. 39 | """ 40 | self.states[date] = deepcopy(portfolio_state) 41 | 42 | def compute_attributes(self): 43 | """Compute the historical time-series attribute curves for the 44 | portfolio. These curves are in particular: 45 | 1. A historical record of the aggregate equity of the portfolio, on 46 | a time period by time period basis. 47 | 2. A historical record of how many positions there were in each time 48 | period. 49 | 3. The historical returns for each time period. 50 | """ 51 | # Create pandas series objects for the historical equity and number of 52 | # positions. The series for the returns will be computed later. 53 | self.equity = pd.Series() 54 | self.n_positions = pd.Series() 55 | # Iterate over each time period in the ordered dictionary of portfolio 56 | # states. 57 | for date in self.states: 58 | state = self.states[date] 59 | self.n_positions.ix[date] = len(state.filled_positions) 60 | self.equity.ix[date] = state.equity 61 | 62 | # Compute the returns in each time period directly from the change in 63 | # portfolio equity. 64 | self.returns = self.equity.pct_change() 65 | -------------------------------------------------------------------------------- /odin/portfolio/simulated_portfolio.py: -------------------------------------------------------------------------------- 1 | from .abstract_portfolio import AbstractPortfolio 2 | from .components import PortfolioHistory 3 | from .. import metrics 4 | 5 | 6 | class SimulatedPortfolio(AbstractPortfolio): 7 | """Simulated Portfolio Class 8 | 9 | The simulated portfolio is a implementation of the abstract portfolio class 10 | specifically for historical backtesting purposes. Specifically, whenever 11 | historical bar data is streamed, the value of our positions in the market is 12 | updated using the value of the underlying asset at the end of the time 13 | period. 14 | 15 | The simulated portfolio also records the historical equity states of the 16 | portfolio for the purposes of computing performance metrics of a backtest. 17 | 18 | Parameters 19 | ---------- 20 | data_handler: Object inheriting from the abstract data handler class. 21 | This object supplies the data to update the prices of held positions and 22 | provide new bar data with which to construct features. 23 | position_handler: Object inheriting from the abstract position handler 24 | class. 25 | The position handler determines how much of an asset to acquire or to 26 | relinquish when trading signals are processed into orders. 27 | portfolio_handler: Portfolio handler object. 28 | The portfolio handler keeps track of positions that are currently held 29 | by the portfolio as well as the current amount of equity and capital in 30 | the portfolio. 31 | """ 32 | def __init__(self, data_handler, position_handler, portfolio_handler): 33 | """Initialize parameters of the simulated portfolio object.""" 34 | super(SimulatedPortfolio, self).__init__( 35 | data_handler, position_handler, portfolio_handler 36 | ) 37 | self.history = PortfolioHistory(self.portfolio_handler.portfolio_id) 38 | 39 | def process_post_events(self): 40 | """Implementation of abstract base class method.""" 41 | # Update the portfolio history of positions, capital, and equity. 42 | date = self.data_handler.current_date 43 | self.history.add_state(date, self.portfolio_handler.state) 44 | 45 | def performance_summary(self): 46 | """Compute the performance summary for the portfolio over the period of 47 | performance. This reports common performance metrics based on the 48 | equity, returns, and holdings of the portfolio at each instance in the 49 | time-series. 50 | 51 | Returns 52 | ------- 53 | summary: Pandas data frame object. 54 | A summary of the key performance parameters of the portfolio in the 55 | designated time period. 56 | """ 57 | self.history.compute_attributes() 58 | return metrics.performance_summary( 59 | self.history, self.portfolio_handler.portfolio_id 60 | ) 61 | -------------------------------------------------------------------------------- /odin/handlers/data_handler/price_handler/interactive_brokers_price_handler.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import pandas as pd 3 | import datetime as dt 4 | from time import sleep 5 | from ib.opt import ibConnection, message 6 | from ib.ext.TickType import TickType 7 | from .abstract_price_handler import AbstractPriceHandler 8 | from ....utilities.params import IB, PriceFields 9 | from ....utilities.mixins import ContractMixin 10 | 11 | 12 | class InteractiveBrokersPriceHandler(AbstractPriceHandler, ContractMixin): 13 | """Interactive Brokers Price Handler Class""" 14 | 15 | def __init__(self): 16 | """Initialize parameters of the Interactive Brokers price handler 17 | object. 18 | """ 19 | super(InteractiveBrokersPriceHandler, self).__init__() 20 | self.conn = ibConnection( 21 | clientId=IB.data_handler_id.value, port=IB.port.value 22 | ) 23 | self.conn.register(self.__tick_price_handler, message.tickPrice) 24 | if not self.conn.connect(): 25 | raise ValueError( 26 | "Odin was unable to connect to the Trader Workstation." 27 | ) 28 | 29 | # Set the target field to download data from. 30 | today = dt.datetime.today() 31 | open_t, close_t = dt.time(9, 30), dt.time(16) 32 | cur_t = today.time() 33 | # If today is a weekday and the timing is correct, then we use the most 34 | # recently observed price. Otherwise we use the close price. 35 | if today.weekday() < 5 and cur_t >= open_t and cur_t <= close_t: 36 | self.field = TickType.LAST 37 | else: 38 | self.field = TickType.CLOSE 39 | 40 | # Initialize a pandas panel to store the price data. 41 | self.bar = pd.Panel(items=[PriceFields.current_price.value]) 42 | 43 | def __tick_price_handler(self, msg): 44 | """Handle incoming prices from the Trader Workstation.""" 45 | if msg.field == self.field: 46 | # The fourth field corresponds to the most recent observed price. 47 | # This price is recorded. The nineth field corresponds to the close 48 | # price. 49 | tick_id = int(msg.tickerId) 50 | price = float(msg.price) 51 | self.bar.ix[0, 0, tick_id] = price 52 | 53 | def request_prices(self, current_date, symbols): 54 | """Implementation of abstract base class method.""" 55 | # Reset the bar object for the latest assets requested. 56 | self.bar = pd.Panel( 57 | items=[PriceFields.current_price.value], major_axis=[current_date], 58 | minor_axis=symbols 59 | ) 60 | # Issue requests to Interactive Brokers for the latest price data of 61 | # each asset in the list of bars. 62 | for i, s in enumerate(symbols): 63 | c = self.create_contract(s) 64 | self.conn.reqMktData(i, c, "", True) 65 | 66 | # Wait a moment. 67 | sleep(0.5) 68 | 69 | return self.bar 70 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/abstract_position_handler.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from ...utilities.params import TradeTypes, PriceFields 3 | 4 | 5 | class AbstractPositionHandler(object): 6 | """Abstract Position Handler Class 7 | 8 | The abstract position handler class determines the size of the order to 9 | place according to a specified criterion. 10 | """ 11 | __metaclass__ = ABCMeta 12 | 13 | def __init__(self, data_handler): 14 | """Initialize parameters of the abstract position handler object.""" 15 | self.order_sizer = { 16 | TradeTypes.buy_trade: self.buy_size_order, 17 | TradeTypes.sell_trade: self.sell_size_order, 18 | TradeTypes.exit_trade: self.exit_size_order 19 | } 20 | self.data_handler = data_handler 21 | 22 | @abstractmethod 23 | def compute_buy_weights(self, signal_event, portfolio_handler): 24 | """Compute the proportion of equity to spend on the provided signal 25 | event when buying into a position. 26 | """ 27 | raise NotImplementedError() 28 | 29 | @abstractmethod 30 | def compute_sell_weights(self, signal_event, portfolio_handler): 31 | """Compute the proportion of equity to spend on the provided signal 32 | event when selling out of a position. 33 | """ 34 | raise NotImplementedError() 35 | 36 | def buy_size_order(self, signal_event, portfolio_handler): 37 | """This method will be called to determine the number of shares that 38 | should be bought into when entering a long or short position. 39 | """ 40 | # Get the current amount of spendable capital as well as the current 41 | # amount of equity in the portfolio. 42 | capital, equity = portfolio_handler.capital, portfolio_handler.equity 43 | # Compute the amount that should be spent on this position as well as an 44 | # estimate of the cost of an individual share of the underlying asset. 45 | symbol = signal_event.symbol 46 | weights = self.compute_buy_weights(signal_event, portfolio_handler) 47 | spend = min(equity * weights[symbol], capital) 48 | 49 | try: 50 | cost = self.data_handler.prices.ix[ 51 | PriceFields.current_price.value, 0, symbol 52 | ] 53 | except KeyError: 54 | # If we can't find a cost of the asset, then don't try to buy any. 55 | return 0 56 | else: 57 | # Compute the number of shares. 58 | return max(int(spend / cost), 0) 59 | 60 | def sell_size_order(self, signal_event, portfolio_handler): 61 | """This method determines the number of shares to liquidate when selling 62 | a position. 63 | """ 64 | symbol = signal_event.symbol 65 | weights = self.compute_sell_weights(signal_event, portfolio_handler) 66 | pos = portfolio_handler.filled_positions[symbol] 67 | return int(weights[symbol] * pos.quantity) 68 | 69 | def exit_size_order(self, signal_event, portfolio_handler): 70 | """This method liquidates the entire position.""" 71 | symbol = signal_event.symbol 72 | try: 73 | val = portfolio_handler.filled_positions[symbol].quantity 74 | except KeyError: 75 | val = 0 76 | finally: 77 | return val 78 | -------------------------------------------------------------------------------- /odin/utilities/finance/modern_portfolio_theory/markowitz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from cvxopt import matrix 4 | from cvxopt.blas import dot 5 | from cvxopt.solvers import qp, options 6 | options["show_progress"] = False 7 | 8 | 9 | def solve_markowitz(prices, omega=1.0): 10 | """This method solves the modern portfolio theory quadratic program to 11 | deduce the optimal weight configuration. Subject to the constraint that the 12 | weights be non-negative and that they sum to one, modern portfolio theory 13 | seeks to minimize risk for a specified level of return. This can be 14 | equivalently interpreted as a trade-off between variance and risk level, 15 | with the ultimate criterion to be optimized being the reward-to-risk ratio, 16 | commonly known as the Sharpe ratio. 17 | 18 | A common criticism of modern portfolio theory is that it is susceptible to 19 | investing all of the liquidity into a single asset. This problem is present 20 | in this implementation as well, though it can be mitigated slightly via the 21 | upper bound parameter omega. 22 | 23 | Parameters 24 | ---------- 25 | prices: Pandas DataFrame. 26 | A DataFrame object containing the historical (adjusted) prices for 27 | specific assets. Assets that do not have the full range of data due to 28 | becoming recently listed on the exchange are dropped and ignored. 29 | omega (optional): Float. 30 | A value between zero and one corresponding to the upper limit on the 31 | proportion of capital that can be placed into any one asset. 32 | """ 33 | # Number of assets. 34 | pct = prices.pct_change().ix[1:].dropna(axis=1) 35 | n = pct.shape[1] 36 | # Assert that the upper bound on the allocation of an individual asset is 37 | # not too low. 38 | if omega < 1. / n: 39 | omega = 1.0 40 | 41 | # Compute mean and covariance of the historical percentage change of the 42 | # stock prices. 43 | mu = pct.mean() 44 | mu_mat = matrix(mu) 45 | S = pct.cov().values 46 | S_mat = matrix(S) 47 | 48 | # Inequality constraints. These constrain the weights to be non-negative. 49 | G = matrix(np.vstack((-np.eye(n), np.eye(n)))) 50 | h = matrix(0.0, (2*n, 1)) 51 | h[n:] = omega 52 | 53 | # Equality constraints. These constrain the weights to sum to unity. 54 | A = matrix(1.0, (1, n)) 55 | b = matrix(1.0) 56 | 57 | # Number of intermittent values of the weight assigned to covariance to try. 58 | N = 100 59 | # Vector in which to store the results of the quadratic program. We store 60 | # not only the weights, but also the corresponding expected return and 61 | # variance of the portfolio. 62 | X = np.empty((N, n+2)) 63 | X[:] = np.nan 64 | 65 | for i, m in enumerate(np.logspace(-1, 4, num=N)): 66 | try: 67 | res = qp(matrix(m*S_mat), -mu_mat, G, h, A, b) 68 | except ValueError: 69 | continue 70 | else: 71 | x = np.abs(np.array(res["x"]).ravel()) 72 | 73 | if res["status"] == "optimal": 74 | X[i, :n] = x 75 | X[i, n] = mu.dot(x) 76 | X[i, n+1] = np.sqrt(x.dot(S).dot(x)) 77 | 78 | # Compute the risk-to-reward ratio for each value of the expected return and 79 | # determine the maximum. 80 | X = X[~np.isnan(X).any(axis=1)] 81 | sharpe = X[:, n+1] / X[:, n] 82 | opt = X[sharpe.argmax()] 83 | cols = list(pct.columns) 84 | cols.extend(["reward", "risk"]) 85 | ret = {c: opt[i] for i, c in enumerate(cols)} 86 | 87 | # Return the weights, the expected reward, and the risk as a Pandas series. 88 | return ret 89 | -------------------------------------------------------------------------------- /odin/tests/test_position.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime as dt 3 | from odin.handlers.position_handler.position import FilledPosition 4 | from odin.utilities import params 5 | 6 | class TestPosition(unittest.TestCase): 7 | def test_to_database_position(self): 8 | s = "SPY" 9 | q = 100 10 | d = params.Directions.long_dir 11 | t = params.TradeTypes.buy_trade 12 | a = params.action_dict[(d, t)] 13 | pid = "test_portfolio_id" 14 | date = dt.datetime.today() 15 | price = 100.0 16 | update_price = 101.0 17 | pos = FilledPosition(s, d, t, pid, date, price) 18 | pos.transact_shares(a, q, price) 19 | pos.to_database_position() 20 | 21 | def test_from_database_position(self): 22 | s = "SPY" 23 | pid = "test_portfolio_id" 24 | pos = FilledPosition.from_database_position(pid, s) 25 | self.assertEqual(pos.avg_price, 100.01) 26 | self.assertEqual(pos.portfolio_id, pid) 27 | self.assertEqual(pos.quantity, 100) 28 | self.assertEqual(pos.direction, params.Directions.long_dir) 29 | self.assertEqual(pos.trade_type, params.TradeTypes.buy_trade) 30 | 31 | def test_long_position(self): 32 | s = "GOOG" 33 | q = 100 34 | d = params.Directions.long_dir 35 | t = params.TradeTypes.buy_trade 36 | a = params.action_dict[(d, t)] 37 | pid = "test_portfolio_id" 38 | date = dt.datetime.today() 39 | price = 100.0 40 | update_price = 101.0 41 | pos = FilledPosition(s, d, t, pid, date, price) 42 | pos.transact_shares(a, q, price) 43 | pos.update_market_value(update_price) 44 | self.assertEqual( 45 | pos.percent_pnl, 46 | 1 + (pos.market_value - pos.cost_basis) / pos.cost_basis 47 | ) 48 | self.assertEqual(pos.quantity, q) 49 | self.assertEqual(pos.market_value, 10100.0) 50 | self.assertEqual(pos.unrealized_pnl, 99.0) 51 | self.assertEqual(pos.tot_commission, 1.0) 52 | 53 | sell_price = 100.5 54 | pos.transact_shares(params.Actions.sell, q // 2, sell_price) 55 | self.assertEqual(pos.quantity, q // 2) 56 | self.assertEqual(pos.realized_pnl, 48.0) 57 | self.assertEqual(pos.unrealized_pnl, 24.5) 58 | self.assertEqual(pos.tot_commission, 2.0) 59 | 60 | sell_price = 101.0 61 | pos.transact_shares(params.Actions.sell, q // 2, sell_price) 62 | self.assertEqual(pos.quantity, 0) 63 | self.assertEqual(pos.realized_pnl, 72.0) 64 | self.assertEqual(pos.unrealized_pnl, 0.) 65 | self.assertEqual(pos.tot_commission, 3.0) 66 | 67 | def test_short_position(self): 68 | s = "GOOG" 69 | q = 100 70 | d = params.Directions.short_dir 71 | t = params.TradeTypes.buy_trade 72 | a = params.action_dict[(d, t)] 73 | pid = "test_portfolio_id" 74 | date = dt.datetime.today() 75 | price = 100.0 76 | update_price = 101.0 77 | pos = FilledPosition(s, d, t, pid, date, price) 78 | pos.transact_shares(a, q, price) 79 | pos.update_market_value(update_price) 80 | self.assertEqual( 81 | pos.percent_pnl, 82 | 1 - (pos.market_value - pos.cost_basis) / pos.cost_basis 83 | ) 84 | self.assertEqual(pos.quantity, q) 85 | self.assertEqual(pos.market_value, -10100.0) 86 | self.assertEqual(pos.unrealized_pnl, -101.0) 87 | self.assertEqual(pos.tot_commission, 1.0) 88 | 89 | buy_price = 100.5 90 | pos.transact_shares(params.Actions.buy, q // 2, buy_price) 91 | self.assertEqual(pos.quantity, q // 2) 92 | self.assertEqual(pos.realized_pnl, -52.0) 93 | self.assertEqual(pos.unrealized_pnl, -25.5) 94 | self.assertEqual(pos.tot_commission, 2.0) 95 | 96 | buy_price = 101.0 97 | pos.transact_shares(params.Actions.buy, q // 2, buy_price) 98 | self.assertEqual(pos.quantity, 0) 99 | self.assertEqual(pos.realized_pnl, -78.0) 100 | self.assertEqual(pos.unrealized_pnl, 0.) 101 | self.assertEqual(pos.tot_commission, 3.0) 102 | 103 | 104 | if __name__ == "__main__": 105 | unittest.main() 106 | 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odin 1.3 2 | 3 | A algorithmic trading platform developed in Python. The platform is built to support both research-driven backtesting as well as production deployment for live trading. Odin can be used for either live trading through Interactive Brokers or simulated backtesting. The software currently exists in an alpha state and is actively seeking developers to add features and fix programmatic errors. 4 | 5 | This software is provided under the MIT license. 6 | 7 | 8 | ## Features 9 | 10 | * **Backtesting** Odin was written with backtesting in mind and it is therefore possible to accurately evaluate strategies on historical data while correctly accounting for transaction costs and commission fees. 11 | * **Event-Driven Architecture** In order to interface more closely with a live-trading system, Odin uses an event-driven architecture rather than a vectorized one. This allows the same strategy implementations in Odin to be utilized in both simulated and live trading. 12 | * **Integration with IB** Odin is built to interface with Interactive Brokers (IB) to execute live trades. As such you can write strategies, backtest them, and deploy them to live trading, all within Odin. 13 | * **PostgreSQL Database** Odin integrates with its own PostgreSQL database to store information on symbols, price data vendors, historical prices, dividends, stock splits, and volumes. This allows data to be served to Odin directly from the filesystem rather than across an internet connection. Odin will also store positions created in live trading for compliance and verification purposes. 14 | * **Performance Metrics** Odin provides a number of performance metrics such as the Sharpe ratio, drawdown, and drawdown duration in order to characterize the performance of backtested portfolios. Odin will also compute measures of equity utilization, such as the average number of positions, to characterize idleness of funds. 15 | * **Fund Simulation** Odin can also simulate funds consisting of multiple portfolios trading their own strategies. For funds, Odin can also perform portfolio equity rebalancing to prevent the equity levels from becoming too lopsided toward a few strategies. Furthermore, Odin will also deduct management fees from the equity levels if so desired. 16 | * **Low-Level Control** Odin allows the user to wield low-level control over the way data is processed. Odin provides control over which ticker symbols are traded, how much equity is transacted in taking up a position, as well as abstract templates that allow users of the software to define their own handlers for use in trading. 17 | 18 | Please refer to `CONTRIBUTING.md` for instructions on how you can contribute to Odin yourself! 19 | 20 | ## Installation 21 | 22 | First, download Odin's source code from this repository. Then, to install the library from source for development purposes, execute the command: 23 | 24 | ``` 25 | sudo python3 setup.py install 26 | ``` 27 | 28 | Odin's and Odin Securities' requirements can be installed by navigating into the `odin` directory and then executing the command: 29 | 30 | ``` 31 | pip3 install -r requirements.txt 32 | ``` 33 | 34 | In addition to Odin itself, using the software will also require you to install Odin's accompanying database [Odin Securities](https://github.com/JamesBrofos/Odin-Securities/). Please refer to the referenced Github page for instructions on building and updating the Odin Securities database. 35 | 36 | 37 | ## Citations 38 | 39 | Many thanks go to Michael Halls-Moore for originally getting me interested in quantitative finance via his website [Quantstart](https://www.quantstart.com). Certain components of Odin were drawn originally (or heavily inspired by) his tutorial series on [how to build an event-driven backtester](https://www.quantstart.com/articles/Event-Driven-Backtesting-with-Python-Part-I) as well as his backtesting environment [QSTrader](https://github.com/mhallsmoore/qstrader). 40 | 41 | Please refer to the `LICENSE` file for information on copyright and rights granted to users of the software. 42 | 43 | 44 | ## Trading Disclaimer 45 | 46 | Trading equities on margin carries a high level of risk, and may not be suitable for all investors. Past performance is not indicative of future results. The high degree of leverage can work against you as well as for you. Before deciding to invest in equities you should carefully consider your investment objectives, level of experience, and risk appetite. The possibility exists that you could sustain a loss of some or all of your initial investment and therefore you should not invest money that you cannot afford to lose. You should be aware of all the risks associated with equities trading, and seek advice from an independent financial advisor if you have any doubts. 47 | -------------------------------------------------------------------------------- /odin/handlers/execution_handler/interactive_brokers_execution_handler.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from time import sleep 3 | from ib.opt import ibConnection, Connection, message 4 | from ib.ext.Contract import Contract 5 | from ib.ext.Order import Order 6 | from .abstract_execution_handler import AbstractExecutionHandler 7 | from ...utilities.params import action_dict, IB, ib_commission, ib_silent_errors 8 | from ...utilities.mixins import ContractMixin 9 | from ...events import FillEvent 10 | 11 | 12 | class InteractiveBrokersExecutionHandler( 13 | AbstractExecutionHandler, ContractMixin 14 | ): 15 | """Interactive Brokers Execution Handler 16 | 17 | The Interactive Brokers execution handler constructs pairs of contracts and 18 | orders to be sent to the brokerage. This execution handler is used in live 19 | trading to actually fill orders that are requested; it can, however, be used 20 | in either real or paper trading environments. The TraderWorkstation needs to 21 | be online before this module can function. 22 | """ 23 | def __init__(self, events): 24 | """Initialize parameters for the interactive brokers execution handler 25 | object. 26 | """ 27 | super(InteractiveBrokersExecutionHandler, self).__init__(events, True) 28 | self.conn = Connection.create( 29 | clientId=IB.execution_handler_id.value, port=IB.port.value 30 | ) 31 | self.conn.register(self.__error_handler, message.error) 32 | self.conn.registerAll(self.__reply_handler) 33 | if not self.conn.connect(): 34 | raise ValueError( 35 | "Odin was unable to connect to the Trader Workstation." 36 | ) 37 | self.orders = {} 38 | self.order_id = 1 39 | # It is important to sleep momentarily so that we can sync with the 40 | # latest order identifier from Interactive Brokers. 41 | sleep(1) 42 | 43 | def __error_handler(self, msg): 44 | """Process errors from the Interactive Brokers Trader Workstation by 45 | displaying them on standard out. 46 | """ 47 | if msg.errorCode not in ib_silent_errors: 48 | print("Interactive Brokers execution handler error: {}".format(msg)) 49 | 50 | def __reply_handler(self, msg): 51 | """This function handles responses from the TraderWorkstation server. It 52 | has two primary duties. The first of these is to determine the next 53 | valid trade identifier so that we know what identifier to assign to our 54 | orders when they are submitted to the brokerage. The second is to detect 55 | when an order has been filled as to place that fill event into the queue 56 | for processing by the portfolio. 57 | """ 58 | if msg.typeName == "nextValidId": 59 | self.order_id = int(msg.orderId) 60 | elif msg.typeName == "orderStatus" and msg.status == "Filled": 61 | o = self.orders[msg.orderId] 62 | if not o["filled"]: 63 | # Extract variables upon order fill. 64 | order_event = o["order"] 65 | fill_cost = msg.avgFillPrice * quantity 66 | commission = ib_commission(quantity, msg.avgFillPrice) 67 | # Place the fill event into the queue and mark the order as 68 | # completed. 69 | fill_event = FillEvent.from_order_event( 70 | order_event, 71 | fill_cost, 72 | commission, 73 | self.is_live 74 | ) 75 | o["filled"] = True 76 | self.events.put(fill_event) 77 | 78 | def create_order(self, action, quantity): 79 | """Create an Interactive Brokers order object. This specifies whether or 80 | not we are selling or buying the asset, the quantity to exchange, and 81 | the order type of the trade (which is assumed to be a market order). 82 | """ 83 | o = Order() 84 | o.m_orderType = "MKT" 85 | o.m_totalQuantity = quantity 86 | o.m_action = action 87 | return o 88 | 89 | def execute_order(self, order_event): 90 | """Implementation of abstract base class method.""" 91 | symbol = order_event.symbol 92 | quantity = order_event.quantity 93 | direction = order_event.direction 94 | trade_type = order_event.trade_type 95 | action = action_dict[(direction, trade_type)].value 96 | 97 | # Create orders and contracts for the trade to be submitted to the 98 | # brokerage. 99 | c = self.create_contract(symbol) 100 | o = self.create_order(action, quantity) 101 | self.orders[self.order_id] = { 102 | "filled": False, 103 | "order": order_event 104 | } 105 | self.conn.placeOrder(self.order_id, c, o) 106 | sleep(2) 107 | # Increment the order identifier for future trades. 108 | self.order_id += 1 109 | -------------------------------------------------------------------------------- /odin/portfolio/interactive_brokers_portfolio.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import pickle 3 | from time import sleep 4 | from email.mime.text import MIMEText 5 | from ib.opt import ibConnection, Connection, message 6 | from .abstract_portfolio import AbstractPortfolio 7 | from ..utilities.params import Directions, IB, ib_silent_errors 8 | 9 | 10 | class InteractiveBrokersPortfolio(AbstractPortfolio): 11 | """Interactive Brokers Portfolio Class 12 | 13 | The Interactive Brokers portfolio object handles market and fill events in 14 | live trading or paper trading scenarios when there are transactions with a 15 | real brokerage. 16 | 17 | The object extends the abstract portfolio object and differs primarily in 18 | the respect that it saves the current state of the portfolio to file 19 | whenever there is a change in available price data or a fill event is 20 | received. 21 | 22 | Parameters 23 | ---------- 24 | data_handler, position_handler, portfolio_handler: Refer to base class 25 | documentation. 26 | account: String. 27 | Identifier for the Interactive Brokers account with which to trade. 28 | gmail_and_password (optional): Tuple. 29 | A tuple (or list-like object) containing the string representation of 30 | your gmail account address and its password. These are used to send 31 | automated emails containing the filled trades for your situational 32 | awareness. 33 | """ 34 | def __init__( 35 | self, data_handler, position_handler, portfolio_handler, account, 36 | gmail_and_password=None 37 | ): 38 | """Initialize parameters of the Interactive Brokers Portfolio.""" 39 | super(InteractiveBrokersPortfolio, self).__init__( 40 | data_handler, position_handler, portfolio_handler 41 | ) 42 | # Set the account identifier for this portfolio. This should correspond 43 | # to either the live trading account or the paper trading account. 44 | self.account = account 45 | # Now create connections to the Trader Workstation. 46 | self.conn = ibConnection( 47 | clientId=IB.portfolio_id.value, port=IB.port.value 48 | ) 49 | self.conn.register( 50 | self.__update_portfolio_handler, message.updatePortfolio 51 | ) 52 | self.conn.register(self.__error_handler, message.error) 53 | if not self.conn.connect(): 54 | raise ValueError( 55 | "Odin was unable to connect to the Trader Workstation." 56 | ) 57 | 58 | # If we want to receive email notifications that trades have been 59 | # filled, then we provide the gmail account and its password. 60 | self.gmail_and_password = gmail_and_password 61 | 62 | def __update_portfolio_handler(self, msg): 63 | """Process portfolio update notifications. 64 | 65 | Receive messages from the Trader Workstation related to the status of 66 | the portfolio. 67 | 68 | Parameters 69 | ---------- 70 | msg: Interactive Brokers message object. 71 | A message containing the portfolio-related update information. 72 | """ 73 | pass 74 | 75 | def __error_handler(self, msg): 76 | """Process error messages from Interactive Brokers. 77 | 78 | Receive error messages from the Trader Workstation related to the 79 | portfolio. 80 | 81 | Parameters 82 | ---------- 83 | msg: Interactive Brokers message object. 84 | An object containing the parameters of the error to be logged. 85 | """ 86 | if msg.errorCode not in ib_silent_errors: 87 | print("Interactive Brokers portfolio error: {}".format(msg)) 88 | 89 | def process_post_events(self): 90 | """Save the portfolio state to the file system. 91 | 92 | Write the portfolio state to file so that it can be accessed again 93 | during a new session. 94 | """ 95 | self.portfolio_handler.to_database_portfolio() 96 | 97 | def process_fill_event(self, fill_event): 98 | """Extension of abstract base class method. The extension implements the 99 | capability to send email notifications when fill events are received. 100 | """ 101 | # Call super class method to handle the fill event. 102 | super(InteractiveBrokersPortfolio, self).process_fill_event(fill_event) 103 | 104 | # Send an email notification containing the details of the fill event. 105 | # 106 | # TODO: Would it be better to make server an instance variable so that we 107 | # don't have to recreate it every time there's a fill event? 108 | if self.gmail_and_password is not None: 109 | server = smtplib.SMTP("smtp.gmail.com", 587) 110 | server.starttls() 111 | server.login(*self.gmail_and_pass) 112 | msg = MIMEText(str(fill_event), "plain", "utf-8") 113 | msg["From"] = msg["To"] = self.gmail_and_pass[0] 114 | msg["Subject"] = "Odin Trade Notification" 115 | server.sendmail( 116 | self.gmail_and_pass[0], self.gmail_and_pass[0], msg.as_string() 117 | ) 118 | server.quit() 119 | -------------------------------------------------------------------------------- /odin/fund/fund.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from ..utilities.params import Events, verbosity_dict, Verbosities 3 | 4 | 5 | class Fund(object): 6 | """Fund Class 7 | 8 | The fund object is responsible for combining portfolios and strategies with 9 | data handlers and execution handlers in a manner that allows for either live 10 | or simulated trading. The fund ensures that new data is streamed and that 11 | signal events are generated, order events are placed, and that fill events 12 | are processed according to the most recent data provided by the data 13 | handler. 14 | 15 | The fund object is also responsible for rebalancing portfolios according to 16 | a specified timeline. 17 | 18 | Parameters 19 | ---------- 20 | data_handler: Object inheriting from the abstract data handler class. 21 | This object supplies the data to update the prices of held positions and 22 | provide new bar data with which to construct features. 23 | execution_handler: Object inheriting from the execution handler class. 24 | The execution handler is responsible for executing trades as they are 25 | placed by the portfolio object. 26 | fund_handler: Fund handler object. 27 | The fund handler rebalances the portfolio when requested and performs 28 | other administrative actions on the level of the fund. 29 | delay: Float 30 | The number of seconds that should elapse before the fund queries the 31 | market for the latest bar data. 32 | verbosity_level: Integer (Optional) 33 | Determines the amount of I/O generated for logging and debugging 34 | purposes. 35 | """ 36 | def __init__( 37 | self, data_handler, execution_handler, fund_handler, delay, 38 | verbosity_level=0 39 | ): 40 | """Initialize parameters of the fund object.""" 41 | self.data_handler = data_handler 42 | self.execution_handler = execution_handler 43 | self.fund_handler = fund_handler 44 | self.delay = delay 45 | # Set the verbosity parameter that will control the amount of output to 46 | # the standard out. 47 | self.verbosity_level = verbosity_level 48 | 49 | def trade(self): 50 | """Trade using the strategy in either a backtest or live-trading 51 | envioronment. This function is executed when a fund object has been 52 | constructed from underlying portfolio, strategy, data handler, and 53 | execution handler objects. It actually permits events to be streamed and 54 | interpreted in proper order. 55 | """ 56 | # Create shortened variable names for convenience. 57 | dh = self.data_handler 58 | eh = self.execution_handler 59 | fh = self.fund_handler 60 | events = dh.events 61 | ports, strats = fh.portfolios, fh.strategies 62 | port_dict = {p.portfolio_handler.portfolio_id: p for p in ports} 63 | 64 | # Sit in a while loop and await new market data or a cease-and-desist 65 | # indicator is recognized. 66 | while True: 67 | # Request pricing data. 68 | dh.request_prices() 69 | # Perform trading until the data handler signals that bar data has 70 | # been exhausted. 71 | if not dh.continue_trading: 72 | break 73 | 74 | # Process events generated by the latest market data. We also check 75 | # event type to ensure that events are properly handled by functions 76 | # meant to process that kind of event. 77 | while not events.empty(): 78 | e = events.get(False) 79 | e_type = e.event_type 80 | 81 | if e_type == Events.market: 82 | # Because the strategy object will sometimes make use of 83 | # holdings information, we should first update the 84 | # portfolio before generating signals. 85 | for s, p in zip(strats, ports): 86 | p.process_market_event(e) 87 | s.generate_signals() 88 | 89 | # Check for a rebalance or management event. 90 | self.fund_handler.process_market_event(e) 91 | elif e_type == Events.signal: 92 | port_dict[e.portfolio_id].process_signal_event(e) 93 | elif e_type == Events.order: 94 | eh.execute_order(e) 95 | elif e_type == Events.fill: 96 | port_dict[e.portfolio_id].process_fill_event(e) 97 | elif e_type == Events.rebalance: 98 | self.fund_handler.rebalance() 99 | elif e_type == Events.management: 100 | self.fund_handler.manage() 101 | else: 102 | raise ValueError("Invalid event type: {}".format(e_type)) 103 | 104 | # Control I/O. 105 | if verbosity_dict[e_type] <= self.verbosity_level: 106 | print(e) 107 | 108 | else: 109 | # Perform processing after all of the time periods events have 110 | # been processed. 111 | for p in ports: 112 | p.process_post_events() 113 | if Verbosities.portfolio.value <= self.verbosity_level: 114 | print(p.portfolio_handler.state) 115 | 116 | # Update the historical price record. 117 | dh.update() 118 | 119 | # Delay the acquisition of new market data. 120 | if self.delay > 0: 121 | sleep(self.delay) 122 | 123 | -------------------------------------------------------------------------------- /odin/handlers/fund_handler/fund_handler.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from odin_securities import conn 3 | from odin_securities.queries import gets, updates, exists, inserts 4 | from ...utilities import compute_days_elapsed, period_dict 5 | from ...events import RebalanceEvent, ManagementEvent 6 | 7 | 8 | class FundHandler(object): 9 | """Fund Handler Class 10 | 11 | The fund handler class controls when Odin's fund objects will either 12 | rebalance the equity holdings of the constituent portfolios or when fund 13 | management events are required. These two events are controlled by detecting 14 | when a certain time interval (measured in days) has elapsed since the 15 | previous such event. 16 | """ 17 | def __init__( 18 | self, 19 | events, 20 | strategies, 21 | date_entered, 22 | fund_id, 23 | rebalance_period=None, 24 | manage_period=None, 25 | ): 26 | """Initialize parameters of the fund handler object.""" 27 | self.events = events 28 | self.strategies = strategies 29 | self.portfolios = [s.portfolio for s in self.strategies] 30 | self.rebalance_period = rebalance_period 31 | self.manage_period = manage_period 32 | self.date_entered = date_entered 33 | self.fund_id = fund_id 34 | # Set the assets under management to be the sum of the capital 35 | # intitially contained in each of the constituent portfolios. 36 | self.aum = sum([p.portfolio_handler.capital for p in self.portfolios]) 37 | 38 | def to_database_fund(self): 39 | """Insert the fund object into the Odin Securities master database.""" 40 | does_exist = exists.fund(self.fund_id) 41 | if not does_exist: 42 | inserts.fund(self) 43 | else: 44 | fid = gets.id_for_fund(self.fund_id) 45 | updates.fund(self, fid) 46 | 47 | conn.commit() 48 | 49 | @classmethod 50 | def from_database_fund(cls, fund_id, events, strategies): 51 | """Create an instance of a fund handler object using the relevant data 52 | from a fund database object. This is useful for preserving the state of 53 | a fund between trading sessions. 54 | """ 55 | fund = gets.fund(fund_id).iloc[0] 56 | rebalance = fund["rebalance_period"] 57 | manage = fund["manage_period"] 58 | date_entered = fund["entry_date"] 59 | 60 | # Check that every portfolio in the strategies has the same identifier 61 | # as the fund. 62 | fund_name = fund["fund"] 63 | for s in strategies: 64 | if s.portfolio.portfolio_handler.fund_id != fund_name: 65 | raise ValueError( 66 | "Portfolio and fund identifier mismatch:\t{}\t{}".format( 67 | s.portfolio.portfolio_handler.fund_id, fund_name 68 | ) 69 | ) 70 | 71 | return cls(events, strategies, date_entered, fund_id, rebalance, manage) 72 | 73 | def process_market_event(self, market_event): 74 | """The fund processes market data after all of the portfolios have done 75 | so and possibly generated signals. The fund will look for indicators to 76 | take action on the scale of the entire fund, which will impact each of 77 | the constituent portfolios. 78 | 79 | For instance, Odin currently implements a utility for rebalancing 80 | positions. 81 | """ 82 | # Extract the management and rebalancing time period indicators. 83 | manage = period_dict.get(self.manage_period, 1) 84 | rebalance = period_dict.get(self.rebalance_period, 1) 85 | # Compute the number of days that the fund has been trading. 86 | date = market_event.datetime 87 | n_days = compute_days_elapsed(self.date_entered, date) 88 | 89 | # Detect when it is time to rebalance the portfolio. 90 | will_rebalance = n_days % rebalance == 0 and rebalance > 1 91 | # Detect when it is time to take management fees from the fund. 92 | will_manage = n_days % manage == 0 and manage > 1 93 | 94 | if will_rebalance or will_manage: 95 | # Note that it is important and necessary to clear the events queue 96 | # at this stage because otherwise we have residual signal events 97 | # that need to be replaced by the order to rebalance positions. 98 | self.events.clear() 99 | # Close all of the positions. 100 | for s in self.strategies: 101 | s.close() 102 | 103 | # If we are managing. 104 | if will_manage: 105 | self.events.put(ManagementEvent(date)) 106 | 107 | # If we are rebalancing. 108 | if will_rebalance: 109 | self.events.put(RebalanceEvent(date)) 110 | 111 | def rebalance(self): 112 | """At a rebalance event, we will partition the total capital of the fund 113 | equally among each of the portfolios. This is done to prevent any one 114 | strategy from vastly outstripping the equity of the other portfolios. 115 | Periodic rebalancing keeps these equity levels approximately the same 116 | and is critical for market neutral strategies. 117 | """ 118 | ports = self.portfolios 119 | avg_equity = np.mean([p.portfolio_handler.equity for p in ports]) 120 | for p in self.portfolios: 121 | p.portfolio_handler.capital = avg_equity 122 | 123 | def manage(self): 124 | """At management events, a portion of the AUM and a portion of the gains 125 | earned so far are taken out as management fees for the portfolio 126 | manager. 127 | """ 128 | # Compute the equity value of the fund and record the number of 129 | # portfolios being traded. 130 | equity = np.sum([p.portfolio_handler.equity for p in self.portfolios]) 131 | n_port = len(self.portfolios) 132 | # Only take a cut if we've exceeded a highwater mark. 133 | if equity > self.aum: 134 | two = 0.02 * self.aum 135 | twenty = 0.2 * (equity - self.aum) 136 | post_equity = equity - two - twenty 137 | for p in self.portfolios: 138 | p.portfolio_handler.capital = post_equity / n_port 139 | 140 | # Reset the assets under management to be the equity earned so far. 141 | self.aum = post_equity 142 | -------------------------------------------------------------------------------- /odin/strategy/abstract_strategy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from abc import ABCMeta, abstractmethod 3 | from ..events import SignalEvent 4 | from ..utilities.params import TradeTypes 5 | 6 | 7 | class AbstractStrategy(object): 8 | """Abstract Strategy Class 9 | 10 | Strategy is an abstract base class providing an interface for all subsequent 11 | (inherited) strategy handling objects. 12 | 13 | The goal of a (derived) Strategy object is to generate signal events for 14 | particular symbols based on bar data streamed from a data handler object. 15 | This is designed to work both with historic and live data as the strategy 16 | object is agnostic to the data source (it obtains the bar data from a queue 17 | object). 18 | """ 19 | __metaclass__ = ABCMeta 20 | 21 | def __init__(self, portfolio): 22 | """Initialize parameters of the abstract strategy object.""" 23 | self.portfolio = portfolio 24 | 25 | def generate_signals(self): 26 | """This function is called to handle all of the processing of buy, sell, 27 | and exit signals whenever a new bar is streamed. The algorithm makes it 28 | impossible for an asset that has already been purchased to be purchased 29 | again; hence, positions that have entered into are only eligible to be 30 | sold off or exited. 31 | """ 32 | # Get salient trading features. 33 | feats = self.generate_features() 34 | # Assign convenience variables that are constant across all elements of 35 | # the signal generating function. 36 | date = self.portfolio.data_handler.current_date 37 | # d = self.direction 38 | pid = self.portfolio.portfolio_handler.portfolio_id 39 | events = self.portfolio.data_handler.events 40 | ph = self.portfolio.portfolio_handler 41 | 42 | # Iterate over the underlying assets in the data handler. For those 43 | # assets that do not yet have an associated position in the portfolio, 44 | # consider them for purchase; otherwise, consider the position for 45 | # liquidation. The order that the assets are considered in is generated 46 | # by a user-specified priority. 47 | for stock in self.generate_priority(feats): 48 | stock_feat = feats.ix[stock] 49 | if stock in ph.filled_positions: 50 | # Process assets that are already held as positions. 51 | sell_trade_indicator = True 52 | if self.sell_indicator(stock_feat): 53 | # Selling a position. We determine the fraction of the 54 | # position that should be liquidated. 55 | trade_type = TradeTypes.sell_trade 56 | prop = self.compute_sell_proportion(stock_feat) 57 | elif self.exit_indicator(stock_feat): 58 | # Exiting a position entirely; hence, the fraction of the 59 | # position to sell is unity. 60 | trade_type = TradeTypes.exit_trade 61 | prop = 1.0 62 | else: 63 | sell_trade_indicator = False 64 | 65 | if sell_trade_indicator: 66 | direction = ph.filled_positions[stock].direction 67 | signal_event = SignalEvent( 68 | stock, prop, trade_type, direction, date, pid 69 | ) 70 | events.put(signal_event) 71 | else: 72 | # Process assets that are candidates to become new positions. 73 | if self.buy_indicator(stock_feat): 74 | direction = self.compute_direction(stock_feat) 75 | prop = self.compute_buy_proportion(stock_feat) 76 | signal_event = SignalEvent( 77 | stock, prop, TradeTypes.buy_trade, direction, date, pid 78 | ) 79 | events.put(signal_event) 80 | 81 | def close(self): 82 | """This function issues signal events that exit every position held by 83 | the portfolio. 84 | """ 85 | # Extract the current date. 86 | date = self.portfolio.data_handler.current_date 87 | # Iterate over the positions of the portfolio and generate an exit 88 | # signal for that position. 89 | for pos in self.portfolio.portfolio_handler.filled_positions.values(): 90 | signal_event = SignalEvent( 91 | pos.symbol, 1.0, TradeTypes.exit_trade, pos.direction, date, 92 | self.portfolio.portfolio_handler.portfolio_id 93 | ) 94 | self.portfolio.data_handler.events.put(signal_event) 95 | 96 | @abstractmethod 97 | def compute_direction(self, feats): 98 | """Indicator of which direction (long or short) the strategy to trade a 99 | specific asset. 100 | """ 101 | raise NotImplementedError() 102 | 103 | @abstractmethod 104 | def compute_buy_proportion(self, feats): 105 | """Determine the recommended proportion of capital to allocate toward 106 | this position. This proportion will either be heeded or modified by the 107 | position handler object for the associated portfolio. 108 | """ 109 | raise NotImplementedError() 110 | 111 | @abstractmethod 112 | def compute_sell_proportion(self, feats): 113 | """Similar to the function that computes the proportion of equity to 114 | invest in a position, this function computes the proportions of an 115 | existing position to liquidate. 116 | """ 117 | raise NotImplementedError() 118 | 119 | @abstractmethod 120 | def buy_indicator(self, feats): 121 | """Indicator function that the strategy should buy into a position.""" 122 | raise NotImplementedError() 123 | 124 | @abstractmethod 125 | def sell_indicator(self, feats): 126 | """Indicator function that the strategy should sell from a position.""" 127 | raise NotImplementedError() 128 | 129 | @abstractmethod 130 | def exit_indicator(self, feats): 131 | """Indicator function that the strategy should exit a position.""" 132 | raise NotImplementedError() 133 | 134 | @abstractmethod 135 | def generate_features(self): 136 | """Generates salient features for constructing trading signals for 137 | either buying into a position or selling from it. 138 | """ 139 | raise NotImplementedError() 140 | 141 | @abstractmethod 142 | def generate_priority(self, feats): 143 | """Generates an order in which to consider stocks. Since the portfolio 144 | has limited capacity, trading decisions for entering a position need to 145 | be prioritized. 146 | """ 147 | raise NotImplementedError() 148 | -------------------------------------------------------------------------------- /odin/portfolio/abstract_portfolio.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from abc import ABCMeta, abstractmethod 3 | from ..utilities.params import TradeTypes, PriceFields, IOFiles 4 | from ..events import OrderEvent, SignalEvent 5 | 6 | 7 | class AbstractPortfolio(object): 8 | """Abstract Portfolio Class 9 | 10 | The abstract portfolio class serves as a template for all of the event 11 | handling that impacts an individual portfolio. For instance, how will the 12 | portfolio respond to updated market data? The class also implements logic 13 | for processing signal events received from the strategy objects, or fill 14 | events generated by the execution handler. 15 | 16 | Subclasses may provide additional functionality streaming data to update the 17 | equity of a relevant portfolio or computing historical performance metrics. 18 | 19 | Parameters 20 | ---------- 21 | data_handler: Object inheriting from the abstract data handler class. 22 | This object supplies the data to update the prices of held positions and 23 | provide new bar data with which to construct features. 24 | position_handler: Object inheriting from the abstract position handler 25 | class. 26 | The position handler determines how much of an asset to acquire or to 27 | relinquish when trading signals are processed into orders. 28 | portfolio_handler: Portfolio handler object. 29 | The portfolio handler keeps track of positions that are currently held 30 | by the portfolio as well as the current amount of equity and capital in 31 | the portfolio. 32 | """ 33 | __metaclass__ = ABCMeta 34 | 35 | def __init__(self, data_handler, position_handler, portfolio_handler): 36 | """Initialize parameters of the abstract portfolio object.""" 37 | self.data_handler = data_handler 38 | self.position_handler = position_handler 39 | self.portfolio_handler = portfolio_handler 40 | 41 | def process_post_events(self): 42 | """Called after all events have been processed but before new bar and 43 | price data is requested. This method is useful for recording attributes 44 | of the portfolio. 45 | """ 46 | raise NotImplementedError() 47 | 48 | def process_market_event(self, market_event): 49 | """This function iterates through the positions of the portfolio and 50 | updates critical quantities such as the current market value of the 51 | position and the number of trading days that the position has been held. 52 | 53 | Parameters 54 | ---------- 55 | market_event: Market event object. 56 | The market event that the portfolio will process. 57 | """ 58 | # Iterate over each position currently held in the portfolio. 59 | for pos in self.portfolio_handler.filled_positions.values(): 60 | # When we value the position, we'll value it using the close price 61 | # of the time period. 62 | s = pos.symbol 63 | if s in self.data_handler.prices.minor_axis: 64 | price = self.data_handler.prices.ix[ 65 | PriceFields.current_price.value, 0, s 66 | ] 67 | # Update the value of the position using the new market data. 68 | pos.update_market_value(price) 69 | else: 70 | # If the position is held in the portfolio, but we do not have 71 | # data for it, then raise an error. 72 | raise ValueError( 73 | "Position {} in portfolio {} does not have associated price" 74 | " data on {}.".format( 75 | s, self.portfolio_handler.portfolio_id, 76 | market_event.datetime.strftime( 77 | IOFiles.date_format.value 78 | ) 79 | ) 80 | ) 81 | 82 | def process_signal_event(self, signal_event): 83 | """This function interprets a signal event received from the portfolio 84 | and, on the basis of portfolio logic, determines whether or not the 85 | signal should be acted upon. In particular, a nonzero number of shares 86 | need to be transacted in order for the trade to be actionable and, in 87 | the event of creating a new position, there needs to be capacity in the 88 | portfolio. 89 | 90 | Parameters 91 | ---------- 92 | signal_event: Signal event object. 93 | The signal event that the portfolio will process. 94 | """ 95 | symbol = signal_event.symbol 96 | trade_type = signal_event.trade_type 97 | # Only trade if there is simultaneously capacity in the portfolio (i.e. 98 | # the number of allowable positions is not exceeded) and if the amount 99 | # of the underlying asset to trade is at least one. 100 | if self.portfolio_handler.is_tradeable(trade_type): 101 | quantity = self.position_handler.order_sizer[trade_type]( 102 | signal_event, self.portfolio_handler 103 | ) 104 | 105 | if quantity > 0: 106 | order_event = OrderEvent.from_signal_event( 107 | signal_event, quantity 108 | ) 109 | self.data_handler.events.put(order_event) 110 | if trade_type == TradeTypes.buy_trade: 111 | self.portfolio_handler.add_pending_position(order_event) 112 | 113 | def process_fill_event(self, fill_event): 114 | """This function updates the portfolio holdings to reflect the results 115 | of a recent fill. In particular, we update the value of the position to 116 | reflect the fill price of the order, and incorporate the commission 117 | fees. 118 | 119 | Depending on whether or not the fill event corresponds to entering or 120 | exiting a position, the function will either add a position to the 121 | dictionary of filled positions or it will remove one. The corresponding 122 | pending position is also removed. 123 | 124 | Parameters 125 | ---------- 126 | fill_event: Fill event object. 127 | The fill event that the portfolio will process. 128 | """ 129 | # Extract variables for the trade type, the cost of filling the position 130 | # and the commission incurred from the transaction. 131 | trade_type = fill_event.trade_type 132 | symbol = fill_event.symbol 133 | 134 | if symbol not in self.portfolio_handler.filled_positions: 135 | # If we are entering a position, then we subtract from the amount of 136 | # free capital and add a new position to the dictionary of fills. 137 | self.portfolio_handler.add_filled_position(fill_event) 138 | elif symbol in self.portfolio_handler.filled_positions: 139 | # If we are exiting from a position, then we add money back to the 140 | # free capital amount and delete the position from the dictionary of 141 | # fills. 142 | self.portfolio_handler.modify_filled_position(fill_event) 143 | -------------------------------------------------------------------------------- /odin/metrics/visualizer.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import seaborn as sns 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | import matplotlib.gridspec as gridspec 6 | import matplotlib.dates as mdates 7 | from matplotlib.ticker import FuncFormatter 8 | from matplotlib import cm 9 | from ..utilities.params import Directions 10 | from .compute_drawdowns import compute_drawdowns 11 | from .compute_sharpe_ratio import compute_sharpe_ratio 12 | 13 | 14 | class Visualizer(object): 15 | """Fund Performance Visualizer Class 16 | 17 | This visualizer object is intended to provide access to a number of 18 | plotting utilities for visualizing the performance of the constituent 19 | portfolios of a fund. Support is provided for basic plots, such as the 20 | equity curve and drawdowns, as well as more specialized plots such as the 21 | rolling Sharpe ratio of the portfolios and month-over-month performance 22 | summaries. 23 | """ 24 | def long_short_equity(self, fund, ax=None): 25 | if ax is None: 26 | ax = plt.gca() 27 | 28 | for i, p in enumerate(fund.fund_handler.portfolios): 29 | states = p.history.states 30 | dates = list(states.keys()) 31 | columns = ("long", "short", "ratio") 32 | equity = pd.DataFrame(index=dates, columns=columns) 33 | for d in dates: 34 | filled = states[d].filled_positions 35 | equity.ix[d, "long"] = np.sum([ 36 | f.relative_value for f in filled.values() 37 | if f.direction == Directions.long_dir 38 | ]) 39 | equity.ix[d, "short"] = np.sum([ 40 | f.relative_value for f in filled.values() 41 | if f.direction == Directions.short_dir 42 | ]) 43 | equity["ratio"] = equity["long"] / (equity["long"] + equity["short"]) 44 | 45 | for v in ("long", "short"): 46 | ax.plot( 47 | equity.index, equity[v], lw=2., 48 | label="_".join((p.portfolio_handler.portfolio_id, v)) 49 | ) 50 | 51 | ax.set_xlabel("Date", fontsize=15.) 52 | ax.set_ylabel("Long / Short Equity", fontsize=15.) 53 | ax.legend(loc="upper left") 54 | ax.grid(True) 55 | 56 | return ax 57 | 58 | def rolling_sharpe(self, window, fund, ax=None): 59 | if ax is None: 60 | ax = plt.gca() 61 | 62 | for i, p in enumerate(fund.fund_handler.portfolios): 63 | rets = p.history.returns.rolling(window=window) 64 | rs = compute_sharpe_ratio(rets) 65 | ax.plot( 66 | rs.index, rs, lw=2., 67 | label=p.portfolio_handler.portfolio_id.title() 68 | ) 69 | 70 | ax.set_xlabel("Date", fontsize=15.) 71 | ax.set_ylabel( 72 | "Rolling Sharpe Ratio ({})".format(window), fontsize=15. 73 | ) 74 | ax.legend(loc="upper left") 75 | ax.grid(True) 76 | 77 | return ax 78 | 79 | def equity(self, fund, ax=None): 80 | if ax is None: 81 | ax = plt.gca() 82 | 83 | for i, p in enumerate(fund.fund_handler.portfolios): 84 | ax.plot( 85 | p.history.equity.index, p.history.equity, lw=2., 86 | label=p.portfolio_handler.portfolio_id.title() 87 | ) 88 | ax.grid() 89 | 90 | ax.set_xlabel("Date", fontsize=15.) 91 | ax.set_ylabel("Equity", fontsize=15.) 92 | ax.legend(loc="upper left") 93 | ax.grid(True) 94 | 95 | return ax 96 | 97 | def positions(self, fund, ax=None): 98 | if ax is None: 99 | ax = plt.gca() 100 | 101 | for i, p in enumerate(fund.fund_handler.portfolios): 102 | ax.plot( 103 | p.history.n_positions.index, p.history.n_positions, lw=2., 104 | label=p.portfolio_handler.portfolio_id.title() 105 | ) 106 | ax.grid() 107 | 108 | ax.set_xlabel("Date", fontsize=15.) 109 | ax.set_ylabel("Number of Positions", fontsize=15.) 110 | ax.legend(loc="upper left") 111 | ax.grid(True) 112 | 113 | return ax 114 | 115 | def drawdown_percentage(self, fund, ax=None): 116 | if ax is None: 117 | ax = plt.gca() 118 | 119 | for i, p in enumerate(fund.fund_handler.portfolios): 120 | dd, dur = compute_drawdowns(p.history.equity, False) 121 | ax.plot( 122 | dd.index, dd, lw=2., 123 | label=p.portfolio_handler.portfolio_id.title() 124 | ) 125 | ax.grid() 126 | 127 | ax.set_xlabel("Date", fontsize=15.) 128 | ax.set_ylabel("Drawdown Percentage", fontsize=15.) 129 | ax.legend(loc="upper left") 130 | ax.grid(True) 131 | 132 | return ax 133 | 134 | def drawdown_duration(self, fund, ax=None): 135 | if ax is None: 136 | ax = plt.gca() 137 | 138 | for i, p in enumerate(fund.fund_handler.portfolios): 139 | dd, dur = compute_drawdowns(p.history.equity, False) 140 | ax.plot( 141 | dur.index, dur, lw=2., 142 | label=p.portfolio_handler.portfolio_id.title() 143 | ) 144 | ax.grid() 145 | 146 | ax.set_xlabel("Date", fontsize=15.) 147 | ax.set_ylabel("Drawdown Duration", fontsize=15.) 148 | ax.legend(loc="upper left") 149 | ax.grid(True) 150 | 151 | return ax 152 | 153 | def monthly_returns(self, fund, ax=None): 154 | if ax is None: 155 | ax = plt.gca() 156 | 157 | # Compute the returns on a month-over-month basis. 158 | history = fund.history 159 | monthly_ret = self.__aggregate_returns(history, 'monthly') 160 | monthly_ret = monthly_ret.unstack() 161 | monthly_ret = np.round(monthly_ret, 3) 162 | monthly_ret.rename( 163 | columns={1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 164 | 5: 'May', 6: 'Jun', 7: 'Jul', 8: 'Aug', 165 | 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'}, 166 | inplace=True 167 | ) 168 | 169 | # Create a heatmap showing the month-over-month returns of the portfolio 170 | # or the fund. 171 | sns.heatmap( 172 | monthly_ret.fillna(0) * 100.0, annot=True, fmt="0.1f", 173 | annot_kws={"size": 12}, alpha=1.0, center=0.0, cbar=False, 174 | cmap=cm.RdYlGn, ax=ax 175 | ) 176 | ax.set_title('Monthly Returns (%)', fontweight='bold') 177 | ax.set_ylabel('') 178 | ax.set_yticklabels(ax.get_yticklabels(), rotation=0) 179 | ax.set_xlabel('') 180 | 181 | return ax 182 | 183 | def yearly_returns(self, fund, ax=None): 184 | if ax is None: 185 | ax = plt.gca() 186 | 187 | history = fund.history 188 | yearly_returns = self.__aggregate_returns(history, 'yearly') * 100.0 189 | yearly_returns.plot(ax=ax, kind="bar") 190 | ax.set_title('Yearly Returns (%)', fontweight='bold') 191 | ax.set_ylabel('') 192 | ax.set_xlabel('') 193 | ax.set_xticklabels(ax.get_xticklabels(), rotation=45) 194 | ax.grid(True) 195 | 196 | return ax 197 | 198 | def __aggregate_returns(self, history, aggregate="monthly"): 199 | cumulate_returns = lambda rets: (1. + rets).prod() - 1. 200 | 201 | if aggregate == "monthly": 202 | return history.returns.groupby( 203 | [lambda x: x.year, lambda x: x.month] 204 | ).apply(cumulate_returns) 205 | elif aggregate == "yearly": 206 | return history.returns.groupby([lambda x: x.year]).apply( 207 | cumulate_returns 208 | ) 209 | else: 210 | raise ValueError( 211 | "Improper aggregate parameter: {}".format(aggregate) 212 | ) 213 | 214 | -------------------------------------------------------------------------------- /odin/handlers/position_handler/position/filled_position.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import numpy as np 3 | import pandas as pd 4 | from odin_securities import conn 5 | # from odin_securities.utilities import get_id_for_symbol, get_id_for_portfolio 6 | from odin_securities.queries import gets, inserts, updates, exists 7 | from .pending_position import PendingPosition 8 | from ....utilities import compute_days_elapsed 9 | from ....utilities.params import ( 10 | Directions, Actions, IB, TradeTypes, ib_commission 11 | ) 12 | 13 | 14 | class FilledPosition(PendingPosition): 15 | """Filled Position Class 16 | 17 | The position object represents a position held by a portfolio object. It 18 | captures how much of the position is held, whether or not the position is 19 | long or short, the profit-and-loss on the position at the current time 20 | period, and the ticker symbol associated with the underlying asset. 21 | """ 22 | def __init__( 23 | self, 24 | symbol, 25 | direction, 26 | trade_type, 27 | portfolio_id, 28 | date_entered, 29 | avg_price, 30 | buys=0, 31 | sells=0, 32 | avg_buys_price=0.0, 33 | avg_sells_price=0.0, 34 | tot_commission=0.0 35 | ): 36 | """Initialize parameters of the position object.""" 37 | super(FilledPosition, self).__init__( 38 | symbol, direction, trade_type, portfolio_id 39 | ) 40 | self.date_entered = date_entered 41 | 42 | # Profit and loss. 43 | self.unrealized_pnl, self.realized_pnl = 0.0, 0.0 44 | self.market_value = 0.0 45 | 46 | # Number of buys and sells. 47 | self.buys, self.sells = buys, sells 48 | self.net = self.buys - self.sells 49 | self.quantity = abs(self.net) 50 | # Price of buys and sells on average. 51 | self.avg_price = avg_price 52 | self.cost_basis = self.net * self.avg_price 53 | self.avg_buys_price = avg_buys_price 54 | self.avg_sells_price = avg_sells_price 55 | self.tot_buys_price = self.buys * self.avg_buys_price 56 | self.tot_sells_price = self.sells * self.avg_sells_price 57 | # Keep track of total commission costs. 58 | self.tot_commission = tot_commission 59 | self.net_tot = self.tot_sells_price - self.tot_buys_price 60 | self.net_tot_incl_comm = self.net_tot - self.tot_commission 61 | 62 | def __str__(self): 63 | """String representation of the filled position object.""" 64 | return "{}\t{}\t{:0.2f}\t{:0.4f}".format( 65 | self.symbol, self.date_entered.date(), self.relative_value, 66 | self.percent_pnl 67 | ) 68 | 69 | def transact_shares(self, action, quantity, price): 70 | """Depending upon whether the action was a buy or sell calculate the 71 | average bought cost, the total bought cost, the average price and the 72 | cost basis. Finally, calculate the net total without commission. 73 | """ 74 | # Record the total commission costs for this position so far. 75 | commission = ib_commission(quantity, price) 76 | self.tot_commission += commission 77 | # Compute the value of the shares being transacted. 78 | value = price * quantity 79 | 80 | # Adjust total bought and sold. 81 | if action == Actions.buy: 82 | # Number of shares transacted. 83 | n_transacted = self.buys + quantity 84 | # If shares are bought, then recompute the average cost of buying 85 | # the shares, accounting for shares already held. 86 | self.avg_buys_price = ( 87 | self.avg_buys_price * self.buys + value 88 | ) / n_transacted 89 | 90 | if self.action != Actions.sell: 91 | self.avg_price = ( 92 | self.avg_price * self.buys + value + commission 93 | ) / n_transacted 94 | 95 | # Increment quantity bought. 96 | self.buys += quantity 97 | self.tot_buys_price = self.buys * self.avg_buys_price 98 | elif action == Actions.sell: 99 | # Number of shares transacted. 100 | n_transacted = self.sells + quantity 101 | # If shares are being sold (even on margin), then recompute the 102 | # average price of selling the shares, accounting for shares already 103 | # sold. 104 | self.avg_sells_price = ( 105 | self.avg_sells_price * self.sells + value 106 | ) / n_transacted 107 | 108 | if self.action != Actions.buy: 109 | self.avg_price = ( 110 | self.avg_price * self.sells + value - commission 111 | ) / n_transacted 112 | 113 | # Increment quantity sold. 114 | self.sells += quantity 115 | self.tot_sells_price = self.sells * self.avg_sells_price 116 | 117 | else: 118 | raise ValueError( 119 | "Invalid action passed to transact shares: {}".format(action) 120 | ) 121 | 122 | # Record the new number of shares bought or sold. Also compute the net 123 | # value accounting for shares bought and sold. 124 | self.net = self.buys - self.sells 125 | self.quantity = abs(self.net) 126 | self.net_tot = self.tot_sells_price - self.tot_buys_price 127 | self.net_tot_incl_comm = self.net_tot - self.tot_commission 128 | # Compute the cost basis. 129 | self.cost_basis = self.net * self.avg_price 130 | # Update market value. 131 | self.update_market_value(price) 132 | 133 | def update_market_value(self, price): 134 | """Compute the current marke value of the position. This is the current 135 | price multiplied by the direction of the trade (r2epresented by the sign 136 | of the net number of shares bought and sold). The function also updated 137 | the unrealized and realized profits and losses. 138 | """ 139 | # Compute the mean of the bid and ask price to compute the assumed value 140 | # of the position. 141 | # 142 | # N.B. That the market value is akin to the amount of cash that is would 143 | # be injected into the portfolio if the position were liquidated. This 144 | # means that if a position is short, then a negative amount will be 145 | # injected (i.e. paid out). On the other hand, the current value is the 146 | # profit-and-loss on a position relative to the cost basis. 147 | self.market_value = self.net * price 148 | self.unrealized_pnl = self.market_value - self.cost_basis 149 | self.realized_pnl = self.market_value + self.net_tot_incl_comm 150 | 151 | def compute_holding_period(self, current_date): 152 | """Compute the time period over which the position has been held. This 153 | is simply the temporal difference between when the position was entered 154 | (which is recorded on creation of a position) and the current time. 155 | 156 | Unlike previous methods that only computed the days held in terms of 157 | days elapsed, this method also accounts for finer-grain temporal 158 | differences. 159 | """ 160 | delta_seconds = 86400 - (self.date_entered - current_date).seconds 161 | delta_days = compute_days_elapsed(self.date_entered, current_date) 162 | if self.date_entered.time() > dt.time(0, 0): 163 | delta_days -= 1 164 | 165 | return dt.timedelta(days=delta_days, seconds=delta_seconds) 166 | 167 | def to_database_position(self): 168 | """Write the position to the database. This allows the position to be 169 | persistent across trading sessions. 170 | """ 171 | # Get identifiers for both the stock symbol and the portfolio. 172 | pid = gets.id_for_portfolio(self.portfolio_id) 173 | sid = gets.id_for_symbol(self.symbol) 174 | does_exist = exists.position(sid, pid) 175 | # Insert the position into the database. 176 | if not does_exist: 177 | inserts.position(self, sid, pid) 178 | else: 179 | updates.position(self, sid, pid) 180 | 181 | # Commit changes to the database. 182 | conn.commit() 183 | 184 | @property 185 | def percent_pnl(self): 186 | """Computes the profit-and-loss on a position. This is the percentage 187 | difference of the current price relative to the average price of the 188 | asset added to or subtracted from unity according to whether or not the 189 | position is long or short the asset. 190 | """ 191 | if self.net == 0: 192 | raise ValueError( 193 | "Percentage profit and loss is not defined for positions with " 194 | "no holdings." 195 | ) 196 | else: 197 | ret = self.unrealized_pnl / self.cost_basis * np.sign(self.net) 198 | return 1.0 + ret 199 | 200 | @property 201 | def relative_value(self): 202 | """Compute the relative value of the position. This is the cost basis 203 | multiplied by the profit-and-loss since entering the position. 204 | """ 205 | return abs(self.cost_basis * self.percent_pnl) 206 | 207 | @classmethod 208 | def from_database_position(cls, portfolio_id, symbol): 209 | """Create an instance of a filled position object using the relevant 210 | data stored in the Odin Securities master database. 211 | """ 212 | # Get the identifiers for both the portfolio and the symbol. 213 | pid = gets.id_for_portfolio(portfolio_id) 214 | sid = gets.id_for_symbol(symbol) 215 | qry = """ 216 | SELECT * FROM positions WHERE symbol_id={} AND portfolio_id={} 217 | """.format(sid, pid) 218 | rec = pd.read_sql(qry, conn, index_col=["id"]).iloc[0] 219 | # Extract variables. 220 | date_entered = rec["date_entered"] 221 | avg_price = float(rec["avg_price"]) 222 | buys, sells = int(rec["buys"]), int(rec["sells"]) 223 | avg_buys_price = float(rec["avg_buys_price"]) 224 | avg_sells_price = float(rec["avg_sells_price"]) 225 | tot_commission = float(rec["tot_commission"]) 226 | direction = Directions(rec["direction"]) 227 | trade_type = TradeTypes(rec["trade_type"]) 228 | # Return a filled position object populated from the database. 229 | return cls( 230 | symbol, 231 | direction, 232 | trade_type, 233 | portfolio_id, 234 | date_entered, 235 | avg_price, 236 | buys, 237 | sells, 238 | avg_buys_price, 239 | avg_sells_price, 240 | tot_commission 241 | ) 242 | 243 | @classmethod 244 | def from_pending_position(cls, pending, date, price): 245 | """Create an instance of a filled position object using the relevant 246 | data stored in a pending position object. 247 | """ 248 | return cls( 249 | pending.symbol, 250 | pending.direction, 251 | pending.trade_type, 252 | pending.portfolio_id, 253 | date, 254 | price, 255 | ) 256 | 257 | -------------------------------------------------------------------------------- /odin/handlers/portfolio_handler/portfolio_handler.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import datetime as dt 3 | from odin_securities import conn 4 | from odin_securities.queries import gets, exists, updates, inserts, deletes 5 | from ..position_handler.position import PendingPosition, FilledPosition 6 | from ...portfolio.components import PortfolioState 7 | from ...utilities.mixins import EquityMixin 8 | from ...utilities.params import TradeTypes, action_dict, Directions 9 | 10 | 11 | class PortfolioHandler(EquityMixin): 12 | """Portfolio Handler Class 13 | 14 | The portfolio handler class is responsible for the book-keeping relating to 15 | transactions of assets when the strategy is trading. This means tracking 16 | pending positions, updating equity and capital, updating filled positions, 17 | and updating inventory when fill events are received. 18 | 19 | Parameters 20 | ---------- 21 | maximum_capacity: Integer. 22 | An integer representing the maximum number of filled positions that can 23 | be held by a portfolio at any given time. 24 | portfolio_id: String. 25 | A unique identifier assigned to the portfolio. 26 | capital: Float. 27 | The amount of capital (measured in USD) presently held by the portfolio. 28 | filled_positions (Optional): Dictionary. 29 | A dictionary of filled position objects mapping symbols to the 30 | corresponding fill representation. 31 | filled_positions (Optional): Dictionary. 32 | A dictionary of closed positions mapping datetimes to positions closed 33 | at those times. 34 | """ 35 | def __init__( 36 | self, 37 | maximum_capacity, 38 | portfolio_id, 39 | capital, 40 | fund_id, 41 | filled_positions=None, 42 | closed_positions=None 43 | ): 44 | """Initialize parameters of the portfolio handler object.""" 45 | self.maximum_capacity = maximum_capacity 46 | self.portfolio_id = portfolio_id 47 | self.fund_id = fund_id 48 | self.capital = capital 49 | self.filled_positions = filled_positions if filled_positions else {} 50 | self.closed_positions = closed_positions if closed_positions else {} 51 | self.pending_positions = {} 52 | self.state = PortfolioState( 53 | self.capital, self.filled_positions, self.maximum_capacity, 54 | self.portfolio_id 55 | ) 56 | 57 | def to_database_portfolio(self): 58 | """Insert the portfolio object into the Odin Securities master database 59 | so that the capital and position information is persistent across 60 | trading sessions. 61 | """ 62 | # Either create an entry or update it depending on whether or not the 63 | # portfolio already exists in the database. 64 | does_exist = exists.portfolio(self.portfolio_id) 65 | if not does_exist: 66 | fid = gets.id_for_fund(self.fund_id) 67 | inserts.portfolio(self, fid) 68 | else: 69 | pid = gets.id_for_portfolio(self.portfolio_id) 70 | updates.portfolio(self, pid) 71 | 72 | # For each filled position in the portfolio, add it to the database. 73 | for pos in self.filled_positions.values(): 74 | pos.to_database_position() 75 | 76 | # Commit changes to the database. 77 | conn.commit() 78 | 79 | @classmethod 80 | def from_database_portfolio(cls, portfolio_id): 81 | """Create an instance of a portfolio handler object using the relevant 82 | data from a portfolio database object. This is useful for preserving the 83 | state of a portfolio between sessions. 84 | 85 | Parameters 86 | ---------- 87 | portfolio_id: String. 88 | A portfolio identifier used to select the appropriate records from 89 | the database corresponding to this portfolio. 90 | """ 91 | # First get portfolio attributes. 92 | port = gets.portfolio(portfolio_id) 93 | max_cap, capital = port["maximum_capacity"], port["capital"] 94 | pid = port.name 95 | fid = gets.fund_for_fund_id(port["fund_id"])["fund"].values[0] 96 | # Then get corresponding positions. 97 | positions = gets.positions_for_portfolio_id(pid) 98 | filled = { 99 | p: FilledPosition.from_database_position(portfolio_id, p) 100 | for p in positions 101 | } 102 | 103 | return cls(max_cap, portfolio_id, capital, fid, filled) 104 | 105 | @property 106 | def available_capacity(self): 107 | """Computes the number of equity positions that are currently unfilled. 108 | This is the difference between the maximum capacity of the portfolio and 109 | the number of currently filled and pending positions. 110 | 111 | The property raises a value error when the number of positions in the 112 | portfolio exceeds the maximum capacity. 113 | """ 114 | filled, pending = self.filled_positions, self.pending_positions 115 | # Extract the maximum number of positions this portfolio can support. 116 | max_cap = self.maximum_capacity 117 | # Compute the combined number of filled and pending positions. 118 | n_filled = len(filled.values()) 119 | n_pending = len(pending.values()) 120 | n_pos = n_filled + n_pending 121 | if n_pos > max_cap: 122 | raise ValueError("Number of positions exceeds portfolio capacity.") 123 | 124 | # Compute the capacity. 125 | return max_cap - n_pos 126 | 127 | def is_tradeable(self, trade_type): 128 | """Determine whether or not the provided trade type can be traded by the 129 | portfolio at the present time. 130 | 131 | Trades that are selling off a position or exiting it completely can 132 | always be traded because they reduce capacity. Trade types to enter a 133 | position can only be traded when there is capacity for the position in 134 | the portfolio. 135 | 136 | Parameters 137 | ---------- 138 | trade_type: String. 139 | A string indicating that the trade is either of type 'BUY', 'SELL' 140 | or 'EXIT'. 141 | """ 142 | if trade_type in (TradeTypes.sell_trade, TradeTypes.exit_trade): 143 | # You can always trade if you're exiting the position. 144 | return True 145 | elif self.available_capacity > 0: 146 | # You can always trade if there is still capacity in the portfolio. 147 | return True 148 | else: 149 | # Otherwise you can't trade. 150 | return False 151 | 152 | def add_filled_position(self, fill_event): 153 | """When a fill event is received that indicates that a new position has 154 | been taken up, that new position is recorded in a dictionary of filled 155 | positions. This function also adjusts the amount of free capital held by 156 | the portfolio down to reflect that a new position is held. 157 | 158 | This function also removes the corresponding pending position since the 159 | position has, when this function is called, been filled by the 160 | brokerage. 161 | 162 | Parameters 163 | ---------- 164 | fill_event: Fill event object. 165 | The fill event that will be added to the positions. 166 | """ 167 | # Extract variables to create the filled position. 168 | price = fill_event.fill_cost / fill_event.quantity 169 | symbol = fill_event.symbol 170 | trade_type = fill_event.trade_type 171 | direction = fill_event.direction 172 | quantity = fill_event.quantity 173 | action = action_dict[(direction, trade_type)] 174 | 175 | # If the position already exists, then raise an error because we 176 | # shouldn't be adding a new one. 177 | if symbol in self.filled_positions: 178 | raise ValueError( 179 | "Symbol is already in the portfolio and cannot be added." 180 | ) 181 | 182 | # We remove the pending position from the dictionary of such positions 183 | # and create a new entry in the filled position dictionary. 184 | pending = self.pending_positions.pop(symbol) 185 | filled = FilledPosition.from_pending_position( 186 | pending, fill_event.datetime, price 187 | ) 188 | 189 | # Transact shares (could be either initially or for an existing 190 | # position). 191 | filled.transact_shares(action, quantity, price) 192 | # Set the new position. 193 | self.filled_positions[symbol] = filled 194 | 195 | # Subtract from capital to reflect the cost of the new investment. 196 | self.capital -= filled.relative_value + fill_event.commission 197 | self.state.capital = self.capital 198 | 199 | def modify_filled_position(self, fill_event): 200 | """Modifies a position when a fill event is received. This includes 201 | removing the position from the dictionary of filled positions when the 202 | quantity is reduced to zero. 203 | 204 | Parameters 205 | ---------- 206 | fill_event: Fill event object. 207 | The fill event whose position will be modified in the positions. 208 | """ 209 | # Extract variables. 210 | symbol = fill_event.symbol 211 | fill_cost = fill_event.fill_cost 212 | price = fill_event.price 213 | direction = fill_event.direction 214 | trade_type = fill_event.trade_type 215 | quantity = fill_event.quantity 216 | datetime = fill_event.datetime 217 | action = action_dict[(direction, trade_type)] 218 | 219 | # Ensure validity of the symbol by ensuring that it is currently a 220 | # position within the portfolio. 221 | if symbol in self.filled_positions: 222 | filled = self.filled_positions[symbol] 223 | else: 224 | raise ValueError( 225 | "Cannot modify position for {} because the symbol is not in the" 226 | "portfolio".format(symbol) 227 | ) 228 | 229 | # Transact shares and evaluate whether or not the position has been 230 | # fully liquidated. 231 | filled.transact_shares(action, quantity, price) 232 | 233 | if filled.quantity == 0: 234 | # Delete from the dictionary of filled positions and append the 235 | # position to the list of closed positions. 236 | del self.filled_positions[symbol] 237 | self.closed_positions.setdefault(datetime, []).append(filled) 238 | 239 | # If we are live trading, then for compliance purposes mark the 240 | # position as being closed but have it retained. 241 | if fill_event.is_live: 242 | # First update the position in the database and then have its 243 | # contents transferred to the closed positions table. 244 | filled.to_database_position() 245 | sid = gets.id_for_symbol(symbol) 246 | pid = gets.id_for_portfolio(self.portfolio_id) 247 | inserts.closed_position(sid, pid) 248 | deletes.position(sid, pid) 249 | conn.commit() 250 | 251 | # Compute the percentage change of the execution price relative to the 252 | # average fill price of the asset. Notice that the amount of value 253 | # retrieved as cash when a position is liquidated depends on whether or 254 | # not we are long or short the underlying asset. 255 | # 256 | # TODO: Make sure this is correct with some rigorous test cases. 257 | p_chng = (price - filled.avg_price) / filled.avg_price 258 | if direction == Directions.long_dir: 259 | value = (1.0 + p_chng) * filled.avg_price * quantity 260 | elif direction == Directions.short_dir: 261 | value = (1.0 - p_chng) * filled.avg_price * quantity 262 | 263 | # Update the capital holdings of the portfolio. 264 | if trade_type in (TradeTypes.sell_trade, TradeTypes.exit_trade): 265 | self.capital += value - fill_event.commission 266 | elif trade_type == TradeTypes.buy_trade: 267 | self.capital -= fill_cost + fill_event.commission 268 | 269 | self.state.capital = self.capital 270 | 271 | def add_pending_position(self, order_event): 272 | """When an order event is initially issued the order is not immediately 273 | filled. To make note of this, we indicate in a dictionary of pending 274 | positions that there is a pending position that has not yet been 275 | completely filled. 276 | 277 | Parameters 278 | ---------- 279 | order_event: Order event object. 280 | The order event that will be appended as a pending order to the 281 | pending positions of the portfolio handler. 282 | """ 283 | symbol = order_event.symbol 284 | self.pending_positions[symbol] = PendingPosition( 285 | symbol, 286 | order_event.direction, 287 | order_event.trade_type, 288 | order_event.portfolio_id 289 | ) 290 | --------------------------------------------------------------------------------