|
|
|
|
|
|
|
|
|
|
| import os
|
| import joblib
|
| import numpy as np
|
| import pandas as pd
|
|
|
| from stable_baselines3 import PPO
|
|
|
| from config import (
|
| PPO_PATH,
|
| RL_FEATURE_DATA,
|
| PRICE_DATA,
|
| CORRELATION_MATRIX,
|
| ALL_STOCKS,
|
| RISK_MAPPING
|
| )
|
|
|
|
|
|
|
|
|
|
|
| def load_ppo():
|
|
|
| model = PPO.load(PPO_PATH)
|
|
|
| return model
|
|
|
|
|
|
|
|
|
|
|
| def load_data():
|
|
|
| rl_feature_data = joblib.load(RL_FEATURE_DATA)
|
|
|
| price_data = joblib.load(PRICE_DATA)
|
|
|
| correlation_matrix = joblib.load(CORRELATION_MATRIX)
|
|
|
| return (
|
|
|
| rl_feature_data,
|
|
|
| price_data,
|
|
|
| correlation_matrix
|
|
|
| )
|
|
|
|
|
|
|
|
|
|
|
| def get_latest_feature_matrix(
|
| rl_feature_data
|
| ):
|
|
|
| feature_matrix = []
|
|
|
| for stock in ALL_STOCKS:
|
|
|
| latest = (
|
|
|
| rl_feature_data[stock]
|
|
|
| .iloc[-1]
|
|
|
| .values
|
|
|
| .astype(np.float32)
|
|
|
| )
|
|
|
| feature_matrix.extend(latest)
|
|
|
| return np.array(
|
| feature_matrix,
|
| dtype=np.float32
|
| )
|
|
|
| def build_observation(
|
|
|
| rl_feature_data,
|
|
|
| budget,
|
|
|
| risk_profile,
|
|
|
| investment_horizon
|
|
|
| ):
|
|
|
| feature_matrix = get_latest_feature_matrix(
|
| rl_feature_data
|
| )
|
|
|
| shares = np.zeros(
|
| len(ALL_STOCKS),
|
| dtype=np.float32
|
| )
|
|
|
| cash = budget
|
|
|
| portfolio_value = budget
|
|
|
| if budget < 100000:
|
|
|
| tier = 0
|
|
|
| elif budget < 500000:
|
|
|
| tier = 1
|
|
|
| else:
|
|
|
| tier = 2
|
|
|
| portfolio_state = np.array(
|
|
|
| [
|
|
|
| budget,
|
|
|
| tier,
|
|
|
| cash,
|
|
|
| RISK_MAPPING[risk_profile],
|
|
|
| investment_horizon,
|
|
|
| portfolio_value
|
|
|
| ],
|
|
|
| dtype=np.float32
|
|
|
| )
|
|
|
| observation = np.concatenate(
|
|
|
| [
|
|
|
| feature_matrix,
|
|
|
| shares,
|
|
|
| portfolio_state
|
|
|
| ]
|
|
|
| )
|
|
|
| return observation
|
|
|
| def predict_portfolio(
|
|
|
| model,
|
|
|
| observation
|
|
|
| ):
|
|
|
| action, _ = model.predict(
|
|
|
| observation,
|
|
|
| deterministic=True
|
|
|
| )
|
|
|
| action = np.clip(
|
|
|
| action,
|
|
|
| 0,
|
|
|
| None
|
|
|
| )
|
|
|
| if action.sum() == 0:
|
|
|
| action += 1
|
|
|
| weights = action / action.sum()
|
|
|
| return weights
|
|
|
| def generate_portfolio(
|
|
|
| weights,
|
|
|
| budget
|
|
|
| ):
|
|
|
| allocation = pd.DataFrame({
|
|
|
| "Stock": ALL_STOCKS,
|
|
|
| "Weight (%)": weights[:-1] * 100,
|
|
|
| "Investment (₹)": weights[:-1] * budget
|
|
|
| })
|
|
|
| allocation = allocation.sort_values(
|
|
|
| "Weight (%)",
|
|
|
| ascending=False
|
|
|
| )
|
|
|
| allocation.reset_index(
|
|
|
| drop=True,
|
|
|
| inplace=True
|
|
|
| )
|
|
|
| return allocation
|
|
|
| def cash_remaining(
|
|
|
| weights,
|
|
|
| budget
|
|
|
| ):
|
|
|
| return weights[-1] * budget
|
|
|
| def portfolio_summary(
|
|
|
| weights,
|
|
|
| budget
|
|
|
| ):
|
|
|
| return {
|
|
|
| "Total Investment":
|
|
|
| budget -
|
|
|
| cash_remaining(
|
|
|
| weights,
|
|
|
| budget
|
|
|
| ),
|
|
|
| "Cash":
|
|
|
| cash_remaining(
|
|
|
| weights,
|
|
|
| budget
|
|
|
| ),
|
|
|
| "Number of Stocks":
|
|
|
| np.sum(
|
|
|
| weights[:-1] > 0
|
|
|
| )
|
|
|
| }
|
|
|
|
|
|
|
|
|
|
|
| def generate_recommendation(
|
| budget,
|
| risk_profile,
|
| investment_horizon
|
| ):
|
|
|
|
|
| model = load_ppo()
|
|
|
| rl_feature_data, price_data, correlation_matrix = load_data()
|
|
|
| snapshot_date = (
|
| pd.to_datetime(
|
| next(iter(rl_feature_data.values())).index[-1]
|
| ).strftime("%d-%b-%Y"))
|
|
|
|
|
| observation = build_observation(
|
| rl_feature_data=rl_feature_data,
|
| budget=budget,
|
| risk_profile=risk_profile,
|
| investment_horizon=investment_horizon
|
| )
|
|
|
|
|
| weights = predict_portfolio(
|
| model,
|
| observation
|
| )
|
|
|
|
|
| allocation = generate_portfolio(
|
| weights,
|
| budget
|
| )
|
|
|
|
|
| cash = cash_remaining(
|
| weights,
|
| budget
|
| )
|
|
|
|
|
| summary = portfolio_summary(
|
| weights,
|
| budget
|
| )
|
|
|
| return allocation, cash, summary, weights, snapshot_date |