goldhand 15.4__py3-none-any.whl → 15.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of goldhand might be problematic. Click here for more details.
- goldhand/backtest.py +30 -1
- goldhand/stocks.py +127 -88
- goldhand/strategy_goldhand_line.py +20 -16
- goldhand/strategy_rsi.py +19 -0
- goldhand/tw.py +115 -32
- {goldhand-15.4.dist-info → goldhand-15.7.dist-info}/METADATA +7 -4
- goldhand-15.7.dist-info/RECORD +11 -0
- goldhand-15.4.dist-info/RECORD +0 -11
- {goldhand-15.4.dist-info → goldhand-15.7.dist-info}/WHEEL +0 -0
- {goldhand-15.4.dist-info → goldhand-15.7.dist-info}/top_level.txt +0 -0
goldhand/backtest.py
CHANGED
|
@@ -6,6 +6,15 @@ import plotly.express as px
|
|
|
6
6
|
|
|
7
7
|
class Backtest:
|
|
8
8
|
def __init__(self, data, strategy_function, plot_title='', **kwargs):
|
|
9
|
+
"""
|
|
10
|
+
Backtest class to test strategies on historical data to see how they would have performed.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
- data: pandas DataFrame with historical data
|
|
14
|
+
- strategy_function: function that takes in the data and returns a DataFrame of trades
|
|
15
|
+
- plot_title: title for the plot
|
|
16
|
+
- kwargs: additional parameters to be passed to the strategy function
|
|
17
|
+
"""
|
|
9
18
|
self.data = data
|
|
10
19
|
self.plot_title = plot_title
|
|
11
20
|
self.strategy_function = strategy_function
|
|
@@ -15,6 +24,9 @@ class Backtest:
|
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
def add_trades(self):
|
|
27
|
+
"""
|
|
28
|
+
Calculate the trades using the strategy function and the data provided
|
|
29
|
+
"""
|
|
18
30
|
self.trades = self.strategy_function(self.data, **self.additional_params)
|
|
19
31
|
self.trades['ticker'] = self.data['ticker'].iloc[0]
|
|
20
32
|
|
|
@@ -26,6 +38,9 @@ class Backtest:
|
|
|
26
38
|
|
|
27
39
|
|
|
28
40
|
def summary_of_trades(self):
|
|
41
|
+
"""
|
|
42
|
+
Calculate the summary of the trades
|
|
43
|
+
"""
|
|
29
44
|
self.trades_summary = {
|
|
30
45
|
'ticker' : self.data['ticker'].iloc[0],
|
|
31
46
|
'number_of_trades' : self.trades.shape[0],
|
|
@@ -57,12 +72,20 @@ class Backtest:
|
|
|
57
72
|
'max_gain(%)' : round(((self.trades['result'].max()-1)*100),2),
|
|
58
73
|
'max_lost(%)' : round(((self.trades['result'].min()-1)*100),2),
|
|
59
74
|
|
|
60
|
-
'first_trade_buy' : min(self.trades['buy_date'])
|
|
75
|
+
'first_trade_buy' : min(self.trades['buy_date']),
|
|
76
|
+
'first_close_price' : self.data['close'].iloc[0],
|
|
77
|
+
'first_date' : self.data['date'].iloc[0],
|
|
78
|
+
'last_price' : self.data['close'].iloc[-1],
|
|
79
|
+
'hold_result' : round(((self.data['close'].iloc[-1] / self.data['close'].iloc[0])-1)*100,2)
|
|
80
|
+
|
|
61
81
|
|
|
62
82
|
}
|
|
63
83
|
self.trades_summary.update(self.additional_params)
|
|
64
84
|
|
|
65
85
|
def show_trades(self):
|
|
86
|
+
"""
|
|
87
|
+
Plot the trades of the strategy on the data provided
|
|
88
|
+
"""
|
|
66
89
|
tdf = self.data
|
|
67
90
|
fig = go.Figure(data=go.Ohlc(x=tdf['date'], open=tdf['open'], high=tdf['high'], low=tdf['low'],close=tdf['close']))
|
|
68
91
|
fig.add_trace( go.Scatter(x=tdf['date'], y=tdf['sma_50'], opacity =0.5, line=dict(color='lightblue', width = 2) , name = 'SMA 50') )
|
|
@@ -116,6 +139,12 @@ class Backtest:
|
|
|
116
139
|
|
|
117
140
|
|
|
118
141
|
def summarize_strategy(self):
|
|
142
|
+
"""
|
|
143
|
+
Display the summary of the strategy:
|
|
144
|
+
- Summary of trades
|
|
145
|
+
- Trades in interactive plot
|
|
146
|
+
- Trades in DataFrame
|
|
147
|
+
"""
|
|
119
148
|
display(pd.DataFrame(self.trades_summary, index=['Strategy summary']).T )
|
|
120
149
|
self.show_trades().show()
|
|
121
150
|
display(self.trades)
|
goldhand/stocks.py
CHANGED
|
@@ -11,6 +11,15 @@ import cloudscraper
|
|
|
11
11
|
|
|
12
12
|
class GoldHand:
|
|
13
13
|
def __init__(self, ticker, ad_ticker=True, range='18y', interval='1d'):
|
|
14
|
+
"""
|
|
15
|
+
GoldHand class to download and analyze stock data
|
|
16
|
+
|
|
17
|
+
Paramseters:
|
|
18
|
+
- ticker: str, ticker symbol of the stocks or crypto or ETF
|
|
19
|
+
- ad_ticker: bool, add ticker column to the DataFrame
|
|
20
|
+
- range: str, time range to download data for example 5y,1y, 1mo, 1d, 1h
|
|
21
|
+
- interval: str, interval to download data for example 1d, 1h, 5m
|
|
22
|
+
"""
|
|
14
23
|
self.scraper = cloudscraper.create_scraper()
|
|
15
24
|
self.ad_ticker = ad_ticker
|
|
16
25
|
self.range = range
|
|
@@ -22,7 +31,8 @@ class GoldHand:
|
|
|
22
31
|
|
|
23
32
|
def get_olhc(self):
|
|
24
33
|
"""
|
|
25
|
-
Download historical stock data
|
|
34
|
+
Download historical stock, crypto or ETF data from yahoo finance
|
|
35
|
+
API documentation location: https://cryptocointracker.com/yahoo-finance/yahoo-finance-api
|
|
26
36
|
"""
|
|
27
37
|
#scraper = cloudscraper.create_scraper()
|
|
28
38
|
response = self.scraper.get(f"https://query1.finance.yahoo.com/v8/finance/chart/{self.ticker}?interval={self.interval}&range={self.range}")
|
|
@@ -36,7 +46,13 @@ class GoldHand:
|
|
|
36
46
|
|
|
37
47
|
def smma(self, data, window, colname):
|
|
38
48
|
"""
|
|
39
|
-
Calculate Smoothed Moving Average (SMMA)
|
|
49
|
+
Calculate Smoothed Simple Moving Average (SMMA)
|
|
50
|
+
Parameters:
|
|
51
|
+
- data: Pandas DataFrame
|
|
52
|
+
- window: int, window size
|
|
53
|
+
- colname: str, name of the column to add to the DataFrame
|
|
54
|
+
|
|
55
|
+
Return: DataFrame with added column
|
|
40
56
|
"""
|
|
41
57
|
hl2 = data['hl2'].values
|
|
42
58
|
smma_values = [hl2[0]]
|
|
@@ -51,105 +67,119 @@ class GoldHand:
|
|
|
51
67
|
|
|
52
68
|
|
|
53
69
|
def download_historical_data(self):
|
|
70
|
+
"""
|
|
71
|
+
Download historical stock, crypto or ETF data
|
|
72
|
+
"""
|
|
54
73
|
# Download historical stock data for the last year
|
|
55
74
|
self.df = self.get_olhc()
|
|
56
75
|
self.df.columns = self.df.columns.str.lower()
|
|
57
76
|
self.df['hl2'] = (self.df['high'] + self.df['low'])/2
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
try:
|
|
79
|
+
# Rsi
|
|
80
|
+
self.df['rsi'] = ta.rsi(self.df['close'], 14)
|
|
81
|
+
|
|
82
|
+
# SMAS
|
|
83
|
+
self.df['sma_50']= ta.sma(self.df['close'], 50)
|
|
84
|
+
self.df['diff_sma50'] = (self.df['close']/self.df['sma_50'] -1)*100
|
|
85
|
+
self.df['sma_100']= ta.sma(self.df['close'], 100)
|
|
86
|
+
self.df['diff_sma100'] = (self.df['close']/self.df['sma_100'] -1)*100
|
|
87
|
+
self.df['sma_200']= ta.sma(self.df['close'], 200)
|
|
88
|
+
self.df['diff_sma200'] = (self.df['close']/self.df['sma_200'] -1)*100
|
|
89
|
+
|
|
90
|
+
#Bolinger bands
|
|
91
|
+
bb = ta.bbands(self.df['close'])
|
|
92
|
+
bb.columns = ['bb_lower', 'bb_mid', 'bb_upper', 'bandwidth', 'percent']
|
|
93
|
+
self.df['bb_lower'] = bb['bb_lower']
|
|
94
|
+
self.df['bb_upper'] = bb['bb_upper']
|
|
95
|
+
self.df['diff_upper_bb'] = (self.df['bb_upper']/self.df['close'] -1)*100
|
|
96
|
+
self.df['diff_lower_bb'] = (self.df['bb_lower']/self.df['close'] -1)*100
|
|
97
|
+
|
|
98
|
+
#local min maxs
|
|
99
|
+
self.df['local'] = ''
|
|
100
|
+
self.df['local_text'] = ''
|
|
101
|
+
max_ids = list(argrelextrema(self.df['high'].values, np.greater, order=30)[0])
|
|
102
|
+
min_ids = list(argrelextrema(self.df['low'].values, np.less, order=30)[0])
|
|
103
|
+
self.df.loc[min_ids, 'local'] = 'minimum'
|
|
104
|
+
self.df.loc[max_ids, 'local'] = 'maximum'
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
states = self.df[self.df['local']!='']['local'].index.to_list()
|
|
108
|
+
problem = []
|
|
109
|
+
for i in range(0, (len(states)-1) ):
|
|
110
|
+
|
|
111
|
+
if (self.df.loc[states[i], 'local'] != self.df.loc[states[i+1], 'local']):
|
|
112
|
+
if (len(problem)==0):
|
|
113
|
+
continue
|
|
114
|
+
else:
|
|
115
|
+
problem.append(states[i])
|
|
116
|
+
text = self.df.loc[states[i], 'local']
|
|
117
|
+
if(text=='minimum'):
|
|
118
|
+
real_min = self.df.loc[problem, 'low'].idxmin()
|
|
119
|
+
problem.remove(real_min)
|
|
120
|
+
self.df.loc[problem, 'local']=''
|
|
121
|
+
else:
|
|
122
|
+
real_max = self.df.loc[problem, 'high'].idxmax()
|
|
123
|
+
problem.remove(real_max)
|
|
124
|
+
self.df.loc[problem, 'local']=''
|
|
125
|
+
|
|
126
|
+
problem = []
|
|
95
127
|
else:
|
|
96
128
|
problem.append(states[i])
|
|
97
|
-
text = self.df.loc[states[i], 'local']
|
|
98
|
-
if(text=='minimum'):
|
|
99
|
-
real_min = self.df.loc[problem, 'low'].idxmin()
|
|
100
|
-
problem.remove(real_min)
|
|
101
|
-
self.df.loc[problem, 'local']=''
|
|
102
|
-
else:
|
|
103
|
-
real_max = self.df.loc[problem, 'high'].idxmax()
|
|
104
|
-
problem.remove(real_max)
|
|
105
|
-
self.df.loc[problem, 'local']=''
|
|
106
|
-
|
|
107
|
-
problem = []
|
|
108
|
-
else:
|
|
109
|
-
problem.append(states[i])
|
|
110
|
-
|
|
111
|
-
states = self.df[self.df['local']!='']['local'].index.to_list()
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
if self.df.loc[states[0], 'local']== 'minimum':
|
|
115
|
-
self.df.loc[states[0],'local_text'] = f"${round(self.df.loc[states[0], 'low'], 2)}"
|
|
116
|
-
else:
|
|
117
|
-
self.df.loc[states[0],'local_text'] = f"${round(self.df.loc[states[0], 'high'], 2)}"
|
|
130
|
+
states = self.df[self.df['local']!='']['local'].index.to_list()
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
self.df.loc[last_min_id , 'local'] = 'minimum'
|
|
123
|
-
|
|
124
|
-
states = self.df[self.df['local']!='']['local'].index.to_list()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for i in range(1,len(states)):
|
|
128
|
-
prev = self.df.loc[states[i-1], 'local']
|
|
129
|
-
current= self.df.loc[states[i], 'local']
|
|
130
|
-
prev_high = self.df.loc[states[i-1], 'high']
|
|
131
|
-
prev_low = self.df.loc[states[i-1], 'low']
|
|
132
|
-
current_high = self.df.loc[states[i], 'high']
|
|
133
|
-
current_low = self.df.loc[states[i], 'low']
|
|
134
|
-
if current == 'maximum':
|
|
135
|
-
# rise
|
|
136
|
-
rise = (current_high/ prev_low -1)*100
|
|
137
|
-
if rise>100:
|
|
138
|
-
self.df.loc[states[i], 'local_text'] = f'🚀🌌{round(((rise+100)/100), 2)}x<br>${round(current_high, 2)}'
|
|
139
|
-
else:
|
|
140
|
-
self.df.loc[states[i], 'local_text'] = f'🚀{round(rise, 2)}%<br>${round(current_high, 2)}'
|
|
132
|
+
# if first is min ad the price
|
|
133
|
+
if self.df.loc[states[0], 'local']== 'minimum':
|
|
134
|
+
self.df.loc[states[0],'local_text'] = f"${round(self.df.loc[states[0], 'low'], 2)}"
|
|
141
135
|
else:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
136
|
+
self.df.loc[states[0],'local_text'] = f"${round(self.df.loc[states[0], 'high'], 2)}"
|
|
137
|
+
|
|
138
|
+
# add last fall if last local is max
|
|
139
|
+
if list(self.df[self.df['local']!='']['local'])[-1]=='maximum':
|
|
140
|
+
last_min_id = self.df.loc[self.df['low']==min(self.df['low'][-3:] )].index.to_list()[0]
|
|
141
|
+
self.df.loc[last_min_id , 'local'] = 'minimum'
|
|
142
|
+
|
|
143
|
+
states = self.df[self.df['local']!='']['local'].index.to_list()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
for i in range(1,len(states)):
|
|
147
|
+
prev = self.df.loc[states[i-1], 'local']
|
|
148
|
+
current= self.df.loc[states[i], 'local']
|
|
149
|
+
prev_high = self.df.loc[states[i-1], 'high']
|
|
150
|
+
prev_low = self.df.loc[states[i-1], 'low']
|
|
151
|
+
current_high = self.df.loc[states[i], 'high']
|
|
152
|
+
current_low = self.df.loc[states[i], 'low']
|
|
153
|
+
if current == 'maximum':
|
|
154
|
+
# rise
|
|
155
|
+
rise = (current_high/ prev_low -1)*100
|
|
156
|
+
if rise>100:
|
|
157
|
+
self.df.loc[states[i], 'local_text'] = f'🚀🌌{round(((rise+100)/100), 2)}x<br>${round(current_high, 2)}'
|
|
158
|
+
else:
|
|
159
|
+
self.df.loc[states[i], 'local_text'] = f'🚀{round(rise, 2)}%<br>${round(current_high, 2)}'
|
|
147
160
|
else:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
fall = round((1-(current_low / prev_high))*100, 2)
|
|
162
|
+
if fall < 30:
|
|
163
|
+
temj = '💸'
|
|
164
|
+
elif fall < 50:
|
|
165
|
+
temj = '💸'
|
|
166
|
+
else:
|
|
167
|
+
temj = '😭💔'
|
|
168
|
+
self.df.loc[states[i], 'local_text'] = f'{temj}{fall}%<br>${round(current_low, 2)}'
|
|
169
|
+
self.df.reset_index(inplace=True, drop=True)
|
|
170
|
+
except:
|
|
171
|
+
pass
|
|
151
172
|
|
|
152
173
|
def plotly_last_year(self, plot_title, plot_height=900, ndays=500, ad_local_min_max=True):
|
|
174
|
+
"""
|
|
175
|
+
Plot last year interactive plot of a stock analyzing the local minimums and maximums
|
|
176
|
+
Parameters:
|
|
177
|
+
- plot_title: str, title of the plot
|
|
178
|
+
- plot_height: int, height of the plot
|
|
179
|
+
- ndays: int, number of days to plot
|
|
180
|
+
- ad_local_min_max: bool, add local min max to the plot
|
|
181
|
+
Return: plotly figure
|
|
182
|
+
"""
|
|
153
183
|
tdf = self.df.tail(ndays)
|
|
154
184
|
|
|
155
185
|
fig = go.Figure(data=go.Ohlc(x=tdf['date'], open=tdf['open'], high=tdf['high'], low=tdf['low'],close=tdf['close']))
|
|
@@ -176,6 +206,15 @@ class GoldHand:
|
|
|
176
206
|
return(fig)
|
|
177
207
|
|
|
178
208
|
def plot_goldhand_line(self, plot_title, plot_height=900, ndays=800, ad_local_min_max=True):
|
|
209
|
+
"""
|
|
210
|
+
Plot last year interactive plot of a stock analyzing the local minimums and maximums using the GoldHandLine indicator
|
|
211
|
+
Parameters:
|
|
212
|
+
- plot_title: str, title of the plot
|
|
213
|
+
- plot_height: int, height of the plot
|
|
214
|
+
- ndays: int, number of days to plot
|
|
215
|
+
- ad_local_min_max: bool, add local min max to the plot
|
|
216
|
+
Return: plotly figure
|
|
217
|
+
"""
|
|
179
218
|
|
|
180
219
|
data = self.df.copy()
|
|
181
220
|
# Apply SMMA to the dataframe
|
|
@@ -10,13 +10,14 @@ from goldhand import *
|
|
|
10
10
|
|
|
11
11
|
def goldhand_line_strategy(data, buy_at='gold', sell_at='grey'):
|
|
12
12
|
"""
|
|
13
|
-
This function implements the
|
|
13
|
+
This function implements the GoldHandLine strategy.
|
|
14
|
+
|
|
14
15
|
Parameters:
|
|
15
|
-
data (pandas
|
|
16
|
-
buy_at (str): The color of the line to buy at. Default is 'gold'.
|
|
17
|
-
sell_at (str): The color of the line to sell at. Default is 'grey'.
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
- data (pandas DataFrame) : The DataFrame containing the data.
|
|
17
|
+
- buy_at (str): The color of the line to buy at. Default is 'gold'.
|
|
18
|
+
- sell_at (str): The color of the line to sell at. Default is 'grey'.
|
|
19
|
+
|
|
20
|
+
Returns: The trades of the GoldHandLine strategy.
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
data['hl2'] = (data['high'] + data['low'])/2
|
|
@@ -112,15 +113,18 @@ def goldhand_line_strategy(data, buy_at='gold', sell_at='grey'):
|
|
|
112
113
|
|
|
113
114
|
def show_indicator_goldhand_line_strategy(ticker, plot_title = '', buy_at='gold', sell_at='grey', ndays=0, plot_height=1000, add_strategy_summary = True):
|
|
114
115
|
"""
|
|
115
|
-
This function shows the
|
|
116
|
+
This function shows the GoldHandLine strategy on a plotly chart including the price, trades, strategy summary and GoldHandLine indicator.
|
|
117
|
+
|
|
116
118
|
Parameters:
|
|
117
|
-
ticker (str): The ticker of the stock or ETF.
|
|
118
|
-
plot_title (str): The title of the plot.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
- ticker (str): The ticker of the stock or crypto or ETF.
|
|
120
|
+
- plot_title (str): The title of the plot.
|
|
121
|
+
- buy_at (str): The color of the line to buy at. Default is 'gold'.
|
|
122
|
+
- sell_at (str): The color of the line to sell at. Default is 'grey'.
|
|
123
|
+
- ndays (int): The number of days to show. If 0, all data will be shown.
|
|
124
|
+
- plot_height (int): The height of the plot.
|
|
125
|
+
- add_strategy_summary (bool): If True, the strategy summary will be added to the plot.
|
|
126
|
+
|
|
127
|
+
Returns: The plot including the price, trades, strategy summary and GoldHandLine indicator.
|
|
124
128
|
"""
|
|
125
129
|
|
|
126
130
|
data = GoldHand(ticker).df
|
|
@@ -159,10 +163,10 @@ def show_indicator_goldhand_line_strategy(ticker, plot_title = '', buy_at='gold'
|
|
|
159
163
|
# Create a 'group' column and increase the value only when there's a color change
|
|
160
164
|
data['group'] = (data['color_change']).cumsum()
|
|
161
165
|
|
|
162
|
-
##### data
|
|
166
|
+
##### data preparation end
|
|
163
167
|
|
|
164
168
|
##### backtest
|
|
165
|
-
backtest = Backtest( data, goldhand_line_strategy, plot_title =plot_title, buy_at=
|
|
169
|
+
backtest = Backtest( data, goldhand_line_strategy, plot_title =plot_title, buy_at= buy_at, sell_at=sell_at)
|
|
166
170
|
trades =backtest.trades
|
|
167
171
|
|
|
168
172
|
if ndays!=0:
|
goldhand/strategy_rsi.py
CHANGED
|
@@ -9,6 +9,14 @@ from goldhand import *
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def rsi_strategy(data, buy_threshold = 30, sell_threshold = 70):
|
|
12
|
+
"""
|
|
13
|
+
RSI strategy for backtesting with Backtest class
|
|
14
|
+
|
|
15
|
+
Parameters:
|
|
16
|
+
- data: pandas DataFrame with columns: date, open, high, low, close, volume and rsi
|
|
17
|
+
- buy_threshold: int, default 30, buy when RSI is below this value
|
|
18
|
+
- sell_threshold: int, default 70, sell when RSI is above this value
|
|
19
|
+
"""
|
|
12
20
|
|
|
13
21
|
in_trade = False # Flag to track if already in a trade
|
|
14
22
|
trade_id = 1
|
|
@@ -78,6 +86,17 @@ def rsi_strategy(data, buy_threshold = 30, sell_threshold = 70):
|
|
|
78
86
|
|
|
79
87
|
|
|
80
88
|
def show_indicator_rsi_strategy(ticker, buy_threshold = 30, sell_threshold = 70, plot_title = '', ndays=0, plot_height=1000, add_strategy_summary = True):
|
|
89
|
+
"""
|
|
90
|
+
Show RSI strategy result in one plot: candlestick chart, SMA lines, trades, RSI indicator, summary of the strategy on the left side of the plot
|
|
91
|
+
Parameters:
|
|
92
|
+
- ticker: str, ticker symbol
|
|
93
|
+
- buy_threshold: int, default 30, buy when RSI is below this value
|
|
94
|
+
- sell_threshold: int, default 70, sell when RSI is above this value
|
|
95
|
+
- plot_title: str, default '', title of the plot
|
|
96
|
+
- ndays: int, default 0, number of days to show, if 0, show all data
|
|
97
|
+
- plot_height: int, default 1000, height of the plot
|
|
98
|
+
- add_strategy_summary: bool, default True, add strategy summary to the plot
|
|
99
|
+
"""
|
|
81
100
|
|
|
82
101
|
tdf = GoldHand(ticker).df
|
|
83
102
|
backtest = Backtest( tdf, rsi_strategy, buy_threshold=buy_threshold, sell_threshold=sell_threshold)
|
goldhand/tw.py
CHANGED
|
@@ -9,13 +9,22 @@ import json
|
|
|
9
9
|
|
|
10
10
|
class Tw:
|
|
11
11
|
def __init__(self):
|
|
12
|
+
"""
|
|
13
|
+
Get all stock, crypto and ETF data from TradingView
|
|
14
|
+
"""
|
|
12
15
|
self.stock = pd.DataFrame()
|
|
13
16
|
self.crypto = pd.DataFrame()
|
|
17
|
+
self.etf = pd.DataFrame()
|
|
14
18
|
|
|
15
19
|
self.get_all_stock()
|
|
16
20
|
self.get_all_crypto()
|
|
21
|
+
self.get_all_etf()
|
|
17
22
|
|
|
18
23
|
def get_all_stock(self):
|
|
24
|
+
"""
|
|
25
|
+
Get all stocks data from TradingView
|
|
26
|
+
"""
|
|
27
|
+
|
|
19
28
|
data_query = '{"filter":[{"left":"type","operation":"in_range","right":["stock","dr","fund"]},{"left":"subtype","operation":"in_range","right":["common","foreign-issuer","","etf","etf,odd","etf,otc","etf,cfd"]},{"left":"exchange","operation":"in_range","right":["AMEX","NASDAQ","NYSE"]},{"left":"is_primary","operation":"equal","right":true},{"left":"active_symbol","operation":"equal","right":true}],"options":{"lang":"en"},"markets":["america"],"symbols":{"query":{"types":[]},"tickers":[]},"columns":["logoid","name","close","change","change_abs","Recommend.All","volume","Value.Traded","market_cap_basic","price_earnings_ttm","earnings_per_share_basic_ttm","number_of_employees","sector","High.3M","Low.3M","Perf.3M","Perf.5Y","High.1M","Low.1M","High.6M","Low.6M","Perf.6M","beta_1_year","price_52_week_high","price_52_week_low","High.All","Low.All","BB.lower","BB.upper","change|1M","change_abs|1M","change|1W","change_abs|1W","change|240","country","EMA50","EMA100","EMA200","MACD.macd","MACD.signal","Mom","Perf.1M","RSI7","SMA50","SMA100","SMA200","Stoch.RSI.K","Stoch.RSI.D","Perf.W","Perf.Y","Perf.YTD","industry","Perf.All","description","type","subtype","update_mode","pricescale","minmov","fractional","minmove2","Mom[1]","RSI7[1]","Rec.Stoch.RSI","currency","fundamental_currency_code"],"sort":{"sortBy":"market_cap_basic","sortOrder":"desc"},"range":[0,8000]}'
|
|
20
29
|
response = requests.post('https://scanner.tradingview.com/america/scan', data=data_query)
|
|
21
30
|
data = response.json()
|
|
@@ -26,6 +35,10 @@ class Tw:
|
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
def get_all_crypto(self):
|
|
38
|
+
"""
|
|
39
|
+
Get all crypto data from TradingView
|
|
40
|
+
"""
|
|
41
|
+
|
|
29
42
|
data_query = '{"columns":["base_currency","base_currency_desc","base_currency_logoid","update_mode","type","typespecs","exchange","crypto_total_rank","close","pricescale","minmov","fractional","minmove2","currency","24h_close_change|5","market_cap_calc","fundamental_currency_code","24h_vol_cmc","circulating_supply","crypto_common_categories","crypto_blockchain_ecosystems"],"ignore_unknown_fields":false,"options":{"lang":"en"},"range":[0,300],"sort":{"sortBy":"crypto_total_rank","sortOrder":"asc"},"markets":["coin"]}'
|
|
30
43
|
response = requests.post('https://scanner.tradingview.com/coin/scan', data=data_query)
|
|
31
44
|
data = response.json()
|
|
@@ -36,21 +49,53 @@ class Tw:
|
|
|
36
49
|
self.crypto = self.crypto.loc[filter, ]
|
|
37
50
|
self.crypto['ticker'] = self.crypto['base_currency'] + '-USD'
|
|
38
51
|
|
|
52
|
+
def get_all_etf(self):
|
|
53
|
+
"""
|
|
54
|
+
Get all ETFs from TradingView
|
|
55
|
+
"""
|
|
56
|
+
data_query = '{"columns":["name","description","logoid","update_mode","type","typespecs","close","pricescale","minmov","fractional","minmove2","currency","change","Value.Traded","relative_volume_10d_calc","aum","fundamental_currency_code","nav_total_return.5Y","expense_ratio","asset_class.tr","focus.tr","nav_discount_premium","category.tr","brand.tr","niche.tr"],"ignore_unknown_fields":false,"options":{"lang":"en"},"price_conversion":{"to_symbol":true},"range":[0,3000],"sort":{"sortBy":"aum","sortOrder":"desc"},"markets":["america"],"filter2":{"operator":"and","operands":[{"operation":{"operator":"or","operands":[{"operation":{"operator":"and","operands":[{"expression":{"left":"typespecs","operation":"has","right":["etn"]}}]}},{"operation":{"operator":"and","operands":[{"expression":{"left":"typespecs","operation":"has","right":["etf"]}}]}}]}}]}}'
|
|
57
|
+
response = requests.post('https://scanner.tradingview.com/america/scan', data=data_query)
|
|
58
|
+
data = response.json()
|
|
59
|
+
list_elements = list(map(lambda x:x['d'], data['data'] ))
|
|
60
|
+
self.etf = pd.DataFrame(list_elements)
|
|
61
|
+
self.etf.columns = json.loads(data_query)['columns']
|
|
62
|
+
self.etf = self.etf[self.etf['name'].str.contains('\\.')!=True]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def moneystring( self, money):
|
|
66
|
+
"""
|
|
67
|
+
Convert money to string with unit
|
|
68
|
+
Parameters:
|
|
69
|
+
- money: money to convert
|
|
70
|
+
Return: string with unit
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if money > 1_000_000_000_000:
|
|
74
|
+
money_str = f"{round(money / 1_000_000_000_000, 2)} Trillion"
|
|
75
|
+
elif money>1_000_000_000:
|
|
76
|
+
money_str = f"{round(money / 1_000_000_000, 2)} Billion"
|
|
77
|
+
else:
|
|
78
|
+
money_str = f"{round(money / 1_000_000, 2)} Million"
|
|
79
|
+
return money_str
|
|
80
|
+
|
|
39
81
|
|
|
40
82
|
def get_one_stock_info(self, ticker):
|
|
83
|
+
"""
|
|
84
|
+
Get info about one stock
|
|
85
|
+
Parameters:
|
|
86
|
+
- ticker: ticker of the stock
|
|
87
|
+
Return: type dictioanry
|
|
88
|
+
"""
|
|
89
|
+
|
|
41
90
|
ticker = ticker.upper()
|
|
42
91
|
one_row = self.stock.loc[self.stock['name']==ticker,].iloc[0]
|
|
43
92
|
tsec = self.stock.loc[self.stock['sector']==one_row['sector']].reset_index(drop=True)
|
|
44
93
|
tind = self.stock.loc[self.stock['industry']== one_row['industry']].reset_index(drop=True)
|
|
45
|
-
if one_row['market_cap_basic'] <100_000_000:
|
|
46
|
-
market_cap = f"💲{round(one_row['market_cap_basic']/1_000_000, 2):,} Million"
|
|
47
|
-
else:
|
|
48
|
-
market_cap = f"💲{round(one_row['market_cap_basic']/1_000_000):,} Million"
|
|
49
94
|
return({'ticker': one_row['name'],
|
|
50
95
|
'price': one_row['close'],
|
|
51
96
|
'market_cap': one_row['market_cap_basic'] ,
|
|
52
97
|
'n_emp': one_row['number_of_employees'],
|
|
53
|
-
'market_cap_text':
|
|
98
|
+
'market_cap_text': self.moneystring(one_row['market_cap_basic']),
|
|
54
99
|
'name': one_row['description'],
|
|
55
100
|
'sector': one_row['sector'],
|
|
56
101
|
'industry': one_row['industry'],
|
|
@@ -61,66 +106,104 @@ class Tw:
|
|
|
61
106
|
|
|
62
107
|
|
|
63
108
|
def get_top_n_stocks_by_sector(self,percent=10):
|
|
64
|
-
|
|
65
|
-
|
|
109
|
+
"""
|
|
110
|
+
Get top n % stocks by sector
|
|
111
|
+
Parameters:
|
|
112
|
+
- percent: percent of stocks to return
|
|
113
|
+
Return: Pandas DataFrame
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
return (
|
|
66
117
|
self.stock.groupby('sector')
|
|
67
118
|
.apply(lambda x: x.nlargest(int(len(x) * round((percent/100),2) ), 'market_cap_basic'))
|
|
68
119
|
.reset_index(drop=True)
|
|
69
|
-
|
|
120
|
+
|
|
70
121
|
)
|
|
71
|
-
|
|
122
|
+
|
|
72
123
|
|
|
73
124
|
def get_plotly_title(self, ticker):
|
|
125
|
+
"""
|
|
126
|
+
Get plotly title for stock or crypto
|
|
127
|
+
Parameters:
|
|
128
|
+
- ticker: ticker of the stock
|
|
129
|
+
Return: Summary of the stock or crypto to be used as plotly title
|
|
130
|
+
"""
|
|
74
131
|
|
|
75
132
|
if '-USD' in ticker:
|
|
133
|
+
# crypto
|
|
76
134
|
coin = self.crypto.loc[self.crypto['ticker']==ticker].iloc[0]
|
|
77
|
-
plotly_title = f"{coin['base_currency_desc']} ({coin['base_currency']})<br>💲{
|
|
135
|
+
plotly_title = f"{coin['base_currency_desc']} ({coin['base_currency']})<br>💲{self.moneystring(coin['market_cap_calc'])} | {', '.join(coin['crypto_common_categories'])}"
|
|
136
|
+
|
|
137
|
+
elif ticker in self.etf['name'].tolist() :
|
|
138
|
+
# ETF
|
|
139
|
+
t = self.etf.loc[self.etf['name']==ticker].iloc[0]
|
|
140
|
+
plotly_title = f"{t['description']} ({t['name']}) | 💲{round(t['close'], 2)} <br>AUM:💲{self.moneystring(t['aum'])} | {t['focus.tr']} | Expense ratio {t['expense_ratio']}"
|
|
78
141
|
else:
|
|
142
|
+
# stock
|
|
79
143
|
t = self.get_one_stock_info(ticker)
|
|
80
|
-
plotly_title = f"{t['name']} ({t['ticker']}) | 💲{round(t['price'], 2)} | {t['sector']} | {t['industry']} <br
|
|
144
|
+
plotly_title = f"{t['name']} ({t['ticker']}) | 💲{round(t['price'], 2)} | {t['sector']} | {t['industry']} <br>💲{t['market_cap_text']} | 👨💼 {round(t['n_emp']):,} <br>Sector location: {t['sec_loc']} | Industry location: {t['ind_loc']}"
|
|
81
145
|
return(plotly_title)
|
|
82
146
|
|
|
147
|
+
|
|
83
148
|
def get_sec_plot(self, ticker):
|
|
149
|
+
"""
|
|
150
|
+
Get plotly figure for a ticker showing the sector location of the stock
|
|
151
|
+
Parameters:
|
|
152
|
+
- ticker: ticker of the stock
|
|
153
|
+
Return: plotly figure showing the sector location of the stock
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
if ticker in self.etf['name'].tolist():
|
|
157
|
+
return f"{ticker} is ETF use get_etf_plot"
|
|
84
158
|
row_df = self.stock.loc[self.stock['name']==ticker]
|
|
85
159
|
row_df.rename(columns = {'description': 'Company'}, inplace=True)
|
|
86
160
|
secdf = self.stock.loc[ (self.stock['sector'] ==row_df['sector'].iloc[0] ) ].reset_index(drop=True)
|
|
87
|
-
if row_df['market_cap_basic'].iloc[0] <100_000_000:
|
|
88
|
-
market_cap = f"💲{round(row_df['market_cap_basic'].iloc[0]/1_000_000, 2):,} Million"
|
|
89
|
-
else:
|
|
90
|
-
market_cap = f"💲{round(row_df['market_cap_basic'].iloc[0]/1_000_000):,} Million"
|
|
91
|
-
|
|
92
|
-
|
|
93
161
|
secdf.rename(columns = {'description': 'Company'}, inplace=True)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
fig = px.bar(secdf, x='name', y='market_cap_basic', title = self.get_plotly_title(ticker), labels={'market_cap_basic':'Market kapitalization'}, text='Company')
|
|
98
|
-
|
|
99
|
-
fig.add_annotation( x=row_df['name'].iloc[0], y=row_df['market_cap_basic'].iloc[0], text= f"{market_cap}", showarrow=True, align="center", bordercolor="#c7c7c7", font=dict(family="Courier New, monospace", size=16, color="#214e34" ), borderwidth=2, borderpad=4, bgcolor="#f4fdff", opacity=0.8, arrowhead=2, arrowsize=1, arrowwidth=1, ax=65,ay=-45)
|
|
162
|
+
fig = px.bar(secdf, x='name', y='market_cap_basic', title = f"{row_df['Company'].iloc[0]} ({row_df['name'].iloc[0]})<br>{row_df['sector'].iloc[0]} | {row_df['industry'].iloc[0]}", labels={'market_cap_basic':'Market kapitalization'}, text='Company')
|
|
163
|
+
fig = px.bar(secdf, x='name', y='market_cap_basic', title = self.get_plotly_title(ticker), labels={'market_cap_basic':'Market Capitalization'}, text='Company')
|
|
164
|
+
fig.add_annotation( x=row_df['name'].iloc[0], y=row_df['market_cap_basic'].iloc[0], text= f"{self.moneystring(row_df['market_cap_basic'].iloc[0])}", showarrow=True, align="center", bordercolor="#c7c7c7", font=dict(family="Courier New, monospace", size=16, color="#214e34" ), borderwidth=2, borderpad=4, bgcolor="#f4fdff", opacity=0.8, arrowhead=2, arrowsize=1, arrowwidth=1, ax=65,ay=-45)
|
|
100
165
|
fig.update_layout(showlegend=False, plot_bgcolor='white', height=600)
|
|
101
166
|
return (fig)
|
|
102
167
|
|
|
168
|
+
|
|
103
169
|
def get_ind_plot(self, ticker):
|
|
170
|
+
"""
|
|
171
|
+
Get plotly figure for a ticker showing the industry location of the stock
|
|
172
|
+
Parameters:
|
|
173
|
+
- ticker: ticker of the stock
|
|
174
|
+
Return: plotly figure showing the industry location of the stock
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
if ticker in self.etf['name'].tolist():
|
|
178
|
+
return f"{ticker} is ETF use get_etf_plot"
|
|
104
179
|
row_df = self.stock.loc[self.stock['name']==ticker]
|
|
105
180
|
inddf = self.stock.loc[ (self.stock['industry'] ==row_df['industry'].iloc[0] ) ].reset_index(drop=True)
|
|
106
181
|
row_df.rename(columns = {'description': 'Company'}, inplace=True)
|
|
107
|
-
|
|
108
|
-
if row_df['market_cap_basic'].iloc[0] <100_000_000:
|
|
109
|
-
market_cap = f"💲{round(row_df['market_cap_basic'].iloc[0]/1_000_000, 2):,} Million"
|
|
110
|
-
else:
|
|
111
|
-
market_cap = f"💲{round(row_df['market_cap_basic'].iloc[0]/1_000_000):,} Million"
|
|
112
|
-
|
|
113
182
|
inddf.rename(columns = {'description': 'Company'}, inplace=True)
|
|
114
|
-
|
|
115
|
-
fig
|
|
183
|
+
fig = px.bar(inddf, x='name', y='market_cap_basic', title = self.get_plotly_title(ticker), labels={'market_cap_basic':'Market Capitalization'}, text='Company')
|
|
184
|
+
fig.add_annotation( x=row_df['name'].iloc[0], y=row_df['market_cap_basic'].iloc[0], text= f"{self.moneystring(row_df['market_cap_basic'].iloc[0])}", showarrow=True, align="center", bordercolor="#c7c7c7", font=dict(family="Courier New, monospace", size=16, color="#214e34" ), borderwidth=2, borderpad=4, bgcolor="#f4fdff", opacity=0.8, arrowhead=2, arrowsize=1, arrowwidth=1, ax=65,ay=-45)
|
|
185
|
+
fig.update_layout(showlegend=False, plot_bgcolor='white', height=600)
|
|
186
|
+
return (fig)
|
|
116
187
|
|
|
117
188
|
|
|
118
|
-
|
|
189
|
+
def get_etf_plot(self, ticker):
|
|
190
|
+
"""
|
|
191
|
+
Get plotly figure for a ETF showing the ETF location with the same focus
|
|
192
|
+
Parameters:
|
|
193
|
+
- ticker: ticker of the ETF
|
|
194
|
+
Return: plotly figure showing the ETF location with the same focus
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
row_df = self.etf.loc[self.etf['name']==ticker]
|
|
198
|
+
focdf = self.etf.loc[ (self.etf['focus.tr'] ==row_df['focus.tr'].iloc[0] ) ].reset_index(drop=True)
|
|
199
|
+
fig = px.bar(focdf, x='name', y='aum', title = self.get_plotly_title(ticker), labels={'aum':'Assets Under Management'}, text='description')
|
|
200
|
+
fig.add_annotation( x=row_df['name'].iloc[0], y=row_df['aum'].iloc[0], text= f"{self.moneystring(row_df['aum'].iloc[0])}", showarrow=True, align="center", bordercolor="#c7c7c7", font=dict(family="Courier New, monospace", size=16, color="#214e34" ), borderwidth=2, borderpad=4, bgcolor="#f4fdff", opacity=0.8, arrowhead=2, arrowsize=1, arrowwidth=1, ax=65,ay=-45)
|
|
119
201
|
fig.update_layout(showlegend=False, plot_bgcolor='white', height=600)
|
|
120
202
|
return (fig)
|
|
121
203
|
|
|
122
204
|
|
|
123
205
|
|
|
206
|
+
|
|
124
207
|
#tw = Tw()
|
|
125
208
|
#print(tw.stock.head(1).T)
|
|
126
209
|
#print(tw.crypto.head(1).T)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: goldhand
|
|
3
|
-
Version: 15.
|
|
3
|
+
Version: 15.7
|
|
4
4
|
Summary: A package working with financial data
|
|
5
5
|
Home-page: https://github.com/misrori/goldhand
|
|
6
6
|
Author: Mihaly
|
|
@@ -25,7 +25,6 @@ pip install goldhand
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
# TradingView
|
|
30
29
|
|
|
31
30
|
|
|
@@ -40,11 +39,15 @@ tw.stock
|
|
|
40
39
|
|
|
41
40
|
# data frame of the top 300 crypto currency
|
|
42
41
|
tw.crypto
|
|
42
|
+
|
|
43
|
+
# data frame of the top 3000 etf
|
|
44
|
+
tw.etf
|
|
45
|
+
|
|
43
46
|
```
|
|
44
47
|
|
|
45
48
|
```python
|
|
46
49
|
# Get a plot of the stock to see the location in the sector
|
|
47
|
-
tw.get_sec_plot('AMD')
|
|
50
|
+
tw.get_sec_plot('AMD').show()
|
|
48
51
|
|
|
49
52
|
```
|
|
50
53
|

|
|
@@ -52,7 +55,7 @@ tw.get_sec_plot('AMD')
|
|
|
52
55
|
|
|
53
56
|
```python
|
|
54
57
|
# Get a plot of the stock to see the location in the industry
|
|
55
|
-
tw.get_sec_plot('AMD')
|
|
58
|
+
tw.get_sec_plot('AMD').show()
|
|
56
59
|
|
|
57
60
|
```
|
|
58
61
|

|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
goldhand/__init__.py,sha256=2D68nqSZuv6sqyLJbOXnWIeeFpNgpYc90rHa2Fo70lk,152
|
|
2
|
+
goldhand/backtest.py,sha256=dfrm2Mntp-qUCXsmayExuQgLur5U30QZ6wYJLt7Wq68,7576
|
|
3
|
+
goldhand/helpers.py,sha256=l9yn0kVTiwfUR8sI5nH1QFx6dYikaUQgRA227Ox7hs0,6130
|
|
4
|
+
goldhand/stocks.py,sha256=knYaAD0pkQepPfDMqHSXZvx4oWhVdts57WRaJQYqF18,13792
|
|
5
|
+
goldhand/strategy_goldhand_line.py,sha256=h2yAA4XGTxQ1qaN1EPVwCpKDEDiWVy2DMZ0ssFVXN88,12315
|
|
6
|
+
goldhand/strategy_rsi.py,sha256=yS6YZ1zeKURvy9wELWXX6clcr0AM-ptPgILgM8hXgPA,10485
|
|
7
|
+
goldhand/tw.py,sha256=4W8xmCbDkI4UGRVxgA7aNtyddXOLTprNgUihwSh0-lI,12557
|
|
8
|
+
goldhand-15.7.dist-info/METADATA,sha256=4o42aSMO8TNT5AzuilA7sWAQ0SQFncDpSq6zpNUuiwI,2007
|
|
9
|
+
goldhand-15.7.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
10
|
+
goldhand-15.7.dist-info/top_level.txt,sha256=siEJ2_a_Fx_7hqRI4Ms6SzCelbXrK_1H_eOF8KAaMdA,9
|
|
11
|
+
goldhand-15.7.dist-info/RECORD,,
|
goldhand-15.4.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
goldhand/__init__.py,sha256=2D68nqSZuv6sqyLJbOXnWIeeFpNgpYc90rHa2Fo70lk,152
|
|
2
|
-
goldhand/backtest.py,sha256=haK0c7wbUv02DUU5LcrvDSC1i0h1nDX7HHYzD9abhb8,6456
|
|
3
|
-
goldhand/helpers.py,sha256=l9yn0kVTiwfUR8sI5nH1QFx6dYikaUQgRA227Ox7hs0,6130
|
|
4
|
-
goldhand/stocks.py,sha256=rQmcBo6cwu8mXmVSqT9PJhHCHJ9X1laPwhqX5sDHS6E,11886
|
|
5
|
-
goldhand/strategy_goldhand_line.py,sha256=TEb16PvaoX07bjtTRVMYlz2ilJ_YlQ2Q6_o-sAC60QQ,12056
|
|
6
|
-
goldhand/strategy_rsi.py,sha256=JhmefDm-PXmIT2tQNeLFjaDb0SpR1yln33wuDPt1Zp4,9540
|
|
7
|
-
goldhand/tw.py,sha256=Rv0oy9QjX3FC_7Rxiv_oUftQhkvrq65LUgbrhwXhWDE,8460
|
|
8
|
-
goldhand-15.4.dist-info/METADATA,sha256=OAiDn38e4u0l9lorFyMv1yali-oDlvdXL1upFholHRE,1952
|
|
9
|
-
goldhand-15.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
10
|
-
goldhand-15.4.dist-info/top_level.txt,sha256=siEJ2_a_Fx_7hqRI4Ms6SzCelbXrK_1H_eOF8KAaMdA,9
|
|
11
|
-
goldhand-15.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|