financegy 1.5__py3-none-any.whl → 3.0__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.
- financegy/__init__.py +31 -6
- financegy/cache/cache_manager.py +11 -13
- financegy/config.py +2 -2
- financegy/core/parser.py +324 -104
- financegy/helpers/safe_text.py +3 -0
- financegy/helpers/to_float.py +7 -0
- financegy/modules/portfolio.py +157 -0
- financegy/modules/securities.py +318 -6
- financegy/utils/utils.py +20 -11
- financegy-3.0.dist-info/METADATA +201 -0
- financegy-3.0.dist-info/RECORD +15 -0
- {financegy-1.5.dist-info → financegy-3.0.dist-info}/WHEEL +1 -1
- financegy-1.5.dist-info/METADATA +0 -141
- financegy-1.5.dist-info/RECORD +0 -12
- {financegy-1.5.dist-info → financegy-3.0.dist-info}/licenses/LICENSE +0 -0
- {financegy-1.5.dist-info → financegy-3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
2
|
+
from financegy.modules import securities
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _to_decimal(value, field_name="value"):
|
|
6
|
+
"""
|
|
7
|
+
Convert strings/numbers to Decimal safely.
|
|
8
|
+
Handles commas like "3,000.0".
|
|
9
|
+
"""
|
|
10
|
+
try:
|
|
11
|
+
clean_value = str(value).replace(",", "")
|
|
12
|
+
return Decimal(clean_value)
|
|
13
|
+
except (InvalidOperation, TypeError) as e:
|
|
14
|
+
raise ValueError(f"Invalid {field_name}: {value}") from e
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def calculate_position_value(symbol: str, shares):
|
|
18
|
+
try:
|
|
19
|
+
shares = _to_decimal(shares, "shares")
|
|
20
|
+
if shares <= 0:
|
|
21
|
+
raise ValueError("Shares must be greater than zero")
|
|
22
|
+
|
|
23
|
+
last_trade = securities.get_recent_trade(symbol)
|
|
24
|
+
last_trade_price = _to_decimal(
|
|
25
|
+
last_trade.get("last_trade_price"), "last_trade_price"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
position_value = last_trade_price * shares
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"last_trade": last_trade,
|
|
32
|
+
"position_value": str(position_value),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
36
|
+
raise ValueError(f"[calculate_position_value] Invalid input or data: {e}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def calculate_position_return(symbol: str, shares, purchase_price):
|
|
40
|
+
try:
|
|
41
|
+
shares = _to_decimal(shares, "shares")
|
|
42
|
+
purchase_price = _to_decimal(purchase_price, "purchase_price")
|
|
43
|
+
|
|
44
|
+
if shares <= 0:
|
|
45
|
+
raise ValueError("Shares must be greater than zero")
|
|
46
|
+
if purchase_price <= 0:
|
|
47
|
+
raise ValueError("Purchase price must be greater than zero")
|
|
48
|
+
|
|
49
|
+
last_trade = securities.get_recent_trade(symbol)
|
|
50
|
+
last_trade_price = _to_decimal(
|
|
51
|
+
last_trade.get("last_trade_price"), "last_trade_price"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
position_return = (last_trade_price - purchase_price) * shares
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"last_trade": last_trade,
|
|
58
|
+
"position_return": str(position_return),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
62
|
+
raise ValueError(f"[calculate_position_return] Invalid input or data: {e}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def calculate_position_return_percent(symbol, shares, purchase_price):
|
|
66
|
+
try:
|
|
67
|
+
shares = _to_decimal(shares, "shares")
|
|
68
|
+
purchase_price = _to_decimal(purchase_price, "purchase_price")
|
|
69
|
+
|
|
70
|
+
if shares <= 0:
|
|
71
|
+
raise ValueError("Shares must be greater than zero")
|
|
72
|
+
if purchase_price <= 0:
|
|
73
|
+
raise ValueError("Purchase price must be greater than zero")
|
|
74
|
+
|
|
75
|
+
last_trade = securities.get_recent_trade(symbol)
|
|
76
|
+
last_trade_price = _to_decimal(
|
|
77
|
+
last_trade.get("last_trade_price"), "last_trade_price"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return_percent = (
|
|
81
|
+
(last_trade_price - purchase_price) / purchase_price
|
|
82
|
+
) * Decimal("100")
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"last_trade": last_trade,
|
|
86
|
+
"position_return_percent": str(round(return_percent, 2)),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"[calculate_position_return_percent] Invalid input or data: {e}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def calculate_portfolio_summary(positions: list[dict]):
|
|
96
|
+
try:
|
|
97
|
+
total_invested = Decimal("0")
|
|
98
|
+
total_value = Decimal("0")
|
|
99
|
+
detailed_positions = []
|
|
100
|
+
|
|
101
|
+
for pos in positions:
|
|
102
|
+
symbol = pos["symbol"]
|
|
103
|
+
shares = _to_decimal(pos.get("shares"), f"shares for {symbol}")
|
|
104
|
+
purchase_price = _to_decimal(
|
|
105
|
+
pos.get("purchase_price"), f"purchase_price for {symbol}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if shares <= 0:
|
|
109
|
+
raise ValueError(f"Invalid shares for {symbol}")
|
|
110
|
+
if purchase_price <= 0:
|
|
111
|
+
raise ValueError(f"Invalid purchase price for {symbol}")
|
|
112
|
+
|
|
113
|
+
last_trade = securities.get_recent_trade(symbol)
|
|
114
|
+
last_trade_price = _to_decimal(
|
|
115
|
+
last_trade.get("last_trade_price"), f"last_trade_price for {symbol}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
invested = shares * purchase_price
|
|
119
|
+
current_value = shares * last_trade_price
|
|
120
|
+
gain_loss = current_value - invested
|
|
121
|
+
|
|
122
|
+
total_invested += invested
|
|
123
|
+
total_value += current_value
|
|
124
|
+
|
|
125
|
+
detailed_positions.append(
|
|
126
|
+
{
|
|
127
|
+
"symbol": symbol,
|
|
128
|
+
"shares": str(shares),
|
|
129
|
+
"purchase_price": str(purchase_price),
|
|
130
|
+
"last_trade_price": str(last_trade_price),
|
|
131
|
+
"invested": str(invested),
|
|
132
|
+
"current_value": str(current_value),
|
|
133
|
+
"gain_loss": str(gain_loss),
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
total_gain_loss = total_value - total_invested
|
|
138
|
+
|
|
139
|
+
if total_invested != 0:
|
|
140
|
+
return_percent = (
|
|
141
|
+
total_gain_loss / total_invested * Decimal("100")
|
|
142
|
+
).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
143
|
+
else:
|
|
144
|
+
return_percent = Decimal("0.00")
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"summary": {
|
|
148
|
+
"total_invested": str(total_invested),
|
|
149
|
+
"current_value": str(total_value),
|
|
150
|
+
"total_gain_loss": str(total_gain_loss),
|
|
151
|
+
"return_percent": str(return_percent),
|
|
152
|
+
},
|
|
153
|
+
"positions": detailed_positions,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
157
|
+
raise ValueError(f"[calculate_portfolio_summary] Invalid portfolio data: {e}")
|
financegy/modules/securities.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from financegy.core import request_handler, parser
|
|
2
2
|
from financegy.cache import cache_manager
|
|
3
|
+
import math
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def get_securities(use_cache=True):
|
|
5
7
|
"""Get names of all currently traded securities"""
|
|
@@ -16,7 +18,8 @@ def get_securities(use_cache=True):
|
|
|
16
18
|
|
|
17
19
|
cache_manager.save_cache(func_name, html)
|
|
18
20
|
|
|
19
|
-
return parser.parse_get_securities(html)
|
|
21
|
+
return parser.parse_get_securities(html)
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
def get_security_by_symbol(symbol: str, use_cache=True):
|
|
22
25
|
"""Get the security details by its ticker symbol"""
|
|
@@ -26,11 +29,16 @@ def get_security_by_symbol(symbol: str, use_cache=True):
|
|
|
26
29
|
symbol = symbol.strip().upper()
|
|
27
30
|
|
|
28
31
|
return next(
|
|
29
|
-
(
|
|
32
|
+
(
|
|
33
|
+
security["name"]
|
|
34
|
+
for security in securities
|
|
35
|
+
if security["symbol"].upper() == symbol
|
|
36
|
+
),
|
|
30
37
|
None,
|
|
31
38
|
)
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
|
|
41
|
+
def get_security_recent_year(symbol: str, use_cache=True):
|
|
34
42
|
"""Get the most recent year's trade data for any of the traded securities"""
|
|
35
43
|
|
|
36
44
|
func_name = "get_security_recent_year"
|
|
@@ -50,11 +58,12 @@ def get_security_recent_year(symbol:str, use_cache=True):
|
|
|
50
58
|
|
|
51
59
|
return parser.parse_get_security_recent_year(html)
|
|
52
60
|
|
|
61
|
+
|
|
53
62
|
def get_recent_trade(symbol: str, use_cache=True):
|
|
54
63
|
"""Get the most recent trade data for any of the traded securities"""
|
|
55
64
|
|
|
56
65
|
func_name = "get_recent_trade"
|
|
57
|
-
|
|
66
|
+
|
|
58
67
|
security_name = get_security_by_symbol(symbol)
|
|
59
68
|
security_name = security_name.lower().replace(" ", "-")
|
|
60
69
|
|
|
@@ -70,6 +79,197 @@ def get_recent_trade(symbol: str, use_cache=True):
|
|
|
70
79
|
|
|
71
80
|
return parser.parse_get_recent_trade(html)
|
|
72
81
|
|
|
82
|
+
|
|
83
|
+
def get_previous_close(symbol: str, use_cache=True):
|
|
84
|
+
"""Get the most recent closing price for any of the traded securities"""
|
|
85
|
+
|
|
86
|
+
func_name = "get_previous_close"
|
|
87
|
+
|
|
88
|
+
security_name = get_security_by_symbol(symbol)
|
|
89
|
+
security_name = security_name.lower().replace(" ", "-")
|
|
90
|
+
|
|
91
|
+
if use_cache:
|
|
92
|
+
cached = cache_manager.load_cache(func_name, symbol)
|
|
93
|
+
if cached:
|
|
94
|
+
return parser.parse_get_previous_close(cached)
|
|
95
|
+
|
|
96
|
+
path = "/security/" + security_name
|
|
97
|
+
html = request_handler.fetch_page(path)
|
|
98
|
+
|
|
99
|
+
cache_manager.save_cache(func_name, html, symbol)
|
|
100
|
+
|
|
101
|
+
return parser.parse_get_previous_close(html)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_price_change(symbol: str, use_cache=True):
|
|
105
|
+
"""Get absolute price difference between the most recent trade and the previous session close."""
|
|
106
|
+
|
|
107
|
+
func_name = "get_price_change"
|
|
108
|
+
|
|
109
|
+
security_name = get_security_by_symbol(symbol)
|
|
110
|
+
security_name = security_name.lower().replace(" ", "-")
|
|
111
|
+
|
|
112
|
+
if use_cache:
|
|
113
|
+
cached = cache_manager.load_cache(func_name, symbol)
|
|
114
|
+
if cached:
|
|
115
|
+
return parser.parse_get_price_change(cached)
|
|
116
|
+
|
|
117
|
+
path = "/security/" + security_name
|
|
118
|
+
html = request_handler.fetch_page(path)
|
|
119
|
+
|
|
120
|
+
cache_manager.save_cache(func_name, html, symbol)
|
|
121
|
+
|
|
122
|
+
return parser.parse_get_price_change(html)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_price_change_percent(symbol: str, use_cache=True):
|
|
126
|
+
"""Get the percentage price change between the most recent trade and the previous session close."""
|
|
127
|
+
|
|
128
|
+
func_name = "get_price_change_percent"
|
|
129
|
+
|
|
130
|
+
security_name = get_security_by_symbol(symbol)
|
|
131
|
+
security_name = security_name.lower().replace(" ", "-")
|
|
132
|
+
|
|
133
|
+
if use_cache:
|
|
134
|
+
cached = cache_manager.load_cache(func_name, symbol)
|
|
135
|
+
if cached:
|
|
136
|
+
return parser.parse_get_price_change_percent(cached)
|
|
137
|
+
|
|
138
|
+
path = "/security/" + security_name
|
|
139
|
+
html = request_handler.fetch_page(path)
|
|
140
|
+
|
|
141
|
+
cache_manager.save_cache(func_name, html, symbol)
|
|
142
|
+
|
|
143
|
+
return parser.parse_get_price_change_percent(html)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_latest_session_for_symbol(symbol: str, use_cache: bool = True):
|
|
147
|
+
"""
|
|
148
|
+
Fetch the security page, parse the most recent trade, return its session as int.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
func_name = "get_latest_session_for_symbol"
|
|
152
|
+
symbol = symbol.strip().upper()
|
|
153
|
+
|
|
154
|
+
security_name = get_security_by_symbol(symbol)
|
|
155
|
+
security_name = security_name.lower().replace(" ", "-")
|
|
156
|
+
|
|
157
|
+
if use_cache:
|
|
158
|
+
cached = cache_manager.load_cache(func_name, symbol)
|
|
159
|
+
if cached:
|
|
160
|
+
return parser.parse_get_recent_trade(cached)
|
|
161
|
+
|
|
162
|
+
path = "/security/" + security_name
|
|
163
|
+
html = request_handler.fetch_page(path)
|
|
164
|
+
|
|
165
|
+
cache_manager.save_cache(func_name, html, symbol)
|
|
166
|
+
|
|
167
|
+
recent = parser.parse_get_recent_trade(html)
|
|
168
|
+
|
|
169
|
+
if not recent or not recent.get("session"):
|
|
170
|
+
raise ValueError(f"Could not determine latest session for {symbol}")
|
|
171
|
+
|
|
172
|
+
return recent
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_sessions_average_price(
|
|
176
|
+
symbol: str, session_start: str, session_end: str, use_cache=True
|
|
177
|
+
):
|
|
178
|
+
"""Get the average last traded price of the security over a specified session range."""
|
|
179
|
+
|
|
180
|
+
func_name = "get_sessions_average_price"
|
|
181
|
+
|
|
182
|
+
start = int(session_start)
|
|
183
|
+
end = int(session_end)
|
|
184
|
+
symbol = symbol.strip().upper()
|
|
185
|
+
|
|
186
|
+
if end < start:
|
|
187
|
+
raise ValueError("session_end must be >= session_start")
|
|
188
|
+
|
|
189
|
+
prices_by_session: dict[int, float] = {}
|
|
190
|
+
|
|
191
|
+
for session in range(start, end + 1):
|
|
192
|
+
|
|
193
|
+
html = None
|
|
194
|
+
if use_cache:
|
|
195
|
+
html = cache_manager.load_cache(func_name, symbol, session)
|
|
196
|
+
|
|
197
|
+
if not html:
|
|
198
|
+
path = f"/financial_session/{session}/"
|
|
199
|
+
html = request_handler.fetch_page(path)
|
|
200
|
+
cache_manager.save_cache(func_name, html, symbol, session)
|
|
201
|
+
|
|
202
|
+
price = parser.parse_get_sessions_average_price(symbol, html)
|
|
203
|
+
|
|
204
|
+
if price is None:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
prices_by_session[session] = price
|
|
208
|
+
|
|
209
|
+
if not prices_by_session:
|
|
210
|
+
raise ValueError(f"No prices found for {symbol} in sessions {start}..{end}")
|
|
211
|
+
|
|
212
|
+
avg = round(sum(prices_by_session.values()) / len(prices_by_session), 2)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"symbol": symbol,
|
|
216
|
+
"session_start": start,
|
|
217
|
+
"session_end": end,
|
|
218
|
+
"observations": len(prices_by_session),
|
|
219
|
+
"average_price": avg,
|
|
220
|
+
"prices_by_session": prices_by_session,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_average_price(symbol: str, session_number: int, use_cache=True):
|
|
225
|
+
"""Average LTP over the most recent `session_number` sessions (ending at latest session)."""
|
|
226
|
+
|
|
227
|
+
func_name = "get_average_price"
|
|
228
|
+
symbol = symbol.strip().upper()
|
|
229
|
+
|
|
230
|
+
if session_number <= 0:
|
|
231
|
+
raise ValueError("session_number must be a positive integer")
|
|
232
|
+
|
|
233
|
+
latest = get_latest_session_for_symbol(symbol, use_cache=use_cache)
|
|
234
|
+
end = int(latest["session"])
|
|
235
|
+
start = max(1, end - session_number + 1)
|
|
236
|
+
|
|
237
|
+
prices_by_session: dict[int, float] = {}
|
|
238
|
+
|
|
239
|
+
for session in range(start, end + 1):
|
|
240
|
+
html = None
|
|
241
|
+
|
|
242
|
+
if use_cache:
|
|
243
|
+
html = cache_manager.load_cache(func_name, symbol, session)
|
|
244
|
+
|
|
245
|
+
if not html:
|
|
246
|
+
path = f"/financial_session/{session}/"
|
|
247
|
+
html = request_handler.fetch_page(path)
|
|
248
|
+
cache_manager.save_cache(func_name, html, symbol, session)
|
|
249
|
+
|
|
250
|
+
price = parser.parse_get_average_price(symbol, html)
|
|
251
|
+
if price is None:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
prices_by_session[session] = price
|
|
255
|
+
|
|
256
|
+
if not prices_by_session:
|
|
257
|
+
raise ValueError(f"No prices found for {symbol} in sessions {start}..{end}")
|
|
258
|
+
|
|
259
|
+
avg = round(sum(prices_by_session.values()) / len(prices_by_session), 2)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"symbol": symbol,
|
|
263
|
+
"latest_session": latest,
|
|
264
|
+
"session_number_requested": session_number,
|
|
265
|
+
"session_start": start,
|
|
266
|
+
"session_end": end,
|
|
267
|
+
"observations": len(prices_by_session),
|
|
268
|
+
"average_price": avg,
|
|
269
|
+
"prices_by_session": prices_by_session,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
73
273
|
def get_session_trades(session: str, use_cache=True):
|
|
74
274
|
"""Get the session trade data for all the available securities"""
|
|
75
275
|
|
|
@@ -87,6 +287,7 @@ def get_session_trades(session: str, use_cache=True):
|
|
|
87
287
|
|
|
88
288
|
return parser.parse_get_session_trades(html)
|
|
89
289
|
|
|
290
|
+
|
|
90
291
|
def get_security_session_trade(symbol: str, session: str, use_cache=True):
|
|
91
292
|
"""Get the session trade data for a given security"""
|
|
92
293
|
|
|
@@ -106,6 +307,114 @@ def get_security_session_trade(symbol: str, session: str, use_cache=True):
|
|
|
106
307
|
|
|
107
308
|
return parser.parse_get_security_session_trade(symbol, html)
|
|
108
309
|
|
|
310
|
+
|
|
311
|
+
def get_sessions_volatility(symbol: str, session_number: int, use_cache=True):
|
|
312
|
+
"""
|
|
313
|
+
Volatility over the last `sessions` observed prices, ending at the latest session.
|
|
314
|
+
Uses log returns and returns weekly volatility (std dev of weekly returns).
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
symbol = symbol.strip().upper()
|
|
318
|
+
|
|
319
|
+
if session_number <= 1:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
"session_number must be >= 2 (need at least 2 prices to compute returns)."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
latest = get_latest_session_for_symbol(symbol, use_cache=use_cache)
|
|
325
|
+
latest_session = int(latest["session"])
|
|
326
|
+
|
|
327
|
+
target_prices = session_number
|
|
328
|
+
prices: list[float] = []
|
|
329
|
+
prices_by_session: dict[int, float] = {}
|
|
330
|
+
|
|
331
|
+
func_name = "get_sessions_volatility"
|
|
332
|
+
|
|
333
|
+
session = latest_session
|
|
334
|
+
safety_limit = latest_session - (session_number * 5)
|
|
335
|
+
|
|
336
|
+
while session >= 1 and session >= safety_limit and len(prices) < target_prices:
|
|
337
|
+
|
|
338
|
+
html = None
|
|
339
|
+
if use_cache:
|
|
340
|
+
html = cache_manager.load_cache(func_name, symbol, session)
|
|
341
|
+
|
|
342
|
+
if not html:
|
|
343
|
+
path = f"/financial_session/{session}/"
|
|
344
|
+
html = request_handler.fetch_page(path)
|
|
345
|
+
cache_manager.save_cache(func_name, html, symbol, session)
|
|
346
|
+
|
|
347
|
+
price = parser.parse_get_session_ltp(symbol, html)
|
|
348
|
+
|
|
349
|
+
if price is not None:
|
|
350
|
+
prices.append(price)
|
|
351
|
+
prices_by_session[session] = price
|
|
352
|
+
|
|
353
|
+
session -= 1
|
|
354
|
+
|
|
355
|
+
if len(prices) < 2:
|
|
356
|
+
raise ValueError(
|
|
357
|
+
f"Not enough price data found for {symbol} to compute volatility."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
prices.reverse()
|
|
361
|
+
|
|
362
|
+
returns = []
|
|
363
|
+
for i in range(1, len(prices)):
|
|
364
|
+
prev_p = prices[i - 1]
|
|
365
|
+
cur_p = prices[i]
|
|
366
|
+
if prev_p and prev_p > 0 and cur_p and cur_p > 0:
|
|
367
|
+
returns.append(math.log(cur_p / prev_p))
|
|
368
|
+
|
|
369
|
+
if len(returns) < 2:
|
|
370
|
+
raise ValueError("Not enough valid returns to compute volatility.")
|
|
371
|
+
|
|
372
|
+
mean = sum(returns) / len(returns)
|
|
373
|
+
variance = sum((r - mean) ** 2 for r in returns) / (len(returns) - 1)
|
|
374
|
+
weekly_vol = round(math.sqrt(variance), 2)
|
|
375
|
+
|
|
376
|
+
annualized_vol = round(weekly_vol * math.sqrt(52), 2)
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
"symbol": symbol,
|
|
380
|
+
"latest_session": latest_session,
|
|
381
|
+
"requested_sessions": session_number,
|
|
382
|
+
"prices_found": len(prices),
|
|
383
|
+
"returns_count": len(returns),
|
|
384
|
+
"weekly_volatility": weekly_vol,
|
|
385
|
+
"annualized_volatility": annualized_vol,
|
|
386
|
+
"prices_by_session": dict(sorted(prices_by_session.items())),
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def get_ytd_high_low(symbol: str, use_cache: bool = True):
|
|
391
|
+
"""Return year-to-date highest and lowest traded prices for the security."""
|
|
392
|
+
|
|
393
|
+
func_name = "get_ytd_high_low"
|
|
394
|
+
symbol = symbol.strip().upper()
|
|
395
|
+
|
|
396
|
+
security_name = get_security_by_symbol(symbol)
|
|
397
|
+
security_name = security_name.lower().replace(" ", "-")
|
|
398
|
+
|
|
399
|
+
html = None
|
|
400
|
+
if use_cache:
|
|
401
|
+
html = cache_manager.load_cache(func_name, symbol)
|
|
402
|
+
|
|
403
|
+
if not html:
|
|
404
|
+
path = f"/security/{security_name}/"
|
|
405
|
+
html = request_handler.fetch_page(path)
|
|
406
|
+
cache_manager.save_cache(func_name, html, symbol)
|
|
407
|
+
|
|
408
|
+
result = parser.parse_get_ytd_high_low(html)
|
|
409
|
+
if not result:
|
|
410
|
+
raise ValueError(f"Could not compute YTD high/low for {symbol}")
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
"symbol": symbol,
|
|
414
|
+
**result,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
109
418
|
def get_trades_for_year(symbol: str, year: str, use_cache=True):
|
|
110
419
|
"""Get security trade information from a specific year"""
|
|
111
420
|
|
|
@@ -128,6 +437,7 @@ def get_trades_for_year(symbol: str, year: str, use_cache=True):
|
|
|
128
437
|
|
|
129
438
|
return parser.parse_get_trades_for_year(year, html)
|
|
130
439
|
|
|
440
|
+
|
|
131
441
|
def get_historical_trades(symbol: str, start_date: str, end_date: str, use_cache=True):
|
|
132
442
|
"""Get historical trade data for a date range"""
|
|
133
443
|
|
|
@@ -150,6 +460,7 @@ def get_historical_trades(symbol: str, start_date: str, end_date: str, use_cache
|
|
|
150
460
|
|
|
151
461
|
return parser.parse_get_historical_trades(start_date, end_date, html)
|
|
152
462
|
|
|
463
|
+
|
|
153
464
|
def search_securities(query: str, use_cache=True):
|
|
154
465
|
"""Search securities by symbol or name (partial match)"""
|
|
155
466
|
|
|
@@ -157,8 +468,9 @@ def search_securities(query: str, use_cache=True):
|
|
|
157
468
|
all_securities = get_securities()
|
|
158
469
|
|
|
159
470
|
matches = [
|
|
160
|
-
sec
|
|
471
|
+
sec
|
|
472
|
+
for sec in all_securities
|
|
161
473
|
if query in sec["symbol"].lower() or query in sec["name"].lower()
|
|
162
474
|
]
|
|
163
475
|
|
|
164
|
-
return matches
|
|
476
|
+
return matches
|
financegy/utils/utils.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pandas as pd
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
def to_dataframe(data: dict | list[dict]):
|
|
5
6
|
"""Output as Dataframe"""
|
|
6
7
|
|
|
@@ -8,13 +9,16 @@ def to_dataframe(data: dict | list[dict]):
|
|
|
8
9
|
raise TypeError("All items in the list must be dictionaries")
|
|
9
10
|
elif not isinstance(data, (dict, list)):
|
|
10
11
|
raise TypeError("data must be a dict or a list of dicts")
|
|
11
|
-
|
|
12
|
+
|
|
12
13
|
if isinstance(data, dict):
|
|
13
|
-
data = [data]
|
|
14
|
+
data = [data]
|
|
14
15
|
|
|
15
16
|
return pd.DataFrame(data)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
def save_to_csv(
|
|
20
|
+
data, filename: str = "output.csv", path: str = None, silent: bool = False
|
|
21
|
+
):
|
|
18
22
|
"""Save a list of dicts to CSV"""
|
|
19
23
|
|
|
20
24
|
if path is None:
|
|
@@ -28,13 +32,20 @@ def save_to_csv(data, filename: str = "output.csv", path: str = None):
|
|
|
28
32
|
|
|
29
33
|
df.to_csv(full_path, index=False)
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
if not silent:
|
|
36
|
+
print(f"\nSaved CSV to: {full_path}")
|
|
32
37
|
|
|
33
38
|
return True
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
def save_to_excel(
|
|
42
|
+
data: dict | list[dict],
|
|
43
|
+
filename: str = "output.xlsx",
|
|
44
|
+
path: str = None,
|
|
45
|
+
silent: bool = False,
|
|
46
|
+
):
|
|
36
47
|
"""Save data to an Excel spreadsheet."""
|
|
37
|
-
|
|
48
|
+
|
|
38
49
|
if path is None:
|
|
39
50
|
path = os.getcwd()
|
|
40
51
|
else:
|
|
@@ -43,10 +54,8 @@ def save_to_excel(data: dict | list[dict], filename: str = "output.xlsx", path:
|
|
|
43
54
|
full_path = os.path.join(path, filename)
|
|
44
55
|
df = to_dataframe(data)
|
|
45
56
|
df.to_excel(full_path, index=False)
|
|
46
|
-
|
|
47
|
-
print(f"Saved Excel Document to: {full_path}")
|
|
48
|
-
|
|
49
|
-
return True
|
|
50
|
-
|
|
51
57
|
|
|
58
|
+
if not silent:
|
|
59
|
+
print(f"\nSaved Excel Document to: {full_path}")
|
|
52
60
|
|
|
61
|
+
return True
|