-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #63 from shikbupt/EastMoneyFund
Add eastmoneyfund(天天基金) source
- Loading branch information
Showing
3 changed files
with
205 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
""" | ||
A source fetching fund price(net value) from eastmoneyfund(天天基金) | ||
which is a chinese securities company. | ||
eastmoneyfund supports many kinds of fund, such as fixed income fund, ETF, etc. | ||
this script only supports specific fund which table's header is following: | ||
https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=377240. | ||
fixed income fund is not supported, likes: | ||
https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=040003 | ||
the API, as far as I know, is undocumented. | ||
Prices are denoted in CNY. | ||
Timezone information: the http API requests GMT+8, | ||
the function transfers timezone to GMT+8 automatically | ||
""" | ||
import datetime | ||
import re | ||
from decimal import Decimal | ||
import requests | ||
from beanprice import source | ||
|
||
|
||
# All of the easymoney funds are in CNY. | ||
CURRENCY = 'CNY' | ||
|
||
TIMEZONE = datetime.timezone(datetime.timedelta(hours=+8), 'Asia/Shanghai') | ||
|
||
|
||
headers = {'content-type': 'application/json', | ||
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:22.0)' | ||
'Gecko/20100101 Firefox/22.0'} | ||
|
||
|
||
class EastMoneyFundError(ValueError): | ||
"An error from the EastMoneyFund API." | ||
|
||
|
||
UnsupportTickerError = EastMoneyFundError( | ||
'header not match, dont support this ticker type') | ||
|
||
|
||
def parse_page(page): | ||
tr_re = re.compile(r'<tr>(.*?)</tr>') | ||
item_re = re.compile( | ||
r'<td>(\d{4}-\d{2}-\d{2})</td><td.*?>(.*?)</td><td.*?>(.*?)</td>' | ||
'<td.*?>(.*?)</td><td.*?>(.*?)</td><td.*?>(.*?)</td><td.*?></td>', | ||
re.X) | ||
header_match = re.compile( | ||
r'<th.*?净值日期</th><th>单位净值</th><th>累计净值</th><th>日增长率</th>' | ||
'<th>申购状态</th><th>赎回状态</th>.*?分红送配</th>') | ||
table = tr_re.findall(page) | ||
if not header_match.match(table[0]): | ||
raise UnsupportTickerError | ||
try: | ||
table = [(datetime.datetime.fromisoformat(t[0]). | ||
replace(hour=15, tzinfo=TIMEZONE), Decimal(t[1])) | ||
for t in map(lambda x: item_re.match(x).groups(), table[1:])] | ||
except AttributeError: | ||
return None | ||
return table | ||
|
||
|
||
def get_price_series(ticker: str, time_begin: datetime.datetime, time_end: datetime.datetime): | ||
base_url = 'https://fundf10.eastmoney.com/F10DataApi.aspx' | ||
time_delta_day = (time_end-time_begin).days+1 | ||
pages = time_delta_day//30 + 1 | ||
res = [] | ||
for page in range(1, pages+1): | ||
query = {'code': ticker, 'page': page, | ||
'sdate': time_begin.astimezone(TIMEZONE).date(), | ||
'edate': time_end.astimezone(TIMEZONE).date(), 'type': 'lsjz', 'per': 30} | ||
response = requests.get(base_url, params=query, headers=headers) | ||
if response.status_code != requests.codes.ok: | ||
raise EastMoneyFundError( | ||
f"Invalid response ({response.status_code}): {response.text}") | ||
|
||
price = parse_page(response.text) | ||
if price is None and page == 1: | ||
raise EastMoneyFundError( | ||
f'Invalid ticker {ticker} or ' | ||
f'search day {time_begin.date().isoformat()}~{time_end.date().isoformat()}') | ||
if price is None: | ||
break | ||
res.extend(price) | ||
return res | ||
|
||
|
||
class Source(source.Source): | ||
|
||
def get_latest_price(self, ticker): | ||
end_time = datetime.datetime.now(TIMEZONE) | ||
begin_time = end_time - datetime.timedelta(days=10) | ||
prices = get_price_series(ticker, begin_time, end_time) | ||
last_price = prices[0] | ||
return source.SourcePrice(last_price[1], last_price[0], CURRENCY) | ||
|
||
def get_historical_price(self, ticker, time): | ||
prices = get_price_series( | ||
ticker, time-datetime.timedelta(days=10), time) | ||
last_price = prices[0] | ||
return source.SourcePrice(last_price[1], last_price[0], CURRENCY) | ||
|
||
def get_prices_series(self, ticker, time_begin, time_end): | ||
res = [source.SourcePrice(x[1], x[0], CURRENCY) | ||
for x in get_price_series(ticker, time_begin, time_end)] | ||
return sorted(res, key=lambda x: x.time) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import datetime | ||
import unittest | ||
from decimal import Decimal | ||
|
||
from unittest import mock | ||
from dateutil import tz | ||
|
||
import requests | ||
|
||
import eastmoneyfund | ||
from beanprice import source | ||
|
||
|
||
contents = ''' | ||
var apidata={ content:"<table class='w782 comm lsjz'><thead><tr><th class='first'>净值日期</th><th>单位净值</th><th>累计净值</th><th>日增长率</th><th>申购状态</th><th>赎回状态</th><th class='tor last'>分红送配</th></tr></thead><tbody><tr><td>2020-10-09</td><td class='tor bold'>5.1890</td><td class='tor bold'>5.1890</td><td class='tor bold red'>4.11%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-30</td><td class='tor bold'>4.9840</td><td class='tor bold'>4.9840</td><td class='tor bold red'>0.12%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-29</td><td class='tor bold'>4.9780</td><td class='tor bold'>4.9780</td><td class='tor bold red'>1.14%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-28</td><td class='tor bold'>4.9220</td><td class='tor bold'>4.9220</td><td class='tor bold red'>0.22%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-25</td><td class='tor bold'>4.9110</td><td class='tor bold'>4.9110</td><td class='tor bold red'>0.88%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-24</td><td class='tor bold'>4.8680</td><td class='tor bold'>4.8680</td><td class='tor bold grn'>-3.81%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-23</td><td class='tor bold'>5.0610</td><td class='tor bold'>5.0610</td><td class='tor bold red'>2.41%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-22</td><td class='tor bold'>4.9420</td><td class='tor bold'>4.9420</td><td class='tor bold grn'>-1.02%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-21</td><td class='tor bold'>4.9930</td><td class='tor bold'>4.9930</td><td class='tor bold grn'>-1.29%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-18</td><td class='tor bold'>5.0580</td><td class='tor bold'>5.0580</td><td class='tor bold red'>0.48%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-17</td><td class='tor bold'>5.0340</td><td class='tor bold'>5.0340</td><td class='tor bold red'>0.60%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-16</td><td class='tor bold'>5.0040</td><td class='tor bold'>5.0040</td><td class='tor bold grn'>-1.28%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-15</td><td class='tor bold'>5.0690</td><td class='tor bold'>5.0690</td><td class='tor bold red'>1.06%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-14</td><td class='tor bold'>5.0160</td><td class='tor bold'>5.0160</td><td class='tor bold red'>0.42%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-11</td><td class='tor bold'>4.9950</td><td class='tor bold'>4.9950</td><td class='tor bold red'>3.39%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr><tr><td>2020-09-10</td><td class='tor bold'>4.8310</td><td class='tor bold'>4.8310</td><td class='tor bold grn'>-0.29%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr></tbody></table>",records:16,pages:1,curpage:1};''' | ||
|
||
unsupport_content = ''' | ||
var apidata={ content:"<table class='w782 comm lsjz'><thead><tr><th class='first'>净值日期</th><th>每万份收益</th><th>7日年化收益率(%)</th><th>申购状态</th><th>赎回状态</th><th class='tor last'>分红送配</th></tr></thead><tbody><tr><td>2020-09-10</td><td class='tor bold'>0.4230</td><td class='tor bold'>1.5730%</td><td>开放申购</td><td>开放赎回</td><td class='red unbold'></td></tr></tbody></table>",records:1,pages:1,curpage:1};''' | ||
|
||
|
||
def response(contents, status_code=requests.codes.ok): | ||
"""Return a context manager to patch a JSON response.""" | ||
response = mock.Mock() | ||
response.status_code = status_code | ||
response.text = contents | ||
return mock.patch('requests.get', return_value=response) | ||
|
||
|
||
class EastMoneyFundFetcher(unittest.TestCase): | ||
|
||
def test_error_network(self): | ||
with response(None, 404): | ||
with self.assertRaises(ValueError) as exc: | ||
eastmoneyfund.get_price_series( | ||
'377240', datetime.datetime.now(), datetime.datetime.now()) | ||
|
||
def test_unsupport_page(self): | ||
with response(unsupport_content): | ||
with self.assertRaises(ValueError) as exc: | ||
eastmoneyfund.get_price_series( | ||
'377240', datetime.datetime.now(), datetime.datetime.now()) | ||
self.assertEqual( | ||
eastmoneyfund.UnsupportTickerError, exc.exception) | ||
|
||
def test_latest_price(self): | ||
with response(contents): | ||
srcprice = eastmoneyfund.Source().get_latest_price('377240') | ||
self.assertIsInstance(srcprice, source.SourcePrice) | ||
self.assertEqual(Decimal('5.1890'), srcprice.price) | ||
self.assertEqual('CNY', srcprice.quote_currency) | ||
|
||
def test_historical_price(self): | ||
with response(contents): | ||
time = datetime.datetime(2018, 3, 27, 0, 0, 0, tzinfo=tz.tzutc()) | ||
srcprice = eastmoneyfund.Source().get_historical_price('377240', time) | ||
self.assertIsInstance(srcprice, source.SourcePrice) | ||
self.assertEqual(Decimal('5.1890'), srcprice.price) | ||
self.assertEqual('CNY', srcprice.quote_currency) | ||
self.assertEqual(datetime.datetime(2020, 10, 9, 15, 0, 0, | ||
tzinfo=eastmoneyfund.TIMEZONE), | ||
srcprice.time) | ||
|
||
def test_get_prices_series(self): | ||
with response(contents): | ||
time = datetime.datetime(2018, 3, 27, 0, 0, 0, tzinfo=tz.tzutc()) | ||
srcprice = eastmoneyfund.Source().get_prices_series( | ||
'377240', time-datetime.timedelta(days=10), time) | ||
self.assertIsInstance(srcprice, list) | ||
self.assertIsInstance(srcprice[-1], source.SourcePrice) | ||
self.assertEqual(Decimal('5.1890'), srcprice[-1].price) | ||
self.assertEqual('CNY', srcprice[-1].quote_currency) | ||
self.assertEqual(datetime.datetime(2020, 10, 9, 15, 0, 0, | ||
tzinfo=eastmoneyfund.TIMEZONE), | ||
srcprice[-1].time) | ||
self.assertIsInstance(srcprice[0], source.SourcePrice) | ||
self.assertEqual(Decimal('4.8310'), srcprice[0].price) | ||
self.assertEqual('CNY', srcprice[0].quote_currency) | ||
self.assertEqual(datetime.datetime(2020, 9, 10, 15, 0, 0, | ||
tzinfo=eastmoneyfund.TIMEZONE), | ||
srcprice[0].time) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |