Spaces:
Running
Running
| """Module for parsing Zerodha CSV exports.""" | |
| import pandas as pd | |
| import io | |
| def clean_numeric_series(series): | |
| """Removes commas and % signs from a pandas Series, then converts to float.""" | |
| return series.astype(str).str.replace(',', '', regex=False).str.replace('%', '', regex=False).astype(float) | |
| def load_portfolio(file_path_or_string): | |
| """ | |
| Reads a Zerodha portfolio CSV and returns a cleaned pandas DataFrame. | |
| Accepts either a file path ending in .csv or a raw CSV text string. | |
| """ | |
| try: | |
| if file_path_or_string.strip().endswith('.csv') and '\n' not in file_path_or_string: | |
| df = pd.read_csv(file_path_or_string.strip()) | |
| else: | |
| df = pd.read_csv(io.StringIO(file_path_or_string.strip())) | |
| # Strip whitespace from column names | |
| df.columns = df.columns.str.strip() | |
| # Rename columns. Using a flexible mapping in case of slight variations (like "Qty." vs "Qty") | |
| col_mapping = { | |
| "Instrument": "symbol", | |
| "Qty.": "qty", | |
| "Qty": "qty", | |
| "Avg. cost": "avg_cost", | |
| "LTP": "ltp", | |
| "Invested": "invested", | |
| "Cur. val": "cur_val", | |
| "P&L": "pnl", | |
| "Net chg.": "net_chg", | |
| "Day chg.": "day_chg" | |
| } | |
| df = df.rename(columns=col_mapping) | |
| # Drop rows where symbol is empty or NaN | |
| if 'symbol' in df.columns: | |
| df = df.dropna(subset=['symbol']) | |
| df = df[df['symbol'].astype(str).str.strip() != ''] | |
| # Convert numeric columns | |
| numeric_cols = ['qty', 'avg_cost', 'ltp', 'invested', 'cur_val', 'pnl', 'net_chg', 'day_chg'] | |
| for col in numeric_cols: | |
| if col in df.columns: | |
| df[col] = clean_numeric_series(df[col]) | |
| return df | |
| except Exception as e: | |
| raise ValueError(f"Failed to load or parse portfolio: {e}") | |
| def validate_portfolio(df): | |
| """ | |
| Validates the parsed portfolio DataFrame. | |
| Returns (True, None) if valid. | |
| Returns (False, error_message) if invalid. | |
| """ | |
| try: | |
| if df.empty: | |
| return False, "Portfolio DataFrame is empty (no rows)." | |
| required_cols = ['symbol', 'qty', 'avg_cost', 'ltp', 'invested', 'cur_val', 'pnl', 'net_chg', 'day_chg'] | |
| missing_cols = [col for col in required_cols if col not in df.columns] | |
| if missing_cols: | |
| found_cols = list(df.columns) | |
| return False, f"Missing required columns: {', '.join(missing_cols)}\nFound columns: {', '.join(found_cols)}" | |
| if df['symbol'].isnull().any() or (df['symbol'].astype(str).str.strip() == '').any(): | |
| return False, "Found rows with null or empty symbols." | |
| return True, None | |
| except Exception as e: | |
| return False, f"Validation failed with error: {e}" | |
| def portfolio_to_text(df): | |
| """ | |
| Converts the cleaned DataFrame into a formatted text summary for AI prompts. | |
| """ | |
| lines = [] | |
| for _, row in df.iterrows(): | |
| # SYMBOL | Qty: X | Avg: ₹Y | LTP: ₹Z | P&L: ₹A (B%) | Day: C% | |
| sym = row['symbol'] | |
| qty = row['qty'] | |
| avg = row['avg_cost'] | |
| ltp = row['ltp'] | |
| pnl = row['pnl'] | |
| net = row['net_chg'] | |
| day = row['day_chg'] | |
| line = f"{sym} | Qty: {qty:g} | Avg: ₹{avg:.2f} | LTP: ₹{ltp:.2f} | P&L: ₹{pnl:.2f} ({net:+.2f}%) | Day: {day:+.2f}%" | |
| lines.append(line) | |
| lines.append("-" * 40) | |
| total_invested = df['invested'].sum() if 'invested' in df.columns else 0.0 | |
| total_cur_val = df['cur_val'].sum() if 'cur_val' in df.columns else 0.0 | |
| lines.append(f"Total Invested: ₹{total_invested:.2f} | Total Current Value: ₹{total_cur_val:.2f}") | |
| return "\n".join(lines) | |
| if __name__ == "__main__": | |
| import sys | |
| if sys.platform == 'win32': | |
| sys.stdout.reconfigure(encoding='utf-8') | |
| # Simple test with sample data matching Zerodha format | |
| sample_csv = '''Instrument,Qty.,Avg. cost,LTP,Invested,Cur. val,P&L,Net chg.,Day chg. | |
| RELIANCE,10,"2,450.50","2,500.00","24,505.00","25,000.00","495.00",2.02%,0.50% | |
| TCS,5,"3,200.00","3,150.00","16,000.00","15,750.00","-250.00",-1.56%,-0.20% | |
| ,10,100,100,1000,1000,0,0%,0% | |
| ''' | |
| print("Loading sample portfolio...") | |
| df = load_portfolio(sample_csv) | |
| print("\nValidating DataFrame...") | |
| is_valid, err = validate_portfolio(df) | |
| print(f"Is valid? {is_valid}") | |
| if not is_valid: | |
| print(f"Error: {err}") | |
| print("\nDataFrame Output:") | |
| print(df.to_string()) | |
| print("\nFormatted Text:") | |
| print(portfolio_to_text(df)) | |