Class: SQA::Portfolio
- Inherits:
-
Object
- Object
- SQA::Portfolio
- Defined in:
- lib/sqa/portfolio.rb
Defined Under Namespace
Instance Attribute Summary collapse
-
#cash ⇒ Object
Returns the value of attribute cash.
-
#commission ⇒ Object
Returns the value of attribute commission.
-
#initial_cash ⇒ Object
Returns the value of attribute initial_cash.
-
#positions ⇒ Object
Returns the value of attribute positions.
-
#trades ⇒ Object
Returns the value of attribute trades.
Class Method Summary collapse
-
.load_from_csv(filename) ⇒ Object
Load portfolio from CSV file.
Instance Method Summary collapse
-
#all_positions ⇒ Hash
Get all current positions.
-
#buy(ticker, shares:, price:, date: Date.today) ⇒ Trade
Buy shares of a stock.
-
#initialize(initial_cash: 10_000.0, commission: 0.0) ⇒ Portfolio
constructor
A new instance of Portfolio.
-
#position(ticker) ⇒ Position?
Get current position for a ticker.
-
#profit_loss(current_prices = {}) ⇒ Float
Calculate total profit/loss across all positions.
-
#profit_loss_percent(current_prices = {}) ⇒ Float
Calculate profit/loss percentage.
-
#save_to_csv(filename) ⇒ Object
Save portfolio to CSV file.
-
#save_trades_to_csv(filename) ⇒ Object
Save trade history to CSV file.
-
#sell(ticker, shares:, price:, date: Date.today) ⇒ Trade
Sell shares of a stock.
-
#summary(current_prices = {}) ⇒ Hash
Get summary statistics.
-
#total_return(current_prices = {}) ⇒ Float
Calculate total return (including dividends if tracked).
-
#trade_history ⇒ Array<Trade>
Get trade history.
-
#value(current_prices = {}) ⇒ Float
Calculate total portfolio value.
Constructor Details
#initialize(initial_cash: 10_000.0, commission: 0.0) ⇒ Portfolio
Returns a new instance of Portfolio.
41 42 43 44 45 46 47 |
# File 'lib/sqa/portfolio.rb', line 41 def initialize(initial_cash: 10_000.0, commission: 0.0) @initial_cash = initial_cash @cash = initial_cash @commission = commission # Commission per trade (flat fee or percentage) @positions = {} # { ticker => Position } @trades = [] # Array of Trade objects end |
Instance Attribute Details
#cash ⇒ Object
Returns the value of attribute cash.
8 9 10 |
# File 'lib/sqa/portfolio.rb', line 8 def cash @cash end |
#commission ⇒ Object
Returns the value of attribute commission.
8 9 10 |
# File 'lib/sqa/portfolio.rb', line 8 def commission @commission end |
#initial_cash ⇒ Object
Returns the value of attribute initial_cash.
8 9 10 |
# File 'lib/sqa/portfolio.rb', line 8 def initial_cash @initial_cash end |
#positions ⇒ Object
Returns the value of attribute positions.
8 9 10 |
# File 'lib/sqa/portfolio.rb', line 8 def positions @positions end |
#trades ⇒ Object
Returns the value of attribute trades.
8 9 10 |
# File 'lib/sqa/portfolio.rb', line 8 def trades @trades end |
Class Method Details
.load_from_csv(filename) ⇒ Object
Load portfolio from CSV file
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/sqa/portfolio.rb', line 290 def self.load_from_csv(filename) portfolio = new(initial_cash: 0) CSV.foreach(filename, headers: true) do |row| ticker = row['ticker'] shares = row['shares'].to_i avg_cost = row['avg_cost'].to_f total_cost = row['total_cost'].to_f portfolio.positions[ticker] = Position.new( ticker, shares, avg_cost, total_cost ) end portfolio end |
Instance Method Details
#all_positions ⇒ Hash
Get all current positions
167 168 169 |
# File 'lib/sqa/portfolio.rb', line 167 def all_positions @positions end |
#buy(ticker, shares:, price:, date: Date.today) ⇒ Trade
Buy shares of a stock
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/sqa/portfolio.rb', line 68 def buy(ticker, shares:, price:, date: Date.today) raise BadParameterError, "Shares must be positive" if shares <= 0 raise BadParameterError, "Price must be positive" if price <= 0 total_cost = shares * price commission = calculate_commission(total_cost) total_with_commission = total_cost + commission raise "Insufficient funds: need #{total_with_commission}, have #{@cash}" if total_with_commission > @cash # Update or create position if @positions[ticker] pos = @positions[ticker] total_shares = pos.shares + shares total_cost_basis = pos.total_cost + total_cost pos.shares = total_shares pos.avg_cost = total_cost_basis / total_shares pos.total_cost = total_cost_basis else @positions[ticker] = Position.new( ticker, shares, price, total_cost ) end # Deduct cash @cash -= total_with_commission # Record trade trade = Trade.new(ticker, :buy, shares, price, date, total_cost, commission) @trades << trade trade end |
#position(ticker) ⇒ Position?
Get current position for a ticker
161 162 163 |
# File 'lib/sqa/portfolio.rb', line 161 def position(ticker) @positions[ticker] end |
#profit_loss(current_prices = {}) ⇒ Float
Calculate total profit/loss across all positions
198 199 200 |
# File 'lib/sqa/portfolio.rb', line 198 def profit_loss(current_prices = {}) value(current_prices) - @initial_cash end |
#profit_loss_percent(current_prices = {}) ⇒ Float
Calculate profit/loss percentage
205 206 207 208 |
# File 'lib/sqa/portfolio.rb', line 205 def profit_loss_percent(current_prices = {}) return 0.0 if @initial_cash.zero? (profit_loss(current_prices) / @initial_cash) * 100.0 end |
#save_to_csv(filename) ⇒ Object
Save portfolio to CSV file
260 261 262 263 264 265 266 267 |
# File 'lib/sqa/portfolio.rb', line 260 def save_to_csv(filename) CSV.open(filename, 'wb') do |csv| csv << ['ticker', 'shares', 'avg_cost', 'total_cost'] @positions.each do |ticker, pos| csv << [ticker, pos.shares, pos.avg_cost, pos.total_cost] end end end |
#save_trades_to_csv(filename) ⇒ Object
Save trade history to CSV file
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/sqa/portfolio.rb', line 271 def save_trades_to_csv(filename) CSV.open(filename, 'wb') do |csv| csv << ['date', 'ticker', 'action', 'shares', 'price', 'total', 'commission'] @trades.each do |trade| csv << [ trade.date, trade.ticker, trade.action, trade.shares, trade.price, trade.total, trade.commission ] end end end |
#sell(ticker, shares:, price:, date: Date.today) ⇒ Trade
Sell shares of a stock
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/sqa/portfolio.rb', line 124 def sell(ticker, shares:, price:, date: Date.today) raise BadParameterError, "Shares must be positive" if shares <= 0 raise BadParameterError, "Price must be positive" if price <= 0 raise "No position in #{ticker}" unless @positions[ticker] pos = @positions[ticker] raise "Insufficient shares: trying to sell #{shares}, have #{pos.shares}" if shares > pos.shares total_sale = shares * price commission = calculate_commission(total_sale) net_proceeds = total_sale - commission # Update position if shares == pos.shares # Selling entire position @positions.delete(ticker) else # Partial sale - reduce shares and total cost proportionally cost_per_share = pos.total_cost / pos.shares pos.shares -= shares pos.total_cost -= (cost_per_share * shares) # avg_cost stays the same end # Add cash @cash += net_proceeds # Record trade trade = Trade.new(ticker, :sell, shares, price, date, total_sale, commission) @trades << trade trade end |
#summary(current_prices = {}) ⇒ Hash
Get summary statistics
243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/sqa/portfolio.rb', line 243 def summary(current_prices = {}) { initial_cash: @initial_cash, current_cash: @cash, positions_count: @positions.size, total_value: value(current_prices), profit_loss: profit_loss(current_prices), profit_loss_percent: profit_loss_percent(current_prices), total_return: total_return(current_prices), total_trades: @trades.size, buy_trades: @trades.count { |t| t.action == :buy }, sell_trades: @trades.count { |t| t.action == :sell } } end |
#total_return(current_prices = {}) ⇒ Float
Calculate total return (including dividends if tracked)
213 214 215 216 |
# File 'lib/sqa/portfolio.rb', line 213 def total_return(current_prices = {}) return 0.0 if @initial_cash.zero? profit_loss(current_prices) / @initial_cash end |
#trade_history ⇒ Array<Trade>
Get trade history
220 221 222 |
# File 'lib/sqa/portfolio.rb', line 220 def trade_history @trades end |
#value(current_prices = {}) ⇒ Float
Calculate total portfolio value
186 187 188 189 190 191 192 193 |
# File 'lib/sqa/portfolio.rb', line 186 def value(current_prices = {}) positions_value = @positions.sum do |ticker, pos| current_price = current_prices[ticker] || pos.avg_cost pos.value(current_price) end @cash + positions_value end |