diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d64efa --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +There are 2 projects combined in this program. +First peogram : localhosr:5000/ +Data Analysis & Signal Generation layer that happens before the simulation even begins. + +Here is a summary of those specific capabilities: + +1. Automated Pricing Retrieval +The program acts as a data bridge. Instead of you manually searching for prices, it can: + +Fetch real-time or historical data for specific tickers (like SPY). + +Maintain a local CSV database, ensuring you have a persistent record of price action to analyze without re-downloading everything every time. + +2. EMA (Exponential Moving Average) Calculation +The program adds a layer of technical analysis to the raw prices: + +It calculates the EMA (typically a 200-day or 50-day trend line). + +Unlike a simple average, the EMA gives more weight to recent prices, making it more responsive to new market information. + +3. Price/EMA Ratio Analysis +This is one of the program's most powerful analytical features. It generates a "Value Ratio" to help you understand market positioning: + +The Math: It divides the current price by the EMA. + +The Signal: * Ratio > 1.0: The asset is trading above its average (potentially overextended or in a strong uptrend). + +Ratio < 1.0: The asset is trading below its average (potentially undervalued or in a downtrend). + +Strategic Use: This ratio can be used to "tune" your DVA strategy—for example, investing more aggressively when the ratio is low and being more cautious when the ratio is high. + +4. Tabular Data Export +All of this technical data—the raw price, the EMA, and the resulting Ratio—is organized into a clean, structured list. This allows you to: + +See a "Heat Map" of when the asset was historically cheap or expensive relative to its trend. + +Use these technical signals to justify the "Buy" or "Sell" recommendations the dashboard provides. +Summary of the "Technical Stack"CapabilityBenefitPrice FetchingSaves time on manual data entry.EMA CalculationIdentifies the long-term "fair value" trend.Price/EMA RatioQuantifies exactly how "expensive" or "cheap" the market is today. +Second Project ; localhost:5000/backtest +a Dynamic Value Averaging (DVA) Backtester. Its primary purpose is to help investors simulate a sophisticated "Value Averaging" strategy to determine exactly how much they should buy or sell of an asset (like SPY) on a specific day of the month to stay on track with a financial goal. + +Here is a summary of what the programs actually do: + +1. Automated Strategy Simulation +The program takes a historical CSV file and "plays back" time. + +It identifies a Monthly Anchor Day (e.g., the 27th of every month) and simulates making a trade on that exact day. + +It calculates your Target Value Goal, which grows every month by a set amount (e.g., $500/month). + +2. Intelligent "Buy/Sell" Recommendations +Instead of just buying a fixed amount (DCA), the program calculates the "Value Gap": + +If the market is down: It tells you to Buy More to reach your target. + +If the market is up: It tells you to Sell (or buy less) to lock in gains. + +It calculates the exact number of units/shares you need to trade based on the most recent price in your data. + +3. Real-Time Data Synchronization +The program is designed to handle "incomplete" months: + +If today is the 28th but your anchor day is the 15th, the program doesn't just stop at the 15th. + +It forces the latest available data point from your CSV to be processed, ensuring your "Next Recommended Move" is based on the most recent price (e.g., yesterday’s close). + +4. Interactive Dashboard Reporting +Once the simulation runs, the program visualizes the health of your investment: + +Target Value Goal: Where you should be according to your plan. + +Total Invested: The actual cash you've put in out of pocket. + +Current Portfolio Value: What your holdings are worth today. + +Data Freshness: A badge that turns red if your CSV data is more than 5 days old. + +Summary of the "User Flow" +Input: You provide a CSV of stock prices and a monthly investment goal. + +Execution: The Python engine calculates the historical performance and the current gap. + +Output: The Web Dashboard gives you a clear instruction: "Buy/Sell X units for $Y.YY." +1. Core Architecture OverviewThe project is split into three distinct layers that communicate in a linear pipeline. +A. The Data Engine (data_engine.py / CSV)This is the "Source of Truth."Role: Manages the ingestion of historical market data (e.g., SPY).Logic: It ensures the CSV is sorted chronologically and provides a path for the strategy engine to read price points. +B. The Strategy Engine (engine.py)This is the "Brain" of the program where the financial math happens.Data Slicing: Uses the Anchor Day logic to filter the CSV for specific monthly dates (e.g., the 27th of every month).Safety Net: Forces the inclusion of the absolute latest CSV row (e.g., 2026-01-27) to ensure the dashboard is never out of date.Simulation Loop: Iterates through dates, calculating:Target Value: (Monthly Increment $\times$ Month Number).Market Value: (Shares Owned $\times$ Current Price).The Move (va_diff): The gap between Target and Market value.JSON Serialization: Rounds all values and packages them into a history list of dictionaries to be sent to the frontend. +C. The Web Dashboard (backtest.html / JS)This is the "Interface" for the user.Controller (runSimulation): Triggers the Python logic via an API/Socket call and receives the history array.KPI Processor (updateKPIs): * Maps Python keys (like va_invested) to UI elements.Determines the Action Verb (Buy/Sell) based on the sign of va_diff.Updates the DOM with formatted currency strings.Integrity Checker (updateSyncBadge): Compares the data date to the current system date to warn the user if data is stale. + +2. Key Data MappingsTo prevent the "Value = 0" issues we encountered, the architecture relies on strict key consistency:Python Key (Source)JavaScript VariableUI Element IDva_diffnextMoveAmountnextInvAmtva_investedtotalInvestedValuetotalSavedva_valuecurrentPortfolioValtotalValpricelatestPricelatestPriceDisplay + +3. Data Flow SequenceTrigger: User clicks "Run Simulation" in the browser.Process: Python reads CSV $\rightarrow$ Filters by Anchor Day $\rightarrow$ Appends Latest Row $\rightarrow$ Calculates DVA Steps.Transmit: Python returns a JSON array of the simulation history.Render: JavaScript grabs the last element of that array to fill the KPI cards and uses the full array to draw the charts. \ No newline at end of file diff --git a/__pycache__/engine.cpython-313.pyc b/__pycache__/engine.cpython-313.pyc index 9fbc4de..af6c17d 100644 Binary files a/__pycache__/engine.cpython-313.pyc and b/__pycache__/engine.cpython-313.pyc differ diff --git a/app.py b/app.py index a438b48..5221744 100644 --- a/app.py +++ b/app.py @@ -65,36 +65,40 @@ def api_backtest(): # 1. Extract and Sanitize Symbol symbol = data.get('symbol', '').strip().upper() - print(f"DEBUG: Processing {symbol}") + print(f"DEBUG: Processing {symbol} with payload: {data}") if not symbol: return jsonify({"error": "Symbol is required"}), 400 try: - # 2. Extract Numerical and Logic Inputs (Matching updated JS keys) + # 2. Extract and Cast Inputs (Ensuring types match engine requirements) + # Note: JS uses 'startDate' (camelCase), Python often uses 'start_date' initial = float(data.get('initial_inv', 0)) monthly = float(data.get('monthly_target', 0)) - start_date = data.get('start_date', '2024-01-01') + start_date = data.get('startDate') or data.get('start_date', '2024-01-01') + frequency = data.get('frequency', 'Monthly') - # Robust Boolean Check for toggles + # Robust Boolean Check allow_sell = data.get('allow_sell') is True allow_frac = data.get('allow_fractional') is True # 3. Initialize Engines - # DataEngine __init__ now calls ensure_data() to download if missing data_eng = DataEngine(symbol=symbol) + # Verify file exists after DataEngine logic if not os.path.exists(data_eng.file_path): return jsonify({"error": f"Data for {symbol} could not be retrieved."}), 404 strat_eng = StrategyEngine(data_eng) - # 4. Calculation - history = strat_eng.calculate_va_vs_dca( - initial_inv=initial, - monthly_target=monthly, - start_date=start_date, - allow_sell=allow_sell, + # 4. Calculation - Calling the correctly named method 'run_simulation' + # Ensure arguments match the signature in your engine.py + history = strat_eng.run_simulation( + start_date=start_date, + monthly_goal=monthly, + initial_inv=initial, + frequency=frequency, + allow_sell=allow_sell, allow_fractional=allow_frac ) @@ -104,7 +108,8 @@ def api_backtest(): import traceback print("CRITICAL ERROR in /api/backtest:") print(traceback.format_exc()) - return jsonify({"error": "Internal Server Error. Check terminal logs."}), 500 + # Returning the actual error message helps debugging the '500' faster + return jsonify({"error": str(e)}), 500 @app.route('/backtest') # This is the URL you will actually visit def backtest_ui(): diff --git a/engine.py b/engine.py index 294e61d..dbe5fa5 100644 --- a/engine.py +++ b/engine.py @@ -478,32 +478,52 @@ class StrategyEngine: anchor_day = start_dt_obj.day latest_csv_date = df['date'].max() # This captures 2026-01-27 - # 3. Filter data starting from your start_date - df_filtered = df[df['date'] >= start_dt_obj].copy() + # --- 3. Filter data starting from your start_date --- + df_from_start = df[df['date'] >= start_dt_obj].copy() + if df_from_start.empty: + return [] - # 4. Select recurring monthly days (The first trading day on/after the anchor day) - monthly_df = df_filtered[df_filtered['date'].dt.day >= anchor_day].groupby([ - df_filtered['date'].dt.year, - df_filtered['date'].dt.month - ], as_index=False).first() + # --- 4. Choose rows based on Frequency --- + if frequency == "Weekly": + # Take every 5th trading day + final_df = df_from_start.iloc[::5].copy() + step_increment = float((monthly_goal * 12) / 52) - # 5. FORCE LAST ROW: If the latest date from CSV isn't in our list, append it - if monthly_df.empty or monthly_df.iloc[-1]['date'] != latest_csv_date: - last_row = df_filtered[df_filtered['date'] == latest_csv_date] - monthly_df = pd.concat([monthly_df, last_row]).drop_duplicates(subset=['date']) + elif frequency == "Bi-Weekly": + # Take every 10th trading day + final_df = df_from_start.iloc[::10].copy() + step_increment = float((monthly_goal * 12) / 26) - # 6. Finalize index for the strategy loop - monthly_df.index = pd.to_datetime(monthly_df['date']) + else: # Default to Monthly + # Group by year/month and take the first available day >= anchor_day + final_df = df_from_start[df_from_start['date'].dt.day >= anchor_day].groupby([ + df_from_start['date'].dt.year, + df_from_start['date'].dt.month + ], as_index=False).first() + step_increment = float(monthly_goal) - if monthly_df.empty: + # --- 5. FORCE LAST ROW SAFETY --- + # Reset index to make concatenation clean + final_df = final_df.reset_index(drop=True) + + if not df_from_start.empty: + latest_row = df_from_start.iloc[[-1]] # Gets the absolute last available day + if final_df.empty or latest_row.iloc[0]['date'] != final_df.iloc[-1]['date']: + final_df = pd.concat([final_df, latest_row]).drop_duplicates(subset=['date']) + + # Sort and final index reset before the loop + final_df = final_df.sort_values('date').reset_index(drop=True) + + if final_df.empty: return [] - # Helper for share calculation based on user toggle + # --- 6. Helper for share calculation --- def get_shares(cash, prc): if prc <= 0: return 0 + # Note: allow_fractional should be defined in your class/scope return cash / prc if allow_fractional else math.floor(cash / prc) - # 2. Initial Setup + # --- 7. Initial Setup --- va_shares = 0 dca_shares = 0 va_invested = 0 @@ -511,41 +531,43 @@ class StrategyEngine: va_target_value = 0 history = [] - # 3. Strategy Loop - for i, row in monthly_df.iterrows(): - actual_date_str = i.strftime('%Y-%m-%d') + # --- 8. Strategy Loop --- + for step, (idx, row) in enumerate(final_df.iterrows()): + actual_date_str = row['date'].strftime('%Y-%m-%d') price = float(row['close']) - if i == monthly_df.index[0]: - # --- MONTH 0: INITIAL DEPOSIT --- - actual_inv = initial_inv # This is the 'va_diff' + if step == 0: + # --- STEP 0: INITIAL DEPOSIT --- + actual_inv = initial_inv dca_actual_inv = initial_inv va_target_value = initial_inv - diff = 0 - va_new_shares = get_shares(actual_inv, price) dca_new_shares = va_new_shares else: - # --- MONTH 1+: DVA vs DCA --- - + # --- STEP 1+: DVA vs DCA --- + # Use step_increment (calculated in your frequency logic) + # to ensure growth matches frequency (Weekly/Monthly) + current_increment = step_increment + # DCA Logic - dca_actual_inv = monthly_target + dca_actual_inv = current_increment dca_new_shares = get_shares(dca_actual_inv, price) # DVA Logic (Fixed Value Path) - va_target_value += monthly_target + va_target_value += current_increment - # Gap calculation: Target vs. current value BEFORE this month's investment + # Gap calculation: Target vs. current value BEFORE this step's investment current_va_val_pre = va_shares * price diff = va_target_value - current_va_val_pre # Apply Buy/Sell constraints + # note: allow_sell should be defined in your class/scope actual_inv = diff if (diff >= 0 or allow_sell) else 0 va_new_shares = get_shares(actual_inv, price) - # --- STATE UPDATES (Must happen for both Month 0 and Month 1+) --- + # --- STATE UPDATES --- va_shares += va_new_shares dca_shares += dca_new_shares @@ -553,7 +575,6 @@ class StrategyEngine: dca_invested += dca_actual_inv # --- Unified History Append --- - # We calculate these here so they are ALWAYS defined for every row history.append({ "date": actual_date_str, "price": round(price, 2), @@ -561,14 +582,124 @@ class StrategyEngine: "dca_invested": round(dca_invested, 2), "dca_shares_trans": round(dca_new_shares, 4), "dca_shares_total": round(dca_shares, 4), - "va_value": round(va_shares * price, 2), # Becomes 'Current Portfolio Value' - "va_invested": round(va_invested, 2), # Becomes 'Total Invested' + "va_value": round(va_shares * price, 2), + "va_invested": round(va_invested, 2), "va_diff": round(actual_inv, 2), "va_shares_trans": round(va_new_shares, 4), "va_shares_total": round(va_shares, 4), - "va_target_value": round(va_target_value, 2) # Used for next goal + "va_target_value": round(va_target_value, 2) }) - # Debugging print - print(f"Date: {i.strftime('%Y-%m')}, Target: {va_target_value:.2f}, Portfolio: {va_invested:.2f}, Diff: {diff:.2f}") + return history + + def run_simulation(self, start_date, monthly_goal, initial_inv, frequency="Monthly", allow_sell=True, allow_fractional=True): + # 1. DATA LOADING + df = pd.read_csv(self.data_engine.file_path) + df['date'] = pd.to_datetime(df['date']) + df = df.sort_values('date') + + # 2. DATE PREP + start_dt_obj = pd.to_datetime(start_date) + latest_csv_date = df['date'].max() + + # Filter data starting from user's choice + df_from_start = df[df['date'] >= start_dt_obj].copy() + + if df_from_start.empty: + return [] + + # 3. FREQUENCY LOGIC (Hardened) + # Standardize string for comparison + freq_check = str(frequency).strip().title() + + if freq_check == "Weekly": + final_df = df_from_start.iloc[::5].copy() + step_increment = float((monthly_goal * 12) / 52) + elif freq_check == "Bi-Weekly": + final_df = df_from_start.iloc[::10].copy() + step_increment = float((monthly_goal * 12) / 26) + else: + # For Monthly, we need the anchor day + anchor_day = start_dt_obj.day + final_df = df_from_start[df_from_start['date'].dt.day >= anchor_day].groupby([ + df_from_start['date'].dt.year, + df_from_start['date'].dt.month + ], as_index=False).first() + step_increment = float(monthly_goal) + + # 4. SAFETY: Ensure most recent price is included + final_df = final_df.reset_index(drop=True) + last_actual_row = df_from_start.iloc[[-1]] + + if final_df.empty or last_actual_row.iloc[0]['date'] != final_df.iloc[-1]['date']: + final_df = pd.concat([final_df, last_actual_row]).drop_duplicates(subset=['date']) + + final_df = final_df.sort_values('date').reset_index(drop=True) + + # 5. STRATEGY INITIALIZATION + def get_shares(cash, prc): + if prc <= 0: return 0 + if allow_fractional: + return float(cash / prc) + else: + # Handles both buying (+) and selling (-) for whole shares + return float(math.floor(cash / prc)) if cash >= 0 else float(math.ceil(cash / prc)) + + # Ensure these are initialized as floats + va_shares, dca_shares = 0.0, 0.0 + va_invested, dca_invested = 0.0, 0.0 + + history = [] + + for step, (idx, row) in enumerate(final_df.iterrows()): + actual_date_str = row['date'].strftime('%Y-%m-%d') + price = float(row['close']) + + if step == 0: + # First row: Both strategies start with the Initial Investment + va_actual_inv = float(initial_inv) + dca_actual_inv = float(initial_inv) + va_target_value = float(initial_inv) + else: + # Subsequent rows: Use the frequency-adjusted step_increment + va_target_value += step_increment + + # DCA logic: Always invests the same amount every period + dca_actual_inv = float(step_increment) + + # VA logic: Invests enough to hit the target value + current_va_market_val = va_shares * price + diff = va_target_value - current_va_market_val + + # Apply "Allow Sell" constraint + va_actual_inv = diff if (diff >= 0 or allow_sell) else 0.0 + + # Update Shares based on fractional setting + va_new_shares = get_shares(va_actual_inv, price) + dca_new_shares = get_shares(dca_actual_inv, price) + + # Running totals for shares + va_shares += va_new_shares + dca_shares += dca_new_shares + + # Running totals for principal invested + va_invested += va_actual_inv + dca_invested += dca_actual_inv + + history.append({ + "date": actual_date_str, + "price": round(price, 2), + "va_diff": round(va_actual_inv, 2), # Invested this step (VA) + "va_shares_trans": round(va_new_shares, 4), + "va_value": round(va_shares * price, 2), # Current Portfolio Value (VA) + "va_invested": round(va_invested, 2), # Total Out-of-Pocket (VA) + "va_shares_total": round(va_shares, 4), + "va_target_value": round(float(va_target_value), 2), + "dca_diff": round(dca_actual_inv, 2), # Invested this step (DCA) + "dca_shares_trans": round(dca_new_shares, 4), + "dca_value": round(dca_shares * price, 2), # Current Portfolio Value (DCA) + "dca_invested": round(dca_invested, 2), # Total Out-of-Pocket (DCA) + "dca_shares_total": round(dca_shares, 4) + }) + return history \ No newline at end of file diff --git a/templates/val_avg_cal.html b/templates/val_avg_cal.html index c8dcb7e..c95e4da 100644 --- a/templates/val_avg_cal.html +++ b/templates/val_avg_cal.html @@ -29,47 +29,57 @@ -
-

🛠️ Value Averging Strategy Analysis

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
+
+
+

🛠️ Value Averaging Strategy Analysis

+ +
-
-
-
- - -
-
-
-
- - -
-
-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
- +
+
+ + +
+
+
+
+ + +
+
+
+ +
@@ -154,73 +164,77 @@