Skip to content

Commit 14bb789

Browse files
committed
Add portfolio rebalance app
1 parent a42dc73 commit 14bb789

10 files changed

Lines changed: 657 additions & 0 deletions

File tree

portfolio-rebalance/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Portfolio rebalancing recommendation app
2+
3+
This Writer Framework application uses the powerful Palmyra-Fin model to analyze portfolio data and provide recommendations for rebalancing an investment portfolio. You can also download the analysis as a text file and clear results to start over. This app supports PDF or Excel files.
4+
5+
## Usage
6+
7+
1. Upload the data sample from the `example_data` folder of the app. Alternatively, you can upload your own portfolio data in Excel (.xls, .xlsx) or PDF format. Note that you may need adjust the prompts in `prompts.py` to achieve the best results.
8+
2. Once the file is uploaded, the application analyzes your portfolio data, highlighting both positive and negative aspects such as stock selection, sector weightings, and more.
9+
3. Based on this analysis, the app generates recommendations for rebalancing your portfolio to optimize performance.
10+
11+
## Running the application
12+
13+
First, ensure you have Poetry installed. Then, in the project directory, install the dependencies by running:
14+
15+
```sh
16+
poetry install
17+
```
18+
19+
To build this application, you'll need to sign up for [Writer AI Studio](https://app.writer.com/aistudio/signup?utm_campaign=devrel) and create a new API Key. To pass your API key to the Writer Framework, you'll need to set an environment variable called `WRITER_API_KEY`:
20+
21+
```sh
22+
export WRITER_API_KEY=your-api-key
23+
```
24+
25+
To make changes or edit the application, navigate to root folder and use the following command:
26+
27+
28+
```sh
29+
writer edit .
30+
```
31+
32+
Depending on how your environment is set up, you may need to run `writer` with `poetry run` like this:
33+
34+
```sh
35+
poetry run writer edit .
36+
```
37+
38+
Once you're ready to run the application, execute:
39+
40+
```sh
41+
writer run .
42+
```
43+
44+
To learn more, check out the [full documentation for Writer Framework](https://dev.writer.com/framework/introduction).
45+
46+
## About Writer
47+
48+
Writer is the full-stack generative AI platform for enterprises. Quickly and easily build and deploy generative AI apps with a suite of developer tools fully integrated with our platform of LLMs, graph-based RAG tools, AI guardrails, and more. Learn more at [writer.com](https://www.writer.com?utm_source=github&utm_medium=readme&utm_campaign=framework).
481 KB
Binary file not shown.
17.8 KB
Binary file not shown.

portfolio-rebalance/main.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import asyncio
2+
import os
3+
import shutil
4+
5+
import pandas as pd
6+
import writer as wf
7+
import writer.ai
8+
from dotenv import load_dotenv
9+
10+
from prompts import (generate_negative_concatenation_prompt,
11+
generate_negative_impacts_prompt,
12+
generate_negative_stock_selection_prompt,
13+
generate_positive_concatenation_prompt,
14+
generate_positive_impacts_prompt,
15+
generate_positive_sector_weighting_prompt,
16+
generate_positive_stock_selection_prompt,
17+
generate_rebalance_recommendation_prompt)
18+
19+
load_dotenv()
20+
21+
pd.options.mode.chained_assignment = None
22+
23+
24+
def handle_file_on_change(state, payload):
25+
clear_results(state)
26+
_save_file(state, payload[0])
27+
28+
file_extension = state["file"]["name"].split(".")[-1].lower()
29+
30+
match file_extension:
31+
case "xlsx" | "xls":
32+
df = _read_excel(state)
33+
case "pdf":
34+
text_data = _read_pdf(state)
35+
case _:
36+
state["processing-message"] = "Unsupported file type"
37+
return
38+
39+
state["processing-message"] = "Analyzing positive and negative impacts..."
40+
prompt = analyze_data(
41+
df if file_extension in ["xlsx", "xls"] else text_data
42+
)
43+
44+
state["processing-message"] = "Generating rebalancing recommendation..."
45+
state["analysis-result"] = ""
46+
47+
# Using Palmyra-Fin model
48+
# for chunk in writer.ai.stream_complete(prompt, {"model": "palmyra-fin-32k", "max_tokens": 2048, "temperature": 0.7}):
49+
# state["analysis-result"] += chunk
50+
51+
# Using Palmyra X 004 model
52+
conversation = writer.ai.Conversation([{"role": "user", "content": prompt}], {"model": "palmyra-x-004", "max_tokens": 2048, "temperature": 0.7})
53+
for chunk in conversation.stream_complete():
54+
if chunk.get("content"):
55+
state["analysis-result"] += chunk.get("content")
56+
57+
state["visual_block_visible"] = True
58+
state["processing-message"] = ""
59+
60+
61+
def clear_results(state):
62+
state["analysis-result"] = "Your analysis will appear here."
63+
_delete_all_files(state)
64+
state["visual_block_visible"] = False
65+
66+
67+
def _save_file(state, file):
68+
name = file.get("name")
69+
state["file"]["name"] = name
70+
state["file"]["file_path"] = f"data/{name}"
71+
state["processing-message"] = f"File {name} saved."
72+
file_data = file.get("data")
73+
with open(f"data/{name}", "wb") as file_handle:
74+
file_handle.write(file_data)
75+
76+
77+
def _delete_all_files(state):
78+
directory = "data"
79+
80+
if os.path.exists(directory):
81+
shutil.rmtree(directory)
82+
83+
os.makedirs(directory)
84+
85+
state["file"]["name"] = ""
86+
state["file"]["file_path"] = ""
87+
state["processing-message"] = "All files have been deleted."
88+
89+
90+
def _read_excel(state):
91+
data = pd.read_excel(state["file"]["file_path"])
92+
df = pd.DataFrame(data)
93+
return df
94+
95+
96+
def _read_pdf(state):
97+
from PyPDF2 import PdfReader
98+
99+
reader = PdfReader(state["file"]["file_path"])
100+
text = ""
101+
for page in reader.pages:
102+
text += page.extract_text()
103+
return text
104+
105+
# Create async tasks for each prompt and then generate the final prompt
106+
async def gather_results_and_generate_rebalance_prompt(data: str):
107+
async def complete_async(prompt):
108+
return await asyncio.to_thread(writer.ai.complete, prompt, {"model": "palmyra-fin-32k", "max_tokens": 3048, "temperature": 0.7})
109+
110+
positive_stock_selection_task = complete_async(
111+
generate_positive_stock_selection_prompt(data)
112+
)
113+
114+
positive_sector_weighting_task = complete_async(
115+
generate_positive_sector_weighting_prompt(data)
116+
)
117+
118+
positive_concatenation_task = complete_async(
119+
generate_positive_concatenation_prompt(data)
120+
)
121+
122+
positive_impacts_task = complete_async(generate_positive_impacts_prompt(data))
123+
124+
negative_stock_selection_task = complete_async(
125+
generate_negative_stock_selection_prompt(data)
126+
)
127+
128+
negative_concatenation_task = complete_async(
129+
generate_negative_concatenation_prompt(data)
130+
)
131+
132+
negative_impacts_task = complete_async(generate_negative_impacts_prompt(data))
133+
134+
(
135+
positive_stock_selection,
136+
positive_sector_weighting,
137+
positive_concatenation,
138+
positive_impacts,
139+
negative_stock_selection,
140+
negative_concatenation,
141+
negative_impacts,
142+
) = await asyncio.gather(
143+
positive_stock_selection_task,
144+
positive_sector_weighting_task,
145+
positive_concatenation_task,
146+
positive_impacts_task,
147+
negative_stock_selection_task,
148+
negative_concatenation_task,
149+
negative_impacts_task,
150+
)
151+
152+
final_prompt = generate_rebalance_recommendation_prompt(
153+
positive_stock_selection,
154+
positive_sector_weighting,
155+
positive_concatenation,
156+
positive_impacts,
157+
negative_stock_selection,
158+
negative_concatenation,
159+
negative_impacts,
160+
)
161+
162+
return final_prompt
163+
164+
165+
def analyze_data(data):
166+
if isinstance(data, pd.DataFrame):
167+
data_str = data.to_string()
168+
else:
169+
data_str = data
170+
171+
return asyncio.run(
172+
gather_results_and_generate_rebalance_prompt(data_str)
173+
)
174+
175+
176+
def handle_file_download(state):
177+
analysis_result = state["analysis-result"]
178+
179+
if analysis_result:
180+
with open("data/analysis_result.txt", "w") as file_handle:
181+
file_handle.write(analysis_result)
182+
183+
file_data = wf.pack_file("data/analysis_result.txt", "text/plain")
184+
state.file_download(file_data, "analysis_result.txt")
185+
186+
187+
initial_state = wf.init_state(
188+
{
189+
"image-path": "static/writer_logo.png",
190+
"app": {"title": "Recommendations for Rebalancing Portfolio"},
191+
"file": {"name": "", "file_path": ""},
192+
"analysis-result": "Your recommendations will appear here.",
193+
"processing-message": "",
194+
"visual_block_visible": False
195+
}
196+
)
197+
198+
199+
initial_state.import_stylesheet("style", "/static/custom.css?")

portfolio-rebalance/prompts.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
def generate_positive_stock_selection_prompt(past_quarter_results):
2+
prompt = f"""
3+
For the ClearBridge Large Cap Growth Fund, when compared to the benchmark of Russell 1000 Growth, which stock sectors had the highest Selection + Interaction? Do not provide more than two sectors.
4+
5+
Please only list the names of the sectors. Do not list more than two and do not put them in an ordered list.
6+
7+
Use this document to help answer your question: {past_quarter_results}
8+
"""
9+
return prompt
10+
11+
12+
def generate_positive_sector_weighting_prompt(past_quarter_results):
13+
prompt = f"""
14+
For the ClearBridge Large Cap Growth Fund, which two sectors had the highest Total Effect?
15+
16+
Please only list the names of the sectors. Do not provide more than two.
17+
18+
Use this document to help answer your question: {past_quarter_results}
19+
"""
20+
return prompt
21+
22+
23+
def generate_positive_concatenation_prompt(past_quarter_results):
24+
prompt = f"""
25+
Stock sectors: {past_quarter_results}
26+
27+
For the two stock sectors mentioned above, format a response as: "stock selection in the X sector and Y sector contributed to the performance", where X is the first sector mentioned above and Y is the second sector mentioned above. Do not capitalize the first word of the sentence.
28+
"""
29+
return prompt
30+
31+
32+
def generate_negative_stock_selection_prompt(past_quarter_results):
33+
prompt = f"""
34+
For the ClearBridge Large Cap Growth Fund, when compared to the benchmark of Russell 1000 Growth, which stock sectors had the lowest Selection + Interaction? Do not provide more than two sectors.
35+
36+
Please only list the names of the sectors. Do not list more than two.
37+
38+
Use this document to help answer your question: {past_quarter_results}
39+
"""
40+
return prompt
41+
42+
43+
def generate_negative_concatenation_prompt(negative_stock_selection):
44+
prompt = f"""
45+
Stock sectors: {negative_stock_selection}
46+
47+
For the two stock sectors mentioned above, format a response as: "stock selection in the X sector and Y sector detracted from performance", where X is the first sector mentioned above and Y is the second sector mentioned above. Do not capitalize the first word of the sentence.
48+
"""
49+
return prompt
50+
51+
52+
def generate_positive_impacts_prompt(past_quarter_results):
53+
prompt = f"""
54+
Give an answer to the question: What individual stocks contributed the most to positive returns? Your answer should be in the format: "In terms of individual stocks, the greatest contributors to returns included the Portfolios' positions in [Company A], [Company B], [Company C], [Company D], and [Company E]."
55+
56+
Here's the data sheet that can be used to answer that question: {past_quarter_results}
57+
58+
Three things to keep in mind:
59+
60+
1. The data sheet has the full names of companies, but you only need to report on its short name. For example, instead of reporting as London Stock Exchange Group plc, you should just say London Stock Exchange Group.
61+
2. You should only list a maximum of five individual stocks.
62+
3. The examples above should strictly be used for format advice. Only use stocks mentioned in the data sheet in your response.
63+
64+
Please provide your answer in this format.
65+
"""
66+
return prompt
67+
68+
69+
def generate_negative_impacts_prompt(past_quarter_results):
70+
prompt = f"""
71+
Give an answer to the question: What individual stocks detracted the most from returns? Your answer should be in the format: "In terms of individual stocks, the greatest detractors from returns included the Portfolios' positions in [Company A], [Company B], [Company C], [Company D], and [Company E]."
72+
73+
Here's the data sheet that can be used to answer that question: {past_quarter_results}
74+
75+
Three things to keep in mind:
76+
77+
1. The data sheet has the full names of companies, but you only need to report its short name. For example, instead of reporting as London Stock Exchange Group plc, you should just say London Stock Exchange Group.
78+
2. You should only list a maximum of five individual stocks.
79+
3. The examples above should strictly be used for format advice. Only use stocks mentioned in the data sheet in your response.
80+
81+
Please provide your answer in this format.
82+
"""
83+
return prompt
84+
85+
86+
def generate_rebalance_recommendation_prompt(
87+
positive_stock_selection,
88+
positive_sector_weighting,
89+
positive_concatenation,
90+
positive_impacts,
91+
negative_stock_selection,
92+
negative_concatenation,
93+
negative_impacts,
94+
):
95+
96+
prompt = f"""
97+
Based on the past results from last quarter, please write a step-by-step analysis of how to rebalance the portfolio.
98+
99+
Here are the results:
100+
101+
<i>Positive Stock Selection:</i>
102+
For the last quarter relative to the benchmark, the highest positive stock selection was in the following sectors: {positive_stock_selection}.
103+
104+
<i>Positive Sector Weighting:</i>
105+
The sectors with the highest total effect were: {positive_sector_weighting}.
106+
107+
<i>Positive Impacts:</i>
108+
Stock selection in {positive_concatenation} contributed to the performance.
109+
In terms of individual stocks, the greatest contributors to returns included: {positive_impacts}.
110+
111+
<i>Negative Stock Selection:</i>
112+
The sectors with the lowest stock selection and interaction were: {negative_stock_selection}.
113+
114+
<i>Negative Impacts:</i>
115+
Stock selection in {negative_concatenation} detracted from the performance.
116+
In terms of individual stocks, the greatest detractors from returns included: {negative_impacts}.
117+
118+
Based on this analysis, please provide your positive impacts, negative impacts and recommendations for rebalancing the portfolio. In your recommendation, be sure to name specific stocks as well as industries.Do not include any extraneous information around general portfolio rebalancing process or additional general resources.
119+
"""
120+
121+
return prompt

portfolio-rebalance/pyproject.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[tool.poetry]
2+
name = "portfolio-rebalance"
3+
version = "1.0"
4+
description = ""
5+
authors = ["Your Name <you@example.com>"]
6+
readme = "README.md"
7+
8+
[tool.poetry.dependencies]
9+
python = "^3.10"
10+
writer = "0.7.5"
11+
openpyxl = "3.1.5"
12+
pandas = "2.2.3"
13+
pypdf2 = "3.0.1"
14+
python-dotenv = "1.0.1"
15+
python-docx = "1.1.2"
16+
asyncio = "^3.4.3"
17+
18+
19+
[tool.poetry.group.dev.dependencies]
20+
flake8 = "^7.1.1"
21+
black = "^24.8.0"
22+
isort = "^5.13.2"
23+
24+
[build-system]
25+
requires = ["poetry-core"]
26+
build-backend = "poetry.core.masonry.api"

0 commit comments

Comments
 (0)