Daveabc12 commited on
Commit
9b35b64
·
verified ·
1 Parent(s): 450b468

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +234 -98
app.py CHANGED
@@ -10,7 +10,8 @@ import json
10
  # --- CORRECTED IMPORTS: Moved MACD ---
11
  from ta.volatility import BollingerBands
12
  from ta.momentum import RSIIndicator # Removed MACD from here
13
- from ta.trend import ADXIndicator, MACD # Added MACD here
 
14
  # --- [NEW] ADDED ATR FOR INTELLIGENT EXIT ---
15
  from ta.volatility import AverageTrueRange
16
  # --- [END NEW] ---
@@ -473,10 +474,12 @@ def clean_data_and_report_outliers(df):
473
 
474
  # --- 2. Custom Backtesting Engine ---
475
 
476
- # --- [NEW] 7-Factor Version (with Markov State and RAW GRADIENT scoring) ---
477
- def calculate_confidence_score(df, primary_driver, # <-- This exists
478
- use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov, # 7 toggles
479
- rsi_w, vol_w, trend_w, vol_w_val, macd_w, ma_slope_w, markov_w, # 7 weights
 
 
480
  bband_params,
481
  best_markov_setup=None):
482
 
@@ -489,92 +492,83 @@ def calculate_confidence_score(df, primary_driver, # <-- This exists
489
  df['RunUp_Return'] = df['Close'].pct_change(periods=run_up_period)
490
  df['RunUp_State'] = df['RunUp_Return'].apply(lambda x: 'Up' if x > 0 else 'Down')
491
 
492
- # --- BBand Factor (Still uses gradient logic as it's a "how far" metric) ---
493
  if primary_driver != 'Bollinger Bands' and 'bband_lower' in df.columns:
494
- bb_weight = 1.0 # This factor has a fixed weight of 1.0
495
  long_entry_pct = bband_params.get('long_entry_threshold_pct', 0.0)
496
  short_entry_pct = bband_params.get('short_entry_threshold_pct', 0.0)
497
  long_bb_trigger_price = df['bband_lower'] * (1 - long_entry_pct)
498
  short_bb_trigger_price = df['bband_upper'] * (1 + short_entry_pct)
499
  long_score_range = (df['large_ma'] - long_bb_trigger_price).replace(0, np.nan)
500
  short_score_range = (short_bb_trigger_price - df['large_ma']).replace(0, np.nan)
501
- long_score_pct = ((df['large_ma'] - df['Close']) / long_score_range).clip(0, 1).fillna(0)
502
- short_score_pct = ((df['Close'] - df['large_ma']) / short_score_range).clip(0, 1).fillna(0)
503
- long_score += long_score_pct * bb_weight
504
- short_score += short_score_pct * bb_weight
505
-
506
- # --- [RESTORED] Gradient Factor Scoring ---
507
 
508
  # Factor 1: RSI
509
- if primary_driver != 'RSI Crossover' and use_rsi and 'RSI' in df.columns and not df['RSI'].isna().all():
510
  long_score += ((30 - df['RSI']) / 30).clip(0, 1).fillna(0) * rsi_w
511
  short_score += ((df['RSI'] - 70) / 30).clip(0, 1).fillna(0) * rsi_w
512
 
513
  # Factor 2: Volatility
514
- if use_volatility and 'Volatility_p' in df.columns and not df['Volatility_p'].isna().all():
515
- # This one is binary
516
  vol_signal = (df['Volatility_p'] > 0.025).astype(float) * vol_w
517
- long_score += vol_signal
518
- short_score += vol_signal
519
 
520
- # Factor 3: Trend (Distance from SMA200)
521
- if use_trend and 'SMA_200' in df.columns and 'Close' in df.columns \
522
- and not df['SMA_200'].isna().all() and not df['Close'].isna().all():
523
- valid_sma = (df['SMA_200'] != 0) & df['SMA_200'].notna() & df['Close'].notna()
524
  pct_dist = pd.Series(0.0, index=df.index)
525
  pct_dist.loc[valid_sma] = df.loc[valid_sma].apply(lambda row: (row['Close'] - row['SMA_200']) / row['SMA_200'] if row['SMA_200'] != 0 else 0, axis=1)
526
  long_score += (pct_dist / 0.10).clip(0, 1).fillna(0) * trend_w
527
  short_score += (-pct_dist / 0.10).clip(0, 1).fillna(0) * trend_w
528
 
529
  # Factor 4: Volume
530
- if use_volume and 'Volume_Ratio' in df.columns and not df['Volume_Ratio'].isna().all():
531
- # This one is a gradient
532
  vol_spike_signal = ((df['Volume_Ratio'] - 1.75) / 2.25).clip(0, 1).fillna(0) * vol_w_val
533
- long_score += vol_spike_signal
534
- short_score += vol_spike_signal
535
 
536
- # Factor 5: MACD Signals
537
- if primary_driver != 'MACD Crossover' and use_macd and 'MACD_line' in df.columns and 'MACD_signal' in df.columns and 'MACD_hist' in df.columns \
538
- and not df['MACD_line'].isna().all() and not df['MACD_signal'].isna().all() and not df['MACD_hist'].isna().all():
539
- # This is a hybrid gradient/binary
540
  macd_cross_long = (df['MACD_line'].shift(1) < df['MACD_signal'].shift(1)) & (df['MACD_line'] >= df['MACD_signal'])
541
  macd_cross_short = (df['MACD_line'].shift(1) > df['MACD_signal'].shift(1)) & (df['MACD_line'] <= df['MACD_signal'])
542
  long_score += macd_cross_long.astype(float) * macd_w * 0.6
543
  short_score += macd_cross_short.astype(float) * macd_w * 0.6
544
- hist_positive = df['MACD_hist'] > 0
545
- hist_negative = df['MACD_hist'] < 0
546
- long_score += hist_positive.astype(float) * macd_w * 0.4
547
- short_score += hist_negative.astype(float) * macd_w * 0.4
548
 
549
  # Factor 6: MA Slope
550
- if primary_driver != 'MA Slope' and use_ma_slope and 'ma_slope' in df.columns and not df['ma_slope'].isna().all():
551
- # This is binary
552
  long_score += (df['ma_slope'] > 0).astype(float) * ma_slope_w
553
  short_score += (df['ma_slope'] < 0).astype(float) * ma_slope_w
554
 
555
  # Factor 7: Markov State
556
  if primary_driver != 'Markov State' and use_markov and best_markov_setup and 'RunUp_State' in df.columns:
557
  strategy = best_markov_setup.get('Strategy')
558
- if strategy == 'Down -> Up':
559
- long_score += (df['RunUp_State'] == 'Down').astype(float) * markov_w
560
- elif strategy == 'Up -> Up':
561
- long_score += (df['RunUp_State'] == 'Up').astype(float) * markov_w
562
- elif strategy == 'Up -> Down':
563
- short_score += (df['RunUp_State'] == 'Up').astype(float) * markov_w
564
- elif strategy == 'Down -> Down':
565
- short_score += (df['RunUp_State'] == 'Down').astype(float) * markov_w
566
- # --- [END] ---
 
 
 
 
 
 
 
 
 
567
 
568
- # --- [REMOVED NORMALIZATION] ---
569
- # Return the raw, un-normalized scores
570
  return long_score.fillna(0), short_score.fillna(0)
571
 
572
- # --- FINAL VERSION: Merged Intelligent Exit + All Missing Features ---
573
-
574
  # --- FULL REPLACEMENT FOR run_backtest FUNCTION ---
575
  def run_backtest(data, params,
576
- use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov,
577
- rsi_w, vol_w, trend_w, vol_w_val, macd_w, ma_slope_w, markov_w,
578
  use_adx_filter, adx_threshold, rsi_logic,
579
  adx_period=14,
580
  veto_setups_list=None,
@@ -643,6 +637,78 @@ def run_backtest(data, params,
643
  else:
644
  df['RSI'], df['Volatility_p'], df['ADX'], df['ATR'] = np.nan, np.nan, np.nan, np.nan
645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  df['SMA_200'] = df['Close'].rolling(window=200, min_periods=1).mean()
647
 
648
  if 'Volume' in df.columns:
@@ -672,7 +738,9 @@ def run_backtest(data, params,
672
  raw_long_score, raw_short_score = calculate_confidence_score(df,
673
  primary_driver,
674
  use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov,
 
675
  rsi_w, vol_w, trend_w, vol_w_val, macd_w, ma_slope_w, markov_w,
 
676
  bband_params_for_score,
677
  best_markov_setup=markov_setup
678
  )
@@ -759,7 +827,11 @@ def run_backtest(data, params,
759
 
760
  # [SURGICAL PATCH START] --------------------------------------------------
761
  # Check if ALL confidence indicators are disabled
762
- all_indicators_off = not any([use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov])
 
 
 
 
763
 
764
  # If indicators are OFF, we bypass the confidence check (allow raw signal)
765
  # Otherwise, we enforce the confidence threshold as usual
@@ -1019,18 +1091,34 @@ def generate_long_plot(df, trades, ticker):
1019
  fig.add_trace(go.Scatter(x=df.index, y=df['bband_upper'], mode='lines', name='Upper Band', line=dict(color='gray', width=0.5)))
1020
  fig.add_trace(go.Scatter(x=df.index, y=df['bband_lower'], mode='lines', name='Lower Band', line=dict(color='gray', width=0.5), fill='tonexty', fillcolor='rgba(211,211,211,0.2)'))
1021
 
1022
- # Add Trades
1023
  long_entries_log, long_exits, _, _ = trades
 
 
1024
  if long_entries_log:
1025
  dates = [t['date'] for t in long_entries_log]
1026
  prices = [t['price'] for t in long_entries_log]
1027
  scores = [f"Confidence: {t['confidence']:.0f}%" for t in long_entries_log]
1028
- fig.add_trace(go.Scatter(x=dates, y=prices, mode='markers', name='Long Entry', marker=dict(color='green', symbol='triangle-up', size=12), text=scores, hoverinfo='text'))
1029
- if not long_exits.empty and 'Close' in df.columns: # Check if Close exists
1030
- exit_prices = df.loc[long_exits,'Close'].dropna() # Drop exits where price might be NaN
1031
- fig.add_trace(go.Scatter(x=exit_prices.index, y=exit_prices, mode='markers', name='Long Exit', marker=dict(color='darkgreen', symbol='x', size=8)))
1032
-
1033
- fig.update_layout(title=f'Long Trades for {ticker}', xaxis_title='Date', yaxis_title='Price', legend_title="Indicator"); return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1034
 
1035
  def generate_short_plot(df, trades, ticker):
1036
  fig = go.Figure()
@@ -3269,11 +3357,14 @@ def main():
3269
  for key, val in defaults.items():
3270
  if key not in st.session_state: st.session_state[key] = val
3271
 
3272
- # 4. Helper to update state from widgets
3273
  def update_state():
3274
- for key in defaults.keys():
3275
- widget_key = f"widget_{key}"
3276
- if widget_key in st.session_state: st.session_state[key] = st.session_state[widget_key]
 
 
 
3277
 
3278
  st.set_page_config(page_title="Dave's Quant System", page_icon="🔴", layout="wide")
3279
 
@@ -3470,13 +3561,20 @@ def main():
3470
  st.sidebar.radio("Mode:", ("Analyse Single Ticker", full_list_label), key='run_mode')
3471
 
3472
  if st.session_state.get('run_mode') == "Analyse Single Ticker":
3473
- if not ticker_list:
3474
- st.sidebar.warning("No tickers loaded.")
3475
- else:
3476
- ticker_index = 0
3477
- try: ticker_index = ticker_list.index(st.session_state.ticker_select)
3478
- except ValueError: ticker_index = 0
3479
- st.sidebar.selectbox("Select a Ticker:", ticker_list, index=ticker_index, key='widget_ticker_select', on_change=update_state)
 
 
 
 
 
 
 
3480
 
3481
  # --- UPDATED: Date Inputs (Fully Unrestricted 2000-2030) ---
3482
  st.sidebar.date_input(
@@ -3813,9 +3911,22 @@ def main():
3813
  if 'widget_markov_w' not in st.session_state: st.session_state.widget_markov_w = st.session_state.markov_w
3814
  st.sidebar.number_input("Markov Weight", 0.1, 5.0, step=0.1, key='widget_markov_w', on_change=update_state, disabled=not st.session_state.get('use_markov', True))
3815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3816
  if 'widget_confidence_slider' not in st.session_state: st.session_state.widget_confidence_slider = st.session_state.confidence_slider
3817
  st.sidebar.slider("Minimum Confidence Threshold (%)", 0, 100, step=5, key='widget_confidence_slider', on_change=update_state)
3818
-
3819
  st.sidebar.markdown("---")
3820
 
3821
  st.sidebar.header("3. Strategy Parameters")
@@ -4243,38 +4354,61 @@ def main():
4243
 
4244
  # A) Single Ticker Analysis
4245
  if st.session_state.run_mode == "Analyse Single Ticker":
 
4246
  selected_ticker = st.session_state.get('ticker_select', ticker_list[0] if ticker_list else None)
4247
- if not selected_ticker: st.error("No ticker selected."); st.stop()
4248
-
 
 
 
 
4249
  cols_to_use = [selected_ticker]
4250
  if f'{selected_ticker}_High' in master_df.columns: cols_to_use.append(f'{selected_ticker}_High')
4251
  if f'{selected_ticker}_Low' in master_df.columns: cols_to_use.append(f'{selected_ticker}_Low')
4252
  if f'{selected_ticker}_Volume' in master_df.columns: cols_to_use.append(f'{selected_ticker}_Volume')
 
4253
  existing_cols = [col for col in cols_to_use if col in master_df.columns]
4254
-
4255
- # Load data with WARMUP
4256
- data_for_backtest_full = master_df.loc[:, existing_cols]
4257
- data_for_backtest = data_for_backtest_full.loc[data_load_start:end_date]
4258
-
 
 
4259
  rename_dict = {selected_ticker: 'Close', f'{selected_ticker}_High': 'High', f'{selected_ticker}_Low': 'Low', f'{selected_ticker}_Volume': 'Volume'}
4260
  rename_dict_filtered = {k: v for k, v in rename_dict.items() if k in existing_cols}
4261
  data_for_backtest = data_for_backtest.rename(columns=rename_dict_filtered)
4262
-
4263
  if not data_for_backtest.empty and 'Close' in data_for_backtest.columns and not data_for_backtest['Close'].isna().all():
 
 
 
4264
  long_pnl, short_pnl, avg_long_trade, avg_short_trade, results_df, trades, open_trades, trade_counts, durations, trade_dates, exit_breakdown = run_backtest(
4265
- data_for_backtest, manual_params,
4266
- st.session_state.use_rsi, st.session_state.use_vol, st.session_state.use_trend, st.session_state.use_volume,
4267
- st.session_state.use_macd, st.session_state.use_ma_slope, st.session_state.use_markov,
4268
- st.session_state.rsi_w, st.session_state.vol_w, st.session_state.trend_w, st.session_state.volume_w,
4269
- st.session_state.macd_w, st.session_state.ma_slope_w, st.session_state.markov_w,
4270
- st.session_state.use_adx_filter, st.session_state.adx_threshold,
4271
- st.session_state.get('rsi_logic', 'Crossover'), st.session_state.adx_period,
4272
- veto_setups_list=veto_list_to_use, primary_driver=st.session_state.primary_driver,
4273
- markov_setup=markov_setup_to_use, exit_logic_type=exit_logic, exit_confidence_threshold=exit_thresh,
4274
- smart_trailing_stop_pct=smart_trailing_stop, smart_exit_atr_period=smart_atr_p,
4275
- smart_exit_atr_multiplier=smart_atr_m, intelligent_tsl_pct=intelligent_tsl,
4276
- analysis_start_date=user_start_date # <--- Pass the filter date
4277
- )
 
 
 
 
 
 
 
 
 
 
 
 
4278
  st.session_state.single_ticker_results = {"long_pnl": long_pnl, "short_pnl": short_pnl, "avg_long_trade": avg_long_trade, "avg_short_trade": avg_short_trade, "results_df": results_df, "trades": trades}
4279
  st.session_state.open_trades_df = pd.DataFrame(open_trades) if open_trades else pd.DataFrame()
4280
  st.session_state.exit_breakdown_totals = {'long_profit_take_count': exit_breakdown[0], 'long_tsl_count': exit_breakdown[1], 'long_time_exit_count': exit_breakdown[2], 'short_profit_take_count': exit_breakdown[3], 'short_tsl_count': exit_breakdown[4], 'short_time_exit_count': exit_breakdown[5]}
@@ -4306,18 +4440,20 @@ def main():
4306
  ticker_data_series = ticker_data_series.rename(columns={k:v for k,v in rename_dict.items() if k in existing_cols})
4307
 
4308
  if not ticker_data_series.empty and 'Close' in ticker_data_series.columns and not ticker_data_series['Close'].isna().all():
4309
- long_pnl, short_pnl, avg_long_trade, avg_short_trade, _, trades, open_trades, trade_counts, durations, trade_dates, exit_breakdown = run_backtest(
4310
- ticker_data_series, manual_params,
4311
  st.session_state.use_rsi, st.session_state.use_vol, st.session_state.use_trend, st.session_state.use_volume,
4312
- st.session_state.use_macd, st.session_state.use_ma_slope, st.session_state.use_markov,
 
4313
  st.session_state.rsi_w, st.session_state.vol_w, st.session_state.trend_w, st.session_state.volume_w,
4314
- st.session_state.macd_w, st.session_state.ma_slope_w, st.session_state.markov_w,
 
4315
  st.session_state.use_adx_filter, st.session_state.adx_threshold, st.session_state.get('rsi_logic', 'Crossover'), st.session_state.adx_period,
4316
  veto_setups_list=veto_list_to_use, primary_driver=st.session_state.primary_driver,
4317
  markov_setup=markov_setup_to_use, exit_logic_type=exit_logic, exit_confidence_threshold=exit_thresh,
4318
  smart_trailing_stop_pct=smart_trailing_stop, smart_exit_atr_period=smart_atr_p,
4319
  smart_exit_atr_multiplier=smart_atr_m, intelligent_tsl_pct=intelligent_tsl,
4320
- analysis_start_date=user_start_date # <--- Pass the filter date
4321
  )
4322
 
4323
  if abs(long_pnl) > PROFIT_THRESHOLD or abs(short_pnl) > PROFIT_THRESHOLD or \
@@ -4467,7 +4603,7 @@ def main():
4467
  if st.button("💾 Add these settings to User-Defined List", key="save_setup_from_analysis", on_click=add_setup_to_user_list): pass
4468
 
4469
  # 5. Open Positions Table (With Filter)
4470
- st.subheader("👨🏽‍💼 Open Positions & Recently Closed",
4471
  help="This table displays all currently ACTIVE trades, plus any trades that closed within the last 30 days.")
4472
 
4473
  if st.session_state.get('open_trades_df') is not None and not st.session_state.open_trades_df.empty:
 
10
  # --- CORRECTED IMPORTS: Moved MACD ---
11
  from ta.volatility import BollingerBands
12
  from ta.momentum import RSIIndicator # Removed MACD from here
13
+ from ta.trend import ADXIndicator, MACD
14
+ from ta.volume import MFIIndicator # Added MFI here
15
  # --- [NEW] ADDED ATR FOR INTELLIGENT EXIT ---
16
  from ta.volatility import AverageTrueRange
17
  # --- [END NEW] ---
 
474
 
475
  # --- 2. Custom Backtesting Engine ---
476
 
477
+ # --- [UPDATED] 9-Factor Version (with MFI & SuperTrend) ---
478
+ def calculate_confidence_score(df, primary_driver,
479
+ use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov,
480
+ use_mfi, use_supertrend,
481
+ rsi_w, vol_w, trend_w, vol_w_val, macd_w, ma_slope_w, markov_w,
482
+ mfi_w, supertrend_w,
483
  bband_params,
484
  best_markov_setup=None):
485
 
 
492
  df['RunUp_Return'] = df['Close'].pct_change(periods=run_up_period)
493
  df['RunUp_State'] = df['RunUp_Return'].apply(lambda x: 'Up' if x > 0 else 'Down')
494
 
495
+ # --- BBand Factor ---
496
  if primary_driver != 'Bollinger Bands' and 'bband_lower' in df.columns:
497
+ bb_weight = 1.0
498
  long_entry_pct = bband_params.get('long_entry_threshold_pct', 0.0)
499
  short_entry_pct = bband_params.get('short_entry_threshold_pct', 0.0)
500
  long_bb_trigger_price = df['bband_lower'] * (1 - long_entry_pct)
501
  short_bb_trigger_price = df['bband_upper'] * (1 + short_entry_pct)
502
  long_score_range = (df['large_ma'] - long_bb_trigger_price).replace(0, np.nan)
503
  short_score_range = (short_bb_trigger_price - df['large_ma']).replace(0, np.nan)
504
+ long_score += ((df['large_ma'] - df['Close']) / long_score_range).clip(0, 1).fillna(0) * bb_weight
505
+ short_score += ((df['Close'] - df['large_ma']) / short_score_range).clip(0, 1).fillna(0) * bb_weight
 
 
 
 
506
 
507
  # Factor 1: RSI
508
+ if primary_driver != 'RSI Crossover' and use_rsi and 'RSI' in df.columns:
509
  long_score += ((30 - df['RSI']) / 30).clip(0, 1).fillna(0) * rsi_w
510
  short_score += ((df['RSI'] - 70) / 30).clip(0, 1).fillna(0) * rsi_w
511
 
512
  # Factor 2: Volatility
513
+ if use_volatility and 'Volatility_p' in df.columns:
 
514
  vol_signal = (df['Volatility_p'] > 0.025).astype(float) * vol_w
515
+ long_score += vol_signal; short_score += vol_signal
 
516
 
517
+ # Factor 3: Trend
518
+ if use_trend and 'SMA_200' in df.columns and 'Close' in df.columns:
519
+ valid_sma = (df['SMA_200'] != 0) & df['SMA_200'].notna()
 
520
  pct_dist = pd.Series(0.0, index=df.index)
521
  pct_dist.loc[valid_sma] = df.loc[valid_sma].apply(lambda row: (row['Close'] - row['SMA_200']) / row['SMA_200'] if row['SMA_200'] != 0 else 0, axis=1)
522
  long_score += (pct_dist / 0.10).clip(0, 1).fillna(0) * trend_w
523
  short_score += (-pct_dist / 0.10).clip(0, 1).fillna(0) * trend_w
524
 
525
  # Factor 4: Volume
526
+ if use_volume and 'Volume_Ratio' in df.columns:
 
527
  vol_spike_signal = ((df['Volume_Ratio'] - 1.75) / 2.25).clip(0, 1).fillna(0) * vol_w_val
528
+ long_score += vol_spike_signal; short_score += vol_spike_signal
 
529
 
530
+ # Factor 5: MACD
531
+ if primary_driver != 'MACD Crossover' and use_macd and 'MACD_line' in df.columns:
 
 
532
  macd_cross_long = (df['MACD_line'].shift(1) < df['MACD_signal'].shift(1)) & (df['MACD_line'] >= df['MACD_signal'])
533
  macd_cross_short = (df['MACD_line'].shift(1) > df['MACD_signal'].shift(1)) & (df['MACD_line'] <= df['MACD_signal'])
534
  long_score += macd_cross_long.astype(float) * macd_w * 0.6
535
  short_score += macd_cross_short.astype(float) * macd_w * 0.6
536
+ long_score += (df['MACD_hist'] > 0).astype(float) * macd_w * 0.4
537
+ short_score += (df['MACD_hist'] < 0).astype(float) * macd_w * 0.4
 
 
538
 
539
  # Factor 6: MA Slope
540
+ if primary_driver != 'MA Slope' and use_ma_slope and 'ma_slope' in df.columns:
 
541
  long_score += (df['ma_slope'] > 0).astype(float) * ma_slope_w
542
  short_score += (df['ma_slope'] < 0).astype(float) * ma_slope_w
543
 
544
  # Factor 7: Markov State
545
  if primary_driver != 'Markov State' and use_markov and best_markov_setup and 'RunUp_State' in df.columns:
546
  strategy = best_markov_setup.get('Strategy')
547
+ if strategy == 'Down -> Up': long_score += (df['RunUp_State'] == 'Down').astype(float) * markov_w
548
+ elif strategy == 'Up -> Up': long_score += (df['RunUp_State'] == 'Up').astype(float) * markov_w
549
+ elif strategy == 'Up -> Down': short_score += (df['RunUp_State'] == 'Up').astype(float) * markov_w
550
+ elif strategy == 'Down -> Down': short_score += (df['RunUp_State'] == 'Down').astype(float) * markov_w
551
+
552
+ # Factor 8: MFI (Money Flow Index) [IMPROVED]
553
+ if use_mfi and 'MFI' in df.columns:
554
+ # Binary Logic: If MFI < 20 (Oversold), give full weight.
555
+ # This fixes the "tiny score" issue.
556
+ long_score += (df['MFI'] < 25).astype(float) * mfi_w
557
+ short_score += (df['MFI'] > 75).astype(float) * mfi_w
558
+
559
+ # Factor 9: SuperTrend [IMPROVED]
560
+ if use_supertrend and 'SuperTrend' in df.columns:
561
+ # Long Score: Price > SuperTrend (Trend is Bullish)
562
+ long_score += (df['Close'] > df['SuperTrend']).astype(float) * supertrend_w
563
+ # Short Score: Price < SuperTrend (Trend is Bearish)
564
+ short_score += (df['Close'] < df['SuperTrend']).astype(float) * supertrend_w
565
 
 
 
566
  return long_score.fillna(0), short_score.fillna(0)
567
 
 
 
568
  # --- FULL REPLACEMENT FOR run_backtest FUNCTION ---
569
  def run_backtest(data, params,
570
+ use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov, use_mfi, use_supertrend, # Added MFI/ST
571
+ rsi_w, vol_w, trend_w, vol_w_val, macd_w, ma_slope_w, markov_w, mfi_w, supertrend_w, # Added MFI/ST weights
572
  use_adx_filter, adx_threshold, rsi_logic,
573
  adx_period=14,
574
  veto_setups_list=None,
 
637
  else:
638
  df['RSI'], df['Volatility_p'], df['ADX'], df['ATR'] = np.nan, np.nan, np.nan, np.nan
639
 
640
+ # --- [NEW] MFI & SUPERTREND CALCULATIONS (Fixed) ---
641
+ if len(df) >= 14:
642
+ # 1. MFI Calculation
643
+ try:
644
+ mfi_ind = MFIIndicator(high=df['High'], low=df['Low'], close=df['Close'], volume=df['Volume'], window=14, fillna=True)
645
+ df['MFI'] = mfi_ind.money_flow_index()
646
+ except Exception: df['MFI'] = 50.0
647
+
648
+ # 2. Proper Recursive SuperTrend Calculation
649
+ try:
650
+ st_period = 10
651
+ st_multiplier = 3.0
652
+
653
+ # Calculate ATR
654
+ high_low = df['High'] - df['Low']
655
+ high_close = np.abs(df['High'] - df['Close'].shift())
656
+ low_close = np.abs(df['Low'] - df['Close'].shift())
657
+ ranges = pd.concat([high_low, high_close, low_close], axis=1)
658
+ true_range = np.max(ranges, axis=1)
659
+ atr = true_range.rolling(st_period).mean().fillna(0)
660
+
661
+ # Basic Bands
662
+ hl2 = (df['High'] + df['Low']) / 2
663
+ basic_upper = hl2 + (st_multiplier * atr)
664
+ basic_lower = hl2 - (st_multiplier * atr)
665
+
666
+ # Initialize Final Bands
667
+ final_upper = basic_upper.copy()
668
+ final_lower = basic_lower.copy()
669
+ trend = np.zeros(len(df), dtype=int)
670
+ supertrend = np.zeros(len(df))
671
+
672
+ # Recursive Loop (Accurate Logic)
673
+ close = df['Close'].values
674
+ bu = basic_upper.values
675
+ bl = basic_lower.values
676
+ fu = final_upper.values
677
+ fl = final_lower.values
678
+
679
+ # 1 = Uptrend, -1 = Downtrend
680
+ curr_trend = 1
681
+
682
+ for i in range(1, len(df)):
683
+ # Calculate Final Upper Band
684
+ if bu[i] < fu[i-1] or close[i-1] > fu[i-1]:
685
+ fu[i] = bu[i]
686
+ else:
687
+ fu[i] = fu[i-1]
688
+
689
+ # Calculate Final Lower Band
690
+ if bl[i] > fl[i-1] or close[i-1] < fl[i-1]:
691
+ fl[i] = bl[i]
692
+ else:
693
+ fl[i] = fl[i-1]
694
+
695
+ # Determine Trend
696
+ if curr_trend == 1 and close[i] < fl[i]:
697
+ curr_trend = -1
698
+ elif curr_trend == -1 and close[i] > fu[i]:
699
+ curr_trend = 1
700
+
701
+ trend[i] = curr_trend
702
+ supertrend[i] = fl[i] if curr_trend == 1 else fu[i]
703
+
704
+ df['SuperTrend'] = supertrend
705
+
706
+ except Exception as e:
707
+ # print(f"ST Error: {e}") # Debug if needed
708
+ df['SuperTrend'] = np.nan
709
+ else:
710
+ df['MFI'], df['SuperTrend'] = 50.0, np.nan
711
+
712
  df['SMA_200'] = df['Close'].rolling(window=200, min_periods=1).mean()
713
 
714
  if 'Volume' in df.columns:
 
738
  raw_long_score, raw_short_score = calculate_confidence_score(df,
739
  primary_driver,
740
  use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov,
741
+ use_mfi, use_supertrend, # <--- NEW
742
  rsi_w, vol_w, trend_w, vol_w_val, macd_w, ma_slope_w, markov_w,
743
+ mfi_w, supertrend_w, # <--- NEW
744
  bband_params_for_score,
745
  best_markov_setup=markov_setup
746
  )
 
827
 
828
  # [SURGICAL PATCH START] --------------------------------------------------
829
  # Check if ALL confidence indicators are disabled
830
+ # CORRECTED: Added use_mfi and use_supertrend to this list so the score isn't ignored when they are On.
831
+ all_indicators_off = not any([
832
+ use_rsi, use_volatility, use_trend, use_volume, use_macd, use_ma_slope, use_markov,
833
+ use_mfi, use_supertrend
834
+ ])
835
 
836
  # If indicators are OFF, we bypass the confidence check (allow raw signal)
837
  # Otherwise, we enforce the confidence threshold as usual
 
1091
  fig.add_trace(go.Scatter(x=df.index, y=df['bband_upper'], mode='lines', name='Upper Band', line=dict(color='gray', width=0.5)))
1092
  fig.add_trace(go.Scatter(x=df.index, y=df['bband_lower'], mode='lines', name='Lower Band', line=dict(color='gray', width=0.5), fill='tonexty', fillcolor='rgba(211,211,211,0.2)'))
1093
 
1094
+ # Unpack Trades Tuple: (Long Entries Log, Long Exits Index, Short Entries Log, Short Exits Index)
1095
  long_entries_log, long_exits, _, _ = trades
1096
+
1097
+ # 1. Plot Long Entries
1098
  if long_entries_log:
1099
  dates = [t['date'] for t in long_entries_log]
1100
  prices = [t['price'] for t in long_entries_log]
1101
  scores = [f"Confidence: {t['confidence']:.0f}%" for t in long_entries_log]
1102
+
1103
+ # Filter out entries that fall outside the dataframe's date range (just in case)
1104
+ valid_points = [(d, p, s) for d, p, s in zip(dates, prices, scores) if d in df.index]
1105
+ if valid_points:
1106
+ v_dates, v_prices, v_scores = zip(*valid_points)
1107
+ fig.add_trace(go.Scatter(x=v_dates, y=v_prices, mode='markers', name='Long Entry',
1108
+ marker=dict(color='green', symbol='triangle-up', size=12),
1109
+ text=v_scores, hoverinfo='text'))
1110
+
1111
+ # 2. Plot Long Exits
1112
+ # Ensure long_exits is not empty and contains valid dates found in df
1113
+ if not long_exits.empty:
1114
+ valid_exits = [date for date in long_exits if date in df.index]
1115
+ if valid_exits:
1116
+ exit_prices = df.loc[valid_exits, 'Close']
1117
+ fig.add_trace(go.Scatter(x=exit_prices.index, y=exit_prices, mode='markers', name='Long Exit',
1118
+ marker=dict(color='darkgreen', symbol='x', size=8)))
1119
+
1120
+ fig.update_layout(title=f'Long Trades for {ticker}', xaxis_title='Date', yaxis_title='Price', legend_title="Indicator")
1121
+ return fig
1122
 
1123
  def generate_short_plot(df, trades, ticker):
1124
  fig = go.Figure()
 
3357
  for key, val in defaults.items():
3358
  if key not in st.session_state: st.session_state[key] = val
3359
 
3360
+ # 4. Helper to update state from widgets (Fixed)
3361
  def update_state():
3362
+ keys = list(st.session_state.keys())
3363
+ for widget_key in keys:
3364
+ if widget_key.startswith('widget_'):
3365
+ main_key = widget_key[len('widget_'):]
3366
+ if main_key in st.session_state:
3367
+ st.session_state[main_key] = st.session_state[widget_key]
3368
 
3369
  st.set_page_config(page_title="Dave's Quant System", page_icon="🔴", layout="wide")
3370
 
 
3561
  st.sidebar.radio("Mode:", ("Analyse Single Ticker", full_list_label), key='run_mode')
3562
 
3563
  if st.session_state.get('run_mode') == "Analyse Single Ticker":
3564
+ if not ticker_list:
3565
+ st.sidebar.warning("No tickers loaded.")
3566
+ else:
3567
+ # Ensure valid selection logic (only reset if invalid/None)
3568
+ if st.session_state.ticker_select not in ticker_list:
3569
+ st.session_state.ticker_select = ticker_list[0]
3570
+
3571
+ try:
3572
+ ticker_index = ticker_list.index(st.session_state.ticker_select)
3573
+ except ValueError:
3574
+ ticker_index = 0
3575
+
3576
+ # [FIXED] Removed the unconditional reset line that was here
3577
+ st.sidebar.selectbox("Select a Ticker:", ticker_list, index=ticker_index, key='widget_ticker_select', on_change=update_state )
3578
 
3579
  # --- UPDATED: Date Inputs (Fully Unrestricted 2000-2030) ---
3580
  st.sidebar.date_input(
 
3911
  if 'widget_markov_w' not in st.session_state: st.session_state.widget_markov_w = st.session_state.markov_w
3912
  st.sidebar.number_input("Markov Weight", 0.1, 5.0, step=0.1, key='widget_markov_w', on_change=update_state, disabled=not st.session_state.get('use_markov', True))
3913
 
3914
+ # --- NEW: MFI Controls ---
3915
+ if 'widget_use_mfi' not in st.session_state: st.session_state.widget_use_mfi = st.session_state.use_mfi
3916
+ st.sidebar.toggle("Use Money Flow (MFI)", key='widget_use_mfi', on_change=update_state)
3917
+
3918
+ if 'widget_mfi_w' not in st.session_state: st.session_state.widget_mfi_w = st.session_state.mfi_w
3919
+ st.sidebar.number_input("MFI Weight", 0.1, 5.0, step=0.1, key='widget_mfi_w', on_change=update_state, disabled=not st.session_state.get('use_mfi', True))
3920
+
3921
+ # --- NEW: SuperTrend Controls ---
3922
+ if 'widget_use_supertrend' not in st.session_state: st.session_state.widget_use_supertrend = st.session_state.use_supertrend
3923
+ st.sidebar.toggle("Use SuperTrend", key='widget_use_supertrend', on_change=update_state)
3924
+
3925
+ if 'widget_supertrend_w' not in st.session_state: st.session_state.widget_supertrend_w = st.session_state.supertrend_w
3926
+ st.sidebar.number_input("SuperTrend Weight", 0.1, 5.0, step=0.1, key='widget_supertrend_w', on_change=update_state, disabled=not st.session_state.get('use_supertrend', True))
3927
+
3928
  if 'widget_confidence_slider' not in st.session_state: st.session_state.widget_confidence_slider = st.session_state.confidence_slider
3929
  st.sidebar.slider("Minimum Confidence Threshold (%)", 0, 100, step=5, key='widget_confidence_slider', on_change=update_state)
 
3930
  st.sidebar.markdown("---")
3931
 
3932
  st.sidebar.header("3. Strategy Parameters")
 
4354
 
4355
  # A) Single Ticker Analysis
4356
  if st.session_state.run_mode == "Analyse Single Ticker":
4357
+ # --- [FIX START] Logic integrated from old_but_working_app.py ---
4358
  selected_ticker = st.session_state.get('ticker_select', ticker_list[0] if ticker_list else None)
4359
+ if not selected_ticker:
4360
+ st.error("No ticker selected.")
4361
+ st.stop()
4362
+
4363
+ user_start_date = pd.Timestamp(st.session_state.start_date)
4364
+
4365
  cols_to_use = [selected_ticker]
4366
  if f'{selected_ticker}_High' in master_df.columns: cols_to_use.append(f'{selected_ticker}_High')
4367
  if f'{selected_ticker}_Low' in master_df.columns: cols_to_use.append(f'{selected_ticker}_Low')
4368
  if f'{selected_ticker}_Volume' in master_df.columns: cols_to_use.append(f'{selected_ticker}_Volume')
4369
+
4370
  existing_cols = [col for col in cols_to_use if col in master_df.columns]
4371
+ if selected_ticker not in existing_cols:
4372
+ st.error(f"Ticker '{selected_ticker}' not found.")
4373
+ st.stop()
4374
+
4375
+ # We use the full dataframe here (without date slicing) to allow run_backtest to handle warm-up
4376
+ data_for_backtest = master_df.loc[:, existing_cols].copy()
4377
+
4378
  rename_dict = {selected_ticker: 'Close', f'{selected_ticker}_High': 'High', f'{selected_ticker}_Low': 'Low', f'{selected_ticker}_Volume': 'Volume'}
4379
  rename_dict_filtered = {k: v for k, v in rename_dict.items() if k in existing_cols}
4380
  data_for_backtest = data_for_backtest.rename(columns=rename_dict_filtered)
4381
+
4382
  if not data_for_backtest.empty and 'Close' in data_for_backtest.columns and not data_for_backtest['Close'].isna().all():
4383
+ # --- [FIX END] ---
4384
+
4385
+ # --- CORRECTED CALL (Matches Analyse Full List) ---
4386
  long_pnl, short_pnl, avg_long_trade, avg_short_trade, results_df, trades, open_trades, trade_counts, durations, trade_dates, exit_breakdown = run_backtest(
4387
+ data_for_backtest, manual_params,
4388
+ # 1. Toggles (Added mfi and supertrend)
4389
+ st.session_state.use_rsi, st.session_state.use_vol, st.session_state.use_trend, st.session_state.use_volume,
4390
+ st.session_state.use_macd, st.session_state.use_ma_slope, st.session_state.use_markov,
4391
+ st.session_state.use_mfi, st.session_state.use_supertrend,
4392
+ # 2. Weights (Added mfi and supertrend)
4393
+ st.session_state.rsi_w, st.session_state.vol_w, st.session_state.trend_w, st.session_state.volume_w,
4394
+ st.session_state.macd_w, st.session_state.ma_slope_w, st.session_state.markov_w,
4395
+ st.session_state.mfi_w, st.session_state.supertrend_w,
4396
+ # 3. Settings
4397
+ st.session_state.use_adx_filter, st.session_state.adx_threshold,
4398
+ st.session_state.get('rsi_logic', 'Crossover'),
4399
+ st.session_state.adx_period,
4400
+ veto_setups_list=veto_list_to_use,
4401
+ primary_driver=st.session_state.primary_driver,
4402
+ markov_setup=markov_setup_to_use,
4403
+ exit_logic_type=exit_logic,
4404
+ exit_confidence_threshold=exit_thresh,
4405
+ smart_trailing_stop_pct=smart_trailing_stop,
4406
+ smart_exit_atr_period=smart_atr_p,
4407
+ smart_exit_atr_multiplier=smart_atr_m,
4408
+ intelligent_tsl_pct=intelligent_tsl,
4409
+ # 4. Start Date Filter (Crucial for matching warmup logic)
4410
+ analysis_start_date=user_start_date
4411
+ )
4412
  st.session_state.single_ticker_results = {"long_pnl": long_pnl, "short_pnl": short_pnl, "avg_long_trade": avg_long_trade, "avg_short_trade": avg_short_trade, "results_df": results_df, "trades": trades}
4413
  st.session_state.open_trades_df = pd.DataFrame(open_trades) if open_trades else pd.DataFrame()
4414
  st.session_state.exit_breakdown_totals = {'long_profit_take_count': exit_breakdown[0], 'long_tsl_count': exit_breakdown[1], 'long_time_exit_count': exit_breakdown[2], 'short_profit_take_count': exit_breakdown[3], 'short_tsl_count': exit_breakdown[4], 'short_time_exit_count': exit_breakdown[5]}
 
4440
  ticker_data_series = ticker_data_series.rename(columns={k:v for k,v in rename_dict.items() if k in existing_cols})
4441
 
4442
  if not ticker_data_series.empty and 'Close' in ticker_data_series.columns and not ticker_data_series['Close'].isna().all():
4443
+ long_pnl, short_pnl, avg_long_trade, avg_short_trade, results_df, trades, open_trades, trade_counts, durations, trade_dates, exit_breakdown = run_backtest(
4444
+ ticker_data_series, manual_params, # <--- THIS WAS THE ERROR SOURCE
4445
  st.session_state.use_rsi, st.session_state.use_vol, st.session_state.use_trend, st.session_state.use_volume,
4446
+ st.session_state.use_macd, st.session_state.use_ma_slope, st.session_state.use_markov,
4447
+ st.session_state.use_mfi, st.session_state.use_supertrend, # Added MFI/ST
4448
  st.session_state.rsi_w, st.session_state.vol_w, st.session_state.trend_w, st.session_state.volume_w,
4449
+ st.session_state.macd_w, st.session_state.ma_slope_w, st.session_state.markov_w,
4450
+ st.session_state.mfi_w, st.session_state.supertrend_w, # Added MFI/ST weights
4451
  st.session_state.use_adx_filter, st.session_state.adx_threshold, st.session_state.get('rsi_logic', 'Crossover'), st.session_state.adx_period,
4452
  veto_setups_list=veto_list_to_use, primary_driver=st.session_state.primary_driver,
4453
  markov_setup=markov_setup_to_use, exit_logic_type=exit_logic, exit_confidence_threshold=exit_thresh,
4454
  smart_trailing_stop_pct=smart_trailing_stop, smart_exit_atr_period=smart_atr_p,
4455
  smart_exit_atr_multiplier=smart_atr_m, intelligent_tsl_pct=intelligent_tsl,
4456
+ analysis_start_date=user_start_date
4457
  )
4458
 
4459
  if abs(long_pnl) > PROFIT_THRESHOLD or abs(short_pnl) > PROFIT_THRESHOLD or \
 
4603
  if st.button("💾 Add these settings to User-Defined List", key="save_setup_from_analysis", on_click=add_setup_to_user_list): pass
4604
 
4605
  # 5. Open Positions Table (With Filter)
4606
+ st.subheader("👨🏻‍💼 Open Positions & Recently Closed",
4607
  help="This table displays all currently ACTIVE trades, plus any trades that closed within the last 30 days.")
4608
 
4609
  if st.session_state.get('open_trades_df') is not None and not st.session_state.open_trades_df.empty: