mirror of
https://github.com/kennethreitz/coinbin.org.git
synced 2026-07-22 05:09:31 +00:00
678722d4d4
Brings the long-dead service back to a bootable state. Addresses the modernization epic (#30) and its work items: - #31 scraper: replace the removed CoinMarketCap `views/all` HTML scrape with a JSON price API (CoinGecko by default, no key required). Keeps the get_coins() return shape and MWT cache; handles non-unique symbols by keeping the highest-market-cap entry. - #32 runtime: drop the removed `gaiohttp` gunicorn worker / `aiohttp<2.0`; Procfile now uses `gthread`. - #33 deps/runtime: migrate off unmaintained libs — flask-cache -> flask-caching, drop flask-common, graphene 1/2 -> graphene 3, raven -> sentry-sdk, Python 3.6 -> 3.11+. Replace flask-graphql (incompatible with graphene 3) with a direct schema.execute() view. Add a JSON provider so Flask 3 can serialize Decimal. Make DB/forecast imports lazy so the app boots without them; only force HTTPS outside DEBUG. - #34 ingestion: add ingest.py worker and committed schema.sql for the api_coin price-history table. - #35 forecasting: migrate fbprophet -> prophet behind a FORECASTS_ENABLED flag with lazy, optional imports. Dependency management moved to uv: pyproject.toml + uv.lock, replacing the 2017-era Pipfile/Pipfile.lock. README and app.json updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UMDxE5JVcuzpKyiHercWaa
108 lines
3.3 KiB
Python
108 lines
3.3 KiB
Python
import os
|
|
|
|
from scraper import Coin, MWT, convert_to_decimal
|
|
|
|
|
|
PERIODS = 30
|
|
GRAPH_PERIODS = 365
|
|
|
|
# Forecasting pulls in a heavy, hard-to-build stack (prophet + cmdstanpy,
|
|
# pandas, numpy, matplotlib, mpld3) and needs the price-history database to be
|
|
# populated, so it is opt-in. Enable with FORECASTS_ENABLED=1 once those
|
|
# dependencies are installed (see the [forecast] extras in the Pipfile).
|
|
FORECASTS_ENABLED = os.environ.get('FORECASTS_ENABLED', '').lower() in ('1', 'true', 'yes')
|
|
|
|
_DISABLED_MESSAGE = {
|
|
'enabled': False,
|
|
'message': 'Forecasting is currently disabled on this instance.',
|
|
}
|
|
|
|
|
|
def _build_predictions(coin, render=False):
|
|
"""The real forecasting routine; only imported when enabled."""
|
|
import records
|
|
import maya
|
|
import numpy as np
|
|
|
|
# Matplotlib must pick a headless backend before pyplot is imported.
|
|
import matplotlib
|
|
matplotlib.use('agg')
|
|
import mpld3
|
|
from prophet import Prophet
|
|
|
|
c = Coin(coin)
|
|
|
|
q = "SELECT date as ds, value as y from api_coin WHERE name=:coin"
|
|
|
|
db = records.Database()
|
|
rows = db.query(q, coin=c.name)
|
|
|
|
df = rows.export('df')
|
|
|
|
df['y_orig'] = df['y'] # to save a copy of the original data..you'll see why shortly.
|
|
|
|
# log-transform y
|
|
df['y'] = np.log(df['y'])
|
|
|
|
model = Prophet(weekly_seasonality=True, yearly_seasonality=True)
|
|
model.fit(df)
|
|
|
|
periods = PERIODS if not render else GRAPH_PERIODS
|
|
|
|
future_data = model.make_future_dataframe(periods=periods, freq='d')
|
|
forecast_data = model.predict(future_data)
|
|
|
|
if render:
|
|
matplotlib.pyplot.gcf()
|
|
fig = model.plot(forecast_data, xlabel='Date', ylabel='log($)')
|
|
return mpld3.fig_to_html(fig)
|
|
|
|
forecast_data_orig = forecast_data # make sure we save the original forecast data
|
|
forecast_data_orig['yhat'] = np.exp(forecast_data_orig['yhat'])
|
|
forecast_data_orig['yhat_lower'] = np.exp(forecast_data_orig['yhat_lower'])
|
|
forecast_data_orig['yhat_upper'] = np.exp(forecast_data_orig['yhat_upper'])
|
|
|
|
df['y_log'] = df['y'] # copy the log-transformed data to another column
|
|
df['y'] = df['y_orig'] # copy the original data to 'y'
|
|
|
|
d = forecast_data_orig['yhat'].to_dict()
|
|
predictions = []
|
|
|
|
for i, k in enumerate(list(d.keys())[-PERIODS:]):
|
|
w = maya.when('{} days from now'.format(i + 1))
|
|
predictions.append({
|
|
'when': w.slang_time(),
|
|
'timestamp': w.iso8601(),
|
|
'usd': convert_to_decimal(d[k]),
|
|
})
|
|
|
|
return predictions
|
|
|
|
|
|
@MWT(timeout=300)
|
|
def get_predictions(coin, render=False):
|
|
"""Returns a list of predictions, unless render is True.
|
|
Otherwise, returns rendered HTML.
|
|
|
|
When forecasting is disabled (the default) or its optional dependencies are
|
|
missing, returns a small explanatory payload instead of raising.
|
|
"""
|
|
if not FORECASTS_ENABLED:
|
|
if render:
|
|
return '<p>Forecasting is currently disabled on this instance.</p>'
|
|
return _DISABLED_MESSAGE
|
|
|
|
try:
|
|
return _build_predictions(coin, render=render)
|
|
except ImportError:
|
|
if render:
|
|
return '<p>Forecasting dependencies are not installed.</p>'
|
|
return {
|
|
'enabled': False,
|
|
'message': 'Forecasting dependencies are not installed.',
|
|
}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print(get_predictions('btc'))
|