Spaces:
Running
Running
Upload 3 files
Browse files- app.py +247 -132
- 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":
|
| 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.
|
| 63 |
-
"
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 844 |
-
long_entry_trigger &=
|
| 845 |
-
short_entry_trigger &=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 902 |
-
intelligent_tsl_pct_short = intelligent_tsl_pct
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 1320 |
-
histnorm=''
|
| 1321 |
))
|
| 1322 |
|
| 1323 |
# 2. Short Distribution
|
| 1324 |
-
if 'Avg Short Profit per Trade' in summary_df.columns:
|
| 1325 |
-
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 1341 |
xaxis_title="Average Profit per Trade (decimal, e.g. 0.01 = 1%)",
|
| 1342 |
-
yaxis_title="Number of
|
| 1343 |
-
barmode='overlay',
|
| 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 |
-
#
|
| 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 |
-
|
| 2336 |
-
scan_rsi_w, scan_vol_w, scan_trend_w, scan_volume_w,
|
| 2337 |
-
|
| 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 |
-
|
| 2344 |
-
|
| 2345 |
-
|
| 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="
|
| 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("🔴 🚀
|
| 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 |
-
|
| 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 |
-
|
| 3653 |
-
|
| 3654 |
-
|
| 3655 |
-
|
| 3656 |
-
|
| 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.
|
| 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"
|
| 3763 |
|
| 3764 |
-
|
| 3765 |
-
|
| 3766 |
-
|
| 3767 |
-
|
| 3768 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 4376 |
-
|
| 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 |
-
|
| 4386 |
-
|
| 4387 |
-
|
| 4388 |
-
|
| 4389 |
-
|
| 4390 |
-
|
| 4391 |
-
|
| 4392 |
-
|
| 4393 |
-
|
| 4394 |
-
|
| 4395 |
-
|
| 4396 |
-
|
| 4397 |
-
|
| 4398 |
-
|
| 4399 |
-
|
| 4400 |
-
|
| 4401 |
-
|
| 4402 |
-
|
| 4403 |
-
|
| 4404 |
-
|
| 4405 |
-
|
| 4406 |
-
|
| 4407 |
-
|
| 4408 |
-
|
| 4409 |
-
|
| 4410 |
-
|
| 4411 |
-
|
| 4412 |
-
|
| 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 |
-
#
|
| 4437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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,
|
| 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,
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 4755 |
-
fig.add_trace(go.Histogram(x=
|
|
|
|
| 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.
|
| 5 |
-
"confidence_threshold":
|
| 6 |
"long_entry_threshold_pct": 0.01,
|
| 7 |
-
"long_exit_ma_threshold_pct": 0.
|
| 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.
|
| 16 |
"rsi_logic": "Level",
|
| 17 |
"primary_driver": "Bollinger Bands",
|
| 18 |
"exit_logic_type": "Intelligent (ADX/MACD/ATR)",
|
| 19 |
-
"use_ma_floor_filter":
|
| 20 |
-
"exit_confidence_threshold":
|
| 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.
|
|
|
|
|
|
|
| 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.
|
| 31 |
"use_adx_filter": false,
|
| 32 |
-
"adx_threshold":
|
| 33 |
"adx_period": 14,
|
| 34 |
-
"use_macd":
|
| 35 |
"macd_w": 2.0,
|
| 36 |
"use_ma_slope": false,
|
| 37 |
-
"ma_slope_w":
|
| 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,
|