├── .gitignore
├── .gitattributes
├── requirements.txt
├── requirements2.txt
├── rateLimitCheck.py
├── LICENSE
├── website_generator.py
├── README.md
├── stocklist.py
├── grapher.py
├── templates
└── template.html
├── market_scanner.py
├── dynamic.html
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | __pycache__/
3 | __pycache__
4 | .vscode
5 | build/
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Using a venv with python@3.10.15
2 |
3 | mplcursors==0.3
4 | numpy==2.0.0
5 | yfinance==0.2.44
6 | tqdm==4.48.0
7 | joblib==0.16.0
8 | matplotlib==3.9.2
9 | Flask==2.2.2
10 | Flask_FlatPages==0.7.2
11 | pandas==2.2.3
12 | Frozen_Flask==1.0.2
13 | python_dateutil==2.9.0
14 | Werkzeug==2.2.2
15 | quandl==3.7.0
16 | joblib==1.4.2
--------------------------------------------------------------------------------
/requirements2.txt:
--------------------------------------------------------------------------------
1 | # Using a venv with python@3.10.15
2 |
3 | beautifulsoup4==4.12.3
4 | certifi==2024.8.30
5 | charset-normalizer==3.4.0
6 | click==8.1.7
7 | contourpy==1.3.0
8 | cycler==0.12.1
9 | Flask==2.2.2
10 | Flask-FlatPages==0.7.2
11 | fonttools==4.54.1
12 | Frozen-Flask==1.0.2
13 | frozendict==2.4.5
14 | html5lib==1.1
15 | idna==3.10
16 | inflection==0.5.1
17 | itsdangerous==2.2.0
18 | Jinja2==3.1.4
19 | joblib==1.4.2
20 | kiwisolver==1.4.7
21 | lxml==5.3.0
22 | Markdown==3.7
23 | MarkupSafe==3.0.1
24 | matplotlib==3.9.2
25 | more-itertools==10.5.0
26 | mplcursors==0.3
27 | multitasking==0.0.11
28 | numpy==2.0.0
29 | packaging==24.1
30 | pandas==2.2.3
31 | peewee==3.17.6
32 | pillow==10.4.0
33 | platformdirs==4.3.6
34 | pyparsing==3.1.4
35 | python-dateutil==2.9.0.post0
36 | pytz==2024.2
37 | PyYAML==6.0.2
38 | Quandl==3.7.0
39 | requests==2.32.3
40 | six==1.16.0
41 | soupsieve==2.6
42 | tqdm==4.48.0
43 | tzdata==2024.2
44 | urllib3==2.2.3
45 | webencodings==0.5.1
46 | Werkzeug==2.2.2
47 | yfinance==0.2.44
48 |
--------------------------------------------------------------------------------
/rateLimitCheck.py:
--------------------------------------------------------------------------------
1 | import yfinance as yf
2 | from datetime import date
3 | import dateutil.relativedelta
4 | import datetime
5 | import pandas as pd
6 | from pandas_datareader import data as pdr
7 | import quandl
8 |
9 |
10 | def getData(ticker):
11 | currentDate = datetime.date.today() + datetime.timedelta(days=1)
12 | pastDate = currentDate - \
13 | dateutil.relativedelta.relativedelta(months=3)
14 | data = yf.download(ticker, pastDate, currentDate)
15 | #with pd.option_context('display.max_rows', None, 'display.max_columns', None):
16 | print(data[["Volume"]])
17 |
18 |
19 | getData("MSFT")
20 |
21 | #when on probation, 18 calls results in a lock
22 |
23 | def getQuan():
24 | start = currentDate = datetime.date.today() + datetime.timedelta(days=1)
25 | end = pastDate = currentDate - \
26 | dateutil.relativedelta.relativedelta(months=1)
27 |
28 | mydata = quandl.get("WIKI/AAPL", start_date=pastDate, end_date=currentDate, rows=50)
29 | mydata = mydata["Volume"]
30 | print(mydata)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Sam Pomerantz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/website_generator.py:
--------------------------------------------------------------------------------
1 | import flask
2 | from flask_flatpages import FlatPages
3 | from flask_frozen import Freezer
4 | from flask import Flask, request, send_from_directory, render_template
5 | from shutil import copyfile
6 | import os
7 | import shutil
8 | import numpy
9 | from market_scanner import mainObj
10 |
11 | # this is used by me AUTOMATICALLY to update the web page you can find at: https://sampom100.github.io/UnusualVolumeDetector/
12 |
13 | app = flask.Flask(__name__, static_url_path='')
14 | app.config["DEBUG"] = False
15 | app.config['SECRET_KEY'] = 'deditaded wam'
16 | pages = FlatPages(app)
17 | freezer = Freezer(app)
18 |
19 |
20 | @app.after_request
21 | def after_request(response):
22 | response.headers.add('Access-Control-Allow-Origin', 'localhost*,192.168.*')
23 | response.headers.add('Access-Control-Allow-Headers',
24 | 'Content-Type,Authorization')
25 | response.headers.add('Access-Control-Allow-Methods',
26 | 'GET,PUT,POST,DELETE,OPTIONS')
27 | return response
28 |
29 |
30 | @app.route('/', methods=['GET'])
31 | def home():
32 | return render_template('template.html', stocks=stock)
33 |
34 |
35 | def sort_by_volume(data):
36 | def get_volume(item):
37 | return int(item['TargetVolume'].replace(',', ''))
38 | return sorted(data, key=get_volume, reverse=True)
39 |
40 | if __name__ == "__main__":
41 | os.system('git fetch')
42 | stock = sort_by_volume(mainObj().main_func()[:15])
43 | freezer.freeze()
44 | copyfile('build/index.html', 'index.html')
45 | # shutil.rmtree('build/')
46 | # I'm lazy :)
47 | # os.system('git add .')
48 | # os.system('git commit -m "updated website"')
49 | # os.system('git push origin master')
50 | # app.run(host='0.0.0.0', port='5000') # run the app on LAN
51 | # app.run() # run the app on your machine
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Unusual Volume Detector --- [New Website! ](https://unusualvolume.info/):
2 |
3 | This scans every ticker on the market, gets their last 5 months of volume history, and alerts you when a stock's volume exceeds 10 standard deviations from the mean within the last 3 days. (these numbers are all adjustable). Helps find anomalies in the stock market
4 |
5 | ## Easiest way to see this:
6 |
7 | Go to the [website](https://unusualvolume.info/)
8 |
9 |
10 | ### How to run the script:
11 | - Download your favorite Python IDE. (I use VSCode)
12 | - Get my script from GitHub
13 | - Open the script in your IDE and install all required dependancies by typing pip install -r requirements.txt into the IDE's terminal. You can get to the the terminal on VSC by pressing CMD and ` at the same time.
14 | - Run the market_scanner.py and it will print out results into the terminal
15 | - You can also graph any ticker's volume in grapher.py
16 |
17 | ### Controlling the Script
18 | - Line 21 controls the amount of months of historical volume the script gets
19 | - Line 22 controls the amount of days before today that it will alert you
20 | - Line 23 controls the number of standard deviations away from the mean volume
21 | - Line 116, "n-jobs" controls the number of threads the script runs on, which I lowered to avoid new rate limits
22 |
23 |
24 |
25 |
26 |
27 | 
28 |
29 | 
30 |
31 | 
32 |
33 |
34 | ### Websites
35 |
36 | [Main Website](https://sampom100.github.io/UnusualVolumeDetector/)
37 |
38 | [alternate website](http://165.22.228.6/)
39 |
40 | [alternate credit](https://www.removeddit.com/r/wallstreetbets/comments/i10mif/i_made_a_website_for_that_scanner_made_by_that/)
41 |
42 |
43 | ### Donations
44 |
45 | If you enjoy my work, please [donate here](https://www.paypal.me/SamPom100)
46 |
--------------------------------------------------------------------------------
/stocklist.py:
--------------------------------------------------------------------------------
1 | from ftplib import FTP
2 | import os
3 | import errno
4 |
5 |
6 | # this is used to get all tickers from the market.
7 |
8 |
9 | exportList = []
10 |
11 |
12 | class NasdaqController:
13 | def getList(self):
14 | return exportList
15 |
16 | def __init__(self, update=False):
17 |
18 | #"otherlisted": "data/otherlisted.txt",
19 | self.filenames = {
20 | "nasdaqlisted": "data/nasdaqlisted.txt"
21 | }
22 |
23 | # Update lists only if update = True
24 |
25 | if update == True:
26 | self.ftp = FTP("ftp.nasdaqtrader.com")
27 | self.ftp.login()
28 |
29 | #print("Nasdaq Controller: Welcome message: " + self.ftp.getwelcome())
30 |
31 | self.ftp.cwd("SymbolDirectory")
32 |
33 | for filename, filepath in self.filenames.items():
34 | if not os.path.exists(os.path.dirname(filepath)):
35 | try:
36 | os.makedirs(os.path.dirname(filepath))
37 | except OSError as exc: # Guard against race condition
38 | if exc.errno != errno.EEXIST:
39 | raise
40 |
41 | self.ftp.retrbinary("RETR " + filename +
42 | ".txt", open(filepath, 'wb').write)
43 |
44 | all_listed = open("data/alllisted.txt", 'w')
45 |
46 | for filename, filepath in self.filenames.items():
47 | with open(filepath, "r") as file_reader:
48 | for i, line in enumerate(file_reader, 0):
49 | if i == 0:
50 | continue
51 |
52 | line = line.strip().split("|")
53 |
54 | # line[6] and line[4] is for ETFs. Let's skip those to make this faster.
55 | if line[0] == "" or line[1] == "" or (filename == 'nasdaqlisted' and line[6] == 'Y') or (filename == 'otherlisted' and line[4] == 'Y'):
56 | continue
57 |
58 | all_listed.write(line[0] + ",")
59 | global exportList
60 | exportList.append(line[0])
61 | all_listed.write(line[0] + "|" + line[1] + "\n")
62 |
63 | if __name__ == "__main__":
64 | StocksController = NasdaqController(True)
65 | print(StocksController.getList())
66 | print("Refresh Done.")
--------------------------------------------------------------------------------
/grapher.py:
--------------------------------------------------------------------------------
1 | import matplotlib.pyplot as plt
2 | import yfinance as yf
3 | from datetime import *
4 | import dateutil.relativedelta
5 | import datetime
6 | import pandas as pd
7 | import mplcursors
8 | import matplotlib
9 | import matplotlib.dates as mdates
10 | from dateutil import parser
11 | import numpy as np
12 |
13 |
14 | # Use this to graph the volume history of your favorite ticker#
15 |
16 |
17 | class mainObj:
18 | def getData(self, ticker):
19 | currentDate = datetime.datetime.strptime(
20 | date.today().strftime("%Y-%m-%d"), "%Y-%m-%d")
21 | pastDate = currentDate - dateutil.relativedelta.relativedelta(months=4)
22 | data = yf.download(ticker, pastDate, currentDate)
23 | return data[["Volume"]]
24 |
25 | def printData(self, data):
26 | with pd.option_context('display.max_rows', None, 'display.max_columns', None):
27 | cleanData_print = data.copy()
28 | cleanData_print.reset_index(level=0, inplace=True)
29 | print(cleanData_print.to_string(index=False))
30 |
31 | def barGraph(self, data):
32 | data.reset_index(level=0, inplace=True)
33 | tempList = []
34 | for x in data['Date']:
35 | tempList.append(x.date())
36 | data['goodDate'] = tempList
37 | data = data.drop('Date', 1)
38 | data.set_index('goodDate', inplace=True)
39 | ################
40 | fig, ax = plt.subplots(figsize=(15, 7))
41 | data.plot(kind='bar', ax=ax)
42 | ax.get_yaxis().set_major_formatter(
43 | matplotlib.ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
44 | mplcursors.cursor(hover=True)
45 | ################
46 | plt.show()
47 |
48 | def lineGraph(self, data):
49 | data.reset_index(level=0, inplace=True)
50 | fig, ax = plt.subplots(figsize=(15, 7))
51 | ax.plot(data['Date'], data['Volume'])
52 | ax.get_yaxis().set_major_formatter(
53 | matplotlib.ticker.FuncFormatter(lambda x, p: format(int(x), ',')))
54 | mplcursors.cursor(hover=True)
55 | currentDate = datetime.datetime.strptime(
56 | date.today().strftime("%Y-%m-%d"), "%Y-%m-%d")
57 | pastDate = currentDate - dateutil.relativedelta.relativedelta(months=4)
58 | plt.show()
59 |
60 | def find_anomalies(self, random_data):
61 | anomalies = []
62 | random_data_std = np.std(random_data)
63 | random_data_mean = np.mean(random_data)
64 | # Set upper and lower limit to 3 standard deviation
65 | anomaly_cut_off = random_data_std * 4
66 | lower_limit = random_data_mean - anomaly_cut_off
67 | upper_limit = random_data_mean + anomaly_cut_off
68 | # Generate outliers
69 | for outlier in random_data:
70 | if outlier > upper_limit or outlier < lower_limit:
71 | anomalies.append(outlier)
72 | return anomalies
73 |
74 |
75 | main = mainObj()
76 |
77 | # INSTRUCTIONS #
78 |
79 | # change KODK to your desired ticker
80 | # feel free to uncomment 'printData' or 'barGraph' depending on what you'd like
81 | data = main.getData("KODK")
82 | # main.printData(data)
83 | # main.barGraph(data)
84 | main.lineGraph(data)
85 |
--------------------------------------------------------------------------------
/templates/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Unusual Volume Scanner
7 |
8 |
9 |
15 |
20 |
25 |
26 |
27 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
Unusual Volume Scanner
51 |
52 | Get alerted when a stock's volume exceeds 10 standard deviations from the mean
53 | within the last 3 days. Dynamic Version.
54 |
55 |
56 |
57 |
58 | | Ticker |
59 | Date |
60 | Volume |
61 |
62 |
63 |
64 | {% for stock in stocks %}
65 |
66 | |
67 | {{stock['Ticker']}}
71 | |
72 | {{stock['TargetDate']}} |
73 | {{stock['TargetVolume']}} |
74 |
75 | {% endfor %}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | Made by Sam Pomerantz and contributors.
86 |
87 |
88 |
89 |
90 | GitHub
91 |
92 |
93 | •
94 |
95 |
96 | LinkedIn
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Thank you for visiting
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/market_scanner.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import yfinance as yf
4 | from datetime import date
5 | import datetime
6 | import numpy as np
7 | import sys
8 | from stocklist import NasdaqController
9 | from tqdm import tqdm
10 | from joblib import Parallel, delayed, parallel_backend
11 | import multiprocessing
12 | import pandas as pd
13 | import quandl
14 | from dateutil.parser import parse
15 | from yfinance.exceptions import YFInvalidPeriodError
16 | import random
17 |
18 | ###########################
19 | # THIS IS THE MAIN SCRIPT #
20 | ###########################
21 |
22 | # Change variables to your liking then run the script
23 | MONTH_CUTTOFF = 6 # 6
24 | DAY_CUTTOFF = 4 # 3
25 | STD_CUTTOFF = 7 # 9
26 | MIN_STOCK_VOLUME = 10000
27 | MIN_PRICE = 20
28 |
29 |
30 | class mainObj:
31 |
32 | def __init__(self):
33 | pass
34 |
35 | def getDataQuandl(self, ticker, pastDate, currentDate):
36 | ticker = "WIKI/"+ticker
37 | mydata = quandl.get(ticker, start_date=pastDate,
38 | end_date=currentDate, rows=50)
39 | mydata = mydata["Volume"]
40 | return mydata
41 |
42 | def getData(self, ticker):
43 | try:
44 | global MONTH_CUTOFF
45 |
46 | sys.stdout = open(os.devnull, "w")
47 | # maybe swap yahoo finance to quandl due to rate limits
48 | try:
49 | data = yf.Ticker(ticker).history(period=str(MONTH_CUTTOFF) + "mo", raise_errors=True)
50 | except (YFInvalidPeriodError) as e:
51 | try:
52 | data = yf.Ticker(ticker).history(period=e.valid_ranges[-1])
53 | except:
54 | return pd.DataFrame(columns=["Volume"])
55 | sys.stdout = sys.__stdout__
56 |
57 | if data.tail(1)["Close"].values[0] < MIN_PRICE:
58 | return pd.DataFrame(columns=["Volume"])
59 |
60 | # avoid yahoo finance rate limits
61 | time.sleep(random.uniform(0.2, 1.5))
62 | return data[["Volume"]]
63 | except:
64 | return pd.DataFrame(columns=["Volume"])
65 |
66 | def find_anomalies(self, data):
67 | global STD_CUTTOFF
68 | global MIN_STOCK_VOLUME
69 | indexs = []
70 | outliers = []
71 | data_std = np.std(data['Volume'])
72 | data_mean = np.mean(data['Volume'])
73 | anomaly_cut_off = data_std * STD_CUTTOFF
74 | upper_limit = data_mean + anomaly_cut_off
75 | data.reset_index(level=0, inplace=True)
76 | for i in range(len(data)):
77 | temp = data['Volume'].iloc[i]
78 | if temp > upper_limit and temp > MIN_STOCK_VOLUME:
79 | indexs.append(str(data['Date'].iloc[i])[:-15])
80 | outliers.append(temp)
81 | d = {'Dates': indexs, 'Volume': outliers}
82 | return d
83 |
84 | def customPrint(self, d, tick):
85 | print("\n\n\n******* " + tick.upper() + " *******")
86 | print("Ticker is: "+tick.upper())
87 | for i in range(len(d['Dates'])):
88 | str1 = str(d['Dates'][i])
89 | str2 = str(d['Volume'][i])
90 | print(str1 + " - " + str2)
91 | print("*********************\n\n\n")
92 |
93 | def days_between(self, d1, d2):
94 | return abs((parse(d2) - parse(d1)).days)
95 |
96 | def parallel_wrapper(self, x, currentDate, positive_scans):
97 | global DAY_CUTTOFF
98 | d = (self.find_anomalies(self.getData(x)))
99 | if d['Dates']:
100 | for i in range(len(d['Dates'])):
101 | if self.days_between(str(currentDate), str(d['Dates'][i])) <= DAY_CUTTOFF:
102 | self.customPrint(d, x)
103 | stock = dict()
104 | stock['Ticker'] = x
105 | stock['TargetDate'] = d['Dates'][0]
106 | stock['TargetVolume'] = str(
107 | '{:,.2f}'.format(d['Volume'][0]))[:-3]
108 | positive_scans.append(stock)
109 |
110 | def main_func(self):
111 | StocksController = NasdaqController(False)
112 | list_of_tickers = StocksController.getList()
113 | currentDate = datetime.date.today().strftime("%m-%d-%Y")
114 | start_time = time.time()
115 |
116 | # positive_scans = []
117 | # for x in tqdm(list_of_tickers):
118 | # self.parallel_wrapper(x, currentDate, positive_scans)
119 |
120 | manager = multiprocessing.Manager()
121 | positive_scans = manager.list()
122 |
123 | cpu_count = multiprocessing.cpu_count()
124 | try:
125 | with parallel_backend('loky', n_jobs=cpu_count):
126 | Parallel()(delayed(self.parallel_wrapper)(x, currentDate, positive_scans)
127 | for x in tqdm(list_of_tickers))
128 | except Exception as e:
129 | print(e)
130 |
131 | print("\n\n\n\n--- this took %s seconds to run ---" %
132 | (time.time() - start_time))
133 |
134 | return positive_scans
135 |
136 |
137 | if __name__ == '__main__':
138 | mainObj().main_func()
139 |
--------------------------------------------------------------------------------
/dynamic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Unusual Volume Scanner
7 |
8 |
9 |
10 |
16 |
21 |
26 |
27 |
28 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
51 |
Unusual Volume Scanner
52 |
53 | Get alerted when a stock's volume exceeds 10 standard deviations from the mean
54 | within the last 3 days. Stay ahead of other retailers.
55 |
56 |
57 |
58 |
59 | | Ticker |
60 | Date |
61 | Volume |
62 |
63 |
64 |
65 |
66 |
67 | |
68 | AIMT
72 | |
73 | |
74 | |
75 |
76 |
77 |
78 | |
79 | AKCA
83 | |
84 | |
85 | |
86 |
87 |
88 |
89 | |
90 | NSYS
94 | |
95 | |
96 | |
97 |
98 |
99 |
100 | |
101 | UTSI
105 | |
106 | |
107 | |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | Made by Sam Pomerantz and contributors.
120 |
121 |
122 |
123 |
124 | GitHub
125 |
126 |
127 | •
128 |
129 |
130 | LinkedIn
131 |
132 |
133 | •
134 |
135 |
136 | Donate
137 |
138 |
139 |
140 |
141 | Thank you for visiting
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Unusual Volume Scanner
7 |
8 |
9 |
15 |
20 |
25 |
26 |
27 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
Unusual Volume Scanner
51 |
52 | Get alerted when a stock's volume exceeds 10 standard deviations from the mean
53 | within the last 3 days. Dynamic Version.
54 |
55 |
56 |
57 |
58 | | Ticker |
59 | Date |
60 | Volume |
61 |
62 |
63 |
64 |
65 |
66 | |
67 | SRRK
71 | |
72 | 2024-10-07 |
73 | 42,667,700 |
74 |
75 |
76 |
77 | |
78 | DOCU
82 | |
83 | 2024-10-10 |
84 | 33,208,400 |
85 |
86 |
87 |
88 | |
89 | FANG
93 | |
94 | 2024-10-08 |
95 | 10,794,600 |
96 |
97 |
98 |
99 | |
100 | ALRS
104 | |
105 | 2024-10-09 |
106 | 1,002,800 |
107 |
108 |
109 |
110 | |
111 | EXEEZ
115 | |
116 | 2024-10-10 |
117 | 108,200 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | Made by Sam Pomerantz and contributors.
130 |
131 |
132 |
133 |
134 | GitHub
135 |
136 |
137 | •
138 |
139 |
140 | LinkedIn
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | Thank you for visiting
149 |
150 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------