Let's assume a stock at time $t$ is worth $X(t)$. If the returns of $X(t)$ are i.i.d. and normally distributed,the probability of $X(t)$ hitting a value $H>X(t)$ before $L<X(t)$ is $\frac{H-X(t)}{H-L}$ (not considering exchange fees and the cost of shorting).
Now, I ran a python simulation on a stochastic process with normal i.i.d returns (price diffs) and obtained results that seem inconsistent with my assumptions about the probabilities mentioned above. I took the folling steps in the simulation:
- obtain a simulated time series of prices using the "fake_stock" function.
- upsample the price data to 5 minute bars.
- obtain the high, low, and close of each 5 minute bar.
- calculate the close to high percentage (ch) of each 5 minute bar. $ch = \frac{high-close}{high-low}, 0 <=ch <= 1 $. In theory, $ch$ denotes the probability of $X(t)$ hitting the high after the low, correct? This is where the inconsistency lies.
- run 'high_after_low' to see if $H$ was hit after $L$.
- based on step five, I obtain a dataframe of close_to_highs and a boolean showing if they hit their respective highs before lows.
def fake_stock():
'''returns a time series of prices based on gaussian returns'''
dti = pd.date_range(start="2018-01-01 17:00", end="2019-01-01 16:00", freq="S")
returns = np.random.normal(0, 1, len(dti))
df = pd.DataFrame(index=dti, columns=["price"])
df["price"] = returns
df = df.between_time("17:00", "16:00")
df["price"] = df["price"].cumsum()
df["price"] = df["price"] - df["price"].min()
return df
def upsample(df, freq="5T"):
'''upsample second bars to minutes. obtain close, high, and low of each bar'''
upsampled_df = pd.DataFrame()
upsampled_df.loc[:, "high"] = df["price"].resample(freq, label="left", closed="left").max()
upsampled_df.loc[:, "low"] = df["price"].resample(freq, label="left", closed="left").min()
upsampled_df.loc[:, "close"] = df["price"].resample(freq, label="left", closed="left").last()
upsampled_df.loc[:, "close_to_high"] = (upsampled_df["high"]-upsampled_df["close"])/(upsampled_df["high"]-upsampled_df["low])
return upsampled
def high_after_low_apply(row, minute_df):
'''returns True if high was reached after low, False otherwise'''
minute_df_after = minute_df.loc[row.end_idx:]
first_highs = (minute_df_after.ge(row.high))
first_lows = (minute_df_after.le(row.low))
if ((len(first_highs) == 0) & (len(first_lows) == 0)):
return None
elif (len(first_highs) == 0):
return True
elif (len(first_lows) == 0):
return False
return first_highs.idxmax() > first_lows.idxmax()
Now, we are ready to run the simulation.
df = fake_stock()
upsampled_df = upsample(df)
upsampled_df.loc[:,"end_idx"]=upsampled_df.index+pd.Timedelta(minutes=5)
upsampled_df.loc[:,"high_after_low"] = upsampled_df.apply(high_after_low_apply, axis=1, args=(df,))
upsampled_df = pd.sort_values(upsampled_df, by="close_to_high")
Now we have the upsampled_df dataframe sorted by close_to_high values. Let's drop close_to_high values that are 0 or 1.
upsampled_df = upsampled_df.loc[(upsampled_df["high_after_low"]!=0)&(upsampled_df["high_after_low"]!=1)]
Now, I can calculate the historic probabilities of hitting high after low using a rolling window.
probs = (upsampled_df["close_to_high"]*upsampled_df["high_after_low]).rolling(1000).sum()/upsampled_df["close_to_high"].rolling(1000).sum()
when I compare probs to the average values of high_after_low, I get inconsistent results. Namely, for small close_to_high values the probability of hitting the high after the low is larger than theory suggests and vice versa.
What is the explanation for this inconsistency?
(H-X(t))/(H-L)
assumption is correct? It would work by symmetry if X(t) were the average of H and L, but I don't see it working otherwise. At the very least, you'd need to apply the normal's pdf? Did you mean uniformly distributed? $\endgroup$