Skip to content

Commit

Permalink
Merge pull request #18 from r0fls/rd/constant-percentage-state-fixes
Browse files Browse the repository at this point in the history
fix constant percentage strategy state issues
  • Loading branch information
r0fls authored Jun 9, 2024
2 parents 767149c + b6f70b6 commit 2826b8e
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 10 deletions.
1 change: 0 additions & 1 deletion init_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,3 @@
session.add_all(fake_accounts)
session.commit()
print("Fake account data inserted into the database.")

2 changes: 1 addition & 1 deletion main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import argparse
import time
from datetime import datetime, timedelta
from database.models import init_db
from database.models import init_db, drop_then_init_db
from ui.app import create_app
from utils.config import parse_config, initialize_brokers, initialize_strategies
from sqlalchemy import create_engine
Expand Down
8 changes: 8 additions & 0 deletions strategies/base_strategy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from database.models import Balance
from utils.logger import logger # Import the logger

class BaseStrategy(ABC):
def __init__(self, broker):
Expand All @@ -11,10 +12,14 @@ def rebalance(self):
pass

def initialize_starting_balance(self):
logger.debug("Initializing starting balance")

account_info = self.broker.get_account_info()
buying_power = account_info.get('buying_power')
logger.debug(f"Account info: {account_info}")

if buying_power < self.starting_capital:
logger.error(f"Not enough cash available. Required: {self.starting_capital}, Available: {buying_power}")
raise ValueError("Not enough cash available to initialize the strategy with the desired starting capital.")

with self.broker.Session() as session:
Expand All @@ -33,3 +38,6 @@ def initialize_starting_balance(self):
)
session.add(strategy_balance)
session.commit()
logger.info(f"Initialized starting balance for {self.strategy_name} strategy with {self.starting_capital}")
else:
logger.info(f"Existing balance found for {self.strategy_name} strategy: {strategy_balance.balance}")
107 changes: 100 additions & 7 deletions strategies/constant_percentage_strategy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from datetime import timedelta
from strategies.base_strategy import BaseStrategy
from database.models import Balance
from database.models import Balance, Position
from utils.logger import logger
from utils.utils import is_market_open
from datetime import datetime

