{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import os\n", "import re\n", "import PyPDF2\n", "import json\n", "\n", "# import nltk\n", "# nltk.download('punkt')\n", "\n", "from dotenv import load_dotenv\n", "from openai import OpenAI\n", "from tqdm import tqdm\n", "\n", "# Load environment variables from the .envrc file\n", "load_dotenv('../.envrc')\n", "\n", "client = OpenAI(\n", " api_key=os.getenv(\"OPENAI_API_KEY\"),\n", ")" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from elasticsearch import Elasticsearch\n", "\n", "# Create an Elasticsearch client instance\n", "es = Elasticsearch(\n", " [{'scheme': 'http', 'host': 'localhost', 'port': 9200}]\n", ")\n", "\n", "index_name = 'enhanced_stock_analyzer'" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "index_mapping = {\n", " 'mappings': {\n", " 'properties': {\n", " 'source': {'type': 'keyword'},\n", " 'chunk_id': {'type': 'keyword'},\n", " 'content': {'type': 'text'},\n", " 'summary': {'type': 'text'},\n", " 'key_topics': {'type': 'text'},\n", " 'embedding': {\n", " 'type': 'dense_vector',\n", " 'dims': 384 # Adjust based on your embedding dimensions\n", " }\n", " }\n", " }\n", "}\n", "\n", "# Create or update the index\n", "if not es.indices.exists(index=index_name):\n", " es.indices.create(index=index_name, body=index_mapping)\n", "else:\n", " es.indices.put_mapping(index=index_name, body=index_mapping)\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "668" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# letters_path = '../data/buffet_letters.json'\n", "# with open(letters_path, 'r') as json_file:\n", "# processed_letters = json.load(json_file)\n", "\n", "# report_path = '../data/annual_reports.json'\n", "# with open(report_path, 'r') as json_file:\n", "# processed_reports = json.load(json_file)\n", "\n", "\n", "# for item in processed_letters:\n", "# metdt = item['metadata'].split('\\n')\n", "# item['summary'] = metdt[0][len(\"summary:\"):].strip()\n", "# item['key_topics'] = [i.strip() for i in metdt[1][len(\"key_topics:\"):].strip().split(',')]\n", "# del item['metadata']\n", " \n", "# for item in processed_reports:\n", " \n", "# metdt = item['metadata'].split('\\n')\n", "\n", "# item['summary'] = metdt[0][len(\"summary:\"):].strip()\n", "# item['key_topics'] = [i.strip() for i in metdt[1][len(\"key_topics:\"):].strip().split(',')]\n", "# del item['metadata']\n", "\n", "# item['source'] = item['ticker'] + \" annual \" + item['source']\n", "# del item['ticker']\n", "\n", "\n", "papers_path = '../data/research_papers.json'\n", "with open(papers_path, 'r') as json_file:\n", " processed_papers = json.load(json_file)\n", "\n", "for item in processed_papers:\n", " \n", " metdt = item['metadata'].split('\\n')\n", "\n", " item['summary'] = metdt[0][len(\"summary:\"):].strip()\n", " item['key_topics'] = [i.strip() for i in metdt[1][len(\"key_topics:\"):].strip().split(',')]\n", " del item['metadata']\n", "\n", "all_data = processed_papers #processed_letters #+ processed_reports\n", "len(all_data)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# from transformers import AutoTokenizer, AutoModel\n", "# import torch\n", "\n", "# def get_embeddings(text, model_name='yiyanghkust/finbert-pretrain'):\n", "# \"\"\"\n", "# Generates embeddings for the given text using a pre-trained model.\n", "\n", "# Parameters:\n", "# - text (str): The input text for which embeddings are to be generated.\n", "# - model_name (str): The name of the pre-trained model to use (default is 'yiyanghkust/finbert-pretrain').\n", "\n", "# Returns:\n", "# - numpy.ndarray: The [CLS] token embedding as a numpy array.\n", "# \"\"\"\n", "# # Load the tokenizer and model\n", "# tokenizer = AutoTokenizer.from_pretrained(model_name)\n", "# model = AutoModel.from_pretrained(model_name)\n", "\n", "# # Tokenize the input text\n", "# encoded_input = tokenizer(\n", "# text,\n", "# add_special_tokens=True,\n", "# max_length=384,\n", "# padding='max_length',\n", "# truncation=True,\n", "# return_tensors='pt'\n", "# )\n", "\n", "# input_ids = encoded_input['input_ids']\n", "# attention_mask = encoded_input['attention_mask']\n", "\n", "# # Ensure the model is in evaluation mode\n", "# model.eval()\n", "\n", "# # Move inputs to GPU if available\n", "# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", "# model.to(device)\n", "# input_ids = input_ids.to(device)\n", "# attention_mask = attention_mask.to(device)\n", "\n", "# # Generate embeddings\n", "# with torch.no_grad():\n", "# outputs = model(input_ids=input_ids, attention_mask=attention_mask)\n", "\n", "# # Extract the [CLS] token embedding\n", "# cls_embedding = outputs.last_hidden_state[:, 0, :] # Shape: (1, hidden_size)\n", "\n", "# # Convert to a NumPy array\n", "# cls_embedding_np = cls_embedding.cpu().numpy() # Move to CPU if necessary\n", "\n", "# return cls_embedding_np\n", "\n", "# # Example usage\n", "# # text = \"The company's revenue increased by 20% in the last quarter.\"\n", "# # embeddings = get_embeddings(text)\n", "# # print(embeddings)\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/whysocurious/.local/share/virtualenvs/buffett-wisdom-rag-yofaZaKX/lib/python3.10/site-packages/sentence_transformers/cross_encoder/CrossEncoder.py:13: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", " from tqdm.autonotebook import tqdm, trange\n", "/Users/whysocurious/.local/share/virtualenvs/buffett-wisdom-rag-yofaZaKX/lib/python3.10/site-packages/transformers/tokenization_utils_base.py:1617: FutureWarning: `clean_up_tokenization_spaces` was not set. It will be set to `True` by default. This behavior will be deprecated in transformers v4.45, and will be then set to `False` by default. For more details check this issue: https://github.com/huggingface/transformers/issues/31884\n", " warnings.warn(\n" ] } ], "source": [ "from elasticsearch.helpers import bulk\n", "\n", "from sentence_transformers import SentenceTransformer\n", "\n", "model = SentenceTransformer('all-MiniLM-L6-v2') #('FinLang/finance-embeddings-investopedia') #('all-MiniLM-L6-v2') #(\"philschmid/bge-base-financial-matryoshka\")\n", "\n", "def generate_actions(data):\n", " for item in data:\n", " embedding = model.encode(item['content']).tolist() #get_embeddings(item['content']).tolist() #\n", " yield {\n", " '_index': index_name,\n", " '_id': item['chunk_id'],\n", " '_source': {\n", " 'source': item['source'],\n", " 'chunk_id': item['chunk_id'],\n", " 'content': item['content'],\n", " 'summary': item.get('summary', ''),\n", " 'key_topics': item.get('key_topics', ''),\n", " 'embedding': embedding\n", " }\n", " }\n", "\n", "# Bulk index the data\n", "bulk(es, generate_actions(all_data))\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "indx = bulk(es, generate_actions(all_data))" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(668, [])" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "indx" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'mappings': {'properties': {'source': {'type': 'keyword'},\n", " 'chunk_id': {'type': 'keyword'},\n", " 'content': {'type': 'text'},\n", " 'summary': {'type': 'text'},\n", " 'key_topics': {'type': 'text'},\n", " 'embedding': {'type': 'dense_vector', 'dims': 384}}}}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "index_mapping " ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "\n", "def hybrid_search(query, index=index_name, keyword_top_k=10, final_top_k=10):\n", " \"\"\"\n", " Performs a hybrid search using keyword (multi_match) and semantic (k-NN) search, then re-ranks the results.\n", "\n", " Parameters:\n", " - query (str): The user's question.\n", " - index (str): The Elasticsearch index name.\n", " - keyword_top_k (int): Number of top documents to retrieve from keyword search.\n", " - final_top_k (int): Number of top documents to return after re-ranking.\n", "\n", " Returns:\n", " - List of dictionaries containing the re-ranked search results.\n", " \"\"\"\n", "\n", " # Step 1: Perform Keyword Search (multi_match)\n", " keyword_query = {\n", " 'size': keyword_top_k,\n", " 'query': {\n", " 'multi_match': {\n", " 'query': query,\n", " 'fields': ['content','summary^2','key_topics^2'],\n", " 'fuzziness': 'AUTO'\n", " }\n", " },\n", " '_source': ['source', 'chunk_id', 'content', 'embedding', 'summary', 'key_topics']\n", " }\n", " keyword_response = es.search(index=index, body=keyword_query)\n", " keyword_hits = keyword_response['hits']['hits']\n", "\n", " # Step 2: Perform Semantic Search (k-NN)\n", " # Generate embedding for the query\n", " query_embedding = model.encode(query).tolist()\n", "\n", " # Build the semantic search query\n", " # Semantic search using script_score\n", " semantic_query = {\n", " 'size': keyword_top_k,\n", " 'query': {\n", " 'script_score': {\n", " 'query': {'match_all': {}},\n", " 'script': {\n", " 'source': \"cosineSimilarity(params.query_vector, 'embedding') + 1.0\",\n", " 'params': {'query_vector': query_embedding}\n", " }\n", " }\n", " },\n", " '_source': ['source', 'chunk_id', 'content', 'embedding', 'summary', 'key_topics']\n", " }\n", "\n", " semantic_response = es.search(index=index, body=semantic_query)\n", " semantic_hits = semantic_response['hits']['hits']\n", "\n", " # Step 3: Combine Results\n", " combined_hits = {}\n", " # Process keyword search results\n", " for hit in keyword_hits:\n", " chunk_id = hit['_id']\n", " combined_hits[chunk_id] = {\n", " 'source': hit['_source']['source'],\n", " 'year': hit['_source'].get('year', ''),\n", " 'chunk_id': chunk_id,\n", " 'content': hit['_source']['content'],\n", " 'summary': hit['_source']['summary'],\n", " 'key_topics': hit['_source']['key_topics'],\n", " 'keyword_score': hit['_score'],\n", " 'semantic_score': 0 # Will be updated if exists in semantic_hits\n", " }\n", " # Process semantic search results\n", " for hit in semantic_hits:\n", " chunk_id = hit['_id']\n", " if chunk_id in combined_hits:\n", " combined_hits[chunk_id]['semantic_score'] = hit['_score']\n", " else:\n", " combined_hits[chunk_id] = {\n", " 'source': hit['_source']['source'],\n", " # 'year': hit['_source'].get('year', ''),\n", " 'chunk_id': chunk_id,\n", " 'content': hit['_source']['content'],\n", " 'summary': hit['_source']['summary'],\n", " 'key_topics': hit['_source']['key_topics'],\n", " 'keyword_score': 0,\n", " 'semantic_score': hit['_score']\n", " }\n", "\n", " # Step 4: Re-rank the Combined Results\n", " # Normalize scores\n", " max_keyword_score = max(hit['keyword_score'] for hit in combined_hits.values()) or 1\n", " max_semantic_score = max(hit['semantic_score'] for hit in combined_hits.values()) or 1\n", "\n", " for hit in combined_hits.values():\n", " hit['keyword_score_normalized'] = hit['keyword_score'] / max_keyword_score\n", " hit['semantic_score_normalized'] = hit['semantic_score'] / max_semantic_score\n", " # Combine scores with weights (adjust weights as needed)\n", " hit['combined_score'] = (0.6 * hit['keyword_score_normalized']) + (0.4 * hit['semantic_score_normalized'])\n", "\n", " # Sort the hits based on combined score\n", " re_ranked_hits = sorted(combined_hits.values(), key=lambda x: x['combined_score'], reverse=True)\n", "\n", " # Step 5: Return Top-K Re-Ranked Documents\n", " final_results = re_ranked_hits[:final_top_k]\n", " return final_results\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# query = \"What is mean reversion strategy ?\" #\"Explain Warren Buffett's investment strategy focusing on companies with strong moats.\" #\n", "\n", "# res = hybrid_search(query)\n", "# res" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def build_prompt(query, search_results):\n", " \"\"\"\n", " Builds a prompt for the LLM using the query and search results.\n", "\n", " Parameters:\n", " - query (str): The user's question or query.\n", " - search_results (list): List of retrieved documents.\n", " \n", " Returns:\n", " - The formatted prompt string.\n", " \"\"\"\n", " # Instruction to the LLM\n", " instruction = (\n", " \"You are a financial analyst assistant with deep knowledge of trading strategies and behavioral finance. \"\n", " \"Using the provided context, answer the user's question. \"\n", " \"Use only the facts from the context when answering the question.\"\n", " \"If the context is insufficient, let the user know. \"\n", " \"Provide clear, concise explanations, and include relevant insights from the research papers.\\n\\n\"\n", " )\n", "\n", " # Build context from search results\n", " context = \"\"\n", " for result in search_results:\n", " # source = result['source']\n", " # year = result.get('year', '')\n", " content = result['content']\n", " summary = result['summary']\n", " key_topics = result['key_topics']\n", " context += f\"Content: {content}\\n Summary: {summary}\\n Key topics: {key_topics}\\n\\n\" #\n", "\n", "\n", " # Assemble the prompt\n", " prompt = f\"{instruction}Context:\\n{context}\\nQuestion: {query}\\nAnswer:\"\n", " return prompt\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# print (build_prompt(query, res))" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def call_llm(prompt, model='gpt-4o-mini'):\n", " response = client.chat.completions.create(\n", " model=model,\n", " messages=[{'role': 'user', 'content': prompt}],\n", " max_tokens=2000,\n", " temperature=0.25\n", " )\n", " answer = response.choices[0].message.content.strip()\n", " return answer\n", "\n", "# Example prompt\n", "# prompt = build_prompt(query, res) #\"Explain Warren Buffett's investment strategy focusing on companies with strong moats.\"\n", "\n", "# # Get the response\n", "# response = call_llm(prompt)\n", "# print(response)\n" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def rag_pipeline(query):\n", " \"\"\"\n", " Runs the Retrieval-Augmented Generation pipeline for a given query.\n", "\n", " Parameters:\n", " - query (str): The user's question or query.\n", " \n", " Returns:\n", " - The final answer from the LLM.\n", " \"\"\"\n", " search_results = hybrid_search(query)\n", " prompt = build_prompt(query, search_results)\n", "\n", " try:\n", " answer = call_llm(prompt)\n", " except Exception as e:\n", " answer = f\"An error occurred while generating the answer: {e}\"\n", "\n", " return answer\n", "\n", "\n", "# print (rag_pipeline(query))" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "def simulate_queries(questions_data):\n", " query_results = []\n", " for item in tqdm(questions_data):\n", " for question in item['questions']:\n", " true_chunk_id = item['chunk_id']\n", " # Retrieve top-N chunks using your RAG system\n", " retrieved_chunks = hybrid_search(question) # Implement this function\n", " retrieved_chunk_ids = [chunk['chunk_id'] for chunk in retrieved_chunks]\n", " query_results.append({\n", " 'question': question,\n", " 'true_chunk_id': true_chunk_id,\n", " 'retrieved_chunk_ids': retrieved_chunk_ids\n", " })\n", " return query_results\n", "\n", "def compute_hit_rate(query_results):\n", " hits = 0\n", " total = len(query_results)\n", " for result in tqdm(query_results):\n", " if result['true_chunk_id'] in result['retrieved_chunk_ids']:\n", " hits += 1\n", " hit_rate = hits / total\n", " return hit_rate\n", "\n", "def compute_mrr(query_results):\n", " rr_sum = 0\n", " total = len(query_results)\n", " for result in tqdm(query_results):\n", " try:\n", " rank = result['retrieved_chunk_ids'].index(result['true_chunk_id']) + 1\n", " rr_sum += 1 / rank\n", " except ValueError:\n", " continue # True chunk not in retrieved results\n", " mrr = rr_sum / total\n", " return mrr" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "668" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "questions_path = '../data/papers_questions_data.json'\n", "with open(questions_path, 'r') as json_file:\n", " questions_data = json.load(json_file)\n", "questions_data = [chunk for chunk in questions_data if chunk['source']=='paper']\n", "len(questions_data)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 668/668 [03:33<00:00, 3.12it/s]\n", "100%|██████████| 3340/3340 [00:00<00:00, 953186.05it/s]\n", "100%|██████████| 3340/3340 [00:00<00:00, 1978389.40it/s]" ] }, { "name": "stdout", "output_type": "stream", "text": [ "0.787125748502994 0.4930921490352624\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "query_results = simulate_queries(questions_data)\n", "print (compute_hit_rate(query_results), compute_mrr(query_results))\n" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "668" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "chunk_content = {chnk['chunk_id']:chnk['content'] for chnk in all_data}\n", "len(chunk_content)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 3340/3340 [09:24<00:00, 5.92it/s]\n" ] } ], "source": [ "import numpy as np\n", "\n", "def compute_relevancy_scores(query_results):\n", " relevancy_scores = []\n", " for result in tqdm(query_results):\n", " question_embedding = model.encode(result['question'])\n", " scores = []\n", " for chunk_id in result['retrieved_chunk_ids']:\n", " # Fetch the embedding of the chunk from PostgreSQL\n", " chunk_embedding = model.encode(chunk_content[chunk_id]).tolist() #get_chunk_embedding(chunk_id) # Implement this function\n", " similarity = np.dot(question_embedding, chunk_embedding) / (np.linalg.norm(question_embedding) * np.linalg.norm(chunk_embedding))\n", " scores.append(similarity)\n", " relevancy_scores.append({\n", " 'question': result['question'],\n", " 'true_chunk_id': result['true_chunk_id'],\n", " 'retrieved_chunk_ids': result['retrieved_chunk_ids'],\n", " 'similarity_scores': scores\n", " })\n", " return relevancy_scores\n", "\n", "relevancy_scores = compute_relevancy_scores(query_results)" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", "\n", "tmpDf = pd.DataFrame(relevancy_scores)[['true_chunk_id','similarity_scores']]\n", "tmpDf['score_avg'] = tmpDf.similarity_scores.apply(lambda x: float(np.median(x)))\n", "tmpDf.score_avg.hist()" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 668/668 [00:00<00:00, 214237.27it/s]\n" ] } ], "source": [ "# Assume you have a list of evaluation data\n", "# Each item in evaluation_data is a dictionary with keys:\n", "# - 'question': The user question\n", "# - 'reference_answer': The expected correct answer (could be the chunk content)\n", "evaluation_data = []\n", "\n", "# Populate evaluation_data with your existing questions and reference answers\n", "for item in tqdm(questions_data):\n", " for question in item['questions']:\n", " # Use the original chunk content as the reference answer\n", " chunk_id = item['chunk_id']\n", " content = chunk_content[chunk_id] # Implement this function to fetch content\n", " evaluation_data.append({\n", " 'question': question,\n", " 'reference_answer': content\n", " })\n" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "\n", "\n", "def evaluate_rag_system(evaluation_data):\n", " for item in tqdm(evaluation_data):\n", " if 'rag_answer' in item.keys() :\n", " pass\n", " else:\n", " question = item['question']\n", " # Generate the RAG answer\n", " rag_answer = rag_pipeline(question)\n", " item['rag_answer'] = rag_answer\n", " return evaluation_data\n", "\n", "# Run the evaluation\n", "# evaluation_data = evaluate_rag_system(evaluation_data)\n" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def llm_judge(question, reference_answer, rag_answer, model='gpt-4o-mini'):\n", " prompt = f\"\"\"\n", "You are an expert evaluator. Compare the following two answers to the question provided. \n", "Assess the RAG-generated answer based on correctness, completeness, and relevance to the question, using the reference answer as the ground truth. \n", "Provide a score between 1 and 5, where 5 is the best; follow the below scoring guidelines for scoring.\n", "Scoring Guidelines:\n", "- 5: The RAG-generated answer is completely correct, fully addresses the question, and is as comprehensive as the reference answer.\n", "- 4: The answer is correct but may lack minor details compared to the reference.\n", "- 3: The answer is partially correct but misses significant details.\n", "- 2: The answer has some correct elements but contains notable inaccuracies.\n", "- 1: The answer is incorrect or irrelevant to the question.\n", "\n", "\n", "**Question:**\n", "{question}\n", "\n", "**Reference Answer:**\n", "{reference_answer}\n", "\n", "**RAG-Generated Answer:**\n", "{rag_answer}\n", "\n", "**Evaluation:**\n", "Score:\"\"\"\n", "\n", " response = client.chat.completions.create(\n", " model=model,\n", " messages=[{'role': 'user', 'content': prompt}],\n", " max_tokens=150,\n", " temperature=0,\n", " n=1,\n", " stop=None\n", " )\n", " evaluation_text = response.choices[0].message.content.strip()\n", " # Parse the score and reasoning\n", " score, reasoning = parse_evaluation(evaluation_text)\n", " return score, reasoning\n", "\n", "def parse_evaluation(evaluation_text):\n", " # Split the evaluation text to extract score and reasoning\n", " lines = evaluation_text.split('\\n')\n", " score = None\n", " reasoning = ''\n", " for line in lines:\n", " if line.strip().startswith('Score:'):\n", " score_line = line.strip()\n", " score_str = score_line.replace('Score:', '').strip()\n", " try:\n", " score = float(score_str)\n", " except ValueError:\n", " score = None\n", " elif line.strip().startswith('Reasoning:'):\n", " reasoning = line.strip().replace('Reasoning:', '').strip()\n", " else:\n", " if reasoning != '':\n", " reasoning += ' ' + line.strip()\n", " return score, reasoning\n" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "def run_llm_evaluation(evaluation_data):\n", " for item in tqdm(evaluation_data):\n", " question = item['question']\n", " reference_answer = item['reference_answer']\n", " rag_answer = item['rag_answer']\n", " try:\n", " score, reasoning = llm_judge(question, reference_answer, rag_answer)\n", " item['score'] = score\n", " item['reasoning'] = reasoning\n", " except Exception as e:\n", " print(f\"Error evaluating question '{question}': {e}\")\n", " item['score'] = None\n", " item['reasoning'] = str(e)\n", " return evaluation_data\n", "\n", "# Run LLM evaluation\n", "# evaluation_data = run_llm_evaluation(evaluation_data)\n", "\n", "\n", "def analyze_results(evaluation_data):\n", " valid_scores = [item['score'] for item in evaluation_data if item['score'] is not None]\n", " average_score = sum(valid_scores) / len(valid_scores) if valid_scores else 0\n", " print(f\"Average Score: {average_score:.2f}\")\n", "\n", " # Optional: Analyze reasoning\n", " for item in evaluation_data:\n", " print(f\"Question: {item['question']}\")\n", " print(f\"Score: {item['score']}\")\n", " print(f\"Reasoning: {item['reasoning']}\")\n", " print('-' * 50)\n", "\n", "# Analyze the results\n", "# analyze_results(evaluation_data)\n" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 3340/3340 [11:16<00:00, 4.94it/s] \n" ] } ], "source": [ "# Step 2: Run RAG pipeline\n", "evaluation_data = evaluate_rag_system(evaluation_data)\n" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "output_path = '../data/evaluation_data.json'\n", "with open(output_path, 'w') as json_file:\n", " json.dump(evaluation_data, json_file, indent=4)" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|██████████| 3340/3340 [2:53:29<00:00, 3.12s/it] \n" ] } ], "source": [ "\n", "# Step 3: Use LLM as a judge\n", "evaluation_data = run_llm_evaluation(evaluation_data)\n" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "output_path = '../data/evaluation_data_fin.json'\n", "with open(output_path, 'w') as json_file:\n", " json.dump(evaluation_data, json_file, indent=4)" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'analyze_results' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[1], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Step 4: Analyze results\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43manalyze_results\u001b[49m(evaluation_data)\n", "\u001b[0;31mNameError\u001b[0m: name 'analyze_results' is not defined" ] } ], "source": [ "\n", "# Step 4: Analyze results\n", "analyze_results(evaluation_data)" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3340\n" ] }, { "data": { "text/plain": [ "{'question': 'What is the significance of detecting lead-lag relationships in stock returns?',\n", " 'reference_answer': 'DETECTING LEAD-LAGRELATIONSHIPS IN STOCK RETURNS AND PORTFOLIO STRATEGIES∗ Álvaro Cartea†‡Mihai Cucuringu∗§†¶Qi Jin∗‡∥ June 9, 2024 Click here for the most recent version ABSTRACT We propose a method to detect linear and nonlinear lead-lag relationships in stock returns. Our approach uses pairwise Lévy-area and cross-correlation of returns to rank the assets from leaders to followers. We use the rankings to construct a portfolio that longs or shorts the followers based on the previous returns of the leaders, and the stocks are ranked every time the portfolio is rebalanced. The portfolio also takes an offsetting position on the SPY ETF so that the initial value of the portfolio is zero. Our data spans from 1963 to 2022, and we use an average of over 500 stocks to construct portfolios for each trading day. The annualized returns of our lead-lag portfolios are over 20 %, and the returns outperform all lead-lag benchmarks in the literature. There is little overlap between the leaders and the followers we find and those that are reported in previous studies based on market capitalization, volume traded, and intra-industry relationships. Our findings support the slow information diffusion hypothesis; i.e., portfolios rebalanced once a day consistently outperform the bidiurnal, weekly, bi-weekly, tri-weekly, and monthly rebalanced portfolios. Keywords : Return prediction, Lead-lag relationships, Ranking, Lévy-area, Clustering JEL classification : G11, G12, G14, G17 ∗We thank Andrew Alden, Torben Andersen, Álvaro Arroyo, Patrick Chang, Fayçal Drissi, Anthony Ledford, Slavi Marinov, Sean Myers (discussant), Roberto Renò, and Harrison Waldon for helpful comments and feedback. We are grateful to audience at Man AHL, J.P. Morgan, GSA Capital, and Oxford Asset Management for comments. We are grateful to audience at the OMI Machine Learning and Financial Econometrics workshop and the Eastern Finance Association Annual Meeting for helpful comments. †Oxford-Man Institute of Quantitative Finance, University of Oxford ‡Mathematical Institute, University of Oxford §Department of Statistics, University of Oxford ¶The Alan Turing Institute, London, UK ∥Corresponding author; Email: qi.jin@st-annes.ox.ac.uk1 Introduction Changes in stock prices of some firms tend to follow those of other firms. This relationship between stock prices is often referred to as a lead-lag relationship. Detecting lead-lag relationships among a large set of stocks is not straightforward. The extant literature uses ad-hoc methods to select leaders and followers, and employs these two sets of stocks in investment strategies to evaluate the economic significance of the lead-lag relationship. For example, Lo and MacKinlay (1990) assume that large market capitalization stocks lead small market capitalization stocks. They build equal-weighted portfolios within each quantile of market capitalizations and use the cross-autocorrelation between the five portfolios to evaluate the trading performance of the lead-lag relationship. Empirical evidence suggests that firm size (Lo and MacKinlay (1990)), trading volume (Chordia and Swaminathan (2000)), institutional ownership (Badrinath et al. (1995)), and other firm characteristics contribute to the lead-lag identity of a stock. Empirically, however, many lead-lag relationships change over time and often cannot be explained by sorting stocks on a single firm characteristic.7This observation motivates that it is necessary to detect, instead of assume and then verify, lead-lag relationships. Our objective is to find lead-lag relationships without explicitly assuming a link between firm characteristics and lead-lag relationships; instead, we develop a data-driven method that employs stock returns to identify leaders and followers, and we show that the lead-lag relationships we find are economically significant. We achieve this in three steps. First, we design an algorithm that identifies the direction and strength of the lead-lag relationship between the returns of two stocks. Second, we propose a framework that uses state-of-the-art algorithms to rank stocks from leaders to followers based on the pairwise relationships. Third, we construct a zero-cost portfolio to assess the returns predictability of the leaders over the followers, and we measure the economic significance of the portfolio’s performance. Specifically, in the first step we design a method to score the lead-lag relationship between pairs of assets. The sign of the score indicates which of the two assets is more likely the leader, and the magnitude of the score quantifies the strength of the lead-lag relationship.',\n", " 'rag_answer': 'Detecting lead-lag relationships in stock returns is significant for several reasons:\\n\\n1. **Return Predictability**: Identifying which stocks lead and which follow allows investors to predict future returns based on past performance. This can enhance trading strategies by enabling investors to take positions in follower stocks based on the movements of leader stocks.\\n\\n2. **Portfolio Construction**: The ability to rank stocks from leaders to followers facilitates the construction of portfolios that can capitalize on these relationships. The study demonstrates that portfolios constructed using detected lead-lag relationships can achieve annualized returns exceeding 20%, significantly outperforming traditional benchmarks.\\n\\n3. **Economic Significance**: The findings support the slow information diffusion hypothesis, suggesting that some stocks react more slowly to common information. This insight can inform trading strategies and risk management practices, as it highlights the varying speeds at which different stocks respond to market events.\\n\\n4. **Robustness Across Time**: The lead-lag relationships identified are not static; they can change over time. This dynamic nature necessitates continuous monitoring and adjustment of trading strategies, which can lead to improved performance over time.\\n\\n5. **Nonlinear Relationships**: The methodology developed in the study captures both linear and nonlinear dependencies between stock returns, providing a more nuanced understanding of market behavior. This can lead to better risk-adjusted returns compared to methods that only consider linear relationships.\\n\\n6. **Market Efficiency Insights**: Understanding lead-lag relationships contributes to the broader discourse on market efficiency, particularly regarding how information is disseminated among market participants. It challenges traditional assumptions based on firm characteristics like market capitalization or trading volume.\\n\\nIn summary, detecting lead-lag relationships enhances trading strategies, informs portfolio construction, and provides insights into market dynamics and information flow, ultimately leading to better investment outcomes.',\n", " 'score': 4.0,\n", " 'reasoning': ''}" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "output_path = '../data/evaluation_data_fin.json'\n", "with open(output_path, 'r') as json_file:\n", " evaluation_data = json.load(json_file)\n", "\n", "print (len(evaluation_data))\n", "evaluation_data[0]" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "score\n", "4.0 1404\n", "5.0 326\n", "1.0 59\n", "2.0 54\n", "3.0 50\n", "Name: count, dtype: int64" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import pandas as pd\n", "pd.DataFrame(evaluation_data)['score'].hist()\n", "pd.DataFrame(evaluation_data)['score'].value_counts()" ] } ], "metadata": { "kernelspec": { "display_name": "buffett-wisdom-rag-yofaZaKX", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.8" } }, "nbformat": 4, "nbformat_minor": 2 }