├── LICENSE.md ├── MANIFEST.in ├── README.md ├── docs └── images │ ├── logo.png │ └── patterns.png ├── pyproject.toml ├── requirements.txt ├── tests ├── __init__.py └── test_hs.py ├── tradingpatterns ├── __init__.py ├── hard_data.py └── tradingpatterns.py └── update_docs.sh /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Keith Orange 2 | All rights reserved. 3 | 4 | This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) license. 5 | 6 | You are free to: 7 | 8 | - Share: copy and redistribute the material in any medium or format 9 | - Adapt: remix, transform, and build upon the material 10 | 11 | Under the following terms: 12 | 13 | - Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You must do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. The credit should be given in the following format: "Original work by [Preetam Sharma] is licensed under CC BY-NC-SA 4.0 and can be found at [link to the original work]." 14 | 15 | - NonCommercial: You may not use the material for commercial purposes. This includes but is not limited to, selling the material, or using it to promote a product or service. 16 | 17 | - ShareAlike: If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. This ensures that others can continue to use and build upon your work in the same way that you have. 18 | 19 | For more information, see the [Creative Commons website](https://creativecommons.org/licenses/by-nc-sa/4.0/) and the [legal code of the license](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). 20 | 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PatternPy: The Premier Python Package for Trading Pattern Recognition 🔥 2 | ![PatternPy Logo](docs/images/logo.png) 3 | 4 | ## Description 5 | PatternPy is a powerful Python package designed to transform the way you analyze financial markets. Our mission is to make complex trading pattern recognition accessible and efficient for all. With PatternPy, you can effortlessly identify intricate patterns like the head and shoulder, multiple tops and bottoms, horizontal support and resistance, and many more from OHLCV data. 6 | 7 | Empowered by the elegance of Pandas and the efficiency of Numpy, PatternPy delivers high-speed performance without compromising on accuracy or user-friendliness. Whether you are a seasoned trader or a beginner, PatternPy is your go-to tool for bringing precision and speed to your market analysis. 8 | 9 | 10 | 11 | ## Why PatternPy? 12 | Our package stands out for several reasons: 13 | 14 | - **Unique in the Market:** There's nothing else like PatternPy. We provide an all-in-one solution for trading pattern identification, combining power, versatility, and simplicity. 15 | - **High-Speed Performance:** Designed with vectorization concepts, PatternPy processes large volumes of data with impressive speed, allowing you to get the information you need, when you need it. 16 | - **Flexible and Customizable**: You can easily adjust the window size to suit your preferences, offering a balance between sensitivity and false-positive control. 17 | - **Potential for Wealth Creation:** PatternPy is designed to help you recognize lucrative trading opportunities with greater efficiency and accuracy, potentially leading to increased wealth. 18 | 19 | 20 | 21 | ## Installation 22 | 23 | You can install PatternPy by cloning this repo and placing it in your working directory, then importing it like usual: 24 | ``` 25 | git clone https://github.com/keithorange/PatternPy 26 | ``` 27 | ## Usage 28 | Once installed and imported, you use PatternPy as follows: 29 | ``` 30 | from patternpy.tradingpatterns import head_and_shoulders 31 | 32 | # Have price data (OCHLV dataframe) 33 | df = pd.Dataframe(stock_data) 34 | 35 | # Apply pattern indicator screener 36 | df = head_and_shoulders(df) 37 | 38 | # New column `head_shoulder_pattern` is created with entries containing either: NaN, 'Head and Shoulder' or 'Inverse Head and Shoulder' 39 | print(df) 40 | ``` 41 | 42 | See our usage guide for more detailed instructions and examples. 43 | 44 | 45 | 46 | ## 📈 Trading Patterns: The Gearhead's Guide to Chart Alchemy! 🔧 47 | 48 | - **Head & Shoulders** and its Mirror-Twin, Inverse Head & Shoulders: Think of this as the stock market's homage to a medieval warrior's stance. The head - the pinnacle of price prowess. The shoulders - slightly lower, but they pack a punch. When it goes inverse, that’s the stock market moonwalking! Keep an eye, because something's about to give. ⚔️ 49 | - **Multiple Tops & Bottoms** - The Horizontal Tango: When stock prices are doing the cha-cha on the charts, swinging back and forth without breaking out – that’s the Horizontal Tango for you! Put on your dancing shoes because reading this pattern needs finesse and perfect timing. 💃 50 | 51 | - **Horizontal Support & Resistance** - The Price Bouncers: These levels are like the elite bouncers at an exclusive club. Prices need VIP access to get past them! They’ve been rejected entry before, so will they turn around or sweet-talk their way through this time? 🕶️ 52 | 53 | - **Ascending & Descending Triangles** - Tension Rising: These triangles are like a rubber band stretching – the suspense is nerve-wracking. Is it going to snap upwards or fizzle out downwards? This pattern is the market's own thriller genre. 🍿 54 | 55 | - **Wedges**: Converging Destiny: Think of wedges as two trendlines playing a high-stakes game of chicken – speeding towards each other to see who veers off first. When they collide, prices could catapult in any direction. Buckle up! 🚀 56 | 57 | - **Channel Up & Down** - The Stock Superhighway: If stocks were cars, channels would be their autobahns. Unfettered, high-octane movement within defined lanes. Just watch out for those exits - detours might lead to whole new landscapes! 🏎️ 58 | 59 | - **Double Top & Bottom** - The Market's Deja Vu: When prices hit a level, recoil, and then - BOOM - they're back again, it’s like the market is trying to perfect a stunt it couldn't nail the first time. A daring double-attempt before the grand finale! 🎯 60 | 61 | - **Trend Line Support & Resistance** - The Market’s Elders: These lines are like the wise old sages of stock lore. They've seen things, they know things. Their wisdom? A roadmap of where prices found refuge or faced their nemesis. Respect the elders! 🧙 62 | 63 | - **Higher-High & Lower-Low**- The Chart Adventurer's Quest: Grab your explorer hat because this pattern is an expedition to uncharted territories. New highs or new lows - they’re the breadcrumbs that lead to the heart of market trends. 🗺️ 64 | 65 | ![PatternPy in action](docs/images/patterns.png) 66 | 67 | 68 | 69 | ## Future Plans 70 | We're always looking to improve PatternPy. Some areas we're exploring include: 71 | 72 | - Adding more trading patterns based on user suggestions. 73 | - Enhancing visualization and plotting features for better pattern understanding. 74 | - Incorporating unit testing for code reliability. 75 | - We welcome your suggestions for new features and improvements! 76 | 77 | ## Contribute 78 | PatternPy is an open-source project, and we welcome contributions of all kinds: new features, bug fixes, documentation, and more. Please see our contribution guide for more information. 79 | 80 | 81 | Join us on this exciting journey to revolutionize trading pattern recognition. Let's create wealth together with PatternPy! 82 | 83 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithorange/PatternPy/2b1f29b17986acafffeb32c83e772417d540c100/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/patterns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithorange/PatternPy/2b1f29b17986acafffeb32c83e772417d540c100/docs/images/patterns.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "patternpy" 3 | version = "1.0" 4 | repository = "https://github.com/keithorange/patternpy" 5 | description = "Trading analysis with high-speed pattern recognition, leveraging Pandas & Numpy. Effortlessly spot Head & Shoulders, Tops & Bottoms, Supports & Resistances. For experts & beginners. #TradingMadeEasy 🔥" 6 | authors = ["Keith Orange"] 7 | license = "CC BY-NC-SA 4.0" 8 | readme = "README.md" 9 | packages = [ 10 | { include = "tradingpatterns" } 11 | ] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.10" 15 | numpy = "^1.24.2" 16 | pandas = "^2.0.0" 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | coverage = "^7.2.2" 20 | nose = "^1.3.7" 21 | Sphinx = "^6.1.3" 22 | sphinx-rtd-theme-citus = "^0.5.25" 23 | 24 | [build-system] 25 | requires = ["poetry-core"] 26 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithorange/PatternPy/2b1f29b17986acafffeb32c83e772417d540c100/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_hs.py: -------------------------------------------------------------------------------- 1 | from tradingpatterns.hard_data import generate_sample_df_with_pattern 2 | from tradingpatterns.tradingpatterns import detect_head_shoulder 3 | 4 | def test_detect_head_shoulder(): 5 | # Generate data with head and shoulder pattern 6 | df_head_shoulder = generate_sample_df_with_pattern("Head and Shoulder") 7 | df_inv_shoulder = generate_sample_df_with_pattern("Inverse Head and Shoulder") 8 | df_with_detection = detect_head_shoulder(df_head_shoulder) 9 | df_with_inv_detection = detect_head_shoulder(df_inv_shoulder) 10 | assert "Head and Shoulder" in df_with_detection['head_shoulder_pattern'].values 11 | assert "Inverse Head and Shoulder" in df_with_inv_detection['head_shoulder_pattern'].values 12 | 13 | 14 | -------------------------------------------------------------------------------- /tradingpatterns/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keithorange/PatternPy/2b1f29b17986acafffeb32c83e772417d540c100/tradingpatterns/__init__.py -------------------------------------------------------------------------------- /tradingpatterns/hard_data.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | def generate_sample_df_with_pattern(pattern): 3 | date_rng = pd.date_range(start='1/1/2020', end='1/10/2020', freq='D') 4 | data = {'date': date_rng} 5 | if pattern == 'Head and Shoulder': 6 | data['Open'] = [90, 85, 80, 90, 85, 80, 75, 80, 85, 90] 7 | data['High'] = [95, 90, 85, 95, 90, 85, 80, 85, 90, 95] 8 | data['Low'] = [80, 75, 70, 80, 75, 70, 65, 70, 75, 80] 9 | data['Close'] = [90, 85, 80, 90, 85, 80, 75, 80, 85, 90] 10 | data['Volume'] = [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000] 11 | elif pattern == 'Inverse Head and Shoulder': 12 | data['Open'] = [20, 25, 30, 20, 25, 30, 35, 30, 25, 20] 13 | data['High'] = [25, 30, 35, 25, 30, 35, 40, 35, 30, 25] 14 | data['Low'] = [15, 20, 25, 15, 20, 25, 30, 25, 20, 15] 15 | data['Close'] = [20, 25, 30, 20, 25, 30, 35, 30, 25, 20] 16 | data['Volume'] = [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000] 17 | elif pattern == "Double Top" or "Double Bottom" or "Ascending Triangle" or "Descending Triangle": 18 | data['High'] = [95, 90, 85, 95, 90, 85, 80, 85, 90, 95] 19 | data['Low'] = [80, 75, 70, 80, 75, 70, 65, 70, 75, 80] 20 | df = pd.DataFrame(data) 21 | df.iloc[3:5,1] =100 22 | df.iloc[6:8,1] =70 23 | df.iloc[6:9,2] =70 24 | df = pd.DataFrame(data) 25 | return df -------------------------------------------------------------------------------- /tradingpatterns/tradingpatterns.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | 4 | 5 | def detect_head_shoulder(df, window=3): 6 | # Define the rolling window 7 | roll_window = window 8 | # Create a rolling window for High and Low 9 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 10 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 11 | # Create a boolean mask for Head and Shoulder pattern 12 | mask_head_shoulder = ((df['high_roll_max'] > df['High'].shift(1)) & (df['high_roll_max'] > df['High'].shift(-1)) & (df['High'] < df['High'].shift(1)) & (df['High'] < df['High'].shift(-1))) 13 | # Create a boolean mask for Inverse Head and Shoulder pattern 14 | mask_inv_head_shoulder = ((df['low_roll_min'] < df['Low'].shift(1)) & (df['low_roll_min'] < df['Low'].shift(-1)) & (df['Low'] > df['Low'].shift(1)) & (df['Low'] > df['Low'].shift(-1))) 15 | # Create a new column for Head and Shoulder and its inverse pattern and populate it using the boolean masks 16 | df['head_shoulder_pattern'] = np.nan 17 | df.loc[mask_head_shoulder, 'head_shoulder_pattern'] = 'Head and Shoulder' 18 | df.loc[mask_inv_head_shoulder, 'head_shoulder_pattern'] = 'Inverse Head and Shoulder' 19 | return df 20 | # return not df['head_shoulder_pattern'].isna().any().item() 21 | 22 | def detect_multiple_tops_bottoms(df, window=3): 23 | # Define the rolling window 24 | roll_window = window 25 | # Create a rolling window for High and Low 26 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 27 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 28 | df['close_roll_max'] = df['Close'].rolling(window=roll_window).max() 29 | df['close_roll_min'] = df['Close'].rolling(window=roll_window).min() 30 | # Create a boolean mask for multiple top pattern 31 | mask_top = (df['high_roll_max'] >= df['High'].shift(1)) & (df['close_roll_max'] < df['Close'].shift(1)) 32 | # Create a boolean mask for multiple bottom pattern 33 | mask_bottom = (df['low_roll_min'] <= df['Low'].shift(1)) & (df['close_roll_min'] > df['Close'].shift(1)) 34 | # Create a new column for multiple top bottom pattern and populate it using the boolean masks 35 | df['multiple_top_bottom_pattern'] = np.nan 36 | df.loc[mask_top, 'multiple_top_bottom_pattern'] = 'Multiple Top' 37 | df.loc[mask_bottom, 'multiple_top_bottom_pattern'] = 'Multiple Bottom' 38 | return df 39 | 40 | def calculate_support_resistance(df, window=3): 41 | # Define the rolling window 42 | roll_window = window 43 | # Set the number of standard deviation 44 | std_dev = 2 45 | # Create a rolling window for High and Low 46 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 47 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 48 | # Calculate the mean and standard deviation for High and Low 49 | mean_high = df['High'].rolling(window=roll_window).mean() 50 | std_high = df['High'].rolling(window=roll_window).std() 51 | mean_low = df['Low'].rolling(window=roll_window).mean() 52 | std_low = df['Low'].rolling(window=roll_window).std() 53 | # Create a new column for support and resistance 54 | df['support'] = mean_low - std_dev * std_low 55 | df['resistance'] = mean_high + std_dev * std_high 56 | return df 57 | def detect_triangle_pattern(df, window=3): 58 | # Define the rolling window 59 | roll_window = window 60 | # Create a rolling window for High and Low 61 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 62 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 63 | # Create a boolean mask for ascending triangle pattern 64 | mask_asc = (df['high_roll_max'] >= df['High'].shift(1)) & (df['low_roll_min'] <= df['Low'].shift(1)) & (df['Close'] > df['Close'].shift(1)) 65 | # Create a boolean mask for descending triangle pattern 66 | mask_desc = (df['high_roll_max'] <= df['High'].shift(1)) & (df['low_roll_min'] >= df['Low'].shift(1)) & (df['Close'] < df['Close'].shift(1)) 67 | # Create a new column for triangle pattern and populate it using the boolean masks 68 | df['triangle_pattern'] = np.nan 69 | df.loc[mask_asc, 'triangle_pattern'] = 'Ascending Triangle' 70 | df.loc[mask_desc, 'triangle_pattern'] = 'Descending Triangle' 71 | return df 72 | 73 | def detect_wedge(df, window=3): 74 | # Define the rolling window 75 | roll_window = window 76 | # Create a rolling window for High and Low 77 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 78 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 79 | df['trend_high'] = df['High'].rolling(window=roll_window).apply(lambda x: 1 if (x[-1]-x[0])>0 else -1 if (x[-1]-x[0])<0 else 0) 80 | df['trend_low'] = df['Low'].rolling(window=roll_window).apply(lambda x: 1 if (x[-1]-x[0])>0 else -1 if (x[-1]-x[0])<0 else 0) 81 | # Create a boolean mask for Wedge Up pattern 82 | mask_wedge_up = (df['high_roll_max'] >= df['High'].shift(1)) & (df['low_roll_min'] <= df['Low'].shift(1)) & (df['trend_high'] == 1) & (df['trend_low'] == 1) 83 | # Create a boolean mask for Wedge Down pattern 84 | # Create a boolean mask for Wedge Down pattern 85 | mask_wedge_down = (df['high_roll_max'] <= df['High'].shift(1)) & (df['low_roll_min'] >= df['Low'].shift(1)) & (df['trend_high'] == -1) & (df['trend_low'] == -1) 86 | # Create a new column for Wedge Up and Wedge Down pattern and populate it using the boolean masks 87 | df['wedge_pattern'] = np.nan 88 | df.loc[mask_wedge_up, 'wedge_pattern'] = 'Wedge Up' 89 | df.loc[mask_wedge_down, 'wedge_pattern'] = 'Wedge Down' 90 | return df 91 | def detect_channel(df, window=3): 92 | # Define the rolling window 93 | roll_window = window 94 | # Define a factor to check for the range of channel 95 | channel_range = 0.1 96 | # Create a rolling window for High and Low 97 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 98 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 99 | df['trend_high'] = df['High'].rolling(window=roll_window).apply(lambda x: 1 if (x[-1]-x[0])>0 else -1 if (x[-1]-x[0])<0 else 0) 100 | df['trend_low'] = df['Low'].rolling(window=roll_window).apply(lambda x: 1 if (x[-1]-x[0])>0 else -1 if (x[-1]-x[0])<0 else 0) 101 | # Create a boolean mask for Channel Up pattern 102 | mask_channel_up = (df['high_roll_max'] >= df['High'].shift(1)) & (df['low_roll_min'] <= df['Low'].shift(1)) & (df['high_roll_max'] - df['low_roll_min'] <= channel_range * (df['high_roll_max'] + df['low_roll_min'])/2) & (df['trend_high'] == 1) & (df['trend_low'] == 1) 103 | # Create a boolean mask for Channel Down pattern 104 | mask_channel_down = (df['high_roll_max'] <= df['High'].shift(1)) & (df['low_roll_min'] >= df['Low'].shift(1)) & (df['high_roll_max'] - df['low_roll_min'] <= channel_range * (df['high_roll_max'] + df['low_roll_min'])/2) & (df['trend_high'] == -1) & (df['trend_low'] == -1) 105 | # Create a new column for Channel Up and Channel Down pattern and populate it using the boolean masks 106 | df['channel_pattern'] = np.nan 107 | df.loc[mask_channel_up, 'channel_pattern'] = 'Channel Up' 108 | df.loc[mask_channel_down, 'channel_pattern'] = 'Channel Down' 109 | return df 110 | 111 | def detect_double_top_bottom(df, window=3, threshold=0.05): 112 | # Define the rolling window 113 | roll_window = window 114 | # Define a threshold to check for the range of pattern 115 | range_threshold = threshold 116 | 117 | # Create a rolling window for High and Low 118 | df['high_roll_max'] = df['High'].rolling(window=roll_window).max() 119 | df['low_roll_min'] = df['Low'].rolling(window=roll_window).min() 120 | 121 | # Create a boolean mask for Double Top pattern 122 | mask_double_top = (df['high_roll_max'] >= df['High'].shift(1)) & (df['high_roll_max'] >= df['High'].shift(-1)) & (df['High'] < df['High'].shift(1)) & (df['High'] < df['High'].shift(-1)) & ((df['High'].shift(1) - df['Low'].shift(1)) <= range_threshold * (df['High'].shift(1) + df['Low'].shift(1))/2) & ((df['High'].shift(-1) - df['Low'].shift(-1)) <= range_threshold * (df['High'].shift(-1) + df['Low'].shift(-1))/2) 123 | # Create a boolean mask for Double Bottom pattern 124 | mask_double_bottom = (df['low_roll_min'] <= df['Low'].shift(1)) & (df['low_roll_min'] <= df['Low'].shift(-1)) & (df['Low'] > df['Low'].shift(1)) & (df['Low'] > df['Low'].shift(-1)) & ((df['High'].shift(1) - df['Low'].shift(1)) <= range_threshold * (df['High'].shift(1) + df['Low'].shift(1))/2) & ((df['High'].shift(-1) - df['Low'].shift(-1)) <= range_threshold * (df['High'].shift(-1) + df['Low'].shift(-1))/2) 125 | 126 | # Create a new column for Double Top and Double Bottom pattern and populate it using the boolean masks 127 | df['double_pattern'] = np.nan 128 | df.loc[mask_double_top, 'double_pattern'] = 'Double Top' 129 | df.loc[mask_double_bottom, 'double_pattern'] = 'Double Bottom' 130 | 131 | return df 132 | 133 | def detect_trendline(df, window=2): 134 | # Define the rolling window 135 | roll_window = window 136 | # Create new columns for the linear regression slope and y-intercept 137 | df['slope'] = np.nan 138 | df['intercept'] = np.nan 139 | 140 | for i in range(window, len(df)): 141 | x = np.array(range(i-window, i)) 142 | y = df['Close'][i-window:i] 143 | A = np.vstack([x, np.ones(len(x))]).T 144 | m, c = np.linalg.lstsq(A, y, rcond=None)[0] 145 | df.at[df.index[i], 'slope'] = m 146 | df.at[df.index[i], 'intercept'] = c 147 | 148 | # Create a boolean mask for trendline support 149 | mask_support = df['slope'] > 0 150 | 151 | # Create a boolean mask for trendline resistance 152 | mask_resistance = df['slope'] < 0 153 | 154 | # Create new columns for trendline support and resistance 155 | df['support'] = np.nan 156 | df['resistance'] = np.nan 157 | 158 | # Populate the new columns using the boolean masks 159 | df.loc[mask_support, 'support'] = df['Close'] * df['slope'] + df['intercept'] 160 | df.loc[mask_resistance, 'resistance'] = df['Close'] * df['slope'] + df['intercept'] 161 | 162 | return df 163 | 164 | def find_pivots(df): 165 | # Calculate differences between consecutive highs and lows 166 | high_diffs = df['high'].diff() 167 | low_diffs = df['low'].diff() 168 | 169 | # Find higher high 170 | higher_high_mask = (high_diffs > 0) & (high_diffs.shift(-1) < 0) 171 | 172 | # Find lower low 173 | lower_low_mask = (low_diffs < 0) & (low_diffs.shift(-1) > 0) 174 | 175 | # Find lower high 176 | lower_high_mask = (high_diffs < 0) & (high_diffs.shift(-1) > 0) 177 | 178 | # Find higher low 179 | higher_low_mask = (low_diffs > 0) & (low_diffs.shift(-1) < 0) 180 | 181 | # Create signals column 182 | df['signal'] = '' 183 | df.loc[higher_high_mask, 'signal'] = 'HH' 184 | df.loc[lower_low_mask, 'signal'] = 'LL' 185 | df.loc[lower_high_mask, 'signal'] = 'LH' 186 | df.loc[higher_low_mask, 'signal'] = 'HL' 187 | 188 | return df -------------------------------------------------------------------------------- /update_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # build the docs 4 | cd docs 5 | make clean 6 | make html 7 | cd .. 8 | 9 | # commit and push 10 | git add -A 11 | git commit -m "building and pushing docs" 12 | git push origin master 13 | 14 | # switch branches and pull the data we want 15 | git checkout gh-pages 16 | rm -rf . 17 | touch .nojekyll 18 | git checkout master docs/build/html 19 | mv ./docs/build/html/* ./ 20 | rm -rf ./docs 21 | git add -A 22 | git commit -m "publishing updated docs..." 23 | git push origin gh-pages 24 | 25 | # switch back 26 | git checkout master --------------------------------------------------------------------------------