这将使货币交换模拟器的第一部分--第一阶段--从histdata.com api下载货币历史数据,使用不同的地块类型绘制给定的时间段和给定的时间框架,并在数据中添加一些外汇技术指标。我将张贴跟进,当我完成了进一步的阶段的项目。
我需要关于整个程序结构的反馈,并欢迎为改进/优化/特性添加的一般建议。我打算添加一个虚拟货币的模拟功能,并自动化一个运行在历史数据上的机器人,它可以预测未来的价值,最大化利润,或者是一个GUI。我还需要一些建议,预测未来某一时期的未来值,使用机器学习,我不知道,也许是神经网络-回归- nlp。我需要关于焦点的指导(什么库/技术.)建立一个强大的机器学习预测系统,因为我在毫升方面不是很有经验。
请下载以下压缩文件,并将内容(以“DAT”开头的两个文件夹)解压到与代码相同的文件夹中(以便能够在eurusd
对的期间(2017-2018年)运行模拟器):
forex_data_handler.py
:它用于从histdata.com api下载外汇历史数据(如果您要下载额外的数据,请先使用pip3安装组数据),这是可选的(上面提供的链接用于最小化试运行),当然可以随意下载任何您想要的数据,使用这个模块来尝试额外的示例。fx_simulator.py
:这是代码的模拟部分,因为它是项目的第一部分,它包含以下特性:( A) FxSimulator
构造函数接受诸如货币对、年份和一些其他参数(您可以在docstring中找到一切) get_interval()
方法,它返回特定的时间框架熊猫DF。M1 =1 min,D5 =5天。查一下文件。C) compare_years()
和compare_year_months()
方法,以便能够使用interval
、comparison_value
等参数进行月份和年份间的比较。( D)用plot_initial_data()
方法绘制历史数据(离散、线、直方图)。( E) add_indicators()
法向熊猫df中添加外汇技术指标值。指标包括(Bollinger带,RSI,移动平均.)pairs.txt
:包含一个从histdata.com api下载数据(可选)时可以选择的66个不同货币对的列表。forex_data_handler.py
:
from histdata import download_hist_data
from concurrent.futures import ThreadPoolExecutor, as_completed
from time import perf_counter
import os
def get_folder_name(year, platform, time_frame, pair, month=None, compressed=True):
"""
Produce folder name in histdata.com download format.
Args:
year: ex: 2010
platform: MT, ASCII, XLSX, NT, MS.
time_frame: M1 (one minute) or T (tick data).
pair: ex: 'eurgbp'
month: int range(1, 12) inclusive
compressed: If compressed, a folder name ending with '.zip' is returned
"""
if not month:
folder_name = '_'.join(['DAT', platform, pair.upper(), time_frame, str(year)])
if compressed:
return folder_name + '.zip'
else:
return folder_name
if month and month <= 9:
month = '0' + str(month)
folder_name = '_'.join(['DAT', platform, pair.upper(), time_frame, str(year) + str(month)])
if compressed:
return folder_name + '.zip'
if not compressed:
return folder_name
def download_fx_data(start_year, end_year, pairs, threads=50, time_frame='M1', platform='MT', output_directory='.',
current_year=2019, current_month=12):
"""
Download Forex data over a given time range from histdata api.
Args:
start_year: int: The starting year.
end_year: int: The ending year.
pairs: A list of pairs ex: ['eurgbp', 'eurusd']
threads: Number of parallel threads.
time_frame: M1 (one minute) or T (tick data).
platform: MT, ASCII, XLSX, NT, MS.
output_directory: Where to dump the data.
current_year: Current year.
current_month: Current month.
"""
month_pairs = []
if end_year >= current_year:
last_year = current_year - 1
else:
last_year = end_year
years_pairs = [(year, pair) for year in range(start_year, last_year + 1) for pair in pairs
if get_folder_name(year, platform, time_frame, pair) not in os.listdir(output_directory)]
if end_year >= current_year:
month_pairs.extend([(month, pair) for pair in pairs for month in range(1, current_month)
if get_folder_name(current_year, platform, time_frame, pair, month)
not in os.listdir(output_directory)])
with ThreadPoolExecutor(max_workers=threads) as executor:
future_years_data = {executor.submit(download_hist_data, str(year), None, pair, time_frame,
platform, output_directory): (year, pair) for year, pair in years_pairs}
future_months_data = {executor.submit(download_hist_data, str(current_year), str(month), pair, time_frame,
platform, output_directory): (month, pair) for month, pair in month_pairs}
for future_file in as_completed(future_years_data):
try:
future_file.result()
except AssertionError:
print(f'Failure to retrieve {future_years_data[future_file]}')
for future_file in as_completed(future_months_data):
try:
future_file.result()
except AssertionError:
print(f'Failure to retrieve {future_months_data[future_file]}')
if __name__ == '__main__':
start_time = perf_counter()
# Change the next line for specific choices, this is set to download all the data available.
# pair_list = [pair.rstrip() for pair in open('pairs.txt').readlines()]
# download_fx_data(2000, 2020, pair_list)
end_time = perf_counter()
print(f'Process finished ... {end_time - start_time} seconds.')
fx_simulator.py
:
from statsmodels.tsa.seasonal import seasonal_decompose
from forex_data_handler import get_folder_name
import matplotlib.pyplot as plt
import pandas as pd
import os
class FxSimulator:
"""A tool for conducting forex backtesting and simulation."""
def __init__(self, pair, years, history_data_path='.', platform='MT', time_frame='M1', current_year=2019,
current_month=12):
"""
Initialize year data.
Args:
pair: A string of currency pair ex: 'eurusd'
years: a list of years.
history_data_path: Folder path containing the historical data.
platform: MT, ASCII, XLSX, NT, MS.
time_frame: M1 or T
current_year: Current year ex: 2019
current_month: Current month ex: 8
"""
not_supported_years = [yr for yr in years if yr not in range(2000, current_year + 1) or yr > current_year]
if not_supported_years:
raise ValueError(f'Invalid year {not_supported_years[0]}'
f'\nYears supported are in range 2000-current year.')
self.currency_pair = pair
self.years = years
self.path = history_data_path
self.platform = platform
self.time_frame = time_frame
self.current_year = current_year
self.current_month = current_month
self.column_names = {'M1': ['Date', 'Time', 'Open', 'High', 'Low', 'Close', 'Volume']}
def combine_frames(self, folders, sep=','):
"""
Combine folder data.
Args:
folders: A list of folders.
sep: Separator of the csv file.
Return:
A data frame of all folders combined.
"""
frames = []
for folder_name in folders:
try:
os.chdir(self.path + folder_name)
current_frame = pd.read_csv(folder_name + '.csv', sep=sep, names=self.column_names[self.time_frame],
parse_dates=True)
frames.append(current_frame)
except FileNotFoundError:
print(f'Folder: {folder_name} not found.')
if frames:
return pd.concat(frames)
def load_data(self, sep=',', year=None):
"""
Load data of the given year range from csv.
Args:
sep: Separator of the csv file.
year: To load a particular year data.
Return:
A data frame with the following columns (check self.column_names)
"""
current_year_month_folders = [get_folder_name(
self.current_year, self.platform, self.time_frame, self.currency_pair, month, False)
for month in range(1, self.current_month)]
if year:
if year not in self.years:
raise ValueError(f'Year {year} not included in self.years')
if year != self.current_year:
folder_name = get_folder_name(year, self.platform, self.time_frame, self.currency_pair, None, False)
return self.combine_frames([folder_name], sep)
if year == self.current_year:
return self.combine_frames(current_year_month_folders, sep)
folder_names = [get_folder_name(yr, self.platform, self.time_frame, self.currency_pair, None, False)
for yr in self.years if yr != self.current_year]
if self.current_year in self.years:
folder_names.extend(current_year_month_folders)
return self.combine_frames(folder_names, sep)
def get_interval(self, interval, year=None, day_timing='12:30'):
"""
Set desired interval.
Args:
interval:
'M' + Minute interval(int 1 - 60) ex: M15 --> 15 minute interval.
'H' + Hour interval(int 1 - 24) ex: H4 --> 4 hour interval.
'D' + Day interval(int 1 - 31) ex: D1 --> 1 day interval.
'W' + Week interval(int 1 - 4)
year: If year, interval of the year will be returned.
day_timing: Timing of the day to get intervals.
Return:
Adjusted data frame.
"""
if year:
period_data = self.load_data(year=year)
else:
period_data = self.load_data()
if 'M' in interval:
current_minutes = period_data['Time'].apply(lambda timing: int(timing.split(':')[-1]))
return period_data[current_minutes % int(interval[1:]) == 0]
if 'H' in interval:
hour_data = period_data[period_data['Time'].apply(lambda timing: timing.split(':')[-1] == '00')]
only_hours = hour_data['Time'].apply(lambda timing: int(timing.split(':')[0]))
valid_hours = only_hours % int(interval[1:]) == 0
return hour_data[valid_hours]
if 'D' in interval:
day_data = period_data[period_data['Time'] == day_timing]
day_only = day_data['Date'].apply(lambda timing: int(timing.split('.')[-1]))
return day_data[day_only % int(interval[1:]) == 0]
if 'W' in interval:
week_data = period_data[period_data['Date'].apply(lambda timing: int(timing.split('.')[-1]) % 7) == 0]
valid_weeks = week_data[week_data['Date'].apply(lambda timing:
int(timing.split('.')[-1]) % int(interval[1:])) == 0]
return valid_weeks[valid_weeks['Time'] == day_timing]
raise ValueError(f'Invalid interval: {interval}')
def compare_years(self, interval, comparison_value):
"""
Get A monthly indexed data for years in self.years.
Args:
interval: Period string indication ex: 'M1', 'D5' ...
comparison_value: A string indication of value to compare: ex: 'Open', Close ...
Return:
A data frame containing year data per month.
"""
frames = []
for year in self.years:
data = self.get_interval(interval, year)
data['Month'] = data['Date'].apply(lambda timing: timing.split('.')[1])
data.reset_index(drop=True, inplace=True)
frames.append(data)
all_years = pd.concat(frames, axis=1).dropna()
all_years.set_index('Month', inplace=True)
all_years.reset_index(inplace=True)
all_years['Month'] = all_years['Month'].apply(lambda months: set(months).pop())
years_data = all_years[['Month', comparison_value]]
years_data.columns = ['Month'] + [comparison_value + '(' + str(yr) + ')' for yr in self.years]
return years_data
def compare_year_months(self, year, interval):
"""
Get A certain year months.
Args:
year: A year specified in self.years
interval: Period string indication ex: 'M1', 'D5' ...
Return:
A data frame containing months of a certain year.
"""
if year not in self.years:
raise ValueError(f'Year {year} not included in self.years')
year_data = self.get_interval(interval, year)
year_data['Month'] = year_data['Date'].apply(lambda timing: timing.split('.')[1])
return year_data
def plot_initial_data(self, plot_type, interval, year=None, years=False, comparison_value=None,
moving_average=None, **kwargs):
"""
Plot initial data in several graph forms.
Args:
plot_type: A string indication to graph type.
- li: Line plot.
- h: Histogram.
- bw: Box & whiskers plot.
- lp: Lag plot.
- ac: Auto-correlation plot.
- sda: Additive Seasonal decompose(Observed-Trend-Seasonal-Residual).
- sdm: Multiplicative Seasonal decompose(Observed-Trend-Seasonal-Residual).
interval: Period string indication ex: 'M1', 'D5' ...
year: If True, only months of given year will be plotted.
years: If True, years will be compared.
comparison_value: If years, this would be a string indication of value to compare against
('Open', Close ...)
moving_average: int representing the moving average window to be displayed (works when a comparison
value is specified).
**kwargs: Additional keyword arguments.
Return:
None
"""
if years and not comparison_value:
raise ValueError(f'Comparison value not specified for years=True')
if year and years:
raise ValueError(f'Cannot compare a single year and all years, please specify year or years')
if moving_average and not comparison_value:
raise ValueError(f'Comparison value not specified for moving average {moving_average}')
if plot_type in ['sda', 'sdm'] and not comparison_value:
raise ValueError(f'Must specify a comparison value for seasonal decomposition')
period_data = pd.DataFrame()
if not year and not years:
period_data = self.get_interval(interval)
period_data.set_index('Date', inplace=True)
if comparison_value:
period_data = period_data.filter(like=comparison_value)
if moving_average:
period_data['Moving Average'] = period_data[comparison_value].rolling(moving_average).mean()
if years:
period_data = self.compare_years(interval, comparison_value)
period_data.set_index('Month', inplace=True)
if moving_average:
for column in period_data.columns:
if comparison_value in column:
period_data['MA ' + column] = period_data[column].rolling(moving_average).mean()
if year:
period_data = self.compare_year_months(year, interval)
period_data.set_index('Month', inplace=True)
if comparison_value:
period_data = period_data.filter(like=comparison_value)
if moving_average:
period_data['Moving Average'] = period_data[comparison_value].rolling(moving_average).mean()
if plot_type == 'li':
period_data.plot(**kwargs)
if plot_type == 'h':
period_data.hist(**kwargs)
if plot_type == 'bw':
period_data.boxplot(**kwargs)
if plot_type == 'sda':
result = seasonal_decompose(period_data.filter(like=comparison_value), model='additive', freq=1)
result.plot()
if plot_type == 'sdm':
result = seasonal_decompose(period_data.filter(like=comparison_value), model='multiplicative', freq=1)
result.plot()
plt.show()
def add_indicators(self, interval, indicators, significant_value='Close', year=None, day_timing='12:30',
rsi_period=14, si_period=14, bb_period=20, kn_period=9, kj_period=26, fast_ema=12,
slow_ema=26, signal_line=9, adx_period=14):
"""
Add Forex technical indicators for the specified interval.
Args:
interval:
'M' + Minute interval(int 1 - 60) ex: M15 --> 15 minute interval.
'H' + Hour interval(int 1 - 24) ex: H4 --> 4 hour interval.
'D' + Day interval(int 1 - 31) ex: D1 --> 1 day interval.
'W' + Week interval(int 1 - 4).
indicators: A list of Technical indicators including:
- 'rsi': Relative strength indicator(RSI).
- 'si': Stochastic oscillator indicator(SI).
- 'bb': Bollinger Bands indicator.
- 'ic': Ichimoku Cloud.
- 'macd': Moving Average Convergence Divergence(MACD).
- 'pp': Pivot Point.
- 'adx': Average Directional movement(ADX)
significant_value: Close - Open - High - Low.
year: If year, interval of the year will be returned.
day_timing: Timing of the day to get intervals.
rsi_period: Period of averaging for the relative strength(RSI) indicator.
si_period: Period of averaging for the stochastic indicator(SI).
bb_period: Period of averaging for the Bollinger Bands indicator.
kn_period: Period of averaging for the Ichimoku Cloud indicator(Kenkan-Sen).
kj_period: Period of averaging for the Ichimoku Cloud indicator(Kijun Sen).
fast_ema: Period of exponential moving average(12 days)
slow_ema: Period of exponential moving average(26 days)
signal_line: MACD signal line.
adx_period: Average True Range(ATR) period.
Return:
Data frame with the adjusted technical indicators.
"""
period_data = self.get_interval(interval, year, day_timing).reset_index(drop=True)
if 'rsi' in indicators:
period_data['Change'] = period_data[significant_value] - period_data[significant_value].shift(1)
period_data['Upward Movement'] = period_data['Change'].apply(lambda change: change if change > 0 else 0)
period_data['Downward Movement'] = period_data['Change'].apply(lambda change: abs(change)
if change < 0 else 0)
period_data['Average Upward Movement'] = period_data['Upward Movement'].rolling(rsi_period).mean()
period_data['Average Downward Movement'] = period_data['Downward Movement'].rolling(rsi_period).mean()
period_data['Relative Strength(RS)'] = (period_data['Average Upward Movement']
/ period_data['Average Downward Movement'])
period_data['Relative Strength Index(RSI)'] = 100 - (100 / (1 + period_data['Relative Strength(RS)']))
if 'si' in indicators:
period_data['Lowest Low'] = period_data['Low'].rolling(si_period).min()
period_data['Highest High'] = period_data['High'].rolling(si_period).max()
period_data['Stochastic Oscillator Index'] = 100 * (period_data['Close'] - period_data['Lowest Low']) / (
period_data['Highest High'] - period_data['Lowest Low'])
if 'bb' in indicators:
period_data['Bollinger Middle Band'] = period_data[significant_value].rolling(bb_period).mean()
period_data['Bollinger Standard Deviation'] = period_data[significant_value].rolling(bb_period).std()
period_data['Upper Bollinger Band'] = period_data['Bollinger Middle Band'] + (
2 * period_data['Bollinger Standard Deviation'])
period_data['Lower Bollinger Band'] = period_data['Bollinger Middle Band'] - (
2 * period_data['Bollinger Standard Deviation'])
if 'ic' in indicators:
period_data['Lowest Low(Kenkan-Sen)'] = period_data['Low'].rolling(kn_period).min()
period_data['Highest High(Kenkan-Sen)'] = period_data['High'].rolling(kn_period).max()
period_data['Kenkan-Sen'] = (
period_data['Highest High(Kenkan-Sen)'] + period_data['Lowest Low(Kenkan-Sen)']) / 2
period_data['Lowest Low(Kijun Sen)'] = period_data['Low'].rolling(kj_period).min()
period_data['Highest High(Kijun Sen)'] = period_data['High'].rolling(kj_period).max()
period_data['Kijun-Sen'] = (
period_data['Highest High(Kijun Sen)'] + period_data['Lowest Low(Kijun Sen)']) / 2
period_data['Chikou-Span'] = period_data[significant_value].shift(-kj_period)
period_data['Senkou(Span A)'] = ((period_data['Kenkan-Sen'] + period_data['Kijun-Sen']) / 2).shift(
kj_period)
period_data['Lowest Low(52)'] = period_data['Low'].rolling(2 * kj_period).min()
period_data['Highest High(52)'] = period_data['High'].rolling(2 * kj_period).max()
period_data['Senkou(Span B)'] = ((period_data['Lowest Low(52)'] + period_data['Highest High(52)']) / 2
).shift(kj_period)
if 'macd' in indicators:
period_data['Fast EMA'] = period_data[significant_value].rolling(fast_ema).mean()
period_data['Slow EMA'] = period_data[significant_value].rolling(slow_ema).mean()
period_data['MACD'] = period_data['Fast EMA'] - period_data['Slow EMA']
period_data['MACD Signal'] = period_data['MACD'].rolling(signal_line).mean()
period_data['MACD Histogram'] = period_data['MACD'] - period_data['MACD Signal']
if 'pp' in indicators:
period_data['Pivot point'] = (period_data['High'] + period_data['Low'] + period_data['Close']) / 3
period_data['First Resistance(R1)'] = (2 * period_data['Pivot point']) - period_data['Low']
period_data['First Support(S1)'] = (2 * period_data['Pivot point']) - period_data['High']
period_data['Second Resistance(R2)'] = period_data['Pivot point'] + period_data[
'High'] - period_data['Low']
period_data['Second Support(S2)'] = period_data['Pivot point'] - (period_data['High'] - period_data['Low'])
period_data['Third Resistance(R3)'] = period_data['High'] + 2 * (
period_data['Pivot point'] - period_data['Low'])
period_data['Third Support(S3)'] = period_data['Low'] - 2 * (
period_data['High'] - period_data['Pivot point'])
if 'adx' in indicators:
period_data['High-Low Difference'] = period_data['High'] - period_data['Low']
period_data['High-Previous Close Difference'] = period_data['High'] - period_data['Close'].shift(1)
period_data['Previous Close and Current Low Difference'] = period_data['Close'].shift(1) - period_data[
'Low']
period_data['True Range(TR)'] = period_data[['High-Low Difference', 'High-Previous Close Difference',
'Previous Close and Current Low Difference']].dropna().max(
axis=1)
period_data['Average True Range(ATR)'] = period_data['True Range(TR)'].rolling(adx_period).mean()
period_data['High-Previous High Difference'] = period_data['High'] - period_data['High'].shift(1)
period_data['Previous Low-Low Difference'] = period_data['Low'].shift(1) - period_data['Low']
positive_condition = period_data[
'High-Previous High Difference'] > period_data['Previous Low-Low Difference']
period_data['Positive DX'] = period_data['High-Previous High Difference'][positive_condition]
period_data['Positive DX'] = period_data['Positive DX'].fillna(0)
negative_condition = period_data[
'High-Previous High Difference'] < period_data['Previous Low-Low Difference']
period_data['Negative DX'] = period_data['Previous Low-Low Difference'][negative_condition]
period_data['Negative DX'] = period_data['Negative DX'].fillna(0)
period_data['Smooth Positive DX'] = period_data['Positive DX'].rolling(adx_period).mean()
period_data['Smooth Negative DX'] = period_data['Negative DX'].rolling(adx_period).mean()
period_data['Positive Directional Movement Index(+DMI)'] = (
period_data['Smooth Positive DX'] / period_data['Average True Range(ATR)']) * 100
period_data['Negative Directional Movement Index(-DMI)'] = (
period_data['Smooth Negative DX'] / period_data['Average True Range(ATR)']) * 100
period_data['Directional Index(DX)'] = abs(
period_data['Positive Directional Movement Index(+DMI)'] -
period_data['Negative Directional Movement Index(-DMI)']) / (
period_data[['Positive Directional Movement Index(+DMI)', 'Negative Directional Movement Index(-DMI)']]
).sum(axis=1) * 100
period_data['Average Directional Index(ADX)'] = period_data['Directional Index(DX)'].rolling(
adx_period).mean()
return period_data
if __name__ == '__main__':
test_years = [year for year in range(2017, 2019)]
x = FxSimulator('eurusd', test_years)
x.plot_initial_data('li', 'D1', comparison_value='Close')
plt.show()
pairs.txt
:
eurusd
eurchf
eurgbp
eurjpy
euraud
usdcad
usdchf
usdjpy
usdmxn
gbpchf
gbpjpy
gbpusd
audjpy
audusd
chfjpy
nzdjpy
nzdusd
xauusd
eurcad
audcad
cadjpy
eurnzd
grxeur
nzdcad
sgdjpy
usdhkd
usdnok
usdtry
xauaud
audchf
auxaud
eurhuf
eurpln
frxeur
hkxhkd
nzdchf
spxusd
usdhuf
usdpln
usdzar
xauchf
zarjpy
bcousd
etxeur
eurczk
eursek
gbpaud
gbpnzd
jpxjpy
udxusd
usdczk
usdsek
wtiusd
xaueur
audnzd
cadchf
eurdkk
eurnok
eurtry
gbpcad
nsxusd
ukxgbp
usddkk
usdsgd
xagusd
xaugbp
发布于 2020-01-03 04:09:21
那你的
程序结构和改进/优化/特性的一般建议
我发现了真实/错误的评估和docString的弱点,并建议您遵循一些代码风格的指南,如谷歌。此外,您还可以对每个匹林特文件调用.py
,就像在forex_data_handler.py pylint forex_data_handler.py
上执行那样,并获得下一个输出:
************* Module forex_data_handler
forex_data_handler.py:33:0: C0301: Line too long (115/100) (line-too-long)
forex_data_handler.py:54:0: C0301: Line too long (108/100) (line-too-long)
forex_data_handler.py:61:0: C0301: Line too long (117/100) (line-too-long)
forex_data_handler.py:62:0: C0301: Line too long (114/100) (line-too-long)
forex_data_handler.py:63:0: C0301: Line too long (120/100) (line-too-long)
forex_data_handler.py:82:0: C0304: Final newline missing (missing-final-newline)
forex_data_handler.py:1:0: C0114: Missing module docstring (missing-module-docstring)
forex_data_handler.py:1:0: E0401: Unable to import 'histdata' (import-error)
forex_data_handler.py:7:0: R0913: Too many arguments (6/5) (too-many-arguments)
forex_data_handler.py:20:8: R1705: Unnecessary "else" after "return" (no-else-return)
forex_data_handler.py:7:0: R1710: Either all return statements in a function should return an expression, or none of them should. (inconsistent-return-statements)
forex_data_handler.py:33:0: R0913: Too many arguments (9/5) (too-many-arguments)
forex_data_handler.py:33:0: R0914: Too many local variables (16/15) (too-many-locals)
forex_data_handler.py:77:4: C0103: Constant name "start_time" doesn't conform to UPPER_CASE naming style (invalid-name)
forex_data_handler.py:81:4: C0103: Constant name "end_time" doesn't conform to UPPER_CASE naming style (invalid-name)
forex_data_handler.py:2:0: C0411: standard import "from concurrent.futures import ThreadPoolExecutor, as_completed" should be placed before "from histdata import download_hist_data" (wrong-import-order)
forex_data_handler.py:3:0: C0411: standard import "from time import perf_counter" should be placed before "from histdata import download_hist_data" (wrong-import-order)
forex_data_handler.py:4:0: C0411: standard import "import os" should be placed before "from histdata import download_hist_data" (wrong-import-order)
------------------------------------------------------------------
Your code has been rated at 4.76/10 (previous run: 8.81/10, -4.05)
```
https://codereview.stackexchange.com/questions/234985
复制