class ConstantPercentageStrategy(BaseStrategy):
def __init__(self, broker, stock_allocations, cash_percentage, rebalance_interval_minutes, starting_capital):
Expand All @@ -11,37 +14,127 @@ def __init__(self, broker, stock_allocations, cash_percentage, rebalance_interva
self.starting_capital = starting_capital
self.strategy_name = 'constant_percentage'
super().__init__(broker)
self.sync_positions_with_broker() # Ensure positions are synced on initialization
logger.info(f"Initialized {self.strategy_name} strategy with starting capital {self.starting_capital}")

def rebalance(self):
logger.debug("Starting rebalance process")
self.sync_positions_with_broker() # Ensure positions are synced before rebalancing

account_info = self.broker.get_account_info()
cash_balance = account_info.get('cash_available')
logger.debug(f"Account info: {account_info}")

with self.broker.Session() as session:
balance = session.query(Balance).filter_by(
strategy=self.strategy_name,
broker=self.broker.broker_name,
type='cash'
).first()
if balance is None:
raise ValueError(f"Strategy balance not initialized for {self.strategy_name} strategy on {self.broker}.")
logger.error(f"Strategy balance not initialized for {self.strategy_name} strategy on {self.broker.broker_name}.")
raise ValueError(f"Strategy balance not initialized for {self.strategy_name} strategy on {self.broker.broker_name}.")
total_balance = balance.balance
logger.debug(f"Total balance retrieved: {total_balance}")

target_cash_balance = total_balance * self.cash_percentage
target_investment_balance = total_balance - target_cash_balance
logger.debug(f"Target cash balance: {target_cash_balance}, Target investment balance: {target_investment_balance}")

current_positions = self.get_current_positions()
logger.debug(f"Current positions: {current_positions}")

# TODO: query the number of current positions in the DB for each ticker
# associated with this strategy, and then get their current value.
for stock, allocation in self.stock_allocations.items():
target_balance = target_investment_balance * allocation
current_position = current_positions.get(stock, 0)
current_price = self.broker.get_current_price(stock)
target_quantity = target_balance // current_price
logger.debug(f"Stock: {stock}, Allocation: {allocation}, Target balance: {target_balance}, Current position: {current_position}, Current price: {current_price}, Target quantity: {target_quantity}")

if current_position < target_quantity:
self.broker.place_order(stock, target_quantity - current_position, 'buy', self.strategy_name)
if is_market_open():
self.broker.place_order(stock, target_quantity - current_position, 'buy', self.strategy_name)
logger.info(f"Placed buy order for {stock}: {target_quantity - current_position} shares")
else:
logger.info(f"Market is closed, not buying {stock}: {target_quantity - current_position} shares")
elif current_position > target_quantity:
self.broker.place_order(stock, current_position - target_quantity, 'sell', self.strategy_name)
if is_market_open():
self.broker.place_order(stock, current_position - target_quantity, 'sell', self.strategy_name)
logger.info(f"Placed sell order for {stock}: {current_position - target_quantity} shares")
else:
logger.info(f"Market is closed, not selling {stock}: {target_quantity - current_position} shares")

def get_current_positions(self):
positions = self.broker.get_positions()
return {position: positions[position]['quantity'] for position in positions}
positions_dict = {position: positions[position]['quantity'] for position in positions}
logger.debug(f"Retrieved current positions: {positions_dict}")
return positions_dict


# TODO: can we abstract this method across strategies?
def sync_positions_with_broker(self):
logger.debug("Syncing positions with broker")

broker_positions = self.broker.get_positions()
logger.debug(f"Broker positions: {broker_positions}")


with self.broker.Session() as session:
# Get the actual positions from the broker
for symbol, data in broker_positions.items():
current_price = self.broker.get_current_price(symbol)
target_quantity = self.should_own(symbol, current_price)
position = session.query(Position).filter_by(
broker=self.broker.broker_name,
strategy=None,
symbol=symbol
).first()
if position:
if target_quantity > 0:
position.strategy = self.strategy_name
position.quantity = data['quantity']
position.latest_price = current_price
position.last_updated = datetime.utcnow()
logger.info(f"Updated uncategorized position for {symbol} to strategy {self.strategy_name} with quantity {data['quantity']} and price {current_price}")
else:
position = session.query(Position).filter_by(
broker=self.broker.broker_name,
strategy=self.strategy_name,
symbol=symbol
).first()
# There is already an existing position as part of this strategy
if position:
position.strategy = self.strategy_name
position.quantity = data['quantity']
position.latest_price = current_price
position.last_updated = datetime.utcnow()
logger.info(f"Updated position for {symbol} with strategy {self.strategy_name} with quantity {data['quantity']} and price {current_price}")
else:
# Create a new position
position = Position(
broker=self.broker.broker_name,
strategy=self.strategy_name,
symbol=symbol,
quantity=data['quantity'],
latest_price=current_price,
last_updated=datetime.utcnow()
)
session.add(position)
logger.info(f"Created new position for {symbol} with quantity {data['quantity']} and price {current_price}")

session.commit()
logger.debug("Positions synced with broker")

def should_own(self, symbol, current_price):
"""Determine the quantity of the given symbol that should be owned according to the strategy."""
with self.broker.Session() as session:
balance = session.query(Balance).filter_by(
strategy=self.strategy_name,
broker=self.broker.broker_name,
type='cash'
).first()
allocation = self.stock_allocations.get(symbol, 0)
total_balance = balance.balance
target_investment_balance = total_balance * (1 - self.cash_percentage)
target_quantity = target_investment_balance * allocation / current_price
return target_quantity
3 changes: 2 additions & 1 deletion utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ def initialize_brokers(config):
def initialize_strategies(brokers, config):
strategies_config = config['strategies']
strategies = []
for strategy_config in strategies_config:
for strategy_name in strategies_config:
strategy_config = strategies_config[strategy_name]
strategy_type = strategy_config['type']
broker_name = strategy_config['broker']
broker = brokers[broker_name]
Expand Down
22 changes: 22 additions & 0 deletions utils/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from datetime import datetime, time
import pytz

def is_market_open():
# Define market open and close times (e.g., 9:30 AM to 4:00 PM Eastern Time)
market_open = time(9, 30)
market_close = time(16, 0)

# Get current time in Eastern Time
eastern = pytz.timezone('US/Eastern')
current_time = datetime.now(eastern).time()

# Check if today is a weekend
current_date = datetime.now(eastern).date()
if current_date.weekday() >= 5: # 5 = Saturday, 6 = Sunday
return False

# Check if current time is within market hours
if market_open <= current_time <= market_close:
return True

return False

0 comments on commit 2826b8e

Please sign in to comment.