|
|
|
|
|
|
|
|
|
|
| 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_prices(price_data):
|
|
|
| latest_prices = {}
|
|
|
| for stock in ALL_STOCKS:
|
|
|
| latest_prices[stock] = float(
|
| price_data[stock].iloc[-1]
|
| )
|
|
|
| return latest_prices
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 adjust_weights_for_risk(weights, risk_profile):
|
| """
|
| Post-process PPO allocation according to investor risk.
|
|
|
| PPO decides WHICH stocks are best.
|
| Risk engine decides HOW MUCH to allocate.
|
|
|
| weights[-1] = cash
|
| """
|
|
|
| weights = weights.copy()
|
|
|
| stock_weights = weights[:-1]
|
| cash = weights[-1]
|
|
|
|
|
| ranked = np.argsort(stock_weights)[::-1]
|
|
|
|
|
|
|
|
|
|
|
| if risk_profile == "Conservative":
|
|
|
| cash = 0.15
|
|
|
| template = np.array([
|
| 0.18,
|
| 0.16,
|
| 0.14,
|
| 0.12,
|
| 0.10,
|
| 0.07,
|
| 0.05,
|
| 0.02,
|
| 0.01,
|
| 0.00
|
| ])
|
|
|
|
|
|
|
|
|
|
|
| elif risk_profile == "Moderate":
|
|
|
| return weights
|
|
|
|
|
|
|
|
|
|
|
| else:
|
|
|
| cash = 0.02
|
|
|
| template = np.array([
|
| 0.28,
|
| 0.20,
|
| 0.15,
|
| 0.11,
|
| 0.09,
|
| 0.07,
|
| 0.05,
|
| 0.02,
|
| 0.01,
|
| 0.00
|
| ])
|
|
|
|
|
| template *= (1 - cash)
|
|
|
| new_stock_weights = np.zeros_like(stock_weights)
|
|
|
| for i, idx in enumerate(ranked):
|
| new_stock_weights[idx] = template[i]
|
|
|
| final = np.concatenate([new_stock_weights, [cash]])
|
|
|
| return final
|
|
|
| def generate_portfolio(
|
| weights,
|
| budget,
|
| price_data
|
| ):
|
|
|
| latest_prices = get_latest_prices(price_data)
|
|
|
| allocation = []
|
|
|
| cash = budget * weights[-1]
|
|
|
|
|
| for stock, weight in zip(ALL_STOCKS, weights[:-1]):
|
|
|
| target = budget * weight
|
|
|
| price = latest_prices[stock]
|
|
|
| shares = int(target // price)
|
|
|
| invested = shares * price
|
|
|
| cash += target - invested
|
|
|
| allocation.append({
|
|
|
| "Stock": stock,
|
| "Price (₹)": round(price,2),
|
| "Target": target,
|
| "Shares": shares,
|
| "Investment (₹)": invested
|
|
|
| })
|
|
|
| allocation = pd.DataFrame(allocation)
|
|
|
|
|
|
|
|
|
|
|
| while True:
|
|
|
| affordable = allocation[
|
| allocation["Price (₹)"] <= cash
|
| ]
|
|
|
| if affordable.empty:
|
| break
|
|
|
|
|
| affordable = affordable.copy()
|
|
|
| affordable["Gap"] = (
|
| affordable["Target"] -
|
| affordable["Investment (₹)"]
|
| )
|
|
|
| affordable = affordable.sort_values(
|
| "Gap",
|
| ascending=False
|
| )
|
|
|
| bought = False
|
|
|
| for idx in affordable.index:
|
|
|
| price = allocation.loc[idx, "Price (₹)"]
|
|
|
| if price <= cash:
|
|
|
| allocation.loc[idx, "Shares"] += 1
|
|
|
| allocation.loc[idx, "Investment (₹)"] += price
|
|
|
| cash -= price
|
|
|
| bought = True
|
|
|
| break
|
|
|
| if not bought:
|
| break
|
|
|
| allocation = allocation[
|
| allocation["Shares"] > 0
|
| ]
|
|
|
| allocation["Weight (%)"] = (
|
| allocation["Investment (₹)"] /
|
| budget
|
| ) * 100
|
|
|
| allocation = allocation.drop(
|
| columns="Target"
|
| )
|
|
|
| allocation = allocation.sort_values(
|
| "Investment (₹)",
|
| ascending=False
|
| )
|
|
|
| allocation.reset_index(
|
| drop=True,
|
| inplace=True
|
| )
|
|
|
| return allocation, round(cash,2)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| )
|
|
|
| weights = adjust_weights_for_risk(
|
| weights,
|
| risk_profile
|
| )
|
|
|
| allocation, cash = generate_portfolio(
|
| weights,
|
| budget,
|
| price_data
|
| )
|
|
|
| summary = {
|
|
|
| "Total Investment":
|
| allocation["Investment (₹)"].sum(),
|
|
|
| "Cash":
|
| cash,
|
|
|
| "Number of Stocks":
|
| len(allocation)
|
|
|
| }
|
|
|
| return allocation, cash, summary, weights, snapshot_date |