Daveabc12 commited on
Commit
ed312bf
·
verified ·
1 Parent(s): d55ad86

Upload 3 files

Browse files
Files changed (2) hide show
  1. app.py +247 -132
  2. config.json +13 -11
app.py CHANGED
@@ -55,12 +55,13 @@ def load_settings():
55
  "primary_driver": "Bollinger Bands",
56
 
57
  "exit_logic_type": "Intelligent (ADX/MACD/ATR)",
58
- "exit_confidence_threshold": 50,
59
- "smart_trailing_stop_pct": 5.0,
60
  "smart_exit_atr_period": 14,
61
  "smart_exit_atr_multiplier": 3.0,
62
- "intelligent_tsl_pct": 0.4,
63
- "use_ma_floor_filter": False,
 
 
64
 
65
  "use_vol": False, "vol_w": 0.5,
66
  "use_trend": False, "trend_w": 2.0,
@@ -579,10 +580,12 @@ def run_backtest(data, params,
579
  smart_trailing_stop_pct=0.05,
580
  long_score_95_percentile=None,
581
  short_score_95_percentile=None,
 
582
  smart_exit_atr_period=14,
583
  smart_exit_atr_multiplier=3.0,
584
  intelligent_tsl_pct=1.0,
585
- analysis_start_date=None):
 
586
 
587
  df = data.copy()
588
  required_cols = ['Close']
@@ -747,16 +750,19 @@ def run_backtest(data, params,
747
 
748
  if long_score_95_percentile is None:
749
  long_scores_gt_zero = raw_long_score[raw_long_score > 0]
750
- long_95 = long_scores_gt_zero.quantile(0.95) if not long_scores_gt_zero.empty else 1.0
 
751
  else:
752
  long_95 = long_score_95_percentile
753
 
754
  if short_score_95_percentile is None:
755
  short_scores_gt_zero = raw_short_score[raw_short_score > 0]
756
- short_95 = short_scores_gt_zero.quantile(0.95) if not short_scores_gt_zero.empty else 1.0
 
757
  else:
758
  short_95 = short_score_95_percentile
759
 
 
760
  df['long_confidence_score'] = (raw_long_score / long_95 * 100).clip(0, 100) if long_95 > 0 else 0.0
761
  df['short_confidence_score'] = (raw_short_score / short_95 * 100).clip(0, 100) if short_95 > 0 else 0.0
762
 
@@ -839,10 +845,16 @@ def run_backtest(data, params,
839
  short_entry_trigger = base_short_trigger & adx_allows_entry & ((df['short_confidence_score'] >= params['confidence_threshold']) | all_indicators_off) & ma_is_valid
840
  # [SURGICAL PATCH END] ----------------------------------------------------
841
 
 
842
  if analysis_start_date is not None:
843
- date_mask = df.index >= pd.Timestamp(analysis_start_date)
844
- long_entry_trigger &= date_mask
845
- short_entry_trigger &= date_mask
 
 
 
 
 
846
 
847
  if apply_veto:
848
  long_entry_trigger &= ~df['any_long_veto_trigger']
@@ -898,35 +910,57 @@ def run_backtest(data, params,
898
  standard_tsl_pct_long = params.get('long_trailing_stop_loss_pct', 0)
899
  standard_tsl_pct_short = params.get('short_trailing_stop_loss_pct', 0)
900
 
901
- intelligent_tsl_pct_long = intelligent_tsl_pct * 0.60 if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else 0.0
902
- intelligent_tsl_pct_short = intelligent_tsl_pct * 0.60 if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else 0.0
903
 
904
  long_tsl_exit = pd.Series(False, index=df.index); short_tsl_exit = pd.Series(False, index=df.index)
905
 
906
  if primary_driver != 'Markov State':
907
- # LONG TSL
908
  has_hit_target = potential_long_price_exit
 
909
  tsl_to_use_long = np.where(has_hit_target, intelligent_tsl_pct_long, standard_tsl_pct_long) if use_ma_floor_filter else (intelligent_tsl_pct_long if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else standard_tsl_pct_long)
910
  if not isinstance(tsl_to_use_long, pd.Series): tsl_to_use_long = pd.Series(tsl_to_use_long, index=df.index)
911
 
 
912
  if (tsl_to_use_long > 0).any():
913
  in_long_trade = (df['long_signal'].ffill().fillna(0) == 1)
914
  long_high_water_mark = df['High'].where(in_long_trade).groupby((~in_long_trade).cumsum()).cummax()
915
  tsl_from_hwm = long_high_water_mark * (1 - tsl_to_use_long)
916
- long_tsl_price = np.maximum(tsl_from_hwm, long_breakeven_floor) if use_ma_floor_filter else tsl_from_hwm
 
 
 
 
 
 
 
 
 
917
  long_tsl_exit = in_long_trade & (df['Close'] < long_tsl_price)
918
  df.loc[long_tsl_exit, 'long_signal'] = 0
919
 
920
- # SHORT TSL
921
  has_hit_target = potential_short_price_exit
 
922
  tsl_to_use_short = np.where(has_hit_target, intelligent_tsl_pct_short, standard_tsl_pct_short) if use_ma_floor_filter else (intelligent_tsl_pct_short if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else standard_tsl_pct_short)
923
  if not isinstance(tsl_to_use_short, pd.Series): tsl_to_use_short = pd.Series(tsl_to_use_short, index=df.index)
924
 
 
925
  if (tsl_to_use_short > 0).any():
926
  in_short_trade = (df['short_signal'].ffill().fillna(0) == -1)
927
  short_low_water_mark = df['Low'].where(in_short_trade).groupby((~in_short_trade).cumsum()).cummin()
928
  tsl_from_lwm = short_low_water_mark * (1 + tsl_to_use_short)
929
- short_tsl_price = np.minimum(tsl_from_lwm, short_breakeven_floor) if use_ma_floor_filter else tsl_from_lwm
 
 
 
 
 
 
 
 
 
930
  short_tsl_exit = in_short_trade & (df['Close'] > short_tsl_price)
931
  df.loc[short_tsl_exit, 'short_signal'] = 0
932
 
@@ -1300,36 +1334,50 @@ def display_summary_analytics(summary_df):
1300
  def generate_profit_distribution_chart(summary_df):
1301
  """
1302
  Creates two histograms showing the distribution of Average Profit per Trade
1303
- for Long and Short strategies across all tickers.
 
 
 
1304
  """
1305
  if summary_df is None or summary_df.empty: return None
1306
 
1307
  fig = go.Figure()
1308
 
1309
  # 1. Long Distribution
1310
- if 'Avg Long Profit per Trade' in summary_df.columns:
1311
  # Filter out tickers that didn't trade
1312
- long_data = summary_df[summary_df['Num Long Trades'] > 0]['Avg Long Profit per Trade']
 
 
 
1313
  if not long_data.empty:
1314
  fig.add_trace(go.Histogram(
1315
  x=long_data,
 
 
1316
  name='Long Strategy',
1317
  marker_color='green',
1318
  opacity=0.7,
1319
- nbinsx=50, # Granularity
1320
- histnorm='' # Raw count
1321
  ))
1322
 
1323
  # 2. Short Distribution
1324
- if 'Avg Short Profit per Trade' in summary_df.columns:
1325
- short_data = summary_df[summary_df['Num Short Trades'] > 0]['Avg Short Profit per Trade']
 
 
 
1326
  if not short_data.empty:
1327
  fig.add_trace(go.Histogram(
1328
  x=short_data,
 
 
1329
  name='Short Strategy',
1330
  marker_color='red',
1331
  opacity=0.7,
1332
- nbinsx=50
 
1333
  ))
1334
 
1335
  # 3. Add Zero Line
@@ -1337,10 +1385,10 @@ def generate_profit_distribution_chart(summary_df):
1337
 
1338
  # 4. Layout
1339
  fig.update_layout(
1340
- title="Distribution of Average Profit per Trade (Per Ticker)",
1341
  xaxis_title="Average Profit per Trade (decimal, e.g. 0.01 = 1%)",
1342
- yaxis_title="Number of Tickers",
1343
- barmode='overlay', # Overlap them slightly
1344
  bargap=0.1,
1345
  template="plotly_white",
1346
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
@@ -2328,23 +2376,19 @@ def run_advisor_scan(main_df, setups_to_run, advisor_type="Advisor"):
2328
 
2329
  if not data_for_scan.empty and 'Close' in data_for_scan.columns and not data_for_scan['Close'].isna().all() and len(data_for_scan) >= current_lookback :
2330
  try:
2331
- # --- FIX APPLIED HERE: Added the 11th underscore ---
2332
  _, _, _, _, _, _, open_trades, _, _, _, _ = run_backtest(
2333
  data_for_scan, params_for_run,
2334
- setup_use_rsi, setup_use_vol, setup_use_trend, setup_use_volume,
2335
- setup_use_macd, setup_use_ma_slope, setup_use_markov,
2336
- scan_rsi_w, scan_vol_w, scan_trend_w, scan_volume_w,
2337
- scan_macd_w, scan_ma_slope_w, scan_markov_w,
2338
- factor_settings['use_adx'], factor_settings['adx_thresh'],
2339
- factor_settings['rsi_logic'],
2340
- factor_settings['adx_period'],
2341
  veto_setups_list=None,
2342
- primary_driver=factor_settings['primary_driver'],
2343
- markov_setup=factor_settings['markov_setup'],
2344
- exit_logic_type=factor_settings['exit_logic'],
2345
- exit_confidence_threshold=factor_settings['exit_thresh'],
2346
- smart_trailing_stop_pct=factor_settings['smart_trailing_stop'],
2347
- smart_exit_atr_period=factor_settings['smart_exit_atr_period'],
2348
  smart_exit_atr_multiplier=factor_settings['smart_exit_atr_multiplier'],
2349
  intelligent_tsl_pct=factor_settings['intelligent_tsl_pct']
2350
  )
@@ -3366,7 +3410,7 @@ def main():
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
 
3371
  # --- [FIX 1: Robust Initialization] ---
3372
  # We check for 'run_mode' explicitly to fix the crash if session state is stale
@@ -3506,7 +3550,7 @@ def main():
3506
  ticker_list = st.session_state.ticker_list
3507
 
3508
  # --- HEADER / GREETING ---
3509
- st.title("🔴 🚀 Dave's Quant system")
3510
 
3511
  current_hour = datetime.now().hour
3512
  if 5 <= current_hour < 12: greeting = "Good morning!"
@@ -3551,6 +3595,29 @@ def main():
3551
  div[data-testid="stSidebar"] div[data-testid="stButton"] button { width: 100%; }
3552
  </style>""", unsafe_allow_html=True)
3553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3554
 
3555
  # --- SIDEBAR SECTION 1: Select Test Mode & Dates (Common to both) ---
3556
  st.sidebar.header("1. Select Test Mode & Dates")
@@ -3594,6 +3661,39 @@ def main():
3594
  on_change=update_state
3595
  )
3596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3597
  st.sidebar.markdown("---")
3598
 
3599
  # --- PRIMARY TRIGGER (Hidden in Production, Forced to BBands) ---
@@ -3615,23 +3715,9 @@ def main():
3615
 
3616
  if production_mode:
3617
  # --- PRODUCTION BUTTONS ---
3618
-
3619
- # 1. Run Analysis Button
3620
- if st.sidebar.button("Run Analysis", type="primary", key="run_analysis_prod",
3621
- help="Runs the test using the manual settings currently visible in Sections 2 & 3 below."):
3622
- st.session_state.run_analysis_button = True
3623
- st.session_state.run_advanced_advisor = False
3624
- st.session_state.run_user_advisor_setup = False
3625
- st.session_state.run_scan_user_setups = False
3626
- st.session_state.advisor_df = None; st.session_state.raw_df = None; st.session_state.deduped_df = None
3627
- st.session_state.param_results_df = None; st.session_state.confidence_results_df = None
3628
- st.session_state.summary_df = None; st.session_state.single_ticker_results = None
3629
- st.session_state.open_trades_df = None; st.session_state.last_run_stats = None
3630
- st.session_state.markov_results_df = None
3631
- st.rerun()
3632
 
3633
- st.sidebar.markdown("---")
3634
-
3635
  # 2. Dropdown for User Setups (With "Default" option)
3636
  user_setups_raw = st.session_state.get("user_setups_data", [])
3637
  valid_user_setups = [s for s in user_setups_raw if not is_row_blank(s)]
@@ -3649,12 +3735,11 @@ def main():
3649
  label = f"Setup {i+1}{note_display}"
3650
  setup_options[label] = i
3651
 
3652
- selected_setup_label = st.sidebar.selectbox("Select User Setup:", list(setup_options.keys()))
3653
-
3654
- # 3. Run User-Selected Option Button
3655
- if st.sidebar.button("Run User-Selected Option", type="primary",
3656
- help="Runs the specific setup selected in the dropdown above, overriding manual settings."):
3657
- idx = setup_options[selected_setup_label]
3658
 
3659
  def sync_param(main_key, value):
3660
  st.session_state[main_key] = value
@@ -3683,7 +3768,6 @@ def main():
3683
  # Reset ADX (With Clamping for Production Mode)
3684
  sync_param('use_adx_filter', defaults.get('use_adx_filter', True))
3685
  raw_adx = defaults.get('adx_threshold', 25.0)
3686
- # FIX: Clamp value to be within 20-30 range to prevent widget crash
3687
  clamped_adx = max(20.0, min(30.0, raw_adx))
3688
  sync_param('adx_threshold', clamped_adx)
3689
 
@@ -3703,7 +3787,7 @@ def main():
3703
  sync_param('short_sl', defaults.get("short_trailing_stop_loss_pct", 8.0) * 100)
3704
  sync_param('short_delay', defaults.get("short_delay_days", 0))
3705
 
3706
- st.toast("Settings reset to Default. Running Analysis...", icon="🔄")
3707
 
3708
  # --- CASE B: USER SETUP ---
3709
  else:
@@ -3734,7 +3818,6 @@ def main():
3734
  sync_param('use_adx_filter', True)
3735
  try:
3736
  thresh = float(adx_val)
3737
- # FIX: Clamp value here too, in case a User Setup has < 20
3738
  clamped_thresh = max(20.0, min(30.0, thresh))
3739
  sync_param('adx_threshold', clamped_thresh)
3740
  except:
@@ -3759,13 +3842,16 @@ def main():
3759
  sync_param('short_sl', get_num('Short Stop Loss (%)', st.session_state.short_sl, float))
3760
  sync_param('short_delay', get_num('Short Delay (Days)', st.session_state.short_delay, int))
3761
 
3762
- st.toast(f"Loaded {selected_setup_label}. Running Analysis...", icon="✅")
3763
 
3764
- st.session_state.run_analysis_button = True
3765
- st.session_state.run_scan_user_setups = False
3766
- st.session_state.run_user_advisor_setup = False
3767
- st.session_state.advisor_df = None
3768
- st.rerun()
 
 
 
3769
 
3770
  else:
3771
  # --- DEVELOPER BUTTONS (Original) ---
@@ -4162,7 +4248,9 @@ def main():
4162
  "smart_trailing_stop_pct": st.session_state.get('smart_trailing_stop_pct', 5.0),
4163
  "smart_exit_atr_period": st.session_state.get('smart_exit_atr_period', 14),
4164
  "smart_exit_atr_multiplier": st.session_state.get('smart_exit_atr_multiplier', 3.0),
4165
- "intelligent_tsl_pct": st.session_state.intelligent_tsl_pct / 100.0,
 
 
4166
  "use_vol": st.session_state.use_vol, "vol_w": st.session_state.vol_w,
4167
  "use_trend": st.session_state.use_trend, "trend_w": st.session_state.trend_w,
4168
  "use_volume": st.session_state.use_volume, "volume_w": st.session_state.volume_w,
@@ -4359,7 +4447,8 @@ def main():
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]
@@ -4372,9 +4461,25 @@ def main():
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)
@@ -4382,40 +4487,41 @@ def main():
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]}
4415
  else: st.warning("No data for ticker.")
4416
 
4417
  # B) Full List Analysis
4418
  elif st.session_state.run_mode.startswith("Analyse Full List"):
 
 
 
4419
  summary_results, all_open_trades = [], []
4420
  total_long_wins, total_long_losses, total_short_wins, total_short_losses = 0, 0, 0, 0
4421
  all_long_durations = []; all_short_durations = []
@@ -4433,27 +4539,44 @@ def main():
4433
  existing_cols = [col for col in cols_to_use if col in master_df.columns]
4434
  if ticker_symbol not in existing_cols: continue
4435
 
4436
- # Load data with WARMUP
4437
- ticker_data_series = master_df.loc[data_load_start:end_date, existing_cols]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4438
 
4439
  rename_dict = {ticker_symbol: 'Close', f'{ticker_symbol}_High': 'High', f'{ticker_symbol}_Low': 'Low', f'{ticker_symbol}_Volume': 'Volume'}
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 \
@@ -4502,7 +4625,6 @@ def main():
4502
  st.session_state.exit_breakdown_totals = {}
4503
 
4504
  st.session_state.open_trades_df = pd.DataFrame(all_open_trades) if all_open_trades else pd.DataFrame()
4505
- st.rerun()
4506
 
4507
  # 10. Display Advisor Scan Results (raw_df)
4508
  elif 'raw_df' in st.session_state and st.session_state.raw_df is not None:
@@ -4619,7 +4741,8 @@ def main():
4619
  display_open_df = full_df[mask_open | mask_recent].copy()
4620
  # -------------------------------------------------------
4621
 
4622
- display_open_df.sort_values(by=['Status', 'Date Open'], ascending=[True, False], inplace=True)
 
4623
 
4624
  cols_order_manual = ['Ticker', 'Status', 'Final % P/L', 'Side', 'Date Open', 'Date Closed', 'Start Confidence']
4625
  existing_cols_open = [col for col in cols_order_manual if col in display_open_df.columns]
@@ -4714,17 +4837,6 @@ def main():
4714
  run_optimization()
4715
  st.session_state.run_advanced_advisor = False
4716
 
4717
- if st.session_state.run_user_advisor_setup:
4718
- with st.spinner("Running User Setup..."):
4719
- current_params = {k: st.session_state[k] for k in defaults.keys() if k in st.session_state}
4720
- current_params['use_mfi'] = st.session_state.use_mfi
4721
- current_params['mfi_w'] = st.session_state.mfi_w
4722
- current_params['use_supertrend'] = st.session_state.use_supertrend
4723
- current_params['supertrend_w'] = st.session_state.supertrend_w
4724
-
4725
- run_user_defined_setup(current_params)
4726
- st.session_state.run_user_advisor_setup = False
4727
-
4728
  # --- CHART FUNCTION (Must be outside main) ---
4729
  def generate_trades_timeline_histogram(trades_df, start_date, end_date):
4730
  """
@@ -4748,11 +4860,12 @@ def generate_trades_timeline_histogram(trades_df, start_date, end_date):
4748
 
4749
  fig = go.Figure()
4750
 
4751
- # Add Stacked Traces
4752
  fig.add_trace(go.Histogram(x=long_wins['Date Closed'], name='Long Winners', marker_color='green'))
4753
  fig.add_trace(go.Histogram(x=long_loss['Date Closed'], name='Long Losers', marker_color='red'))
4754
- fig.add_trace(go.Histogram(x=short_wins['Date Closed'], name='Short Winners', marker_color='blue'))
4755
- fig.add_trace(go.Histogram(x=short_loss['Date Closed'], name='Short Losers', marker_color='orange'))
 
4756
 
4757
  fig.update_layout(
4758
  barmode='stack',
@@ -4761,7 +4874,9 @@ def generate_trades_timeline_histogram(trades_df, start_date, end_date):
4761
  yaxis_title="Number of Trades",
4762
  height=400,
4763
  template="plotly_white",
4764
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
 
 
4765
  )
4766
  return fig
4767
 
 
55
  "primary_driver": "Bollinger Bands",
56
 
57
  "exit_logic_type": "Intelligent (ADX/MACD/ATR)",
58
+ "exit_confidence_threshold": 40,
 
59
  "smart_exit_atr_period": 14,
60
  "smart_exit_atr_multiplier": 3.0,
61
+ "intelligent_tsl_pct": 0.2,
62
+ "norm_lookback_years": 1,
63
+ "benchmark_rank": 99,
64
+ "use_ma_floor_filter": True,
65
 
66
  "use_vol": False, "vol_w": 0.5,
67
  "use_trend": False, "trend_w": 2.0,
 
580
  smart_trailing_stop_pct=0.05,
581
  long_score_95_percentile=None,
582
  short_score_95_percentile=None,
583
+ benchmark_rank=0.95,
584
  smart_exit_atr_period=14,
585
  smart_exit_atr_multiplier=3.0,
586
  intelligent_tsl_pct=1.0,
587
+ analysis_start_date=None,
588
+ analysis_end_date=None):
589
 
590
  df = data.copy()
591
  required_cols = ['Close']
 
750
 
751
  if long_score_95_percentile is None:
752
  long_scores_gt_zero = raw_long_score[raw_long_score > 0]
753
+ # [FIX] Use the variable 'benchmark_rank' instead of hardcoded 0.95
754
+ long_95 = long_scores_gt_zero.quantile(benchmark_rank) if not long_scores_gt_zero.empty else 1.0
755
  else:
756
  long_95 = long_score_95_percentile
757
 
758
  if short_score_95_percentile is None:
759
  short_scores_gt_zero = raw_short_score[raw_short_score > 0]
760
+ # [FIX] Use the variable 'benchmark_rank' instead of hardcoded 0.95
761
+ short_95 = short_scores_gt_zero.quantile(benchmark_rank) if not short_scores_gt_zero.empty else 1.0
762
  else:
763
  short_95 = short_score_95_percentile
764
 
765
+
766
  df['long_confidence_score'] = (raw_long_score / long_95 * 100).clip(0, 100) if long_95 > 0 else 0.0
767
  df['short_confidence_score'] = (raw_short_score / short_95 * 100).clip(0, 100) if short_95 > 0 else 0.0
768
 
 
845
  short_entry_trigger = base_short_trigger & adx_allows_entry & ((df['short_confidence_score'] >= params['confidence_threshold']) | all_indicators_off) & ma_is_valid
846
  # [SURGICAL PATCH END] ----------------------------------------------------
847
 
848
+ # [FIX] Filter entries by BOTH Start and End date
849
  if analysis_start_date is not None:
850
+ start_mask = df.index >= pd.Timestamp(analysis_start_date)
851
+ long_entry_trigger &= start_mask
852
+ short_entry_trigger &= start_mask
853
+
854
+ if analysis_end_date is not None:
855
+ end_mask = df.index <= pd.Timestamp(analysis_end_date)
856
+ long_entry_trigger &= end_mask
857
+ short_entry_trigger &= end_mask
858
 
859
  if apply_veto:
860
  long_entry_trigger &= ~df['any_long_veto_trigger']
 
910
  standard_tsl_pct_long = params.get('long_trailing_stop_loss_pct', 0)
911
  standard_tsl_pct_short = params.get('short_trailing_stop_loss_pct', 0)
912
 
913
+ intelligent_tsl_pct_long = intelligent_tsl_pct if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else 0.0
914
+ intelligent_tsl_pct_short = intelligent_tsl_pct if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else 0.0
915
 
916
  long_tsl_exit = pd.Series(False, index=df.index); short_tsl_exit = pd.Series(False, index=df.index)
917
 
918
  if primary_driver != 'Markov State':
919
+ # --- LONG TSL ---
920
  has_hit_target = potential_long_price_exit
921
+ # Determine which TSL percentage to use (Standard vs Intelligent)
922
  tsl_to_use_long = np.where(has_hit_target, intelligent_tsl_pct_long, standard_tsl_pct_long) if use_ma_floor_filter else (intelligent_tsl_pct_long if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else standard_tsl_pct_long)
923
  if not isinstance(tsl_to_use_long, pd.Series): tsl_to_use_long = pd.Series(tsl_to_use_long, index=df.index)
924
 
925
+ # Only run calculation if TSL is active (> 0)
926
  if (tsl_to_use_long > 0).any():
927
  in_long_trade = (df['long_signal'].ffill().fillna(0) == 1)
928
  long_high_water_mark = df['High'].where(in_long_trade).groupby((~in_long_trade).cumsum()).cummax()
929
  tsl_from_hwm = long_high_water_mark * (1 - tsl_to_use_long)
930
+
931
+ # [FIX] Apply Catcher floor ONLY if target has been hit (Persistent during trade)
932
+ if use_ma_floor_filter:
933
+ trade_groups = (~in_long_trade).cumsum()
934
+ # [FIX] Cast to float before cummax to avoid Object dtype error
935
+ target_hit_persistent = has_hit_target.where(in_long_trade).astype(float).groupby(trade_groups).cummax().fillna(0).astype(bool)
936
+ long_tsl_price = np.where(target_hit_persistent, np.maximum(tsl_from_hwm, long_breakeven_floor), tsl_from_hwm)
937
+ else:
938
+ long_tsl_price = tsl_from_hwm
939
+
940
  long_tsl_exit = in_long_trade & (df['Close'] < long_tsl_price)
941
  df.loc[long_tsl_exit, 'long_signal'] = 0
942
 
943
+ # --- SHORT TSL ---
944
  has_hit_target = potential_short_price_exit
945
+ # Determine which TSL percentage to use (Standard vs Intelligent)
946
  tsl_to_use_short = np.where(has_hit_target, intelligent_tsl_pct_short, standard_tsl_pct_short) if use_ma_floor_filter else (intelligent_tsl_pct_short if exit_logic_type == 'Intelligent (ADX/MACD/ATR)' else standard_tsl_pct_short)
947
  if not isinstance(tsl_to_use_short, pd.Series): tsl_to_use_short = pd.Series(tsl_to_use_short, index=df.index)
948
 
949
+ # Only run calculation if TSL is active (> 0)
950
  if (tsl_to_use_short > 0).any():
951
  in_short_trade = (df['short_signal'].ffill().fillna(0) == -1)
952
  short_low_water_mark = df['Low'].where(in_short_trade).groupby((~in_short_trade).cumsum()).cummin()
953
  tsl_from_lwm = short_low_water_mark * (1 + tsl_to_use_short)
954
+
955
+ # [FIX] Apply Catcher floor ONLY if target has been hit (Persistent during trade)
956
+ if use_ma_floor_filter:
957
+ trade_groups = (~in_short_trade).cumsum()
958
+ # [FIX] Cast to float before cummax to avoid Object dtype error
959
+ target_hit_persistent = has_hit_target.where(in_short_trade).astype(float).groupby(trade_groups).cummax().fillna(0).astype(bool)
960
+ short_tsl_price = np.where(target_hit_persistent, np.minimum(tsl_from_lwm, short_breakeven_floor), tsl_from_lwm)
961
+ else:
962
+ short_tsl_price = tsl_from_lwm
963
+
964
  short_tsl_exit = in_short_trade & (df['Close'] > short_tsl_price)
965
  df.loc[short_tsl_exit, 'short_signal'] = 0
966
 
 
1334
  def generate_profit_distribution_chart(summary_df):
1335
  """
1336
  Creates two histograms showing the distribution of Average Profit per Trade
1337
+ for Long and Short strategies.
1338
+
1339
+ [UPDATED] Now weighted by 'Num Trades' so the Y-axis shows Total Trades,
1340
+ not just Total Tickers.
1341
  """
1342
  if summary_df is None or summary_df.empty: return None
1343
 
1344
  fig = go.Figure()
1345
 
1346
  # 1. Long Distribution
1347
+ if 'Avg Long Profit per Trade' in summary_df.columns and 'Num Long Trades' in summary_df.columns:
1348
  # Filter out tickers that didn't trade
1349
+ mask = summary_df['Num Long Trades'] > 0
1350
+ long_data = summary_df.loc[mask, 'Avg Long Profit per Trade']
1351
+ long_weights = summary_df.loc[mask, 'Num Long Trades'] # Use trade count as weight
1352
+
1353
  if not long_data.empty:
1354
  fig.add_trace(go.Histogram(
1355
  x=long_data,
1356
+ y=long_weights, # [FIX] Weight by number of trades
1357
+ histfunc='sum', # [FIX] Sum the weights to get Total Trades per bin
1358
  name='Long Strategy',
1359
  marker_color='green',
1360
  opacity=0.7,
1361
+ nbinsx=50,
1362
+ histnorm=''
1363
  ))
1364
 
1365
  # 2. Short Distribution
1366
+ if 'Avg Short Profit per Trade' in summary_df.columns and 'Num Short Trades' in summary_df.columns:
1367
+ mask = summary_df['Num Short Trades'] > 0
1368
+ short_data = summary_df.loc[mask, 'Avg Short Profit per Trade']
1369
+ short_weights = summary_df.loc[mask, 'Num Short Trades'] # Use trade count as weight
1370
+
1371
  if not short_data.empty:
1372
  fig.add_trace(go.Histogram(
1373
  x=short_data,
1374
+ y=short_weights, # [FIX] Weight by number of trades
1375
+ histfunc='sum', # [FIX] Sum the weights
1376
  name='Short Strategy',
1377
  marker_color='red',
1378
  opacity=0.7,
1379
+ nbinsx=50,
1380
+ visible='legendonly'
1381
  ))
1382
 
1383
  # 3. Add Zero Line
 
1385
 
1386
  # 4. Layout
1387
  fig.update_layout(
1388
+ title="Distribution of Average Profit per Trade (Weighted by Trade Count)",
1389
  xaxis_title="Average Profit per Trade (decimal, e.g. 0.01 = 1%)",
1390
+ yaxis_title="Number of Trades", # [FIX] Updated Label
1391
+ barmode='overlay',
1392
  bargap=0.1,
1393
  template="plotly_white",
1394
  legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
 
2376
 
2377
  if not data_for_scan.empty and 'Close' in data_for_scan.columns and not data_for_scan['Close'].isna().all() and len(data_for_scan) >= current_lookback :
2378
  try:
2379
+ # Added missing MFI and SuperTrend arguments (set to False/1.0) ---
2380
  _, _, _, _, _, _, open_trades, _, _, _, _ = run_backtest(
2381
  data_for_scan, params_for_run,
2382
+ setup_use_rsi, setup_use_vol, setup_use_trend, setup_use_volume, setup_use_macd, setup_use_ma_slope, setup_use_markov,
2383
+ False, False, # use_mfi, use_supertrend (Default Off for Advisor scans)
2384
+ scan_rsi_w, scan_vol_w, scan_trend_w, scan_volume_w, scan_macd_w, scan_ma_slope_w, scan_markov_w,
2385
+ 1.0, 1.0, # mfi_w, supertrend_w (Default 1.0)
2386
+ factor_settings['use_adx'], factor_settings['adx_thresh'], factor_settings['rsi_logic'], factor_settings['adx_period'],
 
 
2387
  veto_setups_list=None,
2388
+ primary_driver=factor_settings['primary_driver'], markov_setup=factor_settings['markov_setup'],
2389
+ exit_logic_type=factor_settings['exit_logic'], exit_confidence_threshold=factor_settings['exit_thresh'],
2390
+ smart_trailing_stop_pct=factor_settings['smart_trailing_stop'],
2391
+ smart_exit_atr_period=factor_settings['smart_exit_atr_period'],
 
 
2392
  smart_exit_atr_multiplier=factor_settings['smart_exit_atr_multiplier'],
2393
  intelligent_tsl_pct=factor_settings['intelligent_tsl_pct']
2394
  )
 
3410
  if main_key in st.session_state:
3411
  st.session_state[main_key] = st.session_state[widget_key]
3412
 
3413
+ st.set_page_config(page_title="Quant Trader", page_icon="🔴", layout="wide")
3414
 
3415
  # --- [FIX 1: Robust Initialization] ---
3416
  # We check for 'run_mode' explicitly to fix the crash if session state is stale
 
3550
  ticker_list = st.session_state.ticker_list
3551
 
3552
  # --- HEADER / GREETING ---
3553
+ st.title("🔴 🚀 Quant Trader")
3554
 
3555
  current_hour = datetime.now().hour
3556
  if 5 <= current_hour < 12: greeting = "Good morning!"
 
3595
  div[data-testid="stSidebar"] div[data-testid="stButton"] button { width: 100%; }
3596
  </style>""", unsafe_allow_html=True)
3597
 
3598
+ # --- [MOVED] RUN ANALYSIS BUTTON (PRODUCTION MODE) ---
3599
+ if production_mode:
3600
+ # Note: We do NOT use st.rerun() here. We let the script flow down.
3601
+ if st.sidebar.button("Run Analysis", type="primary", key="run_analysis_prod_top",
3602
+ help="Runs the test using the current settings below."):
3603
+
3604
+ # 1. Force a sync of all widgets to session state
3605
+ update_state()
3606
+
3607
+ # 2. Set the flag to TRUE so the engine at the bottom runs
3608
+ st.session_state.run_analysis_button = True
3609
+
3610
+ # 3. Clear previous results to ensure a fresh report
3611
+ st.session_state.run_advanced_advisor = False
3612
+ st.session_state.run_user_advisor_setup = False
3613
+ st.session_state.run_scan_user_setups = False
3614
+ st.session_state.advisor_df = None; st.session_state.raw_df = None; st.session_state.deduped_df = None
3615
+ st.session_state.param_results_df = None; st.session_state.confidence_results_df = None
3616
+ st.session_state.summary_df = None; st.session_state.single_ticker_results = None
3617
+ st.session_state.open_trades_df = None; st.session_state.last_run_stats = None
3618
+ st.session_state.markov_results_df = None
3619
+ st.sidebar.markdown("---")
3620
+
3621
 
3622
  # --- SIDEBAR SECTION 1: Select Test Mode & Dates (Common to both) ---
3623
  st.sidebar.header("1. Select Test Mode & Dates")
 
3661
  on_change=update_state
3662
  )
3663
 
3664
+ # [NEW] Benchmark Settings Sliders (With Tooltips)
3665
+ st.sidebar.markdown("---")
3666
+ st.sidebar.write("**Benchmark Settings**")
3667
+
3668
+ # 1. Lookback Slider - VISIBLE IN ALL MODES
3669
+ norm_lookback_years = st.sidebar.slider(
3670
+ "Lookback (Yrs)",
3671
+ min_value=1,
3672
+ max_value=30,
3673
+ value=st.session_state.get('norm_lookback_years', 1), # Load from Config
3674
+ help=(
3675
+ "How many years of history to use when calculating the 'Confidence Score' baseline.\n\n"
3676
+ "• **30 Years (Recommended):** Stable, long-term benchmark. Ensures trades are judged against all-time history.\n"
3677
+ "• **1-2 Years:** Adaptive. Judges trades only against recent market volatility (good for changing regimes)."
3678
+ "• **Default is set to 1 year as we are usually most interested in the recent year.")
3679
+ )
3680
+
3681
+ # 2. Grade Benchmark - HIDDEN IN PRODUCTION MODE
3682
+ if not production_mode:
3683
+ benchmark_percentile_setting = st.sidebar.slider(
3684
+ "Grade Benchmark (%)",
3685
+ min_value=50,
3686
+ max_value=99,
3687
+ value=st.session_state.get('benchmark_rank', 99), # Load from Config
3688
+ help=(
3689
+ "Sets the 'Bar' for the Strategy Score.\n\n"
3690
+ "• **99% (Default):** The top 1% of historical trades set the standard for 'Perfection'.\n"
3691
+ "• **80%:** Lowers the bar. Trades only need to be in the top 20% to get a high score (Useful for low volatility).")
3692
+ )
3693
+ else:
3694
+ # In Production, keep the value but hide the slider
3695
+ benchmark_percentile_setting = st.session_state.get('benchmark_rank', 99)
3696
+
3697
  st.sidebar.markdown("---")
3698
 
3699
  # --- PRIMARY TRIGGER (Hidden in Production, Forced to BBands) ---
 
3715
 
3716
  if production_mode:
3717
  # --- PRODUCTION BUTTONS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3718
 
3719
+ # [REMOVED] Old "Run Analysis" button was here.
3720
+
3721
  # 2. Dropdown for User Setups (With "Default" option)
3722
  user_setups_raw = st.session_state.get("user_setups_data", [])
3723
  valid_user_setups = [s for s in user_setups_raw if not is_row_blank(s)]
 
3735
  label = f"Setup {i+1}{note_display}"
3736
  setup_options[label] = i
3737
 
3738
+ # --- CALLBACK: AUTO-POPULATE SETTINGS ON SELECTION ---
3739
+ def on_user_setup_change():
3740
+ """Callback to populate settings immediately when dropdown changes."""
3741
+ selected_label = st.session_state.widget_user_setup_select_key
3742
+ idx = setup_options[selected_label]
 
3743
 
3744
  def sync_param(main_key, value):
3745
  st.session_state[main_key] = value
 
3768
  # Reset ADX (With Clamping for Production Mode)
3769
  sync_param('use_adx_filter', defaults.get('use_adx_filter', True))
3770
  raw_adx = defaults.get('adx_threshold', 25.0)
 
3771
  clamped_adx = max(20.0, min(30.0, raw_adx))
3772
  sync_param('adx_threshold', clamped_adx)
3773
 
 
3787
  sync_param('short_sl', defaults.get("short_trailing_stop_loss_pct", 8.0) * 100)
3788
  sync_param('short_delay', defaults.get("short_delay_days", 0))
3789
 
3790
+ st.toast("Settings reset to Default.", icon="🔄")
3791
 
3792
  # --- CASE B: USER SETUP ---
3793
  else:
 
3818
  sync_param('use_adx_filter', True)
3819
  try:
3820
  thresh = float(adx_val)
 
3821
  clamped_thresh = max(20.0, min(30.0, thresh))
3822
  sync_param('adx_threshold', clamped_thresh)
3823
  except:
 
3842
  sync_param('short_sl', get_num('Short Stop Loss (%)', st.session_state.short_sl, float))
3843
  sync_param('short_delay', get_num('Short Delay (Days)', st.session_state.short_delay, int))
3844
 
3845
+ st.toast(f"Populated settings for {selected_label}", icon="✅")
3846
 
3847
+ # Selectbox with on_change callback
3848
+ st.sidebar.selectbox(
3849
+ "Select User Setup:",
3850
+ list(setup_options.keys()),
3851
+ key="widget_user_setup_select_key",
3852
+ on_change=on_user_setup_change,
3853
+ help="Selecting a setup will immediately populate the sidebar settings below."
3854
+ )
3855
 
3856
  else:
3857
  # --- DEVELOPER BUTTONS (Original) ---
 
4248
  "smart_trailing_stop_pct": st.session_state.get('smart_trailing_stop_pct', 5.0),
4249
  "smart_exit_atr_period": st.session_state.get('smart_exit_atr_period', 14),
4250
  "smart_exit_atr_multiplier": st.session_state.get('smart_exit_atr_multiplier', 3.0),
4251
+ "intelligent_tsl_pct": st.session_state.intelligent_tsl_pct / 100.0,
4252
+ "norm_lookback_years": norm_lookback_years,
4253
+ "benchmark_rank": benchmark_percentile_setting,
4254
  "use_vol": st.session_state.use_vol, "vol_w": st.session_state.vol_w,
4255
  "use_trend": st.session_state.use_trend, "trend_w": st.session_state.trend_w,
4256
  "use_volume": st.session_state.use_volume, "volume_w": st.session_state.volume_w,
 
4447
  if not selected_ticker:
4448
  st.error("No ticker selected.")
4449
  st.stop()
4450
+
4451
+ # Define Start Date for Single Ticker
4452
  user_start_date = pd.Timestamp(st.session_state.start_date)
4453
 
4454
  cols_to_use = [selected_ticker]
 
4461
  st.error(f"Ticker '{selected_ticker}' not found.")
4462
  st.stop()
4463
 
4464
+ # [FIX] Smart Lookback Logic: Uses the wider of (User Range) or (Slider Lookback)
4465
+ user_start = pd.Timestamp(st.session_state.start_date)
4466
+ user_end = pd.Timestamp(st.session_state.end_date)
4467
+
4468
+ # Calculate the slider-based start date
4469
+ slider_start = user_end - pd.DateOffset(years=norm_lookback_years)
4470
+
4471
+ # 1. Logic: Start loading from the EARLIER of the two dates (Wider Window)
4472
+ effective_start = min(user_start, slider_start)
4473
+
4474
+ # 2. Safety: Add 365 days EXTRA buffer so MA-200 is ready on Day 1
4475
+ buffer_start = effective_start - pd.DateOffset(days=365)
4476
+
4477
+ # Ensure we don't go before available data
4478
+ if not master_df.empty:
4479
+ buffer_start = max(buffer_start, master_df.index[0])
4480
+
4481
+ data_for_backtest = master_df.loc[buffer_start:user_end, existing_cols].copy()
4482
+
4483
  rename_dict = {selected_ticker: 'Close', f'{selected_ticker}_High': 'High', f'{selected_ticker}_Low': 'Low', f'{selected_ticker}_Volume': 'Volume'}
4484
  rename_dict_filtered = {k: v for k, v in rename_dict.items() if k in existing_cols}
4485
  data_for_backtest = data_for_backtest.rename(columns=rename_dict_filtered)
 
4487
  if not data_for_backtest.empty and 'Close' in data_for_backtest.columns and not data_for_backtest['Close'].isna().all():
4488
  # --- [FIX END] ---
4489
 
4490
+
4491
+ long_pnl, short_pnl, avg_long_trade, avg_short_trade, results_df, trades, open_trades, trade_counts, durations, trade_dates, exit_breakdown = run_backtest(
4492
+ data_for_backtest, manual_params,
4493
+ st.session_state.use_rsi, st.session_state.use_vol, st.session_state.use_trend, st.session_state.use_volume,
4494
+ st.session_state.use_macd, st.session_state.use_ma_slope, st.session_state.use_markov,
4495
+ st.session_state.use_mfi, st.session_state.use_supertrend,
4496
+ st.session_state.rsi_w, st.session_state.vol_w, st.session_state.trend_w, st.session_state.volume_w,
4497
+ st.session_state.macd_w, st.session_state.ma_slope_w, st.session_state.markov_w,
4498
+ st.session_state.mfi_w, st.session_state.supertrend_w,
4499
+ st.session_state.use_adx_filter, st.session_state.adx_threshold,
4500
+ st.session_state.get('rsi_logic', 'Crossover'),
4501
+ st.session_state.adx_period,
4502
+ veto_setups_list=veto_list_to_use,
4503
+ primary_driver=st.session_state.primary_driver,
4504
+ markov_setup=markov_setup_to_use,
4505
+ exit_logic_type=exit_logic,
4506
+ exit_confidence_threshold=exit_thresh,
4507
+ smart_trailing_stop_pct=smart_trailing_stop,
4508
+ smart_exit_atr_period=smart_atr_p,
4509
+ smart_exit_atr_multiplier=smart_atr_m,
4510
+ intelligent_tsl_pct=intelligent_tsl,
4511
+ benchmark_rank=benchmark_percentile_setting / 100.0,
4512
+ analysis_start_date=user_start_date,
4513
+ analysis_end_date=end_date
4514
+ )
4515
+ 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}
4516
+ st.session_state.open_trades_df = pd.DataFrame(open_trades) if open_trades else pd.DataFrame()
4517
+ 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]}
 
 
4518
  else: st.warning("No data for ticker.")
4519
 
4520
  # B) Full List Analysis
4521
  elif st.session_state.run_mode.startswith("Analyse Full List"):
4522
+ # [FIX] Define Start Date for Full List (Critical Fix)
4523
+ user_start_date = pd.Timestamp(st.session_state.start_date)
4524
+
4525
  summary_results, all_open_trades = [], []
4526
  total_long_wins, total_long_losses, total_short_wins, total_short_losses = 0, 0, 0, 0
4527
  all_long_durations = []; all_short_durations = []
 
4539
  existing_cols = [col for col in cols_to_use if col in master_df.columns]
4540
  if ticker_symbol not in existing_cols: continue
4541
 
4542
+ # [FIX] Smart Lookback Logic: Uses the wider of (User Range) or (Slider Lookback)
4543
+ user_start = pd.Timestamp(st.session_state.start_date)
4544
+ user_end = pd.Timestamp(st.session_state.end_date)
4545
+
4546
+ slider_start = user_end - pd.DateOffset(years=norm_lookback_years)
4547
+
4548
+ # 1. Logic: Start loading from the EARLIER of the two dates
4549
+ effective_start = min(user_start, slider_start)
4550
+
4551
+ # 2. Safety: Add 365 days EXTRA buffer
4552
+ buffer_start = effective_start - pd.DateOffset(days=365)
4553
+
4554
+ if not master_df.empty:
4555
+ buffer_start = max(buffer_start, master_df.index[0])
4556
+
4557
+ ticker_data_series = master_df.loc[buffer_start:user_end, existing_cols]
4558
 
4559
  rename_dict = {ticker_symbol: 'Close', f'{ticker_symbol}_High': 'High', f'{ticker_symbol}_Low': 'Low', f'{ticker_symbol}_Volume': 'Volume'}
4560
  ticker_data_series = ticker_data_series.rename(columns={k:v for k,v in rename_dict.items() if k in existing_cols})
4561
 
4562
+
4563
  if not ticker_data_series.empty and 'Close' in ticker_data_series.columns and not ticker_data_series['Close'].isna().all():
4564
  long_pnl, short_pnl, avg_long_trade, avg_short_trade, results_df, trades, open_trades, trade_counts, durations, trade_dates, exit_breakdown = run_backtest(
4565
+ ticker_data_series, manual_params,
4566
  st.session_state.use_rsi, st.session_state.use_vol, st.session_state.use_trend, st.session_state.use_volume,
4567
  st.session_state.use_macd, st.session_state.use_ma_slope, st.session_state.use_markov,
4568
+ st.session_state.use_mfi, st.session_state.use_supertrend,
4569
  st.session_state.rsi_w, st.session_state.vol_w, st.session_state.trend_w, st.session_state.volume_w,
4570
  st.session_state.macd_w, st.session_state.ma_slope_w, st.session_state.markov_w,
4571
+ st.session_state.mfi_w, st.session_state.supertrend_w,
4572
  st.session_state.use_adx_filter, st.session_state.adx_threshold, st.session_state.get('rsi_logic', 'Crossover'), st.session_state.adx_period,
4573
  veto_setups_list=veto_list_to_use, primary_driver=st.session_state.primary_driver,
4574
  markov_setup=markov_setup_to_use, exit_logic_type=exit_logic, exit_confidence_threshold=exit_thresh,
4575
  smart_trailing_stop_pct=smart_trailing_stop, smart_exit_atr_period=smart_atr_p,
4576
  smart_exit_atr_multiplier=smart_atr_m, intelligent_tsl_pct=intelligent_tsl,
4577
+ benchmark_rank=benchmark_percentile_setting / 100.0,
4578
+ analysis_start_date=user_start_date,
4579
+ analysis_end_date=end_date
4580
  )
4581
 
4582
  if abs(long_pnl) > PROFIT_THRESHOLD or abs(short_pnl) > PROFIT_THRESHOLD or \
 
4625
  st.session_state.exit_breakdown_totals = {}
4626
 
4627
  st.session_state.open_trades_df = pd.DataFrame(all_open_trades) if all_open_trades else pd.DataFrame()
 
4628
 
4629
  # 10. Display Advisor Scan Results (raw_df)
4630
  elif 'raw_df' in st.session_state and st.session_state.raw_df is not None:
 
4741
  display_open_df = full_df[mask_open | mask_recent].copy()
4742
  # -------------------------------------------------------
4743
 
4744
+ # [FIX] Sort strictly by Date Open (Newest First)
4745
+ display_open_df.sort_values(by='Date Open', ascending=False, inplace=True)
4746
 
4747
  cols_order_manual = ['Ticker', 'Status', 'Final % P/L', 'Side', 'Date Open', 'Date Closed', 'Start Confidence']
4748
  existing_cols_open = [col for col in cols_order_manual if col in display_open_df.columns]
 
4837
  run_optimization()
4838
  st.session_state.run_advanced_advisor = False
4839
 
 
 
 
 
 
 
 
 
 
 
 
4840
  # --- CHART FUNCTION (Must be outside main) ---
4841
  def generate_trades_timeline_histogram(trades_df, start_date, end_date):
4842
  """
 
4860
 
4861
  fig = go.Figure()
4862
 
4863
+ # Add Stacked Traces (Shorts hidden by default via visible='legendonly')
4864
  fig.add_trace(go.Histogram(x=long_wins['Date Closed'], name='Long Winners', marker_color='green'))
4865
  fig.add_trace(go.Histogram(x=long_loss['Date Closed'], name='Long Losers', marker_color='red'))
4866
+
4867
+ fig.add_trace(go.Histogram(x=short_wins['Date Closed'], name='Short Winners', marker_color='blue', visible='legendonly'))
4868
+ fig.add_trace(go.Histogram(x=short_loss['Date Closed'], name='Short Losers', marker_color='orange', visible='legendonly'))
4869
 
4870
  fig.update_layout(
4871
  barmode='stack',
 
4874
  yaxis_title="Number of Trades",
4875
  height=400,
4876
  template="plotly_white",
4877
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
4878
+ # [FIX] Force the X-axis range to match the user's selected date settings
4879
+ xaxis=dict(range=[start_date, end_date])
4880
  )
4881
  return fig
4882
 
config.json CHANGED
@@ -1,10 +1,10 @@
1
  {
2
  "large_ma_period": 70,
3
  "bband_period": 32,
4
- "bband_std_dev": 1.399999999999999,
5
- "confidence_threshold": 40,
6
  "long_entry_threshold_pct": 0.01,
7
- "long_exit_ma_threshold_pct": 0.01,
8
  "long_trailing_stop_loss_pct": 0.2,
9
  "long_delay_days": 0,
10
  "short_entry_threshold_pct": 0.0,
@@ -12,29 +12,31 @@
12
  "short_trailing_stop_loss_pct": 0.2,
13
  "short_delay_days": 0,
14
  "use_rsi": true,
15
- "rsi_w": 0.5000000000000001,
16
  "rsi_logic": "Level",
17
  "primary_driver": "Bollinger Bands",
18
  "exit_logic_type": "Intelligent (ADX/MACD/ATR)",
19
- "use_ma_floor_filter": false,
20
- "exit_confidence_threshold": 50,
21
  "smart_trailing_stop_pct": 5.0,
22
  "smart_exit_atr_period": 14,
23
  "smart_exit_atr_multiplier": 3.0,
24
- "intelligent_tsl_pct": 0.4,
 
 
25
  "use_vol": false,
26
  "vol_w": 0.5,
27
  "use_trend": false,
28
  "trend_w": 2.0,
29
  "use_volume": true,
30
- "volume_w": 0.5,
31
  "use_adx_filter": false,
32
- "adx_threshold": 26.0,
33
  "adx_period": 14,
34
- "use_macd": true,
35
  "macd_w": 2.0,
36
  "use_ma_slope": false,
37
- "ma_slope_w": 2.0,
38
  "use_markov": false,
39
  "markov_w": 1.0,
40
  "max_trading_days": 60,
 
1
  {
2
  "large_ma_period": 70,
3
  "bband_period": 32,
4
+ "bband_std_dev": 1.4,
5
+ "confidence_threshold": 95,
6
  "long_entry_threshold_pct": 0.01,
7
+ "long_exit_ma_threshold_pct": 0.0,
8
  "long_trailing_stop_loss_pct": 0.2,
9
  "long_delay_days": 0,
10
  "short_entry_threshold_pct": 0.0,
 
12
  "short_trailing_stop_loss_pct": 0.2,
13
  "short_delay_days": 0,
14
  "use_rsi": true,
15
+ "rsi_w": 0.9999999999999997,
16
  "rsi_logic": "Level",
17
  "primary_driver": "Bollinger Bands",
18
  "exit_logic_type": "Intelligent (ADX/MACD/ATR)",
19
+ "use_ma_floor_filter": true,
20
+ "exit_confidence_threshold": 40,
21
  "smart_trailing_stop_pct": 5.0,
22
  "smart_exit_atr_period": 14,
23
  "smart_exit_atr_multiplier": 3.0,
24
+ "intelligent_tsl_pct": 0.2,
25
+ "norm_lookback_years": 1,
26
+ "benchmark_rank": 99,
27
  "use_vol": false,
28
  "vol_w": 0.5,
29
  "use_trend": false,
30
  "trend_w": 2.0,
31
  "use_volume": true,
32
+ "volume_w": 0.9999999999999999,
33
  "use_adx_filter": false,
34
+ "adx_threshold": 10.0,
35
  "adx_period": 14,
36
+ "use_macd": false,
37
  "macd_w": 2.0,
38
  "use_ma_slope": false,
39
+ "ma_slope_w": 0.5,
40
  "use_markov": false,
41
  "markov_w": 1.0,
42
  "max_trading_days": 60,