Skip to content

Commit

Permalink
Portfolio Proof of concept
Browse files Browse the repository at this point in the history
  • Loading branch information
spencer-sliffe committed Nov 19, 2024
1 parent a793029 commit e874d33
Show file tree
Hide file tree
Showing 8 changed files with 456 additions and 10 deletions.
6 changes: 4 additions & 2 deletions Backend/kobrastocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Path: Backend/kobrastocks/__init__.py
Description:
Initializes the Flask application and configures key components, including database, encryption, JWT, CORS, and environment variables. Registers main, authentication, and user blueprints for route handling.
Initializes the Flask application and configures key components, including database, encryption, JWT, CORS, and environment variables. Registers main, authentication, user, and portfolio blueprints for route handling.
Input:
Environment variables (SECRET_KEY, SQLALCHEMY_DATABASE_URI, JWT_SECRET_KEY)
Expand All @@ -23,6 +23,7 @@
from .routes import main as main_blueprint
from .auth_routes import auth as auth_blueprint
from .user_routes import user as user_blueprint
from .portfolio_routes import portfolio as portfolio_blueprint # Updated import
from flask_migrate import Migrate
from flask_cors import CORS
from dotenv import load_dotenv
Expand All @@ -45,4 +46,5 @@
# Register blueprints here
app.register_blueprint(main_blueprint)
app.register_blueprint(auth_blueprint)
app.register_blueprint(user_blueprint)
app.register_blueprint(user_blueprint)
app.register_blueprint(portfolio_blueprint)
29 changes: 29 additions & 0 deletions Backend/kobrastocks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .extensions import db, bcrypt
from datetime import datetime


class User(db.Model):
__tablename__ = 'users'

Expand All @@ -34,23 +35,51 @@ class User(db.Model):

favorite_stocks = db.relationship('FavoriteStock', backref='user', lazy=True)
watched_stocks = db.relationship('WatchedStock', backref='user', lazy=True)
portfolio = db.relationship('Portfolio', back_populates='user', uselist=False)


def check_password(self, password):
return bcrypt.check_password_hash(self.password_hash, password)

def set_password(self, password):
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')


class FavoriteStock(db.Model):
__tablename__ = 'favorite_stocks'

id = db.Column(db.Integer, primary_key=True)
ticker = db.Column(db.String(10), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)


class WatchedStock(db.Model):
__tablename__ = 'watched_stocks'

id = db.Column(db.Integer, primary_key=True)
ticker = db.Column(db.String(10), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)


class Portfolio(db.Model):
__tablename__ = 'portfolios'

id = db.Column(db.Integer, primary_key=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True)

# Relationships
user = db.relationship('User', back_populates='portfolio')
stocks = db.relationship('PortfolioStock', back_populates='portfolio', cascade='all, delete-orphan')


class PortfolioStock(db.Model):
__tablename__ = 'portfolio_stocks'

id = db.Column(db.Integer, primary_key=True)
ticker = db.Column(db.String(10), nullable=False)
amount_invested = db.Column(db.Float, nullable=False)
portfolio_id = db.Column(db.Integer, db.ForeignKey('portfolios.id'))

# Relationships
portfolio = db.relationship('Portfolio', back_populates='stocks')
86 changes: 86 additions & 0 deletions Backend/kobrastocks/portfolio_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
------------------Prologue--------------------
File Name: portfolio_routes.py
Path: Backend/kobrastocks/portfolio_routes.py
Description:
Defines routes for managing the user's portfolio. Includes endpoints for:
- Retrieving the user's portfolio.
- Adding a stock to the portfolio.
- Removing a stock from the portfolio.
- Getting recommendations for the portfolio based on owned stocks.
Input:
JSON data for stock tickers and amounts, JWT tokens for authentication.
Output:
JSON responses with portfolio data, status messages, and recommendations.
Collaborators: Spencer Sliffe
---------------------------------------------
"""

from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from .services import (
add_stock_to_portfolio,
remove_stock_from_portfolio,
get_portfolio,
get_portfolio_recommendations
)
from .serializers import (
portfolio_schema,
portfolio_recommendations_schema
)

portfolio = Blueprint('portfolio', __name__, url_prefix='/api/portfolio')

@portfolio.route('', methods=['GET'])
@jwt_required()
def get_user_portfolio():
user_id = get_jwt_identity()
portfolio_data = get_portfolio(user_id)
return jsonify(portfolio_schema.dump({'stocks': portfolio_data})), 200

@portfolio.route('', methods=['POST'])
@jwt_required()
def add_stock():
user_id = get_jwt_identity()
data = request.get_json()
ticker = data.get('ticker')
amount_invested = data.get('amount_invested')

if not ticker or amount_invested is None:
return jsonify({'message': 'Ticker and amount_invested are required.'}), 400

success = add_stock_to_portfolio(user_id, ticker, amount_invested)
if success:
return jsonify({'message': f'{ticker} added to portfolio.'}), 200
else:
return jsonify({'message': 'Failed to add stock to portfolio.'}), 500

@portfolio.route('/<string:ticker>', methods=['DELETE'])
@jwt_required()
def remove_stock(ticker):
user_id = get_jwt_identity()
success = remove_stock_from_portfolio(user_id, ticker)
if success:
return jsonify({'message': f'{ticker} removed from portfolio.'}), 200
else:
return jsonify({'message': 'Failed to remove stock from portfolio.'}), 500

@portfolio.route('/recommendations', methods=['GET'])
@jwt_required()
def get_portfolio_recs():
user_id = get_jwt_identity()
indicators = {
'MACD': request.args.get('MACD', 'false').lower() == 'true',
'RSI': request.args.get('RSI', 'false').lower() == 'true',
'SMA': request.args.get('SMA', 'false').lower() == 'true',
'EMA': request.args.get('EMA', 'false').lower() == 'true',
'ATR': request.args.get('ATR', 'false').lower() == 'true',
'BBands': request.args.get('BBands', 'false').lower() == 'true',
'VWAP': request.args.get('VWAP', 'false').lower() == 'true',
}
recommendations = get_portfolio_recommendations(user_id, indicators)
return jsonify(portfolio_recommendations_schema.dump({'recommendations': recommendations})), 200
27 changes: 27 additions & 0 deletions Backend/kobrastocks/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,30 @@ class ContactFormSchema(Schema):
message = fields.Str(required=True, validate=validate.Length(min=10))

contact_form_schema = ContactFormSchema()


class PortfolioStockSchema(Schema):
ticker = fields.Str(required=True)
amount_invested = fields.Float(required=True)
open_price = fields.Float()
close_price = fields.Float()
high_price = fields.Float()
low_price = fields.Float()
volume = fields.Int()
name = fields.Str()
percentage_change = fields.Float()


portfolio_stock_schema = PortfolioStockSchema()
portfolio_stock_list_schema = PortfolioStockSchema(many=True)


class PortfolioSchema(Schema):
stocks = fields.Nested(PortfolioStockSchema, many=True)

portfolio_schema = PortfolioSchema()

class PortfolioRecommendationsSchema(Schema):
recommendations = fields.Dict(keys=fields.Str(), values=fields.Raw())

portfolio_recommendations_schema = PortfolioRecommendationsSchema()
76 changes: 76 additions & 0 deletions Backend/kobrastocks/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
import plotly.graph_objs as go
from plotly.subplots import make_subplots
import logging

from . import db
from .models import PortfolioStock, Portfolio
from .utils import *

# Configure logging
Expand Down Expand Up @@ -538,3 +541,76 @@ def send_email(subject, recipient, body):
print(f"Sending email to {recipient}")
print(f"Subject: {subject}")
print(f"Body: {body}")


def get_or_create_portfolio(user_id):
portfolio = Portfolio.query.filter_by(user_id=user_id).first()
if not portfolio:
portfolio = Portfolio(user_id=user_id)
db.session.add(portfolio)
db.session.commit()
return portfolio

def add_stock_to_portfolio(user_id, ticker, amount_invested):
try:
portfolio = get_or_create_portfolio(user_id)
# Check if the stock already exists in the portfolio
existing_stock = PortfolioStock.query.filter_by(portfolio_id=portfolio.id, ticker=ticker).first()
if existing_stock:
existing_stock.amount_invested += amount_invested
else:
new_stock = PortfolioStock(
ticker=ticker.upper(),
amount_invested=amount_invested,
portfolio=portfolio
)
db.session.add(new_stock)
db.session.commit()
return True
except Exception as e:
logger.error(f"Error adding stock to portfolio: {e}")
return False

def remove_stock_from_portfolio(user_id, ticker):
try:
portfolio = get_or_create_portfolio(user_id)
stock = PortfolioStock.query.filter_by(portfolio_id=portfolio.id, ticker=ticker).first()
if stock:
db.session.delete(stock)
db.session.commit()
return True
else:
return False
except Exception as e:
logger.error(f"Error removing stock from portfolio: {e}")
return False

def get_portfolio(user_id):
portfolio = get_or_create_portfolio(user_id)
stocks = PortfolioStock.query.filter_by(portfolio_id=portfolio.id).all()
portfolio_data = []
for stock in stocks:
stock_data = get_stock_data(stock.ticker)
if stock_data:
stock_data['amount_invested'] = stock.amount_invested
portfolio_data.append(stock_data)
return portfolio_data

def get_portfolio_recommendations(user_id, indicators={}):
portfolio = get_or_create_portfolio(user_id)
stocks = PortfolioStock.query.filter_by(portfolio_id=portfolio.id).all()
recommendations = {}
for stock in stocks:
predictions = get_predictions(
stock.ticker,
MACD=indicators.get('MACD', False),
RSI=indicators.get('RSI', False),
SMA=indicators.get('SMA', False),
EMA=indicators.get('EMA', False),
ATR=indicators.get('ATR', False),
BBands=indicators.get('BBands', False),
VWAP=indicators.get('VWAP', False)
)
if predictions:
recommendations[stock.ticker] = predictions
return recommendations
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Add Portfolio and PortfolioStock models
Revision ID: 5bc0760514d0
Revises: 6d99efad94ca
Create Date: 2024-11-19 12:08:28.887038
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '5bc0760514d0'
down_revision = '6d99efad94ca'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('portfolios',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
op.create_table('portfolio_stocks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('ticker', sa.String(length=10), nullable=False),
sa.Column('amount_invested', sa.Float(), nullable=False),
sa.Column('portfolio_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['portfolio_id'], ['portfolios.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('portfolio_stocks')
op.drop_table('portfolios')
# ### end Alembic commands ###
31 changes: 29 additions & 2 deletions client/src/components/AppNavbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<!-- Account Button -->
<div v-if="authState.isAuthenticated" class="account-icon">
<button @click="toggleAccountDrawer">
<!-- You can use an icon here instead of text -->
Account
<!-- Display the user's first name instead of "Account" -->
{{ user ? user.first_name : 'Account' }}
</button>
</div>
<div class="hamburger" @click="toggleMenu">☰</div>
Expand All @@ -33,6 +33,7 @@
</template>

<script>
import axios from 'axios';
import { authState } from '@/auth';
import AccountDrawer from '@/components/AccountDrawer.vue';

Expand All @@ -46,15 +47,41 @@ export default {
menuVisible: false,
authState,
isAccountDrawerVisible: false,
user: null, // Added to store user details
};
},
created() {
if (this.authState.isAuthenticated) {
this.fetchUserDetails();
}
},
watch: {
'authState.isAuthenticated'(newVal) {
if (newVal) {
this.fetchUserDetails();
} else {
this.user = null;
}
},
},
methods: {
toggleMenu() {
this.menuVisible = !this.menuVisible;
},
toggleAccountDrawer() {
this.isAccountDrawerVisible = !this.isAccountDrawerVisible;
},
fetchUserDetails() {
axios
.get('/api/user')
.then((response) => {
this.user = response.data;
})
.catch((error) => {
console.error('Error fetching user details:', error);
// Optionally handle error (e.g., redirect to login)
});
},
},
};
</script>
Loading

0 comments on commit e874d33

Please sign in to comment.