Add multi-model scoring: GPT-4.1-mini, GPT-4.1-nano, Gemini-2.5-flash
- MemelyAlphaStockRanking_multi_model.xlsx: 13-sheet Excel with per-model scores - Multi-Model Comparison (cross-model ranking) - Model Legend (color coding guide) - Scores - GPT-4.1-mini (blue highlights) - Scores - GPT-4.1-nano (green highlights) - Scores - Gemini-2.5-flash (purple highlights) - Rank sheets for each model - scores_gpt41nano.json: GPT-4.1-nano raw scores - scores_gemini25flash.json: Gemini-2.5-flash raw scores - score_multi_model.py: Multi-model scoring script - build_multi_model_excel.py: Excel generation script
这个提交包含在:
二进制文件未显示。
533
build_multi_model_excel.py
普通文件
533
build_multi_model_excel.py
普通文件
@@ -0,0 +1,533 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build a multi-model Excel file:
|
||||||
|
- Copy original sheets for each model
|
||||||
|
- Each model gets its own colored score columns
|
||||||
|
- Add a cross-model comparison summary sheet
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pandas as pd
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
EXCEL_SRC = "/home/ubuntu/upload/MemelyAlphaStockRanking(副本)(副本).xlsx"
|
||||||
|
EXCEL_OUT = "/home/ubuntu/memely-alpha-stock-ranking/MemelyAlphaStockRanking_multi_model.xlsx"
|
||||||
|
|
||||||
|
# Model configs: name, json_path, highlight_color, header_color
|
||||||
|
MODELS = [
|
||||||
|
{
|
||||||
|
"name": "GPT-4.1-mini",
|
||||||
|
"short": "gpt41mini",
|
||||||
|
"json": "/home/ubuntu/memely-alpha-stock-ranking/research_scores.json",
|
||||||
|
"fill_color": "BDD7EE", # Light blue
|
||||||
|
"header_color": "2F75B5", # Dark blue
|
||||||
|
"header_font_color": "FFFFFF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GPT-4.1-nano",
|
||||||
|
"short": "gpt41nano",
|
||||||
|
"json": "/home/ubuntu/memely-alpha-stock-ranking/scores_gpt41nano.json",
|
||||||
|
"fill_color": "C6EFCE", # Light green
|
||||||
|
"header_color": "548235", # Dark green
|
||||||
|
"header_font_color": "FFFFFF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gemini-2.5-flash",
|
||||||
|
"short": "gemini25flash",
|
||||||
|
"json": "/home/ubuntu/memely-alpha-stock-ranking/scores_gemini25flash.json",
|
||||||
|
"fill_color": "E2BFEE", # Light purple
|
||||||
|
"header_color": "7030A0", # Dark purple
|
||||||
|
"header_font_color": "FFFFFF",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Load all scores
|
||||||
|
all_scores = {}
|
||||||
|
for m in MODELS:
|
||||||
|
with open(m["json"], 'r') as f:
|
||||||
|
all_scores[m["name"]] = json.load(f)
|
||||||
|
print(f"Loaded {len(all_scores[m['name']])} scores for {m['name']}")
|
||||||
|
|
||||||
|
# Load original workbook
|
||||||
|
wb = load_workbook(EXCEL_SRC)
|
||||||
|
|
||||||
|
thin_border = Border(
|
||||||
|
left=Side(style='thin'),
|
||||||
|
right=Side(style='thin'),
|
||||||
|
top=Side(style='thin'),
|
||||||
|
bottom=Side(style='thin')
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# For each model, create a copy of "stock master list" sheet
|
||||||
|
# with that model's scores appended in its color
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# Read original data for reference
|
||||||
|
df = pd.read_excel(EXCEL_SRC, sheet_name='stock master list')
|
||||||
|
df4 = pd.read_excel(EXCEL_SRC, sheet_name='Sheet4')
|
||||||
|
|
||||||
|
# Get original sheet as template
|
||||||
|
orig_ws = wb['stock master list']
|
||||||
|
orig_max_col = orig_ws.max_column
|
||||||
|
orig_max_row = orig_ws.max_row
|
||||||
|
|
||||||
|
for model_cfg in MODELS:
|
||||||
|
model_name = model_cfg["name"]
|
||||||
|
scores = all_scores[model_name]
|
||||||
|
|
||||||
|
fill_color = model_cfg["fill_color"]
|
||||||
|
header_color = model_cfg["header_color"]
|
||||||
|
header_font_color = model_cfg["header_font_color"]
|
||||||
|
|
||||||
|
cell_fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid")
|
||||||
|
hdr_fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
|
||||||
|
hdr_font = Font(bold=True, color=header_font_color, size=11)
|
||||||
|
score_font = Font(size=10)
|
||||||
|
|
||||||
|
sheet_name = f"Scores - {model_name}"
|
||||||
|
# Truncate sheet name if too long (max 31 chars)
|
||||||
|
if len(sheet_name) > 31:
|
||||||
|
sheet_name = sheet_name[:31]
|
||||||
|
|
||||||
|
ws = wb.copy_worksheet(orig_ws)
|
||||||
|
ws.title = sheet_name
|
||||||
|
|
||||||
|
# Add score columns
|
||||||
|
new_headers = [
|
||||||
|
f'{model_name} Overall',
|
||||||
|
f'{model_name} Momentum',
|
||||||
|
f'{model_name} Theme',
|
||||||
|
f'{model_name} Risk',
|
||||||
|
f'{model_name} Social Buzz',
|
||||||
|
f'{model_name} Analysis',
|
||||||
|
]
|
||||||
|
|
||||||
|
for j, hdr in enumerate(new_headers):
|
||||||
|
col = orig_max_col + 1 + j
|
||||||
|
cell = ws.cell(row=1, column=col, value=hdr)
|
||||||
|
cell.fill = hdr_fill
|
||||||
|
cell.font = hdr_font
|
||||||
|
cell.alignment = Alignment(horizontal='center', wrap_text=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Write scores
|
||||||
|
for row in range(2, orig_max_row + 1):
|
||||||
|
symbol = ws.cell(row=row, column=1).value
|
||||||
|
if symbol and symbol in scores:
|
||||||
|
s = scores[symbol]
|
||||||
|
|
||||||
|
fields = ['overall_score', 'momentum_score', 'theme_score', 'risk_score', 'social_buzz_score', 'brief_analysis']
|
||||||
|
for j, field in enumerate(fields):
|
||||||
|
col = orig_max_col + 1 + j
|
||||||
|
cell = ws.cell(row=row, column=col, value=s.get(field, ''))
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.font = score_font
|
||||||
|
if field != 'brief_analysis':
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
else:
|
||||||
|
cell.alignment = Alignment(wrap_text=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Set column widths
|
||||||
|
widths = [14, 16, 14, 12, 16, 55]
|
||||||
|
for j, w in enumerate(widths):
|
||||||
|
col_letter = get_column_letter(orig_max_col + 1 + j)
|
||||||
|
ws.column_dimensions[col_letter].width = w
|
||||||
|
|
||||||
|
ws.freeze_panes = 'B2'
|
||||||
|
print(f"Created sheet: {sheet_name}")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Create cross-model comparison summary sheet
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ws_comp = wb.create_sheet("Multi-Model Comparison", 0)
|
||||||
|
|
||||||
|
# Headers
|
||||||
|
comp_headers = ['Rank', 'Symbol', 'Theme']
|
||||||
|
for m in MODELS:
|
||||||
|
comp_headers.append(f'{m["name"]} Overall')
|
||||||
|
comp_headers.extend(['Avg Overall', 'Std Dev', 'Consensus'])
|
||||||
|
for m in MODELS:
|
||||||
|
comp_headers.append(f'{m["name"]} Momentum')
|
||||||
|
for m in MODELS:
|
||||||
|
comp_headers.append(f'{m["name"]} Theme')
|
||||||
|
for m in MODELS:
|
||||||
|
comp_headers.append(f'{m["name"]} Risk')
|
||||||
|
for m in MODELS:
|
||||||
|
comp_headers.append(f'{m["name"]} Social Buzz')
|
||||||
|
|
||||||
|
# Write headers with styling
|
||||||
|
general_hdr_fill = PatternFill(start_color="333333", end_color="333333", fill_type="solid")
|
||||||
|
general_hdr_font = Font(bold=True, color="FFFFFF", size=10)
|
||||||
|
|
||||||
|
for col, hdr in enumerate(comp_headers, 1):
|
||||||
|
cell = ws_comp.cell(row=1, column=col, value=hdr)
|
||||||
|
# Color-code model-specific headers
|
||||||
|
matched = False
|
||||||
|
for m in MODELS:
|
||||||
|
if m["name"] in hdr:
|
||||||
|
cell.fill = PatternFill(start_color=m["header_color"], end_color=m["header_color"], fill_type="solid")
|
||||||
|
cell.font = Font(bold=True, color=m["header_font_color"], size=10)
|
||||||
|
matched = True
|
||||||
|
break
|
||||||
|
if not matched:
|
||||||
|
cell.fill = general_hdr_fill
|
||||||
|
cell.font = general_hdr_font
|
||||||
|
cell.alignment = Alignment(horizontal='center', wrap_text=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Collect all symbols and compute averages
|
||||||
|
symbol_info = {}
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
sym = row['Symbol']
|
||||||
|
if pd.notna(sym) and sym not in symbol_info:
|
||||||
|
symbol_info[sym] = str(row.get('Theme', ''))
|
||||||
|
|
||||||
|
import statistics
|
||||||
|
|
||||||
|
ranking_data = []
|
||||||
|
for sym in symbol_info:
|
||||||
|
overalls = []
|
||||||
|
momentums = []
|
||||||
|
themes = []
|
||||||
|
risks = []
|
||||||
|
buzzes = []
|
||||||
|
|
||||||
|
for m in MODELS:
|
||||||
|
s = all_scores[m["name"]].get(sym, {})
|
||||||
|
o = s.get('overall_score', None)
|
||||||
|
if o is not None:
|
||||||
|
overalls.append(o)
|
||||||
|
mo = s.get('momentum_score', None)
|
||||||
|
if mo is not None:
|
||||||
|
momentums.append(mo)
|
||||||
|
t = s.get('theme_score', None)
|
||||||
|
if t is not None:
|
||||||
|
themes.append(t)
|
||||||
|
r = s.get('risk_score', None)
|
||||||
|
if r is not None:
|
||||||
|
risks.append(r)
|
||||||
|
b = s.get('social_buzz_score', None)
|
||||||
|
if b is not None:
|
||||||
|
buzzes.append(b)
|
||||||
|
|
||||||
|
avg = statistics.mean(overalls) if overalls else 0
|
||||||
|
std = statistics.stdev(overalls) if len(overalls) > 1 else 0
|
||||||
|
|
||||||
|
ranking_data.append({
|
||||||
|
'symbol': sym,
|
||||||
|
'theme': symbol_info[sym],
|
||||||
|
'avg_overall': avg,
|
||||||
|
'std_overall': std,
|
||||||
|
'overalls': overalls,
|
||||||
|
'momentums': momentums,
|
||||||
|
'themes': themes,
|
||||||
|
'risks': risks,
|
||||||
|
'buzzes': buzzes,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by average overall score
|
||||||
|
ranking_data.sort(key=lambda x: x['avg_overall'], reverse=True)
|
||||||
|
|
||||||
|
def get_score_fill_general(score):
|
||||||
|
if score >= 70:
|
||||||
|
return PatternFill(start_color="92D050", end_color="92D050", fill_type="solid")
|
||||||
|
elif score >= 50:
|
||||||
|
return PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
|
||||||
|
elif score >= 30:
|
||||||
|
return PatternFill(start_color="FFC000", end_color="FFC000", fill_type="solid")
|
||||||
|
else:
|
||||||
|
return PatternFill(start_color="FF6B6B", end_color="FF6B6B", fill_type="solid")
|
||||||
|
|
||||||
|
def get_consensus(avg, std):
|
||||||
|
if std <= 5:
|
||||||
|
return "Strong Consensus"
|
||||||
|
elif std <= 10:
|
||||||
|
return "Moderate Consensus"
|
||||||
|
elif std <= 15:
|
||||||
|
return "Weak Consensus"
|
||||||
|
else:
|
||||||
|
return "Divergent"
|
||||||
|
|
||||||
|
for rank, rd in enumerate(ranking_data, 1):
|
||||||
|
row = rank + 1
|
||||||
|
sym = rd['symbol']
|
||||||
|
|
||||||
|
ws_comp.cell(row=row, column=1, value=rank).border = thin_border
|
||||||
|
ws_comp.cell(row=row, column=1).alignment = Alignment(horizontal='center')
|
||||||
|
|
||||||
|
cell = ws_comp.cell(row=row, column=2, value=sym)
|
||||||
|
cell.font = Font(bold=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
theme_val = rd['theme'] if pd.notna(rd['theme']) and rd['theme'] != 'nan' else ''
|
||||||
|
ws_comp.cell(row=row, column=3, value=theme_val).border = thin_border
|
||||||
|
|
||||||
|
# Model overall scores with model-specific colors
|
||||||
|
col = 4
|
||||||
|
for i, m in enumerate(MODELS):
|
||||||
|
s = all_scores[m["name"]].get(sym, {})
|
||||||
|
val = s.get('overall_score', '')
|
||||||
|
cell = ws_comp.cell(row=row, column=col + i, value=val)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
cell.fill = PatternFill(start_color=m["fill_color"], end_color=m["fill_color"], fill_type="solid")
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
col += len(MODELS)
|
||||||
|
|
||||||
|
# Average
|
||||||
|
avg = rd['avg_overall']
|
||||||
|
cell = ws_comp.cell(row=row, column=col, value=round(avg, 1))
|
||||||
|
cell.fill = get_score_fill_general(avg)
|
||||||
|
cell.font = Font(bold=True, size=11)
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
col += 1
|
||||||
|
|
||||||
|
# Std Dev
|
||||||
|
std = rd['std_overall']
|
||||||
|
cell = ws_comp.cell(row=row, column=col, value=round(std, 1))
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
col += 1
|
||||||
|
|
||||||
|
# Consensus
|
||||||
|
consensus = get_consensus(avg, std)
|
||||||
|
cell = ws_comp.cell(row=row, column=col, value=consensus)
|
||||||
|
if "Strong" in consensus:
|
||||||
|
cell.fill = PatternFill(start_color="92D050", end_color="92D050", fill_type="solid")
|
||||||
|
elif "Moderate" in consensus:
|
||||||
|
cell.fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
|
||||||
|
elif "Weak" in consensus:
|
||||||
|
cell.fill = PatternFill(start_color="FFC000", end_color="FFC000", fill_type="solid")
|
||||||
|
else:
|
||||||
|
cell.fill = PatternFill(start_color="FF6B6B", end_color="FF6B6B", fill_type="solid")
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
col += 1
|
||||||
|
|
||||||
|
# Momentum scores per model
|
||||||
|
for i, m in enumerate(MODELS):
|
||||||
|
s = all_scores[m["name"]].get(sym, {})
|
||||||
|
val = s.get('momentum_score', '')
|
||||||
|
cell = ws_comp.cell(row=row, column=col + i, value=val)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
cell.fill = PatternFill(start_color=m["fill_color"], end_color=m["fill_color"], fill_type="solid")
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
col += len(MODELS)
|
||||||
|
|
||||||
|
# Theme scores per model
|
||||||
|
for i, m in enumerate(MODELS):
|
||||||
|
s = all_scores[m["name"]].get(sym, {})
|
||||||
|
val = s.get('theme_score', '')
|
||||||
|
cell = ws_comp.cell(row=row, column=col + i, value=val)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
cell.fill = PatternFill(start_color=m["fill_color"], end_color=m["fill_color"], fill_type="solid")
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
col += len(MODELS)
|
||||||
|
|
||||||
|
# Risk scores per model
|
||||||
|
for i, m in enumerate(MODELS):
|
||||||
|
s = all_scores[m["name"]].get(sym, {})
|
||||||
|
val = s.get('risk_score', '')
|
||||||
|
cell = ws_comp.cell(row=row, column=col + i, value=val)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
cell.fill = PatternFill(start_color=m["fill_color"], end_color=m["fill_color"], fill_type="solid")
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
col += len(MODELS)
|
||||||
|
|
||||||
|
# Social buzz scores per model
|
||||||
|
for i, m in enumerate(MODELS):
|
||||||
|
s = all_scores[m["name"]].get(sym, {})
|
||||||
|
val = s.get('social_buzz_score', '')
|
||||||
|
cell = ws_comp.cell(row=row, column=col + i, value=val)
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
cell.fill = PatternFill(start_color=m["fill_color"], end_color=m["fill_color"], fill_type="solid")
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Set column widths for comparison sheet
|
||||||
|
ws_comp.column_dimensions['A'].width = 6
|
||||||
|
ws_comp.column_dimensions['B'].width = 10
|
||||||
|
ws_comp.column_dimensions['C'].width = 18
|
||||||
|
for col_idx in range(4, 4 + len(comp_headers) - 3):
|
||||||
|
col_letter = get_column_letter(col_idx)
|
||||||
|
ws_comp.column_dimensions[col_letter].width = 16
|
||||||
|
|
||||||
|
ws_comp.freeze_panes = 'D2'
|
||||||
|
print(f"Created 'Multi-Model Comparison' sheet")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Also create per-model ranking sheets (sorted by that model's overall score)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
for model_cfg in MODELS:
|
||||||
|
model_name = model_cfg["name"]
|
||||||
|
scores = all_scores[model_name]
|
||||||
|
fill_color = model_cfg["fill_color"]
|
||||||
|
header_color = model_cfg["header_color"]
|
||||||
|
header_font_color = model_cfg["header_font_color"]
|
||||||
|
|
||||||
|
cell_fill = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid")
|
||||||
|
hdr_fill = PatternFill(start_color=header_color, end_color=header_color, fill_type="solid")
|
||||||
|
hdr_font = Font(bold=True, color=header_font_color, size=10)
|
||||||
|
|
||||||
|
sheet_name = f"Rank - {model_name}"
|
||||||
|
if len(sheet_name) > 31:
|
||||||
|
sheet_name = sheet_name[:31]
|
||||||
|
|
||||||
|
ws_rank = wb.create_sheet(sheet_name)
|
||||||
|
|
||||||
|
rank_headers = ['Rank', 'Symbol', 'Theme', 'Overall', 'Momentum', 'Theme Score', 'Risk', 'Social Buzz', 'YTD Perf', 'Analysis']
|
||||||
|
|
||||||
|
for col, hdr in enumerate(rank_headers, 1):
|
||||||
|
cell = ws_rank.cell(row=1, column=col, value=hdr)
|
||||||
|
cell.fill = hdr_fill
|
||||||
|
cell.font = hdr_font
|
||||||
|
cell.alignment = Alignment(horizontal='center', wrap_text=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Sort by overall score
|
||||||
|
sorted_scores = sorted(scores.items(), key=lambda x: x[1].get('overall_score', 0), reverse=True)
|
||||||
|
|
||||||
|
for rank, (sym, s) in enumerate(sorted_scores, 1):
|
||||||
|
row = rank + 1
|
||||||
|
theme = symbol_info.get(sym, '')
|
||||||
|
if pd.isna(theme) or theme == 'nan':
|
||||||
|
theme = ''
|
||||||
|
|
||||||
|
ws_rank.cell(row=row, column=1, value=rank).border = thin_border
|
||||||
|
ws_rank.cell(row=row, column=1).alignment = Alignment(horizontal='center')
|
||||||
|
|
||||||
|
cell = ws_rank.cell(row=row, column=2, value=sym)
|
||||||
|
cell.font = Font(bold=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
ws_rank.cell(row=row, column=3, value=theme).border = thin_border
|
||||||
|
|
||||||
|
# Overall
|
||||||
|
overall = s.get('overall_score', 0)
|
||||||
|
cell = ws_rank.cell(row=row, column=4, value=overall)
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.font = Font(bold=True, size=11)
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Momentum
|
||||||
|
cell = ws_rank.cell(row=row, column=5, value=s.get('momentum_score', ''))
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Theme Score
|
||||||
|
cell = ws_rank.cell(row=row, column=6, value=s.get('theme_score', ''))
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Risk
|
||||||
|
cell = ws_rank.cell(row=row, column=7, value=s.get('risk_score', ''))
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Social Buzz
|
||||||
|
cell = ws_rank.cell(row=row, column=8, value=s.get('social_buzz_score', ''))
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# YTD Perf from original data
|
||||||
|
ytd_val = ''
|
||||||
|
for _, orig_row in df.iterrows():
|
||||||
|
if orig_row['Symbol'] == sym:
|
||||||
|
ytd_val = orig_row.get('performance Year to Date', '')
|
||||||
|
break
|
||||||
|
if pd.notna(ytd_val) and ytd_val != '':
|
||||||
|
cell = ws_rank.cell(row=row, column=9, value=ytd_val)
|
||||||
|
cell.number_format = '0.00%'
|
||||||
|
else:
|
||||||
|
cell = ws_rank.cell(row=row, column=9, value='N/A')
|
||||||
|
cell.alignment = Alignment(horizontal='center')
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Analysis
|
||||||
|
cell = ws_rank.cell(row=row, column=10, value=s.get('brief_analysis', ''))
|
||||||
|
cell.fill = cell_fill
|
||||||
|
cell.alignment = Alignment(wrap_text=True)
|
||||||
|
cell.border = thin_border
|
||||||
|
|
||||||
|
# Column widths
|
||||||
|
rank_widths = [6, 10, 18, 10, 12, 12, 10, 14, 12, 55]
|
||||||
|
for i, w in enumerate(rank_widths):
|
||||||
|
ws_rank.column_dimensions[get_column_letter(i + 1)].width = w
|
||||||
|
|
||||||
|
ws_rank.freeze_panes = 'D2'
|
||||||
|
print(f"Created ranking sheet: {sheet_name}")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Add a legend/info sheet
|
||||||
|
# ============================================================
|
||||||
|
ws_legend = wb.create_sheet("Model Legend", 1)
|
||||||
|
|
||||||
|
ws_legend.cell(row=1, column=1, value="Model Color Legend").font = Font(bold=True, size=14)
|
||||||
|
ws_legend.merge_cells('A1:D1')
|
||||||
|
|
||||||
|
ws_legend.cell(row=3, column=1, value="Model Name").font = Font(bold=True, size=11)
|
||||||
|
ws_legend.cell(row=3, column=2, value="Provider").font = Font(bold=True, size=11)
|
||||||
|
ws_legend.cell(row=3, column=3, value="Color").font = Font(bold=True, size=11)
|
||||||
|
ws_legend.cell(row=3, column=4, value="Sheet Names").font = Font(bold=True, size=11)
|
||||||
|
|
||||||
|
for i, m in enumerate(MODELS):
|
||||||
|
row = 4 + i
|
||||||
|
ws_legend.cell(row=row, column=1, value=m["name"]).font = Font(bold=True)
|
||||||
|
|
||||||
|
provider = "OpenAI" if "GPT" in m["name"] or "gpt" in m["name"] else "Google"
|
||||||
|
ws_legend.cell(row=row, column=2, value=provider)
|
||||||
|
|
||||||
|
cell = ws_legend.cell(row=row, column=3, value=f"Sample Color")
|
||||||
|
cell.fill = PatternFill(start_color=m["fill_color"], end_color=m["fill_color"], fill_type="solid")
|
||||||
|
|
||||||
|
sheet_name_scores = f"Scores - {m['name']}"[:31]
|
||||||
|
sheet_name_rank = f"Rank - {m['name']}"[:31]
|
||||||
|
ws_legend.cell(row=row, column=4, value=f"{sheet_name_scores}, {sheet_name_rank}")
|
||||||
|
|
||||||
|
ws_legend.cell(row=8, column=1, value="Sheet Overview").font = Font(bold=True, size=12)
|
||||||
|
|
||||||
|
sheets_info = [
|
||||||
|
("Multi-Model Comparison", "Cross-model comparison with all scores side by side, sorted by average overall score"),
|
||||||
|
("Model Legend", "This sheet - explains color coding and sheet structure"),
|
||||||
|
]
|
||||||
|
for m in MODELS:
|
||||||
|
sheets_info.append((f"Scores - {m['name']}"[:31], f"Original stock master list + {m['name']} scores in {m['name']} color"))
|
||||||
|
sheets_info.append((f"Rank - {m['name']}"[:31], f"Stocks ranked by {m['name']} overall score"))
|
||||||
|
|
||||||
|
for i, (sn, desc) in enumerate(sheets_info):
|
||||||
|
row = 9 + i
|
||||||
|
ws_legend.cell(row=row, column=1, value=sn).font = Font(bold=True)
|
||||||
|
ws_legend.cell(row=row, column=2, value=desc)
|
||||||
|
ws_legend.merge_cells(start_row=row, start_column=2, end_row=row, end_column=4)
|
||||||
|
|
||||||
|
ws_legend.column_dimensions['A'].width = 30
|
||||||
|
ws_legend.column_dimensions['B'].width = 20
|
||||||
|
ws_legend.column_dimensions['C'].width = 15
|
||||||
|
ws_legend.column_dimensions['D'].width = 60
|
||||||
|
|
||||||
|
print(f"Created 'Model Legend' sheet")
|
||||||
|
|
||||||
|
# Save
|
||||||
|
wb.save(EXCEL_OUT)
|
||||||
|
print(f"\n=== DONE ===")
|
||||||
|
print(f"Saved multi-model Excel to: {EXCEL_OUT}")
|
||||||
|
print(f"Total sheets: {len(wb.sheetnames)}")
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
print(f" - {sn}")
|
||||||
152
score_multi_model.py
普通文件
152
score_multi_model.py
普通文件
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Score all stocks using a specified AI model.
|
||||||
|
Usage: python3 score_multi_model.py <model_name> <output_json>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
model_name = sys.argv[1]
|
||||||
|
output_json = sys.argv[2]
|
||||||
|
|
||||||
|
client = OpenAI()
|
||||||
|
|
||||||
|
EXCEL_PATH = "/home/ubuntu/upload/MemelyAlphaStockRanking(副本)(副本).xlsx"
|
||||||
|
|
||||||
|
# Read the master list
|
||||||
|
df = pd.read_excel(EXCEL_PATH, sheet_name='stock master list')
|
||||||
|
df4 = pd.read_excel(EXCEL_PATH, sheet_name='Sheet4')
|
||||||
|
|
||||||
|
sector_map = {}
|
||||||
|
for _, row in df4.iterrows():
|
||||||
|
sym = row.get('Symbol')
|
||||||
|
if pd.notna(sym):
|
||||||
|
sector_map[sym] = {
|
||||||
|
'sector': row.get('Sector', ''),
|
||||||
|
'theme': row.get('Theme', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build stock info
|
||||||
|
stocks_info = []
|
||||||
|
seen = set()
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
sym = row['Symbol']
|
||||||
|
if pd.isna(sym) or sym in seen:
|
||||||
|
continue
|
||||||
|
seen.add(sym)
|
||||||
|
info = {
|
||||||
|
'symbol': sym,
|
||||||
|
'theme': str(row.get('Theme', '')),
|
||||||
|
'perf_7d': row.get('performance past 7 days', None),
|
||||||
|
'perf_ytd': row.get('performance Year to Date', None),
|
||||||
|
'perf_specific': row.get('performance on specific dates', None),
|
||||||
|
'first_call_kol': str(row.get('first call X kol', '')),
|
||||||
|
'top_contributor': str(row.get('top contributor', '')),
|
||||||
|
}
|
||||||
|
if sym in sector_map:
|
||||||
|
info['sector'] = sector_map[sym].get('sector', '')
|
||||||
|
info['theme_s4'] = sector_map[sym].get('theme', '')
|
||||||
|
stocks_info.append(info)
|
||||||
|
|
||||||
|
print(f"[{model_name}] Total stocks to score: {len(stocks_info)}")
|
||||||
|
|
||||||
|
def score_batch(batch_stocks, model):
|
||||||
|
stocks_text = ""
|
||||||
|
for s in batch_stocks:
|
||||||
|
perf_7d = f"{s['perf_7d']:.2%}" if pd.notna(s.get('perf_7d')) else "N/A"
|
||||||
|
perf_ytd = f"{s['perf_ytd']:.2%}" if pd.notna(s.get('perf_ytd')) else "N/A"
|
||||||
|
perf_spec = f"{s['perf_specific']:.2%}" if pd.notna(s.get('perf_specific')) else "N/A"
|
||||||
|
sector = s.get('sector', s.get('theme', 'N/A'))
|
||||||
|
theme = s.get('theme_s4', s.get('theme', 'N/A'))
|
||||||
|
stocks_text += f"""
|
||||||
|
---
|
||||||
|
Symbol: {s['symbol']}
|
||||||
|
Sector: {sector}
|
||||||
|
Theme: {theme}
|
||||||
|
Performance (Past 7 Days): {perf_7d}
|
||||||
|
Performance (Year to Date): {perf_ytd}
|
||||||
|
Performance (Specific Date): {perf_spec}
|
||||||
|
First Call KOL: {s.get('first_call_kol', 'N/A')}
|
||||||
|
Top Contributor: {s.get('top_contributor', 'N/A')}
|
||||||
|
"""
|
||||||
|
|
||||||
|
prompt = f"""You are a professional stock/crypto analyst. Evaluate each of the following stocks/assets and provide a comprehensive score.
|
||||||
|
|
||||||
|
For EACH stock, provide:
|
||||||
|
1. **overall_score** (1-100): Overall investment attractiveness score
|
||||||
|
2. **momentum_score** (1-100): Based on recent price performance and momentum
|
||||||
|
3. **theme_score** (1-100): How strong/relevant is the thematic play (e.g., AI, Defense, Aerospace, Crypto, etc.)
|
||||||
|
4. **risk_score** (1-100): Risk level (100 = highest risk)
|
||||||
|
5. **social_buzz_score** (1-100): Social media attention and KOL backing strength
|
||||||
|
6. **brief_analysis**: 2-3 sentence analysis explaining the scores
|
||||||
|
|
||||||
|
Consider these factors:
|
||||||
|
- YTD performance indicates momentum strength
|
||||||
|
- Thematic relevance to current market trends (AI, Defense, Aerospace, Crypto, Quantum Computing are hot in 2025-2026)
|
||||||
|
- Small/micro cap stocks with strong themes get higher theme scores
|
||||||
|
- Stocks with notable KOL backing get higher social buzz scores
|
||||||
|
- Higher volatility = higher risk score
|
||||||
|
|
||||||
|
Here are the stocks to evaluate:
|
||||||
|
{stocks_text}
|
||||||
|
|
||||||
|
Return ONLY a valid JSON array with objects for each stock. Each object must have: symbol, overall_score, momentum_score, theme_score, risk_score, social_buzz_score, brief_analysis.
|
||||||
|
Do not include any text outside the JSON array."""
|
||||||
|
|
||||||
|
for attempt in range(3):
|
||||||
|
try:
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a professional financial analyst. Always respond with valid JSON only."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
temperature=0.3,
|
||||||
|
max_tokens=8000
|
||||||
|
)
|
||||||
|
content = response.choices[0].message.content.strip()
|
||||||
|
if content.startswith("```"):
|
||||||
|
content = content.split("```")[1]
|
||||||
|
if content.startswith("json"):
|
||||||
|
content = content[4:]
|
||||||
|
results = json.loads(content)
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Attempt {attempt+1} error: {e}")
|
||||||
|
time.sleep(3)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Process in batches
|
||||||
|
batch_size = 15
|
||||||
|
scored = {}
|
||||||
|
total_batches = (len(stocks_info) + batch_size - 1) // batch_size
|
||||||
|
|
||||||
|
for i in range(0, len(stocks_info), batch_size):
|
||||||
|
batch = stocks_info[i:i+batch_size]
|
||||||
|
batch_num = i // batch_size + 1
|
||||||
|
symbols_in_batch = [s['symbol'] for s in batch]
|
||||||
|
print(f"[{model_name}] Batch {batch_num}/{total_batches}: {symbols_in_batch}")
|
||||||
|
|
||||||
|
results = score_batch(batch, model_name)
|
||||||
|
|
||||||
|
if results:
|
||||||
|
for r in results:
|
||||||
|
sym = r.get('symbol', '')
|
||||||
|
if sym:
|
||||||
|
scored[sym] = r
|
||||||
|
print(f" ✓ {sym}: Overall={r.get('overall_score')}")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Batch {batch_num} failed after retries")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
with open(output_json, 'w') as f:
|
||||||
|
json.dump(scored, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"\n[{model_name}] COMPLETE: {len(scored)}/{len(stocks_info)} scored")
|
||||||
|
print(f"Saved to: {output_json}")
|
||||||
1090
scores_gemini25flash.json
普通文件
1090
scores_gemini25flash.json
普通文件
文件差异内容过多而无法显示
加载差异
1091
scores_gpt41nano.json
普通文件
1091
scores_gpt41nano.json
普通文件
文件差异内容过多而无法显示
加载差异
在新工单中引用
屏蔽一个用户