1- from typing import Dict , List
2-
31import pandas as pd
2+ import yfinance as yf
3+ from typing import Dict , List , Optional
44
55from contracts .asset import Asset , CashAsset
6- from strategies .stock .base import StrategyBase
6+ from contracts .utils import clean_ticker
7+ from strategies .stock .base import StrategyBase , StrategyFactory
78
89
910class Portfolio :
@@ -13,40 +14,61 @@ class Portfolio:
1314 """
1415 def __init__ (self ,
1516 name : str ,
16- tickers : List [str ],
17+ tickers : str | List [str ],
1718 starting_cash : float ,
18- strategy : StrategyBase ,
19+ strategy : str ,
1920 benchmark : str = "SPY" ,
20- rebalance_freq : str = "monthly" ,
21+ rebalance_freq : Optional [str ] = None ,
22+ recomposition_freq : Optional [str ] = None ,
2123 metadata : Dict = None ):
2224 """
2325 Initialize a Portfolio object.
2426
2527 Args:
2628 name (str): Name of the portfolio.
2729 tickers (List[str]): List of asset tickers.
28- starting_cash (float): Initial cash balance .
30+ starting_cash (float): Initial cash shares .
2931 strategy (StrategyBase): Strategy instance to generate signals.
3032 benchmark (str): Benchmark ticker for reference only.
31- rebalance_freq (str): Rebalancing frequency (e.g., 'monthly').
33+ rebalance_freq (str, optional): Rebalancing frequency (e.g., 'monthly', 'quarterly', 'annually').
34+ recomposition_freq (str, optional): Frequency for recomposition (e.g., 'monthly', 'quarterly', 'annually').
3235 metadata (Dict, optional): Additional metadata.
3336 """
37+ # Initialize Portfolio name
38+ assert (isinstance (name , str ) and not name .strip ()), "Portfolio name must be a non-empty string"
3439 self .name = name
40+
41+ # Initialize tickers
42+ if isinstance (tickers , str ):
43+ tickers = [clean_ticker (ticker ) for ticker in tickers .split ("," )]
44+ assert isinstance (tickers , list ), "tickers must be a list or comma-separated string"
3545 self .tickers = tickers
36- self .strategy = strategy
46+
47+ # Initialize strategy
48+ assert strategy in StrategyFactory .get_supported_strategies (), f"Unsupported strategy: { strategy } . Supported strategies: { StrategyFactory .get_supported_strategies ()} "
49+ self .strategy = StrategyFactory .create_strategy (strategy )
50+
51+ # Initialise benchmark
52+ benchmark = clean_ticker (benchmark )
53+ assert isinstance (benchmark , str ), "Benchmark must be a string"
3754 self .benchmark = benchmark
38- self .rebalance_freq = rebalance_freq
39- self .metadata = metadata or {}
4055
41- self .positions : Dict [str , Asset | CashAsset ] = {
56+ self ._positions : Dict [str , Asset | CashAsset ] = {
4257 ticker : Asset (ticker )
4358 for ticker in tickers
4459 }
45- self .positions ['CASH' ] = CashAsset (starting_cash )
60+ self ._cash = CashAsset (starting_cash )
61+
4662 self .trade_log : List [dict ] = []
4763 self .position_history : Dict [str , List [int ]] = {}
4864
49- def execute_trade (self , date : pd .Timestamp , ticker : str , action : str , shares : int , price : float , note : str = 'Strategy Signal' ):
65+ self .rebalance_freq = rebalance_freq
66+ self .recomposition_freq = recomposition_freq
67+ self .metadata = metadata or {}
68+
69+ def execute_trade (self , date : pd .Timestamp , ticker : str ,
70+ action : str , shares : int , price : float ,
71+ note : str = 'Strategy Signal' ):
5072 """
5173 Execute a trade and update portfolio positions and cash.
5274
@@ -61,28 +83,27 @@ def execute_trade(self, date: pd.Timestamp, ticker: str, action: str, shares: in
6183 ValueError: If insufficient cash or shares.
6284 """
6385 trade_value = shares * price
64- cash_asset = self .positions ['CASH' ]
6586
6687 if action == 'BUY' :
67- if cash_asset . balance < trade_value :
88+ if self . cash < trade_value :
6889 raise ValueError (f"Insufficient cash to buy { shares } shares of { ticker } " )
69- cash_asset .withdraw_cash (trade_value )
90+ self . _cash .withdraw_cash (trade_value )
7091 self .update_position (ticker , shares )
7192
7293 elif action == 'SELL' :
7394 held = self .get_position (ticker )
7495 if held < shares :
7596 raise ValueError (f"Trying to sell more shares than held for { ticker } " )
7697 self .update_position (ticker , - shares )
77- cash_asset .deposit_cash (trade_value )
98+ self . _cash .deposit_cash (trade_value )
7899
79100 else :
80101 raise ValueError ("Action must be either 'BUY' or 'SELL'" )
81102
82- self .add_trade (date , ticker , action , shares , price , cash_asset . balance , note )
103+ self .add_trade (date , ticker , action , shares , price , self . _cash . shares , note )
83104
84105 def get_cash (self ) -> float :
85- return self .positions ['CASH' ].balance
106+ return self .positions ['CASH' ].shares
86107
87108 def add_trade (self , date , ticker , action , shares , price , cash_remaining , note = '' ):
88109 entry = {
@@ -135,3 +156,25 @@ def get_portfolio_value(self, prices: Dict[str, float]) -> float:
135156 for ticker in self .tickers :
136157 value += self .positions [ticker ].shares * prices .get (ticker , 0.0 )
137158 return value
159+
160+ # region Properties
161+
162+ @property
163+ def cash (self ) -> float :
164+ """
165+ Get the current cash shares in the portfolio.
166+ Returns:
167+ float: Current cash shares.
168+ """
169+ return self .positions ['CASH' ].shares
170+
171+ @property
172+ def positions (self ) -> dict :
173+ """
174+ Get the current positions in the portfolio.
175+ Returns:
176+ dict: Dictionary of asset ticker to Asset object.
177+ """
178+ return self .positions
179+
180+ # endregion Properties
0 commit comments