def rank_cumulative_change(df: pd.DataFrame, timeframe: Timeframe): cum_sum = defaultdict(float) # print(df) for date in filter(lambda k: k in df.columns, timeframe.all_dates()): for code, price_change in df[date].fillna(0.0).iteritems(): cum_sum[code] += price_change rank = pd.Series(cum_sum).rank(method="first", ascending=False) df[date] = rank all_available_dates = df.columns avgs = df.mean(axis=1) # NB: do this BEFORE adding columns... assert len(avgs) == len(df) df["x"] = all_available_dates[-1] df["y"] = df[all_available_dates[-1]] bins = ["top", "bin2", "bin3", "bin4", "bin5", "bottom"] average_rank_binned = pd.cut(avgs, len(bins), bins) assert len(average_rank_binned) == len(df) df["bin"] = average_rank_binned df["asx_code"] = df.index stock_sector_df = ( stocks_by_sector() ) # make one DB call (cached) rather than lots of round-trips # print(stock_sector_df) stock_sector_df = stock_sector_df.set_index("asx_code") # print(df.index) df = df.merge( stock_sector_df, left_index=True, right_on="asx_code" ) # NB: this merge will lose rows: those that dont have a sector eg. ETF's df = pd.melt( df, id_vars=["asx_code", "bin", "sector_name", "x", "y"], var_name="date", value_name="rank", value_vars=all_available_dates, ) df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d") df["x"] = pd.to_datetime(df["x"], format="%Y-%m-%d") return df
def show_pe_trends(request): """ Display a plot of per-sector PE trends across stocks in each sector ref: https://www.commsec.com.au/education/learn/choosing-investments/what-is-price-to-earnings-pe-ratio.html """ validate_user(request.user) timeframe = Timeframe(past_n_days=180) pe_df = company_prices(None, timeframe, fields="pe", missing_cb=None, transpose=True) eps_df = company_prices(None, timeframe, fields="eps", missing_cb=None, transpose=True) ss = stocks_by_sector() ss_dict = {row.asx_code: row.sector_name for row in ss.itertuples()} #print(ss_dict) eps_stocks = set(eps_df.index) n_stocks = len(pe_df) positive_pe_stocks = set(pe_df[pe_df.sum(axis=1) > 0.0].index) all_stocks = set(pe_df.index) n_non_zero_sum = len(positive_pe_stocks) #print(exclude_zero_sum) records = [] trading_dates = set(pe_df.columns) sector_counts_all_stocks = ss['sector_name'].value_counts() all_sectors = set(ss['sector_name'].unique()) pe_pos_df = pe_df.filter(items=positive_pe_stocks, axis=0).merge(ss, left_index=True, right_on='asx_code') assert len(pe_pos_df) <= len(positive_pe_stocks) and len(pe_pos_df) > 0 market_avg_pe_df = pe_pos_df.mean(axis=0).to_frame( name='market_pe') # avg P/E by date series market_avg_pe_df['date'] = pd.to_datetime(market_avg_pe_df.index) #print(market_avg_pe_df) breakdown_by_sector_pe_pos_stocks_only = pe_pos_df[ 'sector_name'].value_counts() #print(breakdown_by_sector_pe_pos_stocks_only) sector_counts_pe_pos_stocks_only = { s[0]: s[1] for s in breakdown_by_sector_pe_pos_stocks_only.items() } #print(sector_counts_pe_pos_stocks_only) #print(sector_counts_all_stocks) #print(sector_counts_pe_pos_stocks_only) for ymd in filter(lambda d: d in trading_dates, timeframe.all_dates( )): # needed to avoid KeyError raised during DataFrame.at[] calls below sum_pe_per_sector = defaultdict(float) sum_eps_per_sector = defaultdict(float) for stock in filter(lambda code: code in ss_dict, all_stocks): sector = ss_dict[stock] assert isinstance(sector, str) if stock in eps_stocks: eps = eps_df.at[stock, ymd] if isnan(eps): continue sum_eps_per_sector[sector] += eps if stock in positive_pe_stocks: pe = pe_df.at[stock, ymd] if isnan(pe): continue assert pe >= 0.0 sum_pe_per_sector[sector] += pe #print(sum_pe_per_sector) assert len(sector_counts_pe_pos_stocks_only) == len(sum_pe_per_sector) assert len(sector_counts_all_stocks) == len(sum_eps_per_sector) for sector in all_sectors: pe_sum = sum_pe_per_sector.get(sector, None) n_pe = sector_counts_pe_pos_stocks_only.get(sector, None) pe_mean = pe_sum / n_pe if pe_sum is not None else None eps_sum = sum_eps_per_sector.get(sector, None) records.append({ 'date': ymd, 'sector': sector, 'mean_pe': pe_mean, 'sum_pe': pe_sum, 'sum_eps': eps_sum, 'n_stocks': n_stocks, 'n_sector_stocks_pe_only': n_pe }) df = pd.DataFrame.from_records(records) #print(df[df["sector"] == 'Utilities']) #print(df) context = { "title": "PE Trends: {}".format(timeframe.description), "n_stocks": n_stocks, "timeframe": timeframe, "n_stocks_with_pe": n_non_zero_sum, "sector_pe_plot": plot_sector_field(df, field="mean_pe"), "sector_eps_plot": plot_sector_field(df, field="sum_eps"), "market_pe_plot": plot_series(market_avg_pe_df, x='date', y='market_pe', y_axis_label="Market-wide mean P/E", color=None, use_smooth_line=True) } return render(request, "pe_trends.html", context)
def show_stock(request, stock=None, n_days=2 * 365): """ Displays a view of a single stock via the template and associated state """ validate_stock(stock) validate_user(request.user) plot_timeframe = Timeframe(past_n_days=n_days) # for template def dataframe(ld: LazyDictionary) -> pd.DataFrame: momentum_timeframe = Timeframe( past_n_days=n_days + 200 ) # to warmup MA200 function df = company_prices( (stock,), momentum_timeframe, fields=all_stock_fundamental_fields, missing_cb=None, ) return df # key dynamic images and text for HTML response. We only compute the required data if image(s) not cached # print(df) ld = LazyDictionary() ld["stock_df"] = lambda ld: ld["stock_df_200"].filter( items=plot_timeframe.all_dates(), axis="rows" ) ld["cip_df"] = lambda: cached_all_stocks_cip(plot_timeframe) ld["stock_df_200"] = lambda ld: dataframe(ld) ld["sector_companies"] = lambda: companies_with_same_sector(stock) ld["company_details"] = lambda: stock_info(stock, lambda msg: warning(request, msg)) ld["sector"] = lambda ld: ld["company_details"].get("sector_name", "") # point_score_results is a tuple (point_score_df, net_points_by_rule) ld["point_score_results"] = lambda ld: make_point_score_dataframe( stock, default_point_score_rules(), ld ) ld["stock_vs_sector_df"] = lambda ld: make_stock_vs_sector_dataframe( ld["cip_df"], stock, ld["sector_companies"] ) print(ld["stock_vs_sector_df"]) momentum_plot = cache_plot( f"{plot_timeframe.description}-{stock}-rsi-plot", lambda ld: plot_momentum(stock, plot_timeframe, ld), datasets=ld, ) monthly_maximum_plot = cache_plot( f"{plot_timeframe.description}-{stock}-monthly-maximum-plot", lambda ld: plot_trend("M", ld), datasets=ld, ) monthly_returns_plot = cache_plot( f"{plot_timeframe.description}-{stock}-monthly returns", lambda ld: plot_monthly_returns(plot_timeframe, stock, ld), datasets=ld, ) company_versus_sector_plot = cache_plot( f"{stock}-{ld['sector']}-company-versus-sector", lambda ld: plot_company_versus_sector( ld["stock_vs_sector_df"], stock, ld["sector"] ), datasets=ld, ) point_score_plot = cache_plot( f"{plot_timeframe.description}-{stock}-point-score-plot", lambda ld: plot_series(ld["point_score_results"][0], x="date", y="points"), datasets=ld, ) net_rule_contributors_plot = cache_plot( f"{plot_timeframe.description}-{stock}-rules-by-points", lambda ld: plot_points_by_rule(ld["point_score_results"][1]), datasets=ld, ) # populate template and render HTML page with context context = { "asx_code": stock, "watched": user_watchlist(request.user), "timeframe": plot_timeframe, "information": ld["company_details"], "momentum": { "rsi_plot": momentum_plot, "monthly_highest_price": { "title": "Highest price each month", "plot_uri": monthly_maximum_plot, }, }, "fundamentals": { "plot_uri": cache_plot( f"{stock}-{plot_timeframe.description}-fundamentals-plot", lambda ld: plot_fundamentals( fundamentals_dataframe(plot_timeframe, stock, ld), stock, ), datasets=ld, ), "title": "Stock fundamentals: EPS, PE, DY etc.", "timeframe": plot_timeframe, }, "stock_vs_sector": { "plot_uri": company_versus_sector_plot, "title": "Company versus sector - percentage change", "timeframe": plot_timeframe, }, "point_score": { "plot_uri": point_score_plot, "title": "Points score due to price movements", }, "net_contributors": { "plot_uri": net_rule_contributors_plot, "title": "Contributions to point score by rule", }, "month_by_month_return_uri": monthly_returns_plot, } return render(request, "stock_page.html", context=context)
def show_purchase_performance(request): purchase_buy_dates = [] purchases = [] stocks = [] for stock, purchases_for_stock in user_purchases(request.user).items(): stocks.append(stock) for purchase in purchases_for_stock: purchase_buy_dates.append(purchase.buy_date) purchases.append(purchase) purchase_buy_dates = sorted(purchase_buy_dates) # print("earliest {} latest {}".format(purchase_buy_dates[0], purchase_buy_dates[-1])) timeframe = Timeframe(from_date=str(purchase_buy_dates[0]), to_date=all_available_dates()[-1]) df = company_prices(stocks, timeframe, transpose=True) rows = [] stock_count = defaultdict(int) stock_cost = defaultdict(float) portfolio_cost = 0.0 for d in [ datetime.strptime(x, "%Y-%m-%d").date() for x in timeframe.all_dates() ]: d_str = str(d) if d_str not in df.columns: # not a trading day? continue purchases_to_date = filter(lambda vp, d=d: vp.buy_date <= d, purchases) for purchase in purchases_to_date: if purchase.buy_date == d: portfolio_cost += purchase.amount stock_count[purchase.asx_code] += purchase.n stock_cost[purchase.asx_code] += purchase.amount portfolio_worth = sum_portfolio(df, d_str, stock_count.items()) #print(df) # emit rows for each stock and aggregate portfolio for asx_code in stocks: cur_price = df.at[asx_code, d_str] if np.isnan(cur_price): # price missing? ok, skip record continue assert cur_price is not None and cur_price >= 0.0 stock_worth = cur_price * stock_count[asx_code] rows.append({ "portfolio_cost": portfolio_cost, "portfolio_worth": portfolio_worth, "portfolio_profit": portfolio_worth - portfolio_cost, "stock_cost": stock_cost[asx_code], "stock_worth": stock_worth, "stock_profit": stock_worth - stock_cost[asx_code], "date": d_str, "stock": asx_code, }) t = plot_portfolio(pd.DataFrame.from_records(rows)) portfolio_performance_figure, stock_performance_figure, profit_contributors_figure = t context = { "title": "Portfolio performance", "portfolio_title": "Overall", "portfolio_figure": portfolio_performance_figure, "stock_title": "Stock", "stock_figure": stock_performance_figure, "profit_contributors": profit_contributors_figure, } return render(request, "portfolio_trends.html", context=context)