fima 0.1.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.
- fima/Bonds.py +102 -0
- fima/Options.py +675 -0
- fima/__init__.py +9 -0
- fima-0.1.0.dist-info/METADATA +270 -0
- fima-0.1.0.dist-info/RECORD +11 -0
- fima-0.1.0.dist-info/WHEEL +5 -0
- fima-0.1.0.dist-info/licenses/LICENSE +21 -0
- fima-0.1.0.dist-info/top_level.txt +2 -0
- test/TestBonds.py +26 -0
- test/TestOptions.py +123 -0
- test/__init__.py +0 -0
fima/Bonds.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
from io import StringIO
|
|
6
|
+
import jdatetime as jd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_risk_free_rate() -> float:
|
|
10
|
+
all_bonds_without_coupons = get_all_bonds_without_coupons(deprecated=False)
|
|
11
|
+
all_t_notes = all_bonds_without_coupons[all_bonds_without_coupons['Ticker'].str.contains('سخاب|اخزا')].copy()
|
|
12
|
+
today_date = jd.date.today()
|
|
13
|
+
all_t_notes['DaysTillMaturity'] = (all_t_notes['MaturityDate'] - today_date).apply(lambda delta: delta.days)
|
|
14
|
+
last_traded_t_notes = all_t_notes[all_t_notes['LastTradedDate'] == all_t_notes['LastTradedDate'].max()]
|
|
15
|
+
rf = last_traded_t_notes['YTM'].mean()
|
|
16
|
+
return rf
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_all_bonds_without_coupons(deprecated: bool = True) -> pd.DataFrame:
|
|
20
|
+
url = "https://ifb.ir/YTM.aspx"
|
|
21
|
+
|
|
22
|
+
session = requests.Session()
|
|
23
|
+
response = session.get(url)
|
|
24
|
+
|
|
25
|
+
if response.status_code != 200:
|
|
26
|
+
print(f"Failed to access page. Status code: {response.status_code}")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
soup = BeautifulSoup(response.content, "html.parser")
|
|
30
|
+
table = soup.find("table", {"id": "ContentPlaceHolder1_grdytmforkhazaneh", "class": "KhazanehGrid"})
|
|
31
|
+
|
|
32
|
+
if table is None:
|
|
33
|
+
print("Table not found.")
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
df_list = pd.read_html(StringIO(str(table)))
|
|
37
|
+
bonds_without_coupons = df_list[0]
|
|
38
|
+
|
|
39
|
+
if ~deprecated:
|
|
40
|
+
bonds_without_coupons = bonds_without_coupons[bonds_without_coupons['YTM'] != 'سررسید شده'].copy()
|
|
41
|
+
|
|
42
|
+
bonds_without_coupons.columns = ['Index', 'Ticker', 'LastTradedPrice', 'LastTradedDate', 'MaturityDate', 'YTM', 'SimpleReturn']
|
|
43
|
+
|
|
44
|
+
bonds_without_coupons['LastTradedDate'] = \
|
|
45
|
+
bonds_without_coupons['LastTradedDate'].apply(lambda str_date: jd.date(year=int(str_date[:4]),
|
|
46
|
+
month=int(str_date[5:7]),
|
|
47
|
+
day=int(str_date[8:])))
|
|
48
|
+
|
|
49
|
+
bonds_without_coupons['MaturityDate'] = \
|
|
50
|
+
bonds_without_coupons['MaturityDate'].apply(lambda str_date: jd.date(year=int(str_date[:4]),
|
|
51
|
+
month=int(str_date[5:7]),
|
|
52
|
+
day=int(str_date[8:])))
|
|
53
|
+
|
|
54
|
+
bonds_without_coupons['YTM'] = \
|
|
55
|
+
bonds_without_coupons['YTM'].apply(lambda str_ytm: float(str_ytm.replace('/', '.').replace('%', '')) / 100)
|
|
56
|
+
bonds_without_coupons['SimpleReturn'] = \
|
|
57
|
+
bonds_without_coupons['SimpleReturn'].apply(lambda str_ytm: float(str_ytm.replace('/', '.').replace('%', '')) / 100)
|
|
58
|
+
|
|
59
|
+
bonds_without_coupons.drop('Index', inplace=True, axis=1)
|
|
60
|
+
bonds_without_coupons.reset_index(inplace=True, drop=True)
|
|
61
|
+
|
|
62
|
+
return bonds_without_coupons
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_all_bonds_with_coupons(deprecated: bool = True) -> pd.DataFrame:
|
|
66
|
+
url = "https://ifb.ir/YTM.aspx"
|
|
67
|
+
|
|
68
|
+
session = requests.Session()
|
|
69
|
+
response = session.get(url)
|
|
70
|
+
|
|
71
|
+
if response.status_code != 200:
|
|
72
|
+
print(f"Failed to access page. Status code: {response.status_code}")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
soup = BeautifulSoup(response.content, "html.parser")
|
|
76
|
+
table = soup.find("table", {"id": "ContentPlaceHolder1_grdytm", "class": "mGrid"})
|
|
77
|
+
|
|
78
|
+
if table is None:
|
|
79
|
+
print("Table not found.")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
df_list = pd.read_html(StringIO(str(table)))
|
|
83
|
+
bonds_with_coupons = df_list[0]
|
|
84
|
+
|
|
85
|
+
if ~deprecated:
|
|
86
|
+
bonds_with_coupons = bonds_with_coupons[bonds_with_coupons['YTM'] != 'سررسید شده'].copy()
|
|
87
|
+
|
|
88
|
+
bonds_with_coupons.columns = ['Index', 'Ticker', 'LastTradedPrice', 'LastTradedDate', 'MaturityDate', 'YTM']
|
|
89
|
+
|
|
90
|
+
bonds_with_coupons['LastTradedDate'] = bonds_with_coupons['LastTradedDate'].apply(
|
|
91
|
+
lambda str_date: jd.date(year=int(str_date[:4]), month=int(str_date[5:7]), day=int(str_date[8:])) if pd.notna(str_date) else np.nan)
|
|
92
|
+
|
|
93
|
+
bonds_with_coupons['MaturityDate'] = bonds_with_coupons['MaturityDate'].apply(
|
|
94
|
+
lambda str_date: jd.date(year=int(str_date[:4]), month=int(str_date[5:7]), day=int(str_date[8:])) if str_date != '0' else np.nan)
|
|
95
|
+
|
|
96
|
+
bonds_with_coupons['YTM'] = bonds_with_coupons['YTM'].apply(
|
|
97
|
+
lambda str_ytm: float(str_ytm.replace('/', '.').replace('%', '')) / 100 if (pd.notna(str_ytm) and str_ytm != 'سررسید شده') else np.nan)
|
|
98
|
+
|
|
99
|
+
bonds_with_coupons.drop('Index', inplace=True, axis=1)
|
|
100
|
+
bonds_with_coupons.reset_index(inplace=True, drop=True)
|
|
101
|
+
|
|
102
|
+
return bonds_with_coupons
|
fima/Options.py
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import requests
|
|
3
|
+
import jdatetime as jd
|
|
4
|
+
from fima.Bonds import get_risk_free_rate
|
|
5
|
+
from mibian import BS
|
|
6
|
+
from scipy.stats import norm
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def calculate_d1(s, k, t, r_f, sigma) -> float:
|
|
11
|
+
return (np.log(s / k) + (r_f + 0.5 * sigma ** 2) * t) / (sigma * np.sqrt(t))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def calculate_d2(s, k, t, r_f, sigma) -> float:
|
|
15
|
+
return calculate_d1(s, k, t, r_f, sigma) - sigma * np.sqrt(t)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def calculate_delta(s, k, t, r_f, sigma, option_type) -> float:
|
|
19
|
+
if option_type == 'Call':
|
|
20
|
+
return norm.cdf(calculate_d1(s, k, t, r_f, sigma))
|
|
21
|
+
elif option_type == 'Put':
|
|
22
|
+
return -norm.cdf(-(np.log(s / k) + (r_f + sigma ** 2 / 2) * t) / (sigma * np.sqrt(t)))
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def calculate_theta(s, k, t, r_f, sigma, option_type) -> float:
|
|
27
|
+
if option_type == 'Call':
|
|
28
|
+
p1 = - s * norm.pdf(calculate_d1(s, k, t, r_f, sigma)) * sigma / (2 * np.sqrt(t))
|
|
29
|
+
p2 = r_f * k * np.exp(-r_f * t) * norm.cdf(calculate_d2(s, k, t, r_f, sigma))
|
|
30
|
+
return p1 - p2
|
|
31
|
+
elif option_type == 'Put':
|
|
32
|
+
p1 = - s * norm.pdf(calculate_d1(s, k, t, r_f, sigma)) * sigma / (2 * np.sqrt(t))
|
|
33
|
+
p2 = r_f * k * np.exp(-r_f * t) * norm.cdf(-calculate_d2(s, k, t, r_f, sigma))
|
|
34
|
+
return p1 + p2
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def calculate_gamma(s, k, t, r_f, sigma) -> float:
|
|
39
|
+
return norm.pdf(calculate_d1(s, k, t, r_f, sigma)) / (s * sigma * np.sqrt(t))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def calculate_vega(s, k, t, r_f, sigma) -> float:
|
|
43
|
+
return s * np.sqrt(t) * norm.pdf(calculate_d1(s, k, t, r_f, sigma))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def calculate_rho(s, k, t, r_f, sigma, option_type) -> float:
|
|
47
|
+
if option_type == 'Call':
|
|
48
|
+
return k * t * np.exp(-r_f * t) * norm.cdf(calculate_d2(s, k, t, r_f, sigma))
|
|
49
|
+
elif option_type == 'Put':
|
|
50
|
+
return -k * t * np.exp(-r_f * t) * norm.cdf(-calculate_d2(s, k, t, r_f, sigma))
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calculate_row_wise_implied_volatility(row) -> float:
|
|
55
|
+
# print(f"Calculating {row['Ticker']} implied volatility...")
|
|
56
|
+
return get_implied_volatility(ticker=row['Ticker'], ticker_info_df=row.to_frame().T, r_f=row['RiskFreeRate'],
|
|
57
|
+
ua_historical_data=row['HistoricalData-UA'])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_implied_volatility(ticker: str, ticker_info_df: pd.DataFrame = None, volatility_window_size: int = None,
|
|
61
|
+
r_f: float = None, minimum_required_history: int = 30, ua_historical_data: pd.DataFrame = None) -> float:
|
|
62
|
+
|
|
63
|
+
if ticker_info_df is None:
|
|
64
|
+
ticker_info_df = ticker_info(ticker=ticker, j_date=True)
|
|
65
|
+
|
|
66
|
+
if ua_historical_data is None:
|
|
67
|
+
try:
|
|
68
|
+
_, ua_historical_data = download_historical_data(ticker)
|
|
69
|
+
except requests.exceptions.HTTPError:
|
|
70
|
+
print(f'Couldn\'t download historical data for: {ticker}')
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
if ua_historical_data is None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
if r_f is None:
|
|
77
|
+
r_f = get_risk_free_rate()
|
|
78
|
+
|
|
79
|
+
stock_days_available = len(ua_historical_data)
|
|
80
|
+
if stock_days_available < minimum_required_history:
|
|
81
|
+
print(
|
|
82
|
+
f"Not enough stock history for {ticker_info_df['Ticker-UA']} ({stock_days_available} days available, "
|
|
83
|
+
f"{minimum_required_history} days required).")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
days_to_maturity = ticker_info_df['DaysToMaturity'].values[0]
|
|
87
|
+
if days_to_maturity < 10:
|
|
88
|
+
print(f"Option {ticker} expires too soon ({days_to_maturity} days left). Calculated implied volatility may not be reliable.")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
max_safe_window = min(stock_days_available, days_to_maturity)
|
|
92
|
+
if volatility_window_size is None:
|
|
93
|
+
volatility_window_size = min(30, max(5, max_safe_window // 2)) # dynamic choice
|
|
94
|
+
else:
|
|
95
|
+
volatility_window_size = min(volatility_window_size, max_safe_window) # force safe
|
|
96
|
+
|
|
97
|
+
if volatility_window_size < 5:
|
|
98
|
+
print(f"Volatility window too small ({volatility_window_size} days). Cannot calculate meaningful volatility for {ticker}.")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
sigma = calculate_volatility(ua_historical_data, volatility_window_size)
|
|
102
|
+
s = ticker_info_df['LastPrice-UA'].values[0]
|
|
103
|
+
k = ticker_info_df['StrikePrice'].values[0]
|
|
104
|
+
t = days_to_maturity / 365
|
|
105
|
+
option_type = ticker_info_df['Type'].values[0]
|
|
106
|
+
premium = ticker_info_df['LastPrice'].values[0]
|
|
107
|
+
|
|
108
|
+
if sigma == 0 or t == 0:
|
|
109
|
+
print("Zero volatility or expired option.")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
if option_type == 'Call':
|
|
114
|
+
ticker_implied_volatility = BS([s, k, r_f, t * 365], callPrice=premium)
|
|
115
|
+
elif option_type == 'Put':
|
|
116
|
+
ticker_implied_volatility = BS([s, k, r_f, t * 365], putPrice=premium)
|
|
117
|
+
else:
|
|
118
|
+
print('Option type is not a valid one.')
|
|
119
|
+
return None
|
|
120
|
+
except (UnboundLocalError, IndexError):
|
|
121
|
+
print('mibian library error.')
|
|
122
|
+
return None
|
|
123
|
+
return ticker_implied_volatility.impliedVolatility / 100
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_greeks(ticker: str, ticker_info_df: pd.DataFrame = None, volatility_window_size: int = None, r_f: float = None,
|
|
127
|
+
minimum_required_history: int = 30, ua_historical_data: pd.DataFrame = None) -> pd.Series:
|
|
128
|
+
|
|
129
|
+
delta, gamma, theta, vega, rho = (None, None, None, None, None)
|
|
130
|
+
|
|
131
|
+
if ticker_info_df is None:
|
|
132
|
+
ticker_info_df = ticker_info(ticker=ticker, j_date=True)
|
|
133
|
+
|
|
134
|
+
if ua_historical_data is None:
|
|
135
|
+
try:
|
|
136
|
+
_, ua_historical_data = download_historical_data(ticker)
|
|
137
|
+
except requests.exceptions.HTTPError:
|
|
138
|
+
print(f'Couldn\'t download historical data for: {ticker}')
|
|
139
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
140
|
+
|
|
141
|
+
if ua_historical_data is None:
|
|
142
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
143
|
+
|
|
144
|
+
if r_f is None:
|
|
145
|
+
r_f = get_risk_free_rate()
|
|
146
|
+
|
|
147
|
+
stock_days_available = len(ua_historical_data)
|
|
148
|
+
if stock_days_available < minimum_required_history:
|
|
149
|
+
print(
|
|
150
|
+
f"Not enough stock history for {ticker_info_df['Ticker-UA']} ({stock_days_available} days available, "
|
|
151
|
+
f"{minimum_required_history} days required).")
|
|
152
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
153
|
+
|
|
154
|
+
days_to_maturity = ticker_info_df['DaysToMaturity'].values[0]
|
|
155
|
+
if days_to_maturity < 10:
|
|
156
|
+
print(f"Option {ticker} expires too soon ({days_to_maturity} days left). Calculated greeks may not be reliable.")
|
|
157
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
158
|
+
|
|
159
|
+
max_safe_window = min(stock_days_available, days_to_maturity)
|
|
160
|
+
if volatility_window_size is None:
|
|
161
|
+
volatility_window_size = min(30, max(5, max_safe_window // 2)) # dynamic choice
|
|
162
|
+
else:
|
|
163
|
+
volatility_window_size = min(volatility_window_size, max_safe_window) # force safe
|
|
164
|
+
|
|
165
|
+
if volatility_window_size < 5:
|
|
166
|
+
print(
|
|
167
|
+
f"Volatility window too small ({volatility_window_size} days). Cannot calculate meaningful volatility for {ticker}.")
|
|
168
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
169
|
+
|
|
170
|
+
sigma = calculate_volatility(ua_historical_data, volatility_window_size)
|
|
171
|
+
s = ticker_info_df['LastPrice-UA'].values[0]
|
|
172
|
+
k = ticker_info_df['StrikePrice'].values[0]
|
|
173
|
+
t = days_to_maturity / 365
|
|
174
|
+
option_type = ticker_info_df['Type'].values[0]
|
|
175
|
+
|
|
176
|
+
if sigma == 0 or t == 0:
|
|
177
|
+
print("Zero volatility or expired option.")
|
|
178
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
179
|
+
|
|
180
|
+
delta = calculate_delta(s, k, t, sigma, r_f, option_type)
|
|
181
|
+
gamma = calculate_gamma(s, k, t, sigma, r_f)
|
|
182
|
+
theta = calculate_theta(s, k, t, sigma, r_f, option_type)
|
|
183
|
+
vega = calculate_vega(s, k, t, sigma, r_f)
|
|
184
|
+
rho = calculate_rho(s, k, t, sigma, r_f, option_type)
|
|
185
|
+
|
|
186
|
+
return pd.Series({'Delta': delta, 'Gamma': gamma, 'Theta': theta, 'Vega': vega, 'Rho': rho})
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def calculate_row_wise_greeks(row) -> pd.Series:
|
|
190
|
+
# print(f"Calculating {row['Ticker']} greeks...")
|
|
191
|
+
return get_greeks(ticker=row['Ticker'], ticker_info_df=row.to_frame().T, r_f=row['RiskFreeRate'], ua_historical_data=row['HistoricalData-UA'])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def calculate_row_wise_bsm(row) -> pd.Series:
|
|
195
|
+
# print(f"Calculating {row['Ticker']} price with Black, Scholes and Merton model...")
|
|
196
|
+
volatility, bsm_price = black_scholes_merton(ticker=row['Ticker'], ticker_info_df=row.to_frame().T,
|
|
197
|
+
r_f=row['RiskFreeRate'], ua_historical_data=row['HistoricalData-UA'])
|
|
198
|
+
return pd.Series({'Volatility': volatility, 'BSMPrice': bsm_price})
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def ticker_info(ticker: str, j_date: bool = True) -> pd.DataFrame:
|
|
202
|
+
all_options_market_watch = download_market_watch(market='All', stack='Vertical', j_date=j_date)
|
|
203
|
+
if ticker in list(all_options_market_watch.loc[:, 'Ticker']):
|
|
204
|
+
ticker_info_df = all_options_market_watch[all_options_market_watch['Ticker'] == ticker].copy()
|
|
205
|
+
ticker_info_df.reset_index(inplace=True, drop=True)
|
|
206
|
+
else:
|
|
207
|
+
ticker_info_df = None
|
|
208
|
+
print(f'{ticker} not found.')
|
|
209
|
+
return ticker_info_df
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def calculate_volatility(ua_historical_data, window_size) -> float:
|
|
213
|
+
log_returns = np.log(ua_historical_data['ClosePrice'] / ua_historical_data['ClosePrice'].shift(1))
|
|
214
|
+
volatility = np.sqrt(window_size) * log_returns.std()
|
|
215
|
+
return volatility
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def calculate_black_scholes_merton(s, k, t, sigma, r_f, option_type) -> (float, float):
|
|
219
|
+
d1 = calculate_d1(s, k, t, r_f, sigma)
|
|
220
|
+
d2 = calculate_d2(s, k, t, r_f, sigma)
|
|
221
|
+
|
|
222
|
+
if option_type == 'Call':
|
|
223
|
+
price = s * norm.cdf(d1) - k * np.exp(-r_f * t) * norm.cdf(d2)
|
|
224
|
+
elif option_type == 'Put':
|
|
225
|
+
price = k * np.exp(-r_f * t) * norm.cdf(-d2) - s * norm.cdf(-d1)
|
|
226
|
+
else:
|
|
227
|
+
raise ValueError(f"Unknown option type '{option_type}'. Expected 'Call' or 'Put'.")
|
|
228
|
+
return price
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def black_scholes_merton(ticker: str, ticker_info_df: pd.DataFrame = None, volatility_window_size: int = None,
|
|
232
|
+
r_f: float = None, minimum_required_history: int = 30, ua_historical_data: pd.DataFrame = None) -> (float, float):
|
|
233
|
+
|
|
234
|
+
if ticker_info_df is None:
|
|
235
|
+
ticker_info_df = ticker_info(ticker=ticker, j_date=True)
|
|
236
|
+
|
|
237
|
+
if ua_historical_data is None:
|
|
238
|
+
try:
|
|
239
|
+
_, ua_historical_data = download_historical_data(ticker)
|
|
240
|
+
except requests.exceptions.HTTPError:
|
|
241
|
+
print(f'Couldn\'t download historical data for: {ticker}')
|
|
242
|
+
return None, None
|
|
243
|
+
|
|
244
|
+
if ua_historical_data is None:
|
|
245
|
+
return None, None
|
|
246
|
+
|
|
247
|
+
if r_f is None:
|
|
248
|
+
r_f = get_risk_free_rate()
|
|
249
|
+
|
|
250
|
+
stock_days_available = len(ua_historical_data)
|
|
251
|
+
if stock_days_available < minimum_required_history:
|
|
252
|
+
print(
|
|
253
|
+
f"Not enough stock history for {ticker_info_df['Ticker-UA']} ({stock_days_available} days available, "
|
|
254
|
+
f"{minimum_required_history} days required).")
|
|
255
|
+
return None, None
|
|
256
|
+
|
|
257
|
+
days_to_maturity = ticker_info_df['DaysToMaturity'].values[0]
|
|
258
|
+
if days_to_maturity < 10:
|
|
259
|
+
print(f"Option {ticker} expires too soon ({days_to_maturity} days left). BSM model may not be reliable.")
|
|
260
|
+
return None, None
|
|
261
|
+
|
|
262
|
+
max_safe_window = min(stock_days_available, days_to_maturity)
|
|
263
|
+
if volatility_window_size is None:
|
|
264
|
+
volatility_window_size = min(30, max(5, max_safe_window // 2)) # dynamic choice
|
|
265
|
+
else:
|
|
266
|
+
volatility_window_size = min(volatility_window_size, max_safe_window) # force safe
|
|
267
|
+
|
|
268
|
+
if volatility_window_size < 5:
|
|
269
|
+
print(
|
|
270
|
+
f"Volatility window too small ({volatility_window_size} days). Cannot calculate meaningful volatility for {ticker}.")
|
|
271
|
+
return None, None
|
|
272
|
+
|
|
273
|
+
sigma = calculate_volatility(ua_historical_data, volatility_window_size)
|
|
274
|
+
s = ticker_info_df['LastPrice-UA'].values[0]
|
|
275
|
+
k = ticker_info_df['StrikePrice'].values[0]
|
|
276
|
+
t = days_to_maturity / 365
|
|
277
|
+
option_type = ticker_info_df['Type'].values[0]
|
|
278
|
+
|
|
279
|
+
if sigma == 0 or t == 0:
|
|
280
|
+
print("Zero volatility or expired option. Returning price = 0.")
|
|
281
|
+
return 0, 0
|
|
282
|
+
|
|
283
|
+
price = calculate_black_scholes_merton(s, k, t, sigma, r_f, option_type)
|
|
284
|
+
|
|
285
|
+
return sigma, price
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def download_all_underlying_assets(all_options_market_watch: pd.DataFrame = None, market: str = 'All') -> pd.DataFrame:
|
|
289
|
+
if all_options_market_watch is None:
|
|
290
|
+
all_options_market_watch = download_market_watch(market=market, stack='Horizontal', j_date=True)
|
|
291
|
+
all_underlying_assets = all_options_market_watch.loc[:, [column for column in all_options_market_watch.columns if column.endswith('-UA')]]
|
|
292
|
+
all_underlying_assets.drop_duplicates(inplace=True)
|
|
293
|
+
all_underlying_assets.reset_index(inplace=True, drop=True)
|
|
294
|
+
all_underlying_assets.columns = [column.replace('-UA', '') for column in all_underlying_assets.columns]
|
|
295
|
+
return all_underlying_assets
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def download_ua_historical_data(ticker: str, start_date: str = None, end_date: str = None) -> pd.DataFrame:
|
|
299
|
+
all_options_market_watch = download_market_watch(market='All', stack='Vertical', j_date=False)
|
|
300
|
+
|
|
301
|
+
# underlying asset
|
|
302
|
+
ticker_data = all_options_market_watch[all_options_market_watch['Ticker-UA'] == ticker]
|
|
303
|
+
ticker_instrument_code = ticker_data['InstrumentCode-UA'].values[0]
|
|
304
|
+
ticker_historical_data_url = f'https://cdn.tsetmc.com/api/ClosingPrice/GetClosingPriceDailyList/{ticker_instrument_code}/0'
|
|
305
|
+
|
|
306
|
+
response = requests.get(ticker_historical_data_url)
|
|
307
|
+
response.raise_for_status()
|
|
308
|
+
|
|
309
|
+
data = response.json()
|
|
310
|
+
ticker_historical_data = pd.json_normalize(data["closingPriceDaily"])
|
|
311
|
+
|
|
312
|
+
if ticker_historical_data.empty:
|
|
313
|
+
print(f'Couldn\'t download historical data for ticker: {ticker}')
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
ticker_historical_data.columns = ['PriceChange', 'MinPrice', 'MaxPrice', 'YesterdayPrice', 'FirstPrice', 'Last', 'ID',
|
|
317
|
+
'InstrumentCode', 'Date', 'H', 'ClosePrice', 'I', 'Y', 'LastPrice', 'Quantity', 'Volume', 'Value']
|
|
318
|
+
ticker_historical_data = ticker_historical_data.loc[:, ['Date', 'Quantity', 'Volume', 'Value', 'MinPrice', 'MaxPrice',
|
|
319
|
+
'FirstPrice', 'LastPrice', 'ClosePrice']]
|
|
320
|
+
ticker_historical_data['Date'] = pd.to_datetime(ticker_historical_data['Date'].astype(str), format='%Y%m%d')
|
|
321
|
+
ticker_historical_data['Date'] = ticker_historical_data['Date'].dt.date
|
|
322
|
+
ticker_historical_data['Date'] = \
|
|
323
|
+
ticker_historical_data['Date'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
324
|
+
|
|
325
|
+
if start_date is not None and end_date is not None:
|
|
326
|
+
start_date_year, start_date_month, start_date_day = [int(date) for date in start_date.split('-')]
|
|
327
|
+
start_date = jd.date(start_date_year, start_date_month, start_date_day)
|
|
328
|
+
|
|
329
|
+
end_date_year, end_date_month, end_date_day = [int(date) for date in end_date.split('-')]
|
|
330
|
+
end_date = jd.date(end_date_year, end_date_month, end_date_day)
|
|
331
|
+
|
|
332
|
+
ticker_historical_data = ticker_historical_data[(ticker_historical_data['Date'] >= start_date) & (ticker_historical_data['Date'] <= end_date)]
|
|
333
|
+
ticker_historical_data.reset_index(inplace=True, drop=True)
|
|
334
|
+
|
|
335
|
+
return ticker_historical_data
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def download_historical_data(ticker: str, start_date: str = None, end_date: str = None) -> (pd.DataFrame, pd.DataFrame):
|
|
339
|
+
|
|
340
|
+
all_options_market_watch = download_market_watch(market='All', stack='Vertical', j_date=False)
|
|
341
|
+
|
|
342
|
+
# option
|
|
343
|
+
ticker_data = all_options_market_watch[all_options_market_watch['Ticker'] == ticker]
|
|
344
|
+
ticker_instrument_code = ticker_data['InstrumentCode'].values[0]
|
|
345
|
+
ticker_historical_data_url = f'https://cdn.tsetmc.com/api/ClosingPrice/GetClosingPriceDailyList/{ticker_instrument_code}/0'
|
|
346
|
+
|
|
347
|
+
response = requests.get(ticker_historical_data_url)
|
|
348
|
+
response.raise_for_status()
|
|
349
|
+
|
|
350
|
+
data = response.json()
|
|
351
|
+
ticker_historical_data = pd.json_normalize(data["closingPriceDaily"])
|
|
352
|
+
|
|
353
|
+
if ticker_historical_data.empty:
|
|
354
|
+
print(f'Couldn\'t download historical data for ticker: {ticker}')
|
|
355
|
+
return None, None
|
|
356
|
+
|
|
357
|
+
ticker_historical_data.columns = ['PriceChange', 'MinPrice', 'MaxPrice', 'YesterdayPrice', 'FirstPrice', 'Last', 'ID',
|
|
358
|
+
'InstrumentCode', 'Date', 'H', 'ClosePrice', 'I', 'Y', 'LastPrice', 'Quantity', 'Volume', 'Value']
|
|
359
|
+
ticker_historical_data = ticker_historical_data.loc[:, ['Date', 'Quantity', 'Volume', 'Value', 'MinPrice', 'MaxPrice',
|
|
360
|
+
'FirstPrice', 'LastPrice', 'ClosePrice']]
|
|
361
|
+
ticker_historical_data['Date'] = pd.to_datetime(ticker_historical_data['Date'].astype(str), format='%Y%m%d')
|
|
362
|
+
ticker_historical_data['Date'] = ticker_historical_data['Date'].dt.date
|
|
363
|
+
ticker_historical_data['Date'] = \
|
|
364
|
+
ticker_historical_data['Date'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
365
|
+
|
|
366
|
+
# underlying asset
|
|
367
|
+
ua_instrument_code = ticker_data['InstrumentCode-UA'].values[0]
|
|
368
|
+
ua_ticker_historical_data_url = f'https://cdn.tsetmc.com/api/ClosingPrice/GetClosingPriceDailyList/{ua_instrument_code}/0'
|
|
369
|
+
|
|
370
|
+
response = requests.get(ua_ticker_historical_data_url)
|
|
371
|
+
response.raise_for_status()
|
|
372
|
+
|
|
373
|
+
data = response.json()
|
|
374
|
+
ua_ticker_historical_data = pd.json_normalize(data["closingPriceDaily"])
|
|
375
|
+
|
|
376
|
+
if ua_ticker_historical_data.empty:
|
|
377
|
+
print(f'Couldn\'t download historical data for ticker: {ticker}')
|
|
378
|
+
return None, None
|
|
379
|
+
|
|
380
|
+
ua_ticker_historical_data.columns = ['PriceChange', 'MinPrice', 'MaxPrice', 'YesterdayPrice', 'FirstPrice', 'Last', 'ID',
|
|
381
|
+
'InstrumentCode', 'Date', 'H', 'ClosePrice', 'I', 'Y', 'LastPrice', 'Quantity', 'Volume', 'Value']
|
|
382
|
+
ua_ticker_historical_data = ua_ticker_historical_data.loc[:, ['Date', 'Quantity', 'Volume', 'Value', 'MinPrice', 'MaxPrice',
|
|
383
|
+
'FirstPrice', 'LastPrice', 'ClosePrice']]
|
|
384
|
+
ua_ticker_historical_data['Date'] = pd.to_datetime(ua_ticker_historical_data['Date'].astype(str), format='%Y%m%d')
|
|
385
|
+
ua_ticker_historical_data['Date'] = ua_ticker_historical_data['Date'].dt.date
|
|
386
|
+
ua_ticker_historical_data['Date'] = \
|
|
387
|
+
ua_ticker_historical_data['Date'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
388
|
+
|
|
389
|
+
if start_date is not None and end_date is not None:
|
|
390
|
+
start_date_year, start_date_month, start_date_day = [int(date) for date in start_date.split('-')]
|
|
391
|
+
start_date = jd.date(start_date_year, start_date_month, start_date_day)
|
|
392
|
+
|
|
393
|
+
end_date_year, end_date_month, end_date_day = [int(date) for date in end_date.split('-')]
|
|
394
|
+
end_date = jd.date(end_date_year, end_date_month, end_date_day)
|
|
395
|
+
|
|
396
|
+
ticker_historical_data = ticker_historical_data[(ticker_historical_data['Date'] >= start_date) & (ticker_historical_data['Date'] <= end_date)]
|
|
397
|
+
ua_ticker_historical_data = ua_ticker_historical_data[(ua_ticker_historical_data['Date'] >= start_date) & (ua_ticker_historical_data['Date'] <= end_date)]
|
|
398
|
+
|
|
399
|
+
ticker_historical_data.reset_index(inplace=True, drop=True)
|
|
400
|
+
ua_ticker_historical_data.reset_index(inplace=True, drop=True)
|
|
401
|
+
|
|
402
|
+
return ticker_historical_data, ua_ticker_historical_data
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def download_chain_contracts(underlying_ticker: str, j_date: str = True, bsm: bool = True, greeks: bool = False,
|
|
406
|
+
implied_volatility: bool = False) -> pd.DataFrame:
|
|
407
|
+
try:
|
|
408
|
+
url = "https://cdn.tsetmc.com/api/Instrument/GetInstrumentOptionMarketWatch/0"
|
|
409
|
+
response = requests.get(url)
|
|
410
|
+
response.raise_for_status()
|
|
411
|
+
|
|
412
|
+
data = response.json()
|
|
413
|
+
all_options_market_watch = pd.json_normalize(data["instrumentOptMarketWatch"])
|
|
414
|
+
|
|
415
|
+
all_options_market_watch.columns = ['InstrumentCode-P', 'InstrumentCode-C', 'ContractSize', 'InstrumentCode-UA',
|
|
416
|
+
'Ticker-P', 'Name-P', 'Volume-P', 'Quantity-P', 'Value-P', 'NotionalValue-P',
|
|
417
|
+
'ClosePrice-P', 'YesterdayPrice-P', 'OpenPositions-P', 'LastPrice-P',
|
|
418
|
+
'Ticker-UA', 'ClosePrice-UA', 'LastPrice-UA', 'YesterdayPrice-UA',
|
|
419
|
+
'BeginDate', 'EndDate', 'StrikePrice', 'DaysToMaturity', 'LastPrice-C', 'OpenPositions-C',
|
|
420
|
+
'ClosePrice-C', 'YesterdayPrice-C', 'NotionalValue-C', 'Value-C', 'Quantity-C',
|
|
421
|
+
'Volume-C', 'Name-C', 'Ticker-C', 'BidPrice-P', 'BidVolume-P', 'AskPrice-P',
|
|
422
|
+
'AskVolume-P', 'BidPrice-C', 'BidVolume-C', 'AskPrice-C', 'AskVolume-C',
|
|
423
|
+
'YesterdayOpenPositions-C', 'YesterdayOpenPositions-P']
|
|
424
|
+
chain_contracts = all_options_market_watch[all_options_market_watch['Ticker-UA'] == underlying_ticker].copy()
|
|
425
|
+
|
|
426
|
+
chain_contracts['URL-P'] = chain_contracts['InstrumentCode-P'].apply(lambda ins_code: f'https://tsetmc.ir/instInfo/{ins_code}')
|
|
427
|
+
chain_contracts['URL-C'] = chain_contracts['InstrumentCode-C'].apply(lambda ins_code: f'https://tsetmc.ir/instInfo/{ins_code}')
|
|
428
|
+
chain_contracts['URL-UA'] = chain_contracts['InstrumentCode-UA'].apply(lambda ins_code: f'https://tsetmc.ir/instInfo/{ins_code}')
|
|
429
|
+
|
|
430
|
+
chain_contracts['BeginDate'] = pd.to_datetime(chain_contracts['BeginDate'].astype(str), format='%Y%m%d')
|
|
431
|
+
chain_contracts['BeginDate'] = chain_contracts['BeginDate'].dt.date
|
|
432
|
+
chain_contracts['EndDate'] = pd.to_datetime(chain_contracts['EndDate'].astype(str), format='%Y%m%d')
|
|
433
|
+
chain_contracts['EndDate'] = chain_contracts['EndDate'].dt.date
|
|
434
|
+
|
|
435
|
+
if j_date:
|
|
436
|
+
chain_contracts['BeginDate'] = \
|
|
437
|
+
chain_contracts['BeginDate'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
438
|
+
chain_contracts['EndDate'] = \
|
|
439
|
+
chain_contracts['EndDate'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
440
|
+
|
|
441
|
+
conditions = [chain_contracts['LastPrice-UA'] > chain_contracts['StrikePrice'],
|
|
442
|
+
chain_contracts['LastPrice-UA'] < chain_contracts['StrikePrice']]
|
|
443
|
+
choices = ['ITM', 'OTM']
|
|
444
|
+
default = 'ATM'
|
|
445
|
+
chain_contracts['Status-C'] = np.select(conditions, choices, default=default)
|
|
446
|
+
|
|
447
|
+
conditions = [chain_contracts['LastPrice-UA'] > chain_contracts['StrikePrice'],
|
|
448
|
+
chain_contracts['LastPrice-UA'] < chain_contracts['StrikePrice']]
|
|
449
|
+
choices = ['OTM', 'ITM']
|
|
450
|
+
default = 'ATM'
|
|
451
|
+
chain_contracts['Status-P'] = np.select(conditions, choices, default=default)
|
|
452
|
+
|
|
453
|
+
call_chain_contracts_columns = ['Ticker-C', 'Name-C', 'InstrumentCode-C', 'Status-C', 'Volume-C', 'Quantity-C', 'Value-C',
|
|
454
|
+
'NotionalValue-C', 'OpenPositions-C', 'ClosePrice-C', 'LastPrice-C', 'YesterdayPrice-C',
|
|
455
|
+
'BidPrice-C', 'BidVolume-C', 'AskPrice-C', 'AskVolume-C', 'URL-C']
|
|
456
|
+
ua_chain_contracts_columns = ['ContractSize', 'StrikePrice', 'DaysToMaturity', 'BeginDate', 'EndDate',
|
|
457
|
+
'Ticker-UA', 'InstrumentCode-UA', 'ClosePrice-UA', 'LastPrice-UA',
|
|
458
|
+
'YesterdayPrice-UA', 'URL-UA']
|
|
459
|
+
put_chain_contracts_columns = ['Ticker-P', 'Name-P', 'InstrumentCode-P', 'Status-P', 'Volume-P', 'Quantity-P', 'Value-P',
|
|
460
|
+
'NotionalValue-P', 'OpenPositions-P', 'ClosePrice-P', 'LastPrice-P', 'YesterdayPrice-P',
|
|
461
|
+
'BidPrice-P', 'BidVolume-P', 'AskPrice-P', 'AskVolume-P', 'URL-P']
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
if bsm or greeks or implied_volatility:
|
|
465
|
+
uas_historical_data = {}
|
|
466
|
+
print(f'Downloading {underlying_ticker} historical data...')
|
|
467
|
+
ua_historical_data = download_ua_historical_data(ticker=underlying_ticker).loc[:, ['Date', 'ClosePrice']].copy()
|
|
468
|
+
print(f'Historical data downloaded for {underlying_ticker}.')
|
|
469
|
+
|
|
470
|
+
chain_contracts['HistoricalData-UA'] = None
|
|
471
|
+
for index, row in chain_contracts.iterrows():
|
|
472
|
+
chain_contracts.at[index, 'HistoricalData-UA'] = ua_historical_data.copy()
|
|
473
|
+
ua_chain_contracts_columns.append('HistoricalData-UA')
|
|
474
|
+
|
|
475
|
+
call_chain_contracts = chain_contracts.loc[:, call_chain_contracts_columns + ua_chain_contracts_columns].copy()
|
|
476
|
+
put_chain_contracts = chain_contracts.loc[:, put_chain_contracts_columns + ua_chain_contracts_columns].copy()
|
|
477
|
+
call_chain_contracts['Type'] = 'Call'
|
|
478
|
+
call_chain_contracts.columns = call_chain_contracts.columns.map(lambda column: column.replace('-C', ''))
|
|
479
|
+
call_chain_contracts_columns.append('Type')
|
|
480
|
+
put_chain_contracts['Type'] = 'Put'
|
|
481
|
+
put_chain_contracts.columns = put_chain_contracts.columns.map(lambda column: column.replace('-P', ''))
|
|
482
|
+
put_chain_contracts_columns.append('Type')
|
|
483
|
+
|
|
484
|
+
risk_free_rate = get_risk_free_rate()
|
|
485
|
+
call_chain_contracts['RiskFreeRate'] = risk_free_rate
|
|
486
|
+
put_chain_contracts['RiskFreeRate'] = risk_free_rate
|
|
487
|
+
ua_chain_contracts_columns.append('RiskFreeRate')
|
|
488
|
+
|
|
489
|
+
if bsm:
|
|
490
|
+
call_chain_contracts[['Volatility', 'BSMPrice']] = call_chain_contracts.apply(calculate_row_wise_bsm, axis=1)
|
|
491
|
+
call_chain_contracts_columns.append(['Volatility', 'BSMPrice'])
|
|
492
|
+
put_chain_contracts[['Volatility', 'BSMPrice']] = put_chain_contracts.apply(calculate_row_wise_bsm, axis=1)
|
|
493
|
+
put_chain_contracts_columns.append(['Volatility', 'BSMPrice'])
|
|
494
|
+
|
|
495
|
+
if greeks:
|
|
496
|
+
call_chain_contracts[['Delta', 'Gamma', 'Theta', 'Vega', 'Rho']] = call_chain_contracts.apply(calculate_row_wise_greeks, axis=1)
|
|
497
|
+
call_chain_contracts_columns.append(['Delta', 'Gamma', 'Theta', 'Vega', 'Rho'])
|
|
498
|
+
put_chain_contracts[['Delta', 'Gamma', 'Theta', 'Vega', 'Rho']] = put_chain_contracts.apply(calculate_row_wise_greeks, axis=1)
|
|
499
|
+
put_chain_contracts_columns.append(['Delta', 'Gamma', 'Theta', 'Vega', 'Rho'])
|
|
500
|
+
|
|
501
|
+
if implied_volatility:
|
|
502
|
+
call_chain_contracts['ImpliedVolatility'] = call_chain_contracts.apply(calculate_row_wise_implied_volatility, axis=1)
|
|
503
|
+
call_chain_contracts_columns.append(['ImpliedVolatility'])
|
|
504
|
+
put_chain_contracts['ImpliedVolatility'] = put_chain_contracts.apply(calculate_row_wise_implied_volatility, axis=1)
|
|
505
|
+
put_chain_contracts_columns.append(['ImpliedVolatility'])
|
|
506
|
+
|
|
507
|
+
call_chain_contracts.drop('HistoricalData-UA', inplace=True, axis=1)
|
|
508
|
+
put_chain_contracts.drop('HistoricalData-UA', inplace=True, axis=1)
|
|
509
|
+
ua_chain_contracts_columns.remove('HistoricalData-UA')
|
|
510
|
+
|
|
511
|
+
call_chain_contracts.columns = [f"{column}-C" for column in call_chain_contracts.columns]
|
|
512
|
+
put_chain_contracts.columns = [f"{column}-P" for column in put_chain_contracts.columns]
|
|
513
|
+
chain_contracts = pd.merge(call_chain_contracts, put_chain_contracts,
|
|
514
|
+
left_index=True, right_index=True, how='inner')
|
|
515
|
+
chain_contracts.drop([f"{column}-P" for column in ua_chain_contracts_columns], axis=1, inplace=True)
|
|
516
|
+
chain_contracts.columns = [column.replace('-C', '')
|
|
517
|
+
if column[:-2] in ua_chain_contracts_columns else column for column in chain_contracts.columns]
|
|
518
|
+
chain_contracts.drop(['Type-C', 'Type-P'], axis=1, inplace=True)
|
|
519
|
+
else:
|
|
520
|
+
chain_contracts = chain_contracts.loc[:, call_chain_contracts_columns + ua_chain_contracts_columns + put_chain_contracts_columns]
|
|
521
|
+
|
|
522
|
+
chain_contracts.reset_index(inplace=True, drop=True)
|
|
523
|
+
chain_contracts.sort_values(by=['EndDate', 'StrikePrice'], inplace=True, ignore_index=True)
|
|
524
|
+
return chain_contracts
|
|
525
|
+
except Exception as e:
|
|
526
|
+
print(f"Failed to get the market watch with the error:\n {e}")
|
|
527
|
+
return None
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def download_market_watch(market: str = 'All', stack: str = 'Horizontal', j_date: bool = True,
|
|
531
|
+
bsm: str = False, greeks: bool = False, implied_volatility: bool = False) -> pd.DataFrame:
|
|
532
|
+
|
|
533
|
+
if market == 'TSE':
|
|
534
|
+
url = "https://cdn.tsetmc.com/api/Instrument/GetInstrumentOptionMarketWatch/1"
|
|
535
|
+
elif market == 'IFB':
|
|
536
|
+
url = "https://cdn.tsetmc.com/api/Instrument/GetInstrumentOptionMarketWatch/2"
|
|
537
|
+
else: # 'All'
|
|
538
|
+
url = "https://cdn.tsetmc.com/api/Instrument/GetInstrumentOptionMarketWatch/0"
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
response = requests.get(url)
|
|
542
|
+
response.raise_for_status()
|
|
543
|
+
|
|
544
|
+
data = response.json()
|
|
545
|
+
all_options_market_watch = pd.json_normalize(data["instrumentOptMarketWatch"])
|
|
546
|
+
|
|
547
|
+
all_options_market_watch.columns = ['InstrumentCode-P', 'InstrumentCode-C', 'ContractSize', 'InstrumentCode-UA',
|
|
548
|
+
'Ticker-P', 'Name-P', 'Volume-P', 'Quantity-P', 'Value-P', 'NotionalValue-P',
|
|
549
|
+
'ClosePrice-P', 'YesterdayPrice-P', 'OpenPositions-P', 'LastPrice-P',
|
|
550
|
+
'Ticker-UA', 'ClosePrice-UA', 'LastPrice-UA', 'YesterdayPrice-UA',
|
|
551
|
+
'BeginDate', 'EndDate', 'StrikePrice', 'DaysToMaturity', 'LastPrice-C', 'OpenPositions-C',
|
|
552
|
+
'ClosePrice-C', 'YesterdayPrice-C', 'NotionalValue-C', 'Value-C', 'Quantity-C',
|
|
553
|
+
'Volume-C', 'Name-C', 'Ticker-C', 'BidPrice-P', 'BidVolume-P', 'AskPrice-P',
|
|
554
|
+
'AskVolume-P', 'BidPrice-C', 'BidVolume-C', 'AskPrice-C', 'AskVolume-C',
|
|
555
|
+
'YesterdayOpenPositions-C', 'YesterdayOpenPositions-P']
|
|
556
|
+
all_options_market_watch['URL-P'] = all_options_market_watch['InstrumentCode-P'].apply(lambda ins_code: f'https://tsetmc.ir/instInfo/{ins_code}')
|
|
557
|
+
all_options_market_watch['URL-C'] = all_options_market_watch['InstrumentCode-C'].apply(lambda ins_code: f'https://tsetmc.ir/instInfo/{ins_code}')
|
|
558
|
+
all_options_market_watch['URL-UA'] = all_options_market_watch['InstrumentCode-UA'].apply(lambda ins_code: f'https://tsetmc.ir/instInfo/{ins_code}')
|
|
559
|
+
|
|
560
|
+
all_options_market_watch['BeginDate'] = pd.to_datetime(all_options_market_watch['BeginDate'].astype(str), format='%Y%m%d')
|
|
561
|
+
all_options_market_watch['BeginDate'] = all_options_market_watch['BeginDate'].dt.date
|
|
562
|
+
all_options_market_watch['EndDate'] = pd.to_datetime(all_options_market_watch['EndDate'].astype(str), format='%Y%m%d')
|
|
563
|
+
all_options_market_watch['EndDate'] = all_options_market_watch['EndDate'].dt.date
|
|
564
|
+
|
|
565
|
+
if j_date:
|
|
566
|
+
all_options_market_watch['BeginDate'] = \
|
|
567
|
+
all_options_market_watch['BeginDate'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
568
|
+
all_options_market_watch['EndDate'] = \
|
|
569
|
+
all_options_market_watch['EndDate'].apply(lambda g_date: jd.date.fromgregorian(year=g_date.year, month=g_date.month, day=g_date.day))
|
|
570
|
+
|
|
571
|
+
conditions = [all_options_market_watch['LastPrice-UA'] > all_options_market_watch['StrikePrice'],
|
|
572
|
+
all_options_market_watch['LastPrice-UA'] < all_options_market_watch['StrikePrice']]
|
|
573
|
+
choices = ['ITM', 'OTM']
|
|
574
|
+
default = 'ATM'
|
|
575
|
+
all_options_market_watch['Status-C'] = np.select(conditions, choices, default=default)
|
|
576
|
+
|
|
577
|
+
conditions = [all_options_market_watch['LastPrice-UA'] > all_options_market_watch['StrikePrice'],
|
|
578
|
+
all_options_market_watch['LastPrice-UA'] < all_options_market_watch['StrikePrice']]
|
|
579
|
+
choices = ['OTM', 'ITM']
|
|
580
|
+
default = 'ATM'
|
|
581
|
+
all_options_market_watch['Status-P'] = np.select(conditions, choices, default=default)
|
|
582
|
+
|
|
583
|
+
call_options_market_watch_columns = ['Ticker-C', 'Name-C', 'InstrumentCode-C', 'Status-C', 'Volume-C', 'Quantity-C', 'Value-C',
|
|
584
|
+
'NotionalValue-C', 'OpenPositions-C', 'ClosePrice-C', 'LastPrice-C', 'YesterdayPrice-C',
|
|
585
|
+
'BidPrice-C', 'BidVolume-C', 'AskPrice-C', 'AskVolume-C', 'URL-C']
|
|
586
|
+
ua_market_watch_columns = ['ContractSize', 'StrikePrice', 'DaysToMaturity', 'BeginDate', 'EndDate',
|
|
587
|
+
'Ticker-UA', 'InstrumentCode-UA', 'ClosePrice-UA', 'LastPrice-UA',
|
|
588
|
+
'YesterdayPrice-UA', 'URL-UA']
|
|
589
|
+
put_options_market_watch_columns = ['Ticker-P', 'Name-P', 'InstrumentCode-P', 'Status-P', 'Volume-P', 'Quantity-P', 'Value-P',
|
|
590
|
+
'NotionalValue-P', 'OpenPositions-P', 'ClosePrice-P', 'LastPrice-P', 'YesterdayPrice-P',
|
|
591
|
+
'BidPrice-P', 'BidVolume-P', 'AskPrice-P', 'AskVolume-P', 'URL-P']
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
if bsm or greeks or implied_volatility:
|
|
595
|
+
uas_historical_data = {}
|
|
596
|
+
all_uas = download_all_underlying_assets(all_options_market_watch.copy(), market=market)
|
|
597
|
+
print('Starting to download historical data for all tickers...')
|
|
598
|
+
for ua in all_uas['Ticker']:
|
|
599
|
+
print(f'Downloading {ua} historical data...')
|
|
600
|
+
uas_historical_data[ua] = download_ua_historical_data(ticker=ua).loc[:, ['Date', 'ClosePrice']].copy()
|
|
601
|
+
print('Historical data downloaded for all underlying assets.')
|
|
602
|
+
|
|
603
|
+
all_options_market_watch['HistoricalData-UA'] = None
|
|
604
|
+
for index, row in all_options_market_watch.iterrows():
|
|
605
|
+
all_options_market_watch.at[index, 'HistoricalData-UA'] = uas_historical_data[row['Ticker-UA']].copy()
|
|
606
|
+
ua_market_watch_columns.append('HistoricalData-UA')
|
|
607
|
+
|
|
608
|
+
call_options_market_watch = all_options_market_watch.loc[:, call_options_market_watch_columns + ua_market_watch_columns].copy()
|
|
609
|
+
put_options_market_watch = all_options_market_watch.loc[:, put_options_market_watch_columns + ua_market_watch_columns].copy()
|
|
610
|
+
call_options_market_watch['Type'] = 'Call'
|
|
611
|
+
call_options_market_watch.columns = call_options_market_watch.columns.map(lambda column: column.replace('-C', ''))
|
|
612
|
+
call_options_market_watch_columns.append('Type')
|
|
613
|
+
put_options_market_watch['Type'] = 'Put'
|
|
614
|
+
put_options_market_watch.columns = put_options_market_watch.columns.map(lambda column: column.replace('-P', ''))
|
|
615
|
+
put_options_market_watch_columns.append('Type')
|
|
616
|
+
|
|
617
|
+
risk_free_rate = get_risk_free_rate()
|
|
618
|
+
call_options_market_watch['RiskFreeRate'] = risk_free_rate
|
|
619
|
+
put_options_market_watch['RiskFreeRate'] = risk_free_rate
|
|
620
|
+
ua_market_watch_columns.append('RiskFreeRate')
|
|
621
|
+
|
|
622
|
+
if bsm:
|
|
623
|
+
call_options_market_watch[['Volatility', 'BSMPrice']] = call_options_market_watch.apply(calculate_row_wise_bsm, axis=1)
|
|
624
|
+
call_options_market_watch_columns.append(['Volatility', 'BSMPrice'])
|
|
625
|
+
put_options_market_watch[['Volatility', 'BSMPrice']] = put_options_market_watch.apply(calculate_row_wise_bsm, axis=1)
|
|
626
|
+
put_options_market_watch_columns.append(['Volatility', 'BSMPrice'])
|
|
627
|
+
|
|
628
|
+
if greeks:
|
|
629
|
+
call_options_market_watch[['Delta', 'Gamma', 'Theta', 'Vega', 'Rho']] = call_options_market_watch.apply(calculate_row_wise_greeks, axis=1)
|
|
630
|
+
call_options_market_watch_columns.append(['Delta', 'Gamma', 'Theta', 'Vega', 'Rho'])
|
|
631
|
+
put_options_market_watch[['Delta', 'Gamma', 'Theta', 'Vega', 'Rho']] = put_options_market_watch.apply(calculate_row_wise_greeks, axis=1)
|
|
632
|
+
put_options_market_watch_columns.append(['Delta', 'Gamma', 'Theta', 'Vega', 'Rho'])
|
|
633
|
+
|
|
634
|
+
if implied_volatility:
|
|
635
|
+
call_options_market_watch['ImpliedVolatility'] = call_options_market_watch.apply(calculate_row_wise_implied_volatility, axis=1)
|
|
636
|
+
call_options_market_watch_columns.append(['ImpliedVolatility'])
|
|
637
|
+
put_options_market_watch['ImpliedVolatility'] = put_options_market_watch.apply(calculate_row_wise_implied_volatility, axis=1)
|
|
638
|
+
put_options_market_watch_columns.append(['ImpliedVolatility'])
|
|
639
|
+
|
|
640
|
+
call_options_market_watch.drop('HistoricalData-UA', inplace=True, axis=1)
|
|
641
|
+
put_options_market_watch.drop('HistoricalData-UA', inplace=True, axis=1)
|
|
642
|
+
ua_market_watch_columns.remove('HistoricalData-UA')
|
|
643
|
+
|
|
644
|
+
if stack == 'Vertical':
|
|
645
|
+
all_options_market_watch = pd.concat([call_options_market_watch, put_options_market_watch], axis=0, ignore_index=True)
|
|
646
|
+
else: # 'Horizontal'
|
|
647
|
+
call_options_market_watch.columns = [f"{column}-C" for column in call_options_market_watch.columns]
|
|
648
|
+
put_options_market_watch.columns = [f"{column}-P" for column in put_options_market_watch.columns]
|
|
649
|
+
all_options_market_watch = pd.merge(call_options_market_watch, put_options_market_watch,
|
|
650
|
+
left_index=True, right_index=True, how='inner')
|
|
651
|
+
all_options_market_watch.drop([f"{column}-P" for column in ua_market_watch_columns], axis=1, inplace=True)
|
|
652
|
+
all_options_market_watch.columns = [column.replace('-C', '')
|
|
653
|
+
if column[:-2] in ua_market_watch_columns else column for column in all_options_market_watch.columns]
|
|
654
|
+
all_options_market_watch.drop(['Type-C', 'Type-P'], axis=1, inplace=True)
|
|
655
|
+
else:
|
|
656
|
+
if stack == 'Vertical':
|
|
657
|
+
call_options_market_watch = all_options_market_watch.loc[:, call_options_market_watch_columns + ua_market_watch_columns].copy()
|
|
658
|
+
put_options_market_watch = all_options_market_watch.loc[:, put_options_market_watch_columns + ua_market_watch_columns].copy()
|
|
659
|
+
call_options_market_watch['Type'] = 'Call'
|
|
660
|
+
call_options_market_watch.columns = call_options_market_watch.columns.map(lambda column: column.replace('-C', ''))
|
|
661
|
+
call_options_market_watch_columns.append('Type')
|
|
662
|
+
put_options_market_watch['Type'] = 'Put'
|
|
663
|
+
put_options_market_watch.columns = put_options_market_watch.columns.map(lambda column: column.replace('-P', ''))
|
|
664
|
+
put_options_market_watch_columns.append('Type')
|
|
665
|
+
all_options_market_watch = pd.concat([call_options_market_watch, put_options_market_watch], axis=0, ignore_index=True)
|
|
666
|
+
else: # Horizontal
|
|
667
|
+
all_options_market_watch = all_options_market_watch.loc[:, call_options_market_watch_columns + ua_market_watch_columns + put_options_market_watch_columns]
|
|
668
|
+
|
|
669
|
+
all_options_market_watch.reset_index(inplace=True, drop=True)
|
|
670
|
+
all_options_market_watch.sort_values(by=['Ticker-UA', 'EndDate', 'StrikePrice'], inplace=True, ignore_index=True)
|
|
671
|
+
return all_options_market_watch
|
|
672
|
+
|
|
673
|
+
except Exception as e:
|
|
674
|
+
print(f"Failed to get the market watch with the error:\n {e}")
|
|
675
|
+
return None
|
fima/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .Options import (download_chain_contracts, get_greeks, get_implied_volatility, black_scholes_merton,
|
|
2
|
+
download_market_watch, download_all_underlying_assets, download_historical_data, ticker_info,
|
|
3
|
+
calculate_delta, calculate_vega, calculate_theta, calculate_gamma, calculate_rho, calculate_d1,
|
|
4
|
+
calculate_d2, calculate_black_scholes_merton, download_ua_historical_data, calculate_volatility)
|
|
5
|
+
from .Bonds import get_risk_free_rate, get_all_bonds_without_coupons, get_all_bonds_with_coupons
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# __all__ = ["download_chain_contracts", "download_market_watch", "download_historical_data", "get_greeks",
|
|
9
|
+
# "get_implied_volatility", "black_scholes_merton", "get_risk_free_rate", "get_all_bonds_without_coupons", "get_all_bonds_with_coupons"]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fima
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: fima – A Python package for scraping and analyzing Iran’s financial market data. It currently supports bonds and options, with tools for calculating BSM prices, greeks, and implied volatility. Future versions will expand to include other instruments such as commodities and more.
|
|
5
|
+
Home-page: https://github.com/hemoboghosian/fima
|
|
6
|
+
Author: Hemo Boghosian
|
|
7
|
+
Author-email: Hemo Boghosian <HemoBoghosian@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/hemoboghosian/fima
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: jdatetime
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: bs4
|
|
19
|
+
Requires-Dist: selenium
|
|
20
|
+
Requires-Dist: mibian
|
|
21
|
+
Requires-Dist: lxml
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
|
|
27
|
+
# fima
|
|
28
|
+
## معرفی و هدف
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
این کتابخانه جهت دسترسی آسان و سریع به دادههای بازارهای مالی ایران با زبان برنامهنویسی پایتون پیادهسازی شده است. هدف نهایی این پروژه، ساخت کتابخانهای جامع جهت استخراج، ساختاردهی و تحلیل دادههای مالی از منابع مختلف موجود است.
|
|
32
|
+
**این کتابخانه هنوز در حال توسعه و تکامل است و ممکن است در برخی قسمتها اشتباهاتی داشته باشد. لطفاً اگر خطا یا نقصی دیدید اطلاع دهید.**
|
|
33
|
+
|
|
34
|
+
## اطلاعات تماس
|
|
35
|
+
* ایمیل: HemoBoghosian@gmail.com
|
|
36
|
+
* تلگرام: @HemoBoghosian
|
|
37
|
+
|
|
38
|
+
## نصب کتابخانه
|
|
39
|
+
```
|
|
40
|
+
pip install fima
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## توابع مربوط به اختیارهای معامله
|
|
44
|
+
### فراخوانی تمام توابع
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from fima.Options import *
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### دریافت سابقه قیمت اختیار معامله و دارایی پایه آن
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fima.Options import download_historical_data
|
|
54
|
+
|
|
55
|
+
option_historical_data, ua_historical_data = download_historical_data(ticker='ضهرم4018', start_date='1404-01-01',
|
|
56
|
+
end_date='1404-01-31')
|
|
57
|
+
```
|
|
58
|
+
#### ورودی
|
|
59
|
+
* ticker: نماد اختیار معامله
|
|
60
|
+
* start_date: تاریخ شروع دادهها
|
|
61
|
+
* end_date: تاریخ پایان دادهها
|
|
62
|
+
|
|
63
|
+
#### خروجی
|
|
64
|
+
* option_historical_data: سابقه قیمت اختیار معامله
|
|
65
|
+
* ua_historical_data: سابقه قیمت دارایی پایه نماد وارد شده
|
|
66
|
+
|
|
67
|
+
##### توجه: در صورت وارد نکردن تاریخ شروع و پایان، خروجی تابع تمام سابقه قیمت اختیار معامله و دارایی پایه آن خواهد بود.
|
|
68
|
+
|
|
69
|
+
### دریافت اطلاعات لحظهای یک نماد اختیار معامله
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from fima.Options import ticker_info
|
|
73
|
+
|
|
74
|
+
ticker_info_data = ticker_info(ticker='ضهرم4018', j_date=True)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### ورود
|
|
78
|
+
* ticker: نماد اختیار معامله مد نظرم
|
|
79
|
+
* j_date: تاریخ شمسی
|
|
80
|
+
|
|
81
|
+
#### خروجی
|
|
82
|
+
* ticker_info_data: اطلاعات لحظهای مربوط به نماد مد نظر
|
|
83
|
+
|
|
84
|
+
### دریافت زنجیره قراردادهای یک دارایی پایه
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from fima.Options import download_chain_contracts
|
|
88
|
+
|
|
89
|
+
chain_contracts = download_chain_contracts(underlying_ticker='اهرم', j_date=True, bsm=False, greeks=False,
|
|
90
|
+
implied_volatility=False)
|
|
91
|
+
```
|
|
92
|
+
#### ورودی
|
|
93
|
+
* underlying_ticker: نماد دارایی پایه
|
|
94
|
+
* j_date: تاریخ شمسی
|
|
95
|
+
* bsm: محاسبه ارزش اختیار معامله با مدل بلک، شولز و مرتون
|
|
96
|
+
* greeks: محاسبه دلتا، تتا، گاما، رو و وگا
|
|
97
|
+
* implied_volatility: محاسبه نوسانات ضمنی
|
|
98
|
+
|
|
99
|
+
#### خروجی
|
|
100
|
+
* chain_contracts: اطلاعات تمامی زنجیره قراردادهای مربوط به دارایی پایه مد نظر
|
|
101
|
+
|
|
102
|
+
##### توجه: برای محاسبات مربوط به مدل بلک، شولز و مرتون از میانگین نرخ بازده تا سررسید اسناد خزانه در آخرین تاریخ (دریافتشده از سایت فرابورس ایران) به عنوان نرخ بازده بدون ریسک استفاده میشود.
|
|
103
|
+
|
|
104
|
+
### دریافت دیدهبان بازار
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from fima.Options import download_market_watch
|
|
108
|
+
|
|
109
|
+
market_watch = download_market_watch(market='All', stack='Vertical', j_date=True, bsm=False, greeks=False,
|
|
110
|
+
implied_volatility=False)
|
|
111
|
+
```
|
|
112
|
+
#### ورودی
|
|
113
|
+
* market: بازار معامله اختیار معامله
|
|
114
|
+
* 'TSE': بورس اوراق بهادار تهران
|
|
115
|
+
* 'IFB': فرابورس ایران
|
|
116
|
+
* 'All': تمامی اختیارهای معامله
|
|
117
|
+
* stack: نحوه چینش دادهها
|
|
118
|
+
* 'Vertical': چینش عمودی دادهها
|
|
119
|
+
* 'Horizontal': چینش افقی دادهها
|
|
120
|
+
* j_date: تاریخ شمسی
|
|
121
|
+
* bsm: محاسبه ارزش اختیار معامله با مدل بلک، شولز و مرتون
|
|
122
|
+
* greeks: محاسبه دلتا، تتا، گاما، رو و وگا
|
|
123
|
+
* implied_volatility: محاسبه نوسانات ضمنی
|
|
124
|
+
|
|
125
|
+
#### خروجی
|
|
126
|
+
* market_watch: اطلاعات تمامی اختیارهای معامله بازار مد نظر
|
|
127
|
+
|
|
128
|
+
##### توجه: برای محاسبات مربوط به مدل بلک، شولز و مرتون از میانگین نرخ بازده تا سررسید اسناد خزانه در آخرین تاریخ (دریافتشده از سایت فرابورس ایران) به عنوان نرخ بازده بدون ریسک استفاده میشود.
|
|
129
|
+
|
|
130
|
+
### دریافت ارزش محاسبهشده توسط مدل بلک، شولز و مرتون برای یک نماد
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from fima.Options import black_scholes_merton
|
|
134
|
+
|
|
135
|
+
volatility, bsm_price = black_scholes_merton(ticker='ضهرم4018')
|
|
136
|
+
```
|
|
137
|
+
#### ورودی
|
|
138
|
+
* ticker: نماد اختیار معامله مد نظر
|
|
139
|
+
|
|
140
|
+
#### خروجی
|
|
141
|
+
* volatility: نوسانات دارایی پایه محاسبهشده در مدل
|
|
142
|
+
* bsm_price: ارزش محاسبهشده اختیار معامله مد نظر با مدل بلک، شولز و مرتون
|
|
143
|
+
|
|
144
|
+
##### توجه
|
|
145
|
+
* توجه: در صورت عدم وارد کردن متغیر مربوط به نرخ بازده بدون ریسک، برای محاسبات مربوط به مدل بلک، شولز و مرتون از میانگین نرخ بازده تا سررسید اسناد خزانه در آخرین تاریخ (دریافتشده از سایت فرابورس ایران) به عنوان نرخ بازده بدون ریسک استفاده میشود.
|
|
146
|
+
* در صورتی که روزهای باقیمانده تا سررسید اختیار معامله کمتر از ۱۰ روز باشند، به دلیل احتمال خطا، مدل بلک، شولز و مرتون اعمال نخواهد شد.
|
|
147
|
+
|
|
148
|
+
### دریافت گریکز برای یک نماد
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from fima.Options import get_greeks
|
|
152
|
+
|
|
153
|
+
ticker_greeks = get_greeks(ticker='ضهرم4018')
|
|
154
|
+
```
|
|
155
|
+
#### ورودی
|
|
156
|
+
* ticker: نماد اختیار معامله مد نظر
|
|
157
|
+
* r_f: نرخ بازده بدون ریسک
|
|
158
|
+
* minimum_required_history: کمینه دیتای مورد نیاز برای دارایی پایهجهت محاسبه نوسانات که به صورت پیشفرض ۳۰ روز در نظر گرفته شده است.
|
|
159
|
+
|
|
160
|
+
#### خروجی
|
|
161
|
+
* ticker_greeks: دلتا، تتا، گاما، وگا و رو برای نماد مد نظر
|
|
162
|
+
|
|
163
|
+
##### توجه
|
|
164
|
+
* در صورت عدم وارد کردن متغیر مربوط به نرخ بازده بدون ریسک، برای محاسبات مربوط به مدل بلک، شولز و مرتون از میانگین نرخ بازده تا سررسید اسناد خزانه در آخرین تاریخ (دریافتشده از سایت فرابورس ایران) به عنوان نرخ بازده بدون ریسک استفاده میشود.
|
|
165
|
+
* در صورتی که روزهای باقیمانده تا سررسید اختیار معامله کمتر از ۱۰ روز باشند، به دلیل احتمال خطا، مدل بلک، شولز و مرتون اعمال نخواهد شد.
|
|
166
|
+
|
|
167
|
+
### دریافت نوسانات ضمنی برای یک نماد
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from fima.Options import get_implied_volatility
|
|
171
|
+
|
|
172
|
+
ticker_implied_volatility = get_implied_volatility(ticker='ضهرم4018')
|
|
173
|
+
```
|
|
174
|
+
#### ورودی
|
|
175
|
+
* ticker: نماد اختیار معامله مد نظر
|
|
176
|
+
* r_f: نرخ بازده بدون ریسک
|
|
177
|
+
* minimum_required_history: کمینه دیتای مورد نیاز برای دارایی پایهجهت محاسبه نوسانات که به صورت پیشفرض ۳۰ روز در نظر گرفته شده است.
|
|
178
|
+
|
|
179
|
+
#### خروجی
|
|
180
|
+
* ticker_implied_volatility: نوسانات ضمنی برای نماد مد نظر
|
|
181
|
+
|
|
182
|
+
##### توجه
|
|
183
|
+
* در صورت عدم وارد کردن متغیر مربوط به نرخ بازده بدون ریسک، برای محاسبات مربوط به مدل بلک، شولز و مرتون از میانگین نرخ بازده تا سررسید اسناد خزانه در آخرین تاریخ (دریافتشده از سایت فرابورس ایران) به عنوان نرخ بازده بدون ریسک استفاده میشود.
|
|
184
|
+
* در صورتی که روزهای باقیمانده تا سررسید اختیار معامله کمتر از ۱۰ روز باشند، به دلیل احتمال خطا، مدل بلک، شولز و مرتون اعمال نخواهد شد.
|
|
185
|
+
|
|
186
|
+
### دریافت تمامی نمادهای دارای اختیار معامله
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from fima.Options import download_all_underlying_assets
|
|
190
|
+
|
|
191
|
+
all_underlying_assets = download_all_underlying_assets(market='All')
|
|
192
|
+
```
|
|
193
|
+
#### ورودی
|
|
194
|
+
* market: بازار معامله اختیار معامله
|
|
195
|
+
* 'TSE': بورس اوراق بهادار تهران
|
|
196
|
+
* 'IFB': فرابورس ایران
|
|
197
|
+
* 'All': تمامی اختیارهای معامله (حالت پیشفرض)
|
|
198
|
+
|
|
199
|
+
#### خروجی
|
|
200
|
+
* all_underlying_assets: اطلاعات مربوط به نمادهای دارای اختیار معامله
|
|
201
|
+
|
|
202
|
+
#### توجه: در صورت عدم وارد کردن market، تمامی اختیارهای معامله دریافت خواهد شد.
|
|
203
|
+
|
|
204
|
+
### توابع مربوط به محاسبات
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from fima.Options import (calculate_black_scholes_merton, calculate_delta, calculate_vega,
|
|
208
|
+
calculate_theta, calculate_gamma, calculate_rho)
|
|
209
|
+
|
|
210
|
+
bsm_price = calculate_black_scholes_merton(s=550, k=28000, option_type='Call', t=71, sigma=0.9588, r_f=0.3372)
|
|
211
|
+
delta = calculate_delta(s=550, k=28000, option_type='Call', t=71, sigma=0.9588, r_f=0.3372)
|
|
212
|
+
theta = calculate_theta(s=550, k=28000, option_type='Call', t=71, sigma=0.9588, r_f=0.3372)
|
|
213
|
+
gamma = calculate_gamma(s=550, k=28000, t=71, sigma=0.9588, r_f=0.3372)
|
|
214
|
+
rho = calculate_rho(s=550, k=28000, option_type='Call', t=71, sigma=0.9588, r_f=0.3372)
|
|
215
|
+
vega = calculate_vega(s=550, k=28000, t=71, sigma=0.9588, r_f=0.3372)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## توابع مربوط به اوراق درآمد ثابت
|
|
219
|
+
### فراخوانی تمام توابع
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from fima.Bonds import *
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### دریافت سابقه قیمت اختیار معامله و دارایی پایه آن
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from fima.Bonds import get_risk_free_rate
|
|
229
|
+
|
|
230
|
+
risk_free_rate = get_risk_free_rate()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### خروجی
|
|
234
|
+
* risk_free_rate: میانگین نرخ بازده تا سررسید اسناد خزانه در آخرین تاریخ (دریافتشده از سایت فرابورس ایران)
|
|
235
|
+
|
|
236
|
+
### دریافت اوراق درآمد ثابت بدون کوپن
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from fima.Bonds import get_all_bonds_without_coupons
|
|
240
|
+
|
|
241
|
+
all_bonds_without_coupons = get_all_bonds_without_coupons(deprecated=True)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### ورودی
|
|
245
|
+
* deprecated:
|
|
246
|
+
* True: اوراق سررسید شده نیز در خروجی بیاید
|
|
247
|
+
* False: اوراق سررسید شده در خروجی نیاید
|
|
248
|
+
#### خروجی
|
|
249
|
+
* all_bonds_without_coupons: اطلاعات مربوط به اوراق درآمد ثابت بدون کوپن (دریاقتشده از سایت فرابورس ایران)
|
|
250
|
+
|
|
251
|
+
### دریافت اوراق درآمد ثابت با کوپن
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from fima.Bonds import get_all_bonds_with_coupons
|
|
255
|
+
|
|
256
|
+
all_bonds_with_coupons = get_all_bonds_with_coupons(deprecated=True)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### ورودی
|
|
260
|
+
* deprecated:
|
|
261
|
+
* True: اوراق سررسید شده نیز در خروجی بیاید
|
|
262
|
+
* False: اوراق سررسید شده در خروجی نیاید
|
|
263
|
+
#### خروجی
|
|
264
|
+
* all_bonds_without_coupons: اطلاعات مربوط به اوراق درآمد ثابت با کوپن (دریاقتشده از سایت فرابورس ایران)
|
|
265
|
+
|
|
266
|
+
## نقشه راه
|
|
267
|
+
1. [ ] عیبیابی و بهبود توابع پیادهسازیشده
|
|
268
|
+
2. [ ] دریافت اطلاعات از سایت tgju
|
|
269
|
+
3. [ ] دریافت اطلاعات از سایت tsetmc
|
|
270
|
+
4. [ ] دریافت و پاکسازی اطلاعات سایت codal
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
fima/Bonds.py,sha256=u12dNTrPPUPc3cnZ1aERbWhltVry7YrxFeUq8iEC0RE,4580
|
|
2
|
+
fima/Options.py,sha256=CUOulN8OcErTwE4Gyfid6Xct-AsyIQvmeCo_sMlY4og,39999
|
|
3
|
+
fima/__init__.py,sha256=pjSRFxliqrZRva4pjqzMybxZb_3xN4oPcKJWgtt94uc,823
|
|
4
|
+
fima-0.1.0.dist-info/licenses/LICENSE,sha256=l6425dmjnidzk4JQwhT01C8SpICmjSBMNREAdPnPA9E,1087
|
|
5
|
+
test/TestBonds.py,sha256=gO3tUgiOJsEzX5uD1UsKLQze4Zl8_isT6BlbcQuGeAY,1217
|
|
6
|
+
test/TestOptions.py,sha256=nRVR65dsOO7DZfKXWk30jtAn0_aVHzLYVx7e95F5Mbw,8523
|
|
7
|
+
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fima-0.1.0.dist-info/METADATA,sha256=-ZmCg0dsRUXzNSql_CO1KPnDmNexg_4Tsy-SmbSeUYo,13351
|
|
9
|
+
fima-0.1.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
|
10
|
+
fima-0.1.0.dist-info/top_level.txt,sha256=3iVjx26xETazVXH-Vc_8TGy95qyk4BX05vce6KpYP_E,10
|
|
11
|
+
fima-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Your Name
|
|
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.
|
test/TestBonds.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fima.Bonds import get_risk_free_rate, get_all_bonds_without_coupons, get_all_bonds_with_coupons
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_get_risk_free_rate_range():
|
|
6
|
+
risk_free_rate = get_risk_free_rate()
|
|
7
|
+
assert isinstance(risk_free_rate, float)
|
|
8
|
+
assert 0.25 <= risk_free_rate <= 0.5 # Typical YTM for Iranian T-bills
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize("deprecated", [True, False])
|
|
12
|
+
def test_get_all_bonds_without_coupons(deprecated):
|
|
13
|
+
all_bonds_without_coupons = get_all_bonds_without_coupons(deprecated=deprecated)
|
|
14
|
+
assert all_bonds_without_coupons is not None
|
|
15
|
+
assert not all_bonds_without_coupons.empty
|
|
16
|
+
for column in ['Ticker', 'LastTradedPrice', 'LastTradedDate', 'MaturityDate', 'YTM', 'SimpleReturn']:
|
|
17
|
+
assert column in all_bonds_without_coupons.columns
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize("deprecated", [True, False])
|
|
21
|
+
def test_get_all_bonds_with_coupons(deprecated):
|
|
22
|
+
all_bonds_with_coupons = get_all_bonds_with_coupons(deprecated=deprecated)
|
|
23
|
+
assert all_bonds_with_coupons is not None
|
|
24
|
+
assert not all_bonds_with_coupons.empty
|
|
25
|
+
for column in ['Ticker', 'LastTradedPrice', 'LastTradedDate', 'MaturityDate', 'YTM']:
|
|
26
|
+
assert column in all_bonds_with_coupons.columns
|
test/TestOptions.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
from fima.Options import (get_greeks, get_implied_volatility, black_scholes_merton, download_chain_contracts,
|
|
4
|
+
download_market_watch, download_all_underlying_assets, download_historical_data)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.parametrize("market", ["All", "IFB", "TSE"])
|
|
8
|
+
def test_download_all_underlying_assets(market):
|
|
9
|
+
all_underlying_assets = download_all_underlying_assets(market=market)
|
|
10
|
+
assert all_underlying_assets is not None
|
|
11
|
+
assert not all_underlying_assets.empty
|
|
12
|
+
assert isinstance(all_underlying_assets, pd.DataFrame)
|
|
13
|
+
for column in ['Ticker', 'InstrumentCode', 'ClosePrice', 'LastPrice', 'YesterdayPrice', 'URL']:
|
|
14
|
+
assert column in all_underlying_assets
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(["ticker", "start_date", "end_date"], [("ضهرم4018", '1404-02-01', '1404-02-31'),
|
|
18
|
+
("طهرم4018", '1404-02-01', '1404-02-31'),
|
|
19
|
+
("ضهرم4018", None, None),
|
|
20
|
+
("طهرم4018", None, None)])
|
|
21
|
+
def test_download_historical_data(ticker, start_date, end_date):
|
|
22
|
+
option_historical_data, ua_historical_data = download_historical_data(ticker=ticker, start_date=start_date, end_date=end_date)
|
|
23
|
+
assert option_historical_data is not None
|
|
24
|
+
assert ua_historical_data is not None
|
|
25
|
+
assert not option_historical_data.empty
|
|
26
|
+
assert not ua_historical_data.empty
|
|
27
|
+
assert isinstance(option_historical_data, pd.DataFrame)
|
|
28
|
+
assert isinstance(ua_historical_data, pd.DataFrame)
|
|
29
|
+
for column in ['Date', 'Quantity', 'Volume', 'Value', 'MinPrice', 'MaxPrice', 'FirstPrice', 'LastPrice', 'ClosePrice']:
|
|
30
|
+
assert column in option_historical_data
|
|
31
|
+
for column in ['Date', 'Quantity', 'Volume', 'Value', 'MinPrice', 'MaxPrice', 'FirstPrice', 'LastPrice', 'ClosePrice']:
|
|
32
|
+
assert column in option_historical_data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("ticker", ["ضهرم4018", "ظهرم4018"])
|
|
36
|
+
def test_get_greeks(ticker):
|
|
37
|
+
greeks = get_greeks(ticker)
|
|
38
|
+
assert greeks is not None
|
|
39
|
+
assert isinstance(greeks, pd.Series) # pandas Series
|
|
40
|
+
for greek in ['Delta', 'Gamma', 'Theta', 'Vega', 'Rho']:
|
|
41
|
+
assert greek in greeks
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("ticker", ["ضهرم4018", "طهرم4018"])
|
|
45
|
+
def test_black_scholes_merton(ticker):
|
|
46
|
+
volatility, bsm_price = black_scholes_merton(ticker)
|
|
47
|
+
assert all(isinstance(v, float) for v in [volatility, bsm_price])
|
|
48
|
+
assert 0 <= volatility <= 2
|
|
49
|
+
assert isinstance(bsm_price, float)
|
|
50
|
+
assert 0 <= bsm_price
|
|
51
|
+
assert all(v is not None for v in [volatility, bsm_price])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize("ticker", ["ضهرم4018", "طهرم4018"])
|
|
55
|
+
def test_get_implied_volatility(ticker):
|
|
56
|
+
implied_volatility = get_implied_volatility(ticker)
|
|
57
|
+
assert isinstance(implied_volatility, float)
|
|
58
|
+
assert 0 <= implied_volatility <= 2
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize(["underlying_ticker", "j_date", "bsm", "greeks", "implied_volatility"],
|
|
62
|
+
[('اهرم', True, True, True, True), ('اهرم', True, True, True, False),
|
|
63
|
+
('اهرم', True, True, False, True), ('اهرم', True, True, False, False),
|
|
64
|
+
('اهرم', True, False, True, True), ('اهرم', True, False, True, False),
|
|
65
|
+
('اهرم', True, False, False, True), ('اهرم', True, False, False, False),
|
|
66
|
+
('اهرم', False, True, True, True), ('اهرم', False, True, True, False),
|
|
67
|
+
('اهرم', False, True, False, True), ('اهرم', False, True, False, False),
|
|
68
|
+
('اهرم', False, False, True, True), ('اهرم', False, False, True, False),
|
|
69
|
+
('اهرم', False, False, False, True), ('اهرم', False, False, False, False)])
|
|
70
|
+
def test_download_chain_contracts(underlying_ticker, j_date, bsm, greeks, implied_volatility):
|
|
71
|
+
chain_contracts = download_chain_contracts(underlying_ticker=underlying_ticker, j_date=j_date, bsm=bsm,
|
|
72
|
+
greeks=greeks, implied_volatility=implied_volatility)
|
|
73
|
+
assert chain_contracts is not None
|
|
74
|
+
assert not chain_contracts.empty
|
|
75
|
+
assert isinstance(chain_contracts, pd.DataFrame)
|
|
76
|
+
|
|
77
|
+
if bsm:
|
|
78
|
+
assert all(column in chain_contracts.columns for column in ['BSMPrice-C', 'BSMPrice-P', 'Volatility-C', 'Volatility-P']), "Horizontal, BSM"
|
|
79
|
+
if greeks:
|
|
80
|
+
assert all(column in chain_contracts.columns for column in ['Delta-C', 'Gamma-C', 'Theta-C', 'Vega-C', 'Rho-C',
|
|
81
|
+
'Delta-P', 'Gamma-P', 'Theta-P', 'Vega-P', 'Rho-P']), "Horizontal, Greeks"
|
|
82
|
+
if implied_volatility:
|
|
83
|
+
assert all(column in chain_contracts.columns for column in ['ImpliedVolatility-C', 'ImpliedVolatility-P']), "Horizontal, IV"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.parametrize(["market", "stack", "j_date", "bsm", "greeks", "implied_volatility"],
|
|
87
|
+
[('TSE', 'Horizontal', False, False, False, False), ('TSE', 'Vertical', False, False, False, False),
|
|
88
|
+
('TSE', 'Horizontal', False, False, False, True), ('TSE', 'Vertical', False, False, False, True),
|
|
89
|
+
('TSE', 'Horizontal', True, True, True, True), ('TSE', 'Horizontal', True, True, True, False),
|
|
90
|
+
('TSE', 'Horizontal', True, True, False, True), ('TSE', 'Horizontal', True, True, False, False),
|
|
91
|
+
('TSE', 'Horizontal', True, False, True, True), ('TSE', 'Horizontal', True, False, True, False),
|
|
92
|
+
('TSE', 'Horizontal', True, False, False, True), ('TSE', 'Horizontal', True, False, False, False),
|
|
93
|
+
('TSE', 'Horizontal', False, True, True, True), ('TSE', 'Horizontal', False, True, True, False),
|
|
94
|
+
('TSE', 'Horizontal', False, True, False, True), ('TSE', 'Horizontal', False, True, False, False),
|
|
95
|
+
('TSE', 'Horizontal', False, False, True, True), ('TSE', 'Horizontal', False, False, True, False),
|
|
96
|
+
('TSE', 'Vertical', True, True, True, True), ('TSE', 'Vertical', True, True, True, False),
|
|
97
|
+
('TSE', 'Vertical', True, True, False, True), ('TSE', 'Vertical', True, True, False, False),
|
|
98
|
+
('TSE', 'Vertical', True, False, True, True), ('TSE', 'Vertical', True, False, True, False),
|
|
99
|
+
('TSE', 'Vertical', True, False, False, True), ('TSE', 'Vertical', True, False, False, False),
|
|
100
|
+
('TSE', 'Vertical', False, True, True, True), ('TSE', 'Vertical', False, True, True, False),
|
|
101
|
+
('TSE', 'Vertical', False, True, False, True), ('TSE', 'Vertical', False, True, False, False),
|
|
102
|
+
('TSE', 'Vertical', False, False, True, True), ('TSE', 'Vertical', False, False, True, False)])
|
|
103
|
+
def test_download_market_watch(market, stack, j_date, bsm, greeks, implied_volatility):
|
|
104
|
+
market_watch = download_market_watch(market=market, j_date=j_date, bsm=bsm, greeks=greeks, implied_volatility=implied_volatility)
|
|
105
|
+
assert market_watch is not None
|
|
106
|
+
assert not market_watch.empty
|
|
107
|
+
assert isinstance(market_watch, pd.DataFrame)
|
|
108
|
+
|
|
109
|
+
if stack == 'Horizontal':
|
|
110
|
+
if bsm:
|
|
111
|
+
assert all(column in market_watch.columns for column in ['BSMPrice-C', 'BSMPrice-P', 'Volatility-C', 'Volatility-P']), "Horizontal, BSM"
|
|
112
|
+
if greeks:
|
|
113
|
+
assert all(column in market_watch.columns for column in ['Delta-C', 'Gamma-C', 'Theta-C', 'Vega-C', 'Rho-C',
|
|
114
|
+
'Delta-P', 'Gamma-P', 'Theta-P', 'Vega-P', 'Rho-P']), "Horizontal, Greeks"
|
|
115
|
+
if implied_volatility:
|
|
116
|
+
assert all(column in market_watch.columns for column in ['ImpliedVolatility-C', 'ImpliedVolatility-P']), "Horizontal, IV"
|
|
117
|
+
if stack == 'Vertical':
|
|
118
|
+
if bsm:
|
|
119
|
+
assert all(column in market_watch.columns for column in ['BSMPrice', 'Volatility']), "Vertical, BSM"
|
|
120
|
+
if greeks:
|
|
121
|
+
assert all(column in market_watch.columns for column in ['Delta', 'Gamma', 'Theta', 'Vega', 'Rho']), "Vertical, Greeks"
|
|
122
|
+
if implied_volatility:
|
|
123
|
+
assert all(column in market_watch.columns for column in ['ImpliedVolatility']), "Vertical, IV"
|
test/__init__.py
ADDED
|
File without changes
|