├── README.md ├── __init__.py ├── aspect.py ├── cal.py ├── location.py ├── misc.py ├── plot.py └── timeutil.py /README.md: -------------------------------------------------------------------------------- 1 | astoolbox 2 | ========= 3 | 4 | My personal toolbox for astrology calculations 5 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import location 2 | import aspect 3 | import timeutil 4 | import misc 5 | -------------------------------------------------------------------------------- /aspect.py: -------------------------------------------------------------------------------- 1 | '''aspect related functions''' 2 | from location import * 3 | import pandas as pd 4 | from datetime import datetime, timedelta 5 | import numpy as np 6 | 7 | 8 | def aspected(x): 9 | if x[0] * x[1] < 0 and abs(x[0] - x[1]) < 90: 10 | return True 11 | else: 12 | return False 13 | 14 | 15 | def _sign(x): 16 | if x >= 0: 17 | return 1 18 | else: 19 | return -1 20 | 21 | 22 | def aspect_list(planet1, planet2, start, end, aspect, freq='3H', scale=1): 23 | """ return a list of aspect made by 2 planets in given time span and aspect 24 | I only need the exact day so the calculation can be simplified 25 | 26 | modify freq to get different accurance 27 | """ 28 | 29 | # we don't normalize the distance when calculating 0/180 degree. 30 | if aspect in [0, 180]: 31 | diffs = location_diff(planet1, planet2, start, end, freq, 32 | scale=scale, fit180=False) 33 | # only search 180 degree aspect when distance bigger than 90 degree. 34 | if aspect == 180: 35 | diffs_new = diffs[abs(diffs) > 90].apply( 36 | lambda x: x - _sign(x) * 180) 37 | else: 38 | diffs_new = diffs 39 | # for other aspects 40 | else: 41 | diffs = location_diff(planet1, planet2, start, end, freq, scale=scale) 42 | # now we can treat all aspect like conjection 43 | diffs_new = diffs - aspect 44 | aspectlist = pd.rolling_apply(diffs_new, 2, aspected) 45 | tindex = aspectlist[aspectlist==True].index 46 | res = pd.Series([aspect] * len(tindex), tindex) 47 | 48 | return res 49 | 50 | 51 | def aspect_list2(planet1, planet2, start, end, aspl, freq='3H', scale=1): 52 | """ Just another version of aspect_list but accept a list of aspects. """ 53 | 54 | res = [] 55 | for asp in aspl: 56 | t = aspect_list(planet1, planet2, start, end, asp, freq, scale=scale) 57 | res.append(t) 58 | n = pd.concat(res) 59 | return n.sort_index() 60 | 61 | 62 | def next_aspect(planet1, planet2, asps=None, 63 | start=None, num=1, freq='3H', scale=1): 64 | """get next n aspects""" 65 | 66 | if not start: 67 | start = datetime.today() 68 | elif type(start) is str: 69 | start = pd.to_datetime(start) 70 | 71 | if not asps: 72 | asps = [0, 60, 90, 120, 180] 73 | step = timedelta(days=30) 74 | res = pd.Series([]) 75 | 76 | while len(res) < num: 77 | ne = start + step 78 | asp = aspect_list2(planet1, planet2, start, ne, asps, freq, scale) 79 | if len(asp) >= 1: 80 | res = pd.concat([res, asp]) 81 | start = ne 82 | 83 | return res.sort_index().head(num) 84 | 85 | def previous_aspect(planet1, planet2, asps=None, 86 | start=None, num=1, freq='3H', scale=1): 87 | """get previous n aspects""" 88 | 89 | if not start: 90 | start = datetime.today() 91 | elif type(start) is str: 92 | start = pd.to_datetime(start) 93 | 94 | if not asps: 95 | asps = [0, 60, 90, 120, 180] 96 | step = timedelta(days=30) 97 | res = pd.Series([]) 98 | 99 | while len(res) < num: 100 | pr = start - step 101 | asp = aspect_list2(planet1, planet2, pr, start, asps, freq, scale) 102 | if len(asp) >= 1: 103 | res = pd.concat([res, asp]) 104 | start = pr 105 | 106 | return res.sort_index().head(num) 107 | 108 | def revert(n): 109 | """return the remainder of the given aspect in the 360 cycle. 110 | e.g. revert(30) == 330 111 | """ 112 | if n > 0 and n < 360: 113 | return 360 - n 114 | else : 115 | raise ValueError 116 | 117 | def next_aspect_states(planet1, planet2, start=None, 118 | num=1, asps=None, freq='3H', scale=1): 119 | """return aspect state changes(station/direction) of given planets 120 | within given time span. 121 | """ 122 | if not start: 123 | start = datetime.today() 124 | elif type(start) is str: 125 | start = pd.to_datetime(start) 126 | if not asps: 127 | asps = [0, 60, 90, 120, 180] 128 | 129 | res = dict() 130 | # get last asp 131 | pre_asp = previous_aspect(planet1, planet2, start=start, asps=asps, freq=freq, 132 | scale=scale, num=1) 133 | 134 | while len(res) < num: 135 | next_asps = next_aspect(planet1, planet2, start=start, asps=asps, 136 | num=10, freq=freq, scale=scale) 137 | 138 | asps_list = pd.concat([pre_asp, next_asps]) 139 | stay_list = pd.rolling_apply(asps_list, 2, lambda xs: True if xs[0] == xs[1] else np.nan) 140 | stay_list = stay_list.dropna() 141 | for i in range(len(stay_list)): 142 | i0 = asps_list.index.get_loc(stay_list.index[i]) - 1 143 | i1 = i0 + 1 144 | res[asps_list.index[i0]] = -1 145 | res[asps_list.index[i1]] = 1 146 | 147 | pre_asp = asps_list[-1:] 148 | start = asps_list.index[-1] 149 | 150 | return pd.Series(res) 151 | -------------------------------------------------------------------------------- /cal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import astoolbox 4 | import swisseph as swe 5 | from datetime import datetime, timedelta 6 | import icalendar as ic 7 | 8 | 9 | def get_next(num=10, p1=swe.URANUS, p2=swe.SATURN, scale=24): 10 | '''return next aspects between 2 points''' 11 | asps1 = [0, 45, 90, 135, 180] 12 | # asps2 = [0, 30, 45, 60, 90, 135, 150, 120, 180] 13 | 14 | start = datetime.today() - timedelta(20) 15 | 16 | nx = astoolbox.aspect.next_aspect(p1, p2, asps=asps1, num=num, 17 | scale=scale, start=start, freq='6H') 18 | 19 | return nx 20 | 21 | 22 | def make_cal(target, num=20): 23 | '''make a ical file 24 | :parameters: 25 | target: the target ical file to write 26 | num: total numbers of aspects. 27 | ''' 28 | nx = get_next(num) 29 | cal = ic.Calendar() 30 | for k, v in nx.iteritems(): 31 | dt = k.to_datetime() 32 | summary = "{}: URANvsSATU@24".format(v) 33 | 34 | ev = ic.Event() 35 | ev.add('dtstart', dt.date()) 36 | ev.add('summary', summary) 37 | 38 | # alarm = ic.Alarm() 39 | # alarm.add('trigger', timedelta(hours=-12)) 40 | # alarm.add('action', 'DISPLAY') 41 | # alarm.add('description', summary) 42 | 43 | # ev.add_component(alarm) 44 | cal.add_component(ev) 45 | 46 | with open(target, 'w') as f: 47 | f.write(cal.to_ical()) 48 | f.close() 49 | -------------------------------------------------------------------------------- /location.py: -------------------------------------------------------------------------------- 1 | '''location related functions''' 2 | import pandas as pd 3 | import swisseph as swe 4 | import math 5 | 6 | 7 | def _fit360(n): 8 | while n >= 360: 9 | n = n - 360 10 | 11 | if n >= 0: 12 | return n 13 | else: 14 | n = n + 360 15 | return n 16 | # 17 | 18 | def loc_of_planet(planet, start, end, freq='1D', scale=1, fit360=False): 19 | """Calculate the locations of planet within a time span. 20 | 21 | parameters: 22 | 23 | planet: the planet variable in swisseph 24 | start, end: the time span 25 | freq: the calculation freq 26 | scale: mulitply the planet location 27 | 28 | return a pandas Series with planet location 29 | 30 | """ 31 | 32 | results = [] 33 | drange = pd.date_range(start, end, freq=freq, tz='utc') 34 | 35 | for date in drange: 36 | year = date.year 37 | month = date.month 38 | day = date.day 39 | hour = date.hour 40 | minute = date.minute 41 | second = date.second 42 | 43 | jd = swe.utc_to_jd(year, month, day, hour, minute, second, 1) 44 | ut = jd[1] 45 | 46 | loc = swe.calc_ut(ut, planet) 47 | 48 | results.append(loc[0]*scale) 49 | 50 | res = pd.Series(results, drange, name=swe.get_planet_name(planet)) 51 | 52 | if scale > 1 and fit360: 53 | return res.apply(_fit360) 54 | 55 | return res 56 | # 57 | 58 | def lon_to_text(lon): 59 | """return the text represetation of the location""" 60 | 61 | signs = ['Ari', 'Tau', 'Gem', 'Can', 62 | 'Leo', 'Vir', 'Lib', 'Sco', 63 | 'Sag', 'Cap', 'Aqu', 'Pis' 64 | ] 65 | 66 | unifiedLon = _fit360(lon) 67 | 68 | signum = int(unifiedLon // 30) 69 | deg_float = unifiedLon % 30 70 | 71 | minutef, deg = math.modf(deg_float) 72 | 73 | minute = round(minutef * 60) 74 | 75 | string = str(int(deg)) + signs[signum] + str(int(minute)) 76 | 77 | return string 78 | 79 | # 80 | 81 | def _fit180(n): 82 | if abs(n) >= 180: 83 | return 360 - abs(n) 84 | else: 85 | return abs(n) 86 | 87 | def location_diff(planet1, planet2, start, end, freq="1D", scale=1, fit180=True): 88 | """Calculate the difference of location between 2 planets in given time span""" 89 | 90 | loc_planet1 = loc_of_planet(planet1, start, end, freq, scale, fit360=True) 91 | loc_planet2 = loc_of_planet(planet2, start, end, freq, scale, fit360=True) 92 | 93 | #name1 = swe.get_planet_name(planet1) 94 | #name2 = swe.get_planet_name(planet2) 95 | 96 | diff = loc_planet1 - loc_planet2 97 | 98 | if fit180: 99 | return diff.apply(_fit180) 100 | 101 | return diff 102 | 103 | # 104 | 105 | -------------------------------------------------------------------------------- /misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # misc functions(data downloader, drawings etc.) 4 | 5 | import pandas as pd 6 | import numpy as np 7 | 8 | def load_from_xueqiu(stock, start=None, end=None): 9 | """ load bars data from XueQiu """ 10 | import requests 11 | from StringIO import StringIO 12 | link = "http://xueqiu.com/S/%s/historical.csv" % stock 13 | 14 | header = { 15 | "User-Agent" : 16 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36" 17 | } 18 | 19 | r = requests.get(link, headers=header) 20 | 21 | df = pd.DataFrame.from_csv(StringIO(r.content), index_col=1, parse_dates=True) 22 | 23 | if start: 24 | if end: 25 | return df[start:end] 26 | else: 27 | return df[start:] 28 | elif end: 29 | return df[:end] 30 | else: 31 | return df 32 | 33 | def candlestick(ax, df, width=0.4, colorup='k', colordown='r',alpha=1.0): 34 | """ draw candle stick chart on an matplotlib axis """ 35 | from matplotlib.lines import Line2D 36 | from matplotlib.patches import Rectangle 37 | from matplotlib.dates import date2num 38 | import matplotlib.dates as mdates 39 | import matplotlib.ticker as mticker 40 | 41 | OFFSET = width / 2.0 42 | 43 | low = df.low.values 44 | high = df.high.values 45 | op = df.open.values 46 | co = df.close.values 47 | t = df.index.to_pydatetime() 48 | tt = date2num(t) 49 | 50 | lines = [] 51 | rects = [] 52 | 53 | for i in range(len(tt)): 54 | 55 | if co[i] >= op[i]: 56 | color = colorup 57 | lower = op[i] 58 | height = co[i] - op[i] 59 | else: 60 | color = colordown 61 | lower = co[i] 62 | height = op[i] - co[i] 63 | 64 | vline = Line2D( 65 | xdata=(tt[i], tt[i]), 66 | ydata=(low[i], high[i]), 67 | color=color, 68 | linewidth=0.5, 69 | #antialiased=True, 70 | ) 71 | vline.set_alpha(alpha) 72 | 73 | rect = Rectangle( 74 | xy=(tt[i] - OFFSET, lower), 75 | width = width, 76 | height = height, 77 | facecolor = color, 78 | edgecolor = color, 79 | ) 80 | rect.set_alpha(alpha) 81 | 82 | lines.append(vline) 83 | rects.append(rect) 84 | ax.add_line(vline) 85 | ax.add_patch(rect) 86 | # end of for 87 | ax.autoscale_view() 88 | return lines, rects 89 | -------------------------------------------------------------------------------- /plot.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def draw(data, planet1, planet2, start, end, asps, scale): 4 | """draw a plot with matplotlib using given data then annotate with the aspect 5 | with given planets. 6 | """ 7 | 8 | pass -------------------------------------------------------------------------------- /timeutil.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | def middate(a, b): 4 | """find the middle date between 2 dates""" 5 | if b > a: 6 | return a + (b - a) / 2 7 | else: 8 | raise ValueError 9 | --------------------------------------------------------------------------------