Updraft redesign: brand, live value chart vs S&P, strategy tab with backtest and projections
Browse files- app.py +2 -0
- qt3/performance.py +17 -1
- static/data/strategy.json +1 -0
- static/index.html +474 -168
- tests/test_qt3_space.py +6 -0
app.py
CHANGED
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
| 16 |
|
| 17 |
from fastapi import FastAPI, Header, HTTPException
|
| 18 |
from fastapi.responses import FileResponse
|
|
|
|
| 19 |
from starlette.concurrency import run_in_threadpool
|
| 20 |
|
| 21 |
from qt3 import config, performance, ranking, rebalance, scheduler
|
|
@@ -32,6 +33,7 @@ async def lifespan(_app):
|
|
| 32 |
|
| 33 |
app = FastAPI(title="Updraft", lifespan=lifespan)
|
| 34 |
STATIC = Path(__file__).parent / "static"
|
|
|
|
| 35 |
_cache: dict = {}
|
| 36 |
|
| 37 |
|
|
|
|
| 16 |
|
| 17 |
from fastapi import FastAPI, Header, HTTPException
|
| 18 |
from fastapi.responses import FileResponse
|
| 19 |
+
from fastapi.staticfiles import StaticFiles
|
| 20 |
from starlette.concurrency import run_in_threadpool
|
| 21 |
|
| 22 |
from qt3 import config, performance, ranking, rebalance, scheduler
|
|
|
|
| 33 |
|
| 34 |
app = FastAPI(title="Updraft", lifespan=lifespan)
|
| 35 |
STATIC = Path(__file__).parent / "static"
|
| 36 |
+
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
| 37 |
_cache: dict = {}
|
| 38 |
|
| 39 |
|
qt3/performance.py
CHANGED
|
@@ -20,6 +20,20 @@ def twr(dates: list[str], equity: list[float], flows: dict[str, float]) -> list[
|
|
| 20 |
return out
|
| 21 |
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
def max_drawdown(g: list[float]) -> float:
|
| 24 |
peak, worst = 0.0, 0.0
|
| 25 |
for x in g:
|
|
@@ -64,11 +78,13 @@ def series(a: Alpaca, inception: Optional[str] = None) -> dict:
|
|
| 64 |
j += 1
|
| 65 |
spy.append(last)
|
| 66 |
base = next((x for x in spy if x), None)
|
|
|
|
| 67 |
spy = [round(x / base, 6) if (x and base) else None for x in spy]
|
| 68 |
days = (date.fromisoformat(dates[-1]) - date.fromisoformat(dates[0])).days
|
| 69 |
tot = g[-1] - 1
|
| 70 |
spy_tot = (spy[-1] - 1) if spy and spy[-1] else None
|
| 71 |
ann = (g[-1] ** (365.25 / days) - 1) if days >= 365 else None
|
| 72 |
-
return {"start": start or dates[0], "base": dates[0], "dates": dates,
|
|
|
|
| 73 |
"stats": {"return": tot, "spy_return": spy_tot, "annualised": ann, "max_drawdown": max_drawdown(g),
|
| 74 |
"days": days, "equity": eq[-1]}}
|
|
|
|
| 20 |
return out
|
| 21 |
|
| 22 |
|
| 23 |
+
def bench_value(equity: list[float], spy_px: list, dates: list[str], flows: dict[str, float]) -> list:
|
| 24 |
+
"""Dollar value of the S&P 500 holding the same money: starts at the account's first value and receives the
|
| 25 |
+
same deposits / withdrawals on the same days."""
|
| 26 |
+
out, v, prev = [], None, None
|
| 27 |
+
for e, px, d in zip(equity, spy_px, dates):
|
| 28 |
+
if v is None:
|
| 29 |
+
v = e
|
| 30 |
+
elif px and prev:
|
| 31 |
+
v = v * px / prev + flows.get(d, 0.0)
|
| 32 |
+
out.append(round(v, 2) if v is not None else None)
|
| 33 |
+
prev = px or prev
|
| 34 |
+
return out
|
| 35 |
+
|
| 36 |
+
|
| 37 |
def max_drawdown(g: list[float]) -> float:
|
| 38 |
peak, worst = 0.0, 0.0
|
| 39 |
for x in g:
|
|
|
|
| 78 |
j += 1
|
| 79 |
spy.append(last)
|
| 80 |
base = next((x for x in spy if x), None)
|
| 81 |
+
bench = bench_value(eq, spy, dates, flows) # the same money (and deposits) in the S&P
|
| 82 |
spy = [round(x / base, 6) if (x and base) else None for x in spy]
|
| 83 |
days = (date.fromisoformat(dates[-1]) - date.fromisoformat(dates[0])).days
|
| 84 |
tot = g[-1] - 1
|
| 85 |
spy_tot = (spy[-1] - 1) if spy and spy[-1] else None
|
| 86 |
ann = (g[-1] ** (365.25 / days) - 1) if days >= 365 else None
|
| 87 |
+
return {"start": start or dates[0], "base": dates[0], "dates": dates,
|
| 88 |
+
"value": [round(x, 2) for x in eq], "bench_value": bench, "strategy": [round(x, 6) for x in g], "spy": spy,
|
| 89 |
"stats": {"return": tot, "spy_return": spy_tot, "annualised": ann, "max_drawdown": max_drawdown(g),
|
| 90 |
"days": days, "equity": eq[-1]}}
|
static/data/strategy.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"built":"2026-10-13","period":"1999-08 \u2192 2026-09","stats":{"cagr":0.309045816994999,"max_dd":-0.33989845010898423,"sharpe":0.9847741391408332,"vol":0.3011311580642123,"alpha_t":4.650752436779442,"losing_years":4,"n_years":28,"worst_month":-0.23694458493988768,"cagr_1999_2012":0.2150787663087499,"cagr_2013_2026":0.4077353305866389,"cagr_2013_2023":0.25030768425581673,"cagr_2014_2026":0.37265254775819723},"spy":{"cagr":0.08531750714356745,"max_dd":-0.5079831548109492,"sharpe":0.48985136944536745,"losing_years":6,"cagr_1999_2012":0.023550980220163442,"cagr_2013_2026":0.14917781617813874,"cagr_2013_2023":0.1364398465531631,"cagr_2014_2026":0.13792128122575198},"rolling_12m":{"n":315,"median":0.24384318390809234,"p10":-0.03072797978492498,"p90":0.7861088376087704,"worst":-0.2583066825381193,"worst_end":"2008-02","best":3.1594439696787617,"best_end":"2026-06","share_positive":0.873015873015873,"share_beat_spy":0.7714285714285715,"spy_median":0.13103103763537094,"spy_share_positive":0.7777777777777778},"holding":{"positions":373,"median_months":2.0,"share_within_3m":0.774798927613941,"share_year_plus":0.0,"max_months":9},"growth":{"dates":["1999-07","1999-08","1999-09","1999-10","1999-11","1999-12","2000-01","2000-02","2000-03","2000-04","2000-05","2000-06","2000-07","2000-08","2000-09","2000-10","2000-11","2000-12","2001-01","2001-02","2001-03","2001-04","2001-05","2001-06","2001-07","2001-08","2001-09","2001-10","2001-11","2001-12","2002-01","2002-02","2002-03","2002-04","2002-05","2002-06","2002-07","2002-08","2002-09","2002-10","2002-11","2002-12","2003-01","2003-02","2003-03","2003-04","2003-05","2003-06","2003-07","2003-08","2003-09","2003-10","2003-11","2003-12","2004-01","2004-02","2004-03","2004-04","2004-05","2004-06","2004-07","2004-08","2004-09","2004-10","2004-11","2004-12","2005-01","2005-02","2005-03","2005-04","2005-05","2005-06","2005-07","2005-08","2005-09","2005-10","2005-11","2005-12","2006-01","2006-02","2006-03","2006-04","2006-05","2006-06","2006-07","2006-08","2006-09","2006-10","2006-11","2006-12","2007-01","2007-02","2007-03","2007-04","2007-05","2007-06","2007-07","2007-08","2007-09","2007-10","2007-11","2007-12","2008-01","2008-02","2008-03","2008-04","2008-05","2008-06","2008-07","2008-08","2008-09","2008-10","2008-11","2008-12","2009-01","2009-02","2009-03","2009-04","2009-05","2009-06","2009-07","2009-08","2009-09","2009-10","2009-11","2009-12","2010-01","2010-02","2010-03","2010-04","2010-05","2010-06","2010-07","2010-08","2010-09","2010-10","2010-11","2010-12","2011-01","2011-02","2011-03","2011-04","2011-05","2011-06","2011-07","2011-08","2011-09","2011-10","2011-11","2011-12","2012-01","2012-02","2012-03","2012-04","2012-05","2012-06","2012-07","2012-08","2012-09","2012-10","2012-11","2012-12","2013-01","2013-02","2013-03","2013-04","2013-05","2013-06","2013-07","2013-08","2013-09","2013-10","2013-11","2013-12","2014-01","2014-02","2014-03","2014-04","2014-05","2014-06","2014-07","2014-08","2014-09","2014-10","2014-11","2014-12","2015-01","2015-02","2015-03","2015-04","2015-05","2015-06","2015-07","2015-08","2015-09","2015-10","2015-11","2015-12","2016-01","2016-02","2016-03","2016-04","2016-05","2016-06","2016-07","2016-08","2016-09","2016-10","2016-11","2016-12","2017-01","2017-02","2017-03","2017-04","2017-05","2017-06","2017-07","2017-08","2017-09","2017-10","2017-11","2017-12","2018-01","2018-02","2018-03","2018-04","2018-05","2018-06","2018-07","2018-08","2018-09","2018-10","2018-11","2018-12","2019-01","2019-02","2019-03","2019-04","2019-05","2019-06","2019-07","2019-08","2019-09","2019-10","2019-11","2019-12","2020-01","2020-02","2020-03","2020-04","2020-05","2020-06","2020-07","2020-08","2020-09","2020-10","2020-11","2020-12","2021-01","2021-02","2021-03","2021-04","2021-05","2021-06","2021-07","2021-08","2021-09","2021-10","2021-11","2021-12","2022-01","2022-02","2022-03","2022-04","2022-05","2022-06","2022-07","2022-08","2022-09","2022-10","2022-11","2022-12","2023-01","2023-02","2023-03","2023-04","2023-05","2023-06","2023-07","2023-08","2023-09","2023-10","2023-11","2023-12","2024-01","2024-02","2024-03","2024-04","2024-05","2024-06","2024-07","2024-08","2024-09","2024-10","2024-11","2024-12","2025-01","2025-02","2025-03","2025-04","2025-05","2025-06","2025-07","2025-08","2025-09","2025-10","2025-11","2025-12","2026-01","2026-02","2026-03","2026-04","2026-05","2026-06","2026-07","2026-08","2026-09"],"updraft":[10000.0,9996.31,10823.18,12934.3,14774.76,15150.25,13749.62,18836.26,16329.73,14774.75,13808.43,13836.59,12882.79,12433.84,13656.88,15204.47,15415.72,16960.82,14222.57,15132.37,15103.18,15846.42,15734.71,14973.31,15404.79,15627.77,14479.56,14565.14,16781.78,18154.26,18625.1,17886.8,18228.4,20038.35,19529.09,17186.93,16980.06,17173.0,17163.51,17484.42,16973.29,16702.36,16747.06,17161.35,17976.1,20431.14,19797.47,19229.39,21495.58,23229.03,24288.83,26120.76,28507.31,27861.97,28580.7,30171.47,28777.87,27673.93,28756.27,32553.84,32666.78,33585.25,37946.81,43189.75,48709.11,51310.77,47848.7,49166.05,49181.17,48082.92,51404.46,55436.84,55064.35,57376.26,57933.04,53968.35,54413.94,59387.57,63142.62,59297.11,67661.62,67820.08,66870.14,68297.66,61230.64,59985.13,61751.47,60143.74,62889.91,66055.39,68225.35,67207.85,67846.82,68394.88,70421.29,67556.41,59909.49,58240.68,62217.42,60364.99,55154.79,56110.13,54402.09,49847.61,53248.57,61020.77,66714.0,63444.26,64765.88,65126.56,60902.05,61831.18,62822.48,64648.66,71173.73,61698.67,66566.9,58911.53,54923.44,54589.05,57893.79,60361.57,68866.64,62226.12,73239.71,79530.62,77821.28,83217.49,86819.21,91551.4,89090.76,86375.95,83402.54,89606.73,97419.72,102727.3,105206.52,105200.67,105893.98,107947.9,112165.35,114764.63,112022.16,115192.26,109283.25,98792.42,91765.18,107230.55,106719.95,108440.24,108223.18,113819.97,118796.47,119938.35,103549.76,108829.52,126059.51,133786.49,141182.1,140327.16,141553.94,136492.71,142182.1,153882.8,175070.8,164966.05,186234.56,181720.7,199463.98,218369.01,229308.08,212104.9,243556.7,267704.42,279169.16,286347.4,286923.15,279541.92,311206.01,328387.42,336699.48,373053.92,377333.26,387314.46,365067.65,363600.85,353510.31,356435.74,376681.24,363151.22,415555.59,405096.53,421356.43,411814.27,395312.39,432178.09,433708.26,401835.76,377221.75,395297.68,405178.26,385061.83,359378.46,365718.43,394469.13,412794.29,426510.44,413787.96,436615.25,423469.4,453579.29,485954.86,460879.92,432511.47,445547.03,442103.88,502212.09,574797.07,587616.07,619437.31,660683.81,662540.67,690247.62,652362.43,665908.21,650870.03,683676.31,709149.08,738326.47,869165.51,915062.53,708301.67,679893.03,639259.9,713811.42,756492.24,782903.07,817090.88,710069.13,774839.81,832367.87,872393.96,757016.49,738932.84,774812.7,845307.49,822151.87,774887.53,649396.62,702664.17,763782.41,829104.02,893115.74,925777.27,895779.09,844552.99,1059586.44,1130106.04,1085121.72,1230069.0,1167849.19,1126689.73,1080609.84,1044648.9,1066753.13,1139953.77,1082734.69,1209361.47,1267489.41,1245358.96,1075910.95,1112637.81,1205712.98,1171069.5,1396058.19,1085022.45,1214322.05,1240853.48,1096010.24,1230792.71,1323619.84,1205724.46,1188424.27,1218070.83,1179465.91,1203626.07,1388605.08,1560208.43,1704167.14,1697998.1,1558959.13,1413095.53,1466322.13,1593291.94,1624005.77,1956920.41,2223655.95,2156092.88,2583977.81,2597940.42,2420475.15,2664952.4,3079459.97,3271446.43,4480253.07,4008257.71,4814907.31,4068487.54,3624065.18,3639087.01,4290688.92,4634427.06,4624935.24,4965783.88,6083901.49,6806403.37,7196045.43,7191298.24,8906741.72,10229442.07,9576338.29,12678622.33,14988812.02,19276639.69,14709144.3,14264699.74,15040883.6],"spy":[10000.0,9948.11,9725.63,10348.81,10521.09,11121.83,10568.13,10407.12,11415.6,11014.66,10841.53,11055.03,10881.54,11592.49,10957.07,10905.91,10091.66,10039.65,10486.05,9485.82,8954.79,9719.81,9665.38,9435.02,9338.76,8784.69,8067.92,8173.03,8810.27,8859.74,8772.93,8615.56,8902.05,8384.36,8334.53,7719.84,7111.33,7159.71,6408.99,6936.39,7364.24,6947.18,6776.23,6684.93,6698.63,7265.43,7663.83,7745.54,7885.21,8047.79,7960.26,8386.3,8477.95,8904.6,9080.64,9203.82,9082.33,8910.54,9063.18,9230.73,8933.33,8955.15,9044.87,9161.38,9569.11,9857.66,9636.64,9838.14,9658.22,9477.33,9782.74,9797.41,10172.28,10076.87,10157.49,9917.19,10353.05,10333.41,10581.59,10642.21,10817.76,10954.4,10624.51,10652.51,10700.28,10933.79,11228.89,11582.79,11813.14,11971.0,12151.04,11912.68,12050.9,12584.72,13011.6,12821.01,12419.59,12579.02,13065.8,13243.05,12730.08,12585.81,11824.91,11519.26,11415.24,11959.36,12140.13,11126.44,11026.42,11196.88,10140.15,8465.1,7875.87,7953.35,7300.23,6515.8,7059.81,7761.18,8214.86,8209.28,8821.67,9147.56,9471.88,9289.78,9862.15,10050.19,9684.9,9987.03,10595.29,10759.21,9904.34,9391.86,10033.34,9581.96,10440.1,10838.98,10838.98,11563.51,11832.91,12243.91,12245.24,12599.87,12458.51,12248.15,12003.13,11343.34,10555.52,11707.67,11660.02,11781.74,12328.17,12863.2,13276.88,13188.25,12396.19,12898.61,13051.25,13378.23,13717.34,13467.71,13543.97,13665.81,14365.37,14548.68,15101.17,15391.3,15754.66,15544.19,16347.52,15857.23,16359.53,17117.15,17624.42,18080.77,17443.53,18237.41,18388.96,18516.87,18946.55,19337.54,19077.61,19830.51,19556.99,20017.58,20567.52,20514.91,19907.01,21025.93,20694.83,20898.39,21167.06,20737.5,21205.97,19913.56,19407.01,21057.82,21134.81,20771.69,19737.64,19721.27,21047.39,21130.44,21489.8,21564.97,22351.57,22378.24,22380.06,21991.97,22802.22,23264.63,23680.97,24611.49,24642.4,24887.06,25238.3,25399.18,25921.12,25996.78,26520.41,27145.28,27975.05,28313.19,29908.95,28821.43,28031.18,28176.06,28861.07,29027.17,30102.57,31063.4,31248.29,29088.88,29628.4,27022.95,29186.6,30132.76,30677.49,31930.75,29894.52,31974.88,32458.38,31915.23,32536.58,33255.78,34459.57,35460.05,35445.74,32639.64,28571.31,32199.42,33733.59,34332.76,36354.68,38892.11,37437.59,36504.16,40474.9,41975.24,41547.51,42702.68,44641.68,47003.67,47312.23,48375.5,49556.5,51031.27,48654.48,52068.23,51649.83,54040.69,51190.46,49679.44,51545.2,47021.13,47127.34,43240.75,47222.63,45295.89,41109.83,44450.96,46922.08,44220.49,47001.37,45819.64,47520.64,48279.85,48502.69,51646.32,53336.89,52470.02,49983.39,48898.29,53364.77,55801.58,56690.27,59648.77,61601.34,59117.61,62107.76,64299.17,65077.78,66598.37,67997.6,67390.79,71409.54,69689.14,71560.72,70652.27,66715.49,66137.05,70293.4,73905.99,75608.2,77159.59,79907.01,81811.81,81971.36,82034.53,83243.53,82524.1,78452.25,86693.91,91256.17,90314.5,90345.9,92767.18,92461.29]},"drawdown":{"dates":["1999-08","1999-09","1999-10","1999-11","1999-12","2000-01","2000-02","2000-03","2000-04","2000-05","2000-06","2000-07","2000-08","2000-09","2000-10","2000-11","2000-12","2001-01","2001-02","2001-03","2001-04","2001-05","2001-06","2001-07","2001-08","2001-09","2001-10","2001-11","2001-12","2002-01","2002-02","2002-03","2002-04","2002-05","2002-06","2002-07","2002-08","2002-09","2002-10","2002-11","2002-12","2003-01","2003-02","2003-03","2003-04","2003-05","2003-06","2003-07","2003-08","2003-09","2003-10","2003-11","2003-12","2004-01","2004-02","2004-03","2004-04","2004-05","2004-06","2004-07","2004-08","2004-09","2004-10","2004-11","2004-12","2005-01","2005-02","2005-03","2005-04","2005-05","2005-06","2005-07","2005-08","2005-09","2005-10","2005-11","2005-12","2006-01","2006-02","2006-03","2006-04","2006-05","2006-06","2006-07","2006-08","2006-09","2006-10","2006-11","2006-12","2007-01","2007-02","2007-03","2007-04","2007-05","2007-06","2007-07","2007-08","2007-09","2007-10","2007-11","2007-12","2008-01","2008-02","2008-03","2008-04","2008-05","2008-06","2008-07","2008-08","2008-09","2008-10","2008-11","2008-12","2009-01","2009-02","2009-03","2009-04","2009-05","2009-06","2009-07","2009-08","2009-09","2009-10","2009-11","2009-12","2010-01","2010-02","2010-03","2010-04","2010-05","2010-06","2010-07","2010-08","2010-09","2010-10","2010-11","2010-12","2011-01","2011-02","2011-03","2011-04","2011-05","2011-06","2011-07","2011-08","2011-09","2011-10","2011-11","2011-12","2012-01","2012-02","2012-03","2012-04","2012-05","2012-06","2012-07","2012-08","2012-09","2012-10","2012-11","2012-12","2013-01","2013-02","2013-03","2013-04","2013-05","2013-06","2013-07","2013-08","2013-09","2013-10","2013-11","2013-12","2014-01","2014-02","2014-03","2014-04","2014-05","2014-06","2014-07","2014-08","2014-09","2014-10","2014-11","2014-12","2015-01","2015-02","2015-03","2015-04","2015-05","2015-06","2015-07","2015-08","2015-09","2015-10","2015-11","2015-12","2016-01","2016-02","2016-03","2016-04","2016-05","2016-06","2016-07","2016-08","2016-09","2016-10","2016-11","2016-12","2017-01","2017-02","2017-03","2017-04","2017-05","2017-06","2017-07","2017-08","2017-09","2017-10","2017-11","2017-12","2018-01","2018-02","2018-03","2018-04","2018-05","2018-06","2018-07","2018-08","2018-09","2018-10","2018-11","2018-12","2019-01","2019-02","2019-03","2019-04","2019-05","2019-06","2019-07","2019-08","2019-09","2019-10","2019-11","2019-12","2020-01","2020-02","2020-03","2020-04","2020-05","2020-06","2020-07","2020-08","2020-09","2020-10","2020-11","2020-12","2021-01","2021-02","2021-03","2021-04","2021-05","2021-06","2021-07","2021-08","2021-09","2021-10","2021-11","2021-12","2022-01","2022-02","2022-03","2022-04","2022-05","2022-06","2022-07","2022-08","2022-09","2022-10","2022-11","2022-12","2023-01","2023-02","2023-03","2023-04","2023-05","2023-06","2023-07","2023-08","2023-09","2023-10","2023-11","2023-12","2024-01","2024-02","2024-03","2024-04","2024-05","2024-06","2024-07","2024-08","2024-09","2024-10","2024-11","2024-12","2025-01","2025-02","2025-03","2025-04","2025-05","2025-06","2025-07","2025-08","2025-09","2025-10","2025-11","2025-12","2026-01","2026-02","2026-03","2026-04","2026-05","2026-06","2026-07","2026-08","2026-09"],"updraft":[0.0,0.0,0.0,0.0,0.0,-0.0924,0.0,-0.1331,-0.2156,-0.2669,-0.2654,-0.3161,-0.3399,-0.275,-0.1928,-0.1816,-0.0996,-0.2449,-0.1966,-0.1982,-0.1587,-0.1647,-0.2051,-0.1822,-0.1703,-0.2313,-0.2267,-0.1091,-0.0362,-0.0112,-0.0504,-0.0323,0.0,-0.0254,-0.1423,-0.1526,-0.143,-0.1435,-0.1275,-0.153,-0.1665,-0.1642,-0.1436,-0.1029,0.0,-0.031,-0.0588,0.0,0.0,0.0,0.0,0.0,-0.0226,0.0,0.0,-0.0462,-0.0828,-0.0469,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.0675,-0.0418,-0.0415,-0.0629,0.0,0.0,-0.0067,0.0,0.0,-0.0684,-0.0607,0.0,0.0,-0.0609,0.0,0.0,-0.014,0.0,-0.1035,-0.1217,-0.0958,-0.1194,-0.0792,-0.0328,-0.0011,-0.016,-0.0066,0.0,0.0,-0.0407,-0.1493,-0.173,-0.1165,-0.1428,-0.2168,-0.2032,-0.2275,-0.2922,-0.2439,-0.1335,-0.0526,-0.0991,-0.0803,-0.0752,-0.1352,-0.122,-0.1079,-0.082,0.0,-0.1331,-0.0647,-0.1723,-0.2283,-0.233,-0.1866,-0.1519,-0.0324,-0.1257,0.0,0.0,-0.0215,0.0,0.0,0.0,-0.0269,-0.0565,-0.089,-0.0212,0.0,0.0,0.0,-0.0001,0.0,0.0,0.0,0.0,-0.0239,0.0,-0.0513,-0.1424,-0.2034,-0.0691,-0.0735,-0.0586,-0.0605,-0.0119,0.0,0.0,-0.1366,-0.0926,0.0,0.0,0.0,-0.0061,0.0,-0.0358,0.0,0.0,0.0,-0.0577,0.0,-0.0242,0.0,0.0,0.0,-0.075,0.0,0.0,0.0,0.0,0.0,-0.0257,0.0,0.0,0.0,0.0,0.0,0.0,-0.0574,-0.0612,-0.0873,-0.0797,-0.0275,-0.0624,0.0,-0.0252,0.0,-0.0226,-0.0618,0.0,0.0,-0.0735,-0.1302,-0.0886,-0.0658,-0.1122,-0.1714,-0.1568,-0.0905,-0.0482,-0.0166,-0.0459,0.0,-0.0301,0.0,0.0,-0.0516,-0.11,-0.0832,-0.0902,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.0549,-0.0353,-0.057,-0.0095,0.0,0.0,0.0,0.0,-0.226,-0.257,-0.3014,-0.2199,-0.1733,-0.1444,-0.1071,-0.224,-0.1532,-0.0904,-0.0466,-0.1727,-0.1925,-0.1533,-0.0762,-0.1015,-0.1532,-0.2903,-0.2321,-0.1653,-0.0939,-0.024,0.0,-0.0324,-0.0877,0.0,0.0,-0.0398,0.0,-0.0506,-0.084,-0.1215,-0.1507,-0.1328,-0.0733,-0.1198,-0.0168,0.0,-0.0175,-0.1511,-0.1222,-0.0487,-0.0761,0.0,-0.2228,-0.1302,-0.1112,-0.2149,-0.1184,-0.0519,-0.1363,-0.1487,-0.1275,-0.1551,-0.1378,-0.0053,0.0,0.0,-0.0036,-0.0852,-0.1708,-0.1396,-0.0651,-0.047,0.0,0.0,-0.0304,0.0,0.0,-0.0683,0.0,0.0,0.0,0.0,-0.1054,0.0,-0.155,-0.2473,-0.2442,-0.1089,-0.0375,-0.0395,0.0,0.0,0.0,0.0,-0.0007,0.0,0.0,-0.0638,0.0,0.0,0.0,-0.2369,-0.26,-0.2197],"spy":[0.0,-0.0224,0.0,0.0,0.0,-0.0498,-0.0643,0.0,-0.0351,-0.0503,-0.0316,-0.0468,0.0,-0.0548,-0.0592,-0.1295,-0.134,-0.0954,-0.1817,-0.2275,-0.1615,-0.1662,-0.1861,-0.1944,-0.2422,-0.304,-0.295,-0.24,-0.2357,-0.2432,-0.2568,-0.2321,-0.2767,-0.281,-0.3341,-0.3866,-0.3824,-0.4471,-0.4016,-0.3647,-0.4007,-0.4155,-0.4233,-0.4222,-0.3733,-0.3389,-0.3318,-0.3198,-0.3058,-0.3133,-0.2766,-0.2687,-0.2319,-0.2167,-0.2061,-0.2165,-0.2314,-0.2182,-0.2037,-0.2294,-0.2275,-0.2198,-0.2097,-0.1745,-0.1497,-0.1687,-0.1513,-0.1669,-0.1825,-0.1561,-0.1548,-0.1225,-0.1307,-0.1238,-0.1445,-0.1069,-0.1086,-0.0872,-0.082,-0.0668,-0.055,-0.0835,-0.0811,-0.077,-0.0568,-0.0314,-0.0008,0.0,0.0,0.0,-0.0196,-0.0082,0.0,0.0,-0.0146,-0.0455,-0.0332,0.0,0.0,-0.0387,-0.0496,-0.1071,-0.1302,-0.138,-0.0969,-0.0833,-0.1598,-0.1674,-0.1545,-0.2343,-0.3608,-0.4053,-0.3994,-0.4488,-0.508,-0.4669,-0.4139,-0.3797,-0.3801,-0.3339,-0.3093,-0.2848,-0.2985,-0.2553,-0.2411,-0.2687,-0.2459,-0.1999,-0.1876,-0.2521,-0.2908,-0.2424,-0.2765,-0.2117,-0.1815,-0.1815,-0.1268,-0.1065,-0.0754,-0.0753,-0.0486,-0.0592,-0.0751,-0.0936,-0.1434,-0.2029,-0.1159,-0.1195,-0.1103,-0.0691,-0.0287,0.0,-0.0067,-0.0663,-0.0285,-0.017,0.0,0.0,-0.0182,-0.0126,-0.0038,0.0,0.0,0.0,0.0,0.0,-0.0134,0.0,-0.03,0.0,0.0,0.0,0.0,-0.0352,0.0,0.0,0.0,0.0,0.0,-0.0134,0.0,-0.0138,0.0,0.0,-0.0026,-0.0321,0.0,-0.0157,-0.0061,0.0,-0.0203,0.0,-0.0609,-0.0848,-0.007,-0.0034,-0.0205,-0.0692,-0.07,-0.0075,-0.0036,0.0,0.0,0.0,0.0,0.0,-0.0173,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.0364,-0.0628,-0.0579,-0.035,-0.0295,0.0,0.0,0.0,-0.0691,-0.0518,-0.1352,-0.066,-0.0357,-0.0183,0.0,-0.0638,0.0,0.0,-0.0167,0.0,0.0,0.0,0.0,-0.0004,-0.0795,-0.1943,-0.092,-0.0487,-0.0318,0.0,0.0,-0.0374,-0.0614,0.0,0.0,-0.0102,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.0466,0.0,-0.008,0.0,-0.0527,-0.0807,-0.0462,-0.1299,-0.1279,-0.1998,-0.1262,-0.1618,-0.2393,-0.1775,-0.1317,-0.1817,-0.1303,-0.1521,-0.1207,-0.1066,-0.1025,-0.0443,-0.013,-0.0291,-0.0751,-0.0952,-0.0125,0.0,0.0,0.0,0.0,-0.0403,0.0,0.0,0.0,0.0,0.0,-0.0089,0.0,-0.0241,0.0,-0.0127,-0.0677,-0.0758,-0.0177,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.0086,-0.0576,0.0,0.0,-0.0103,-0.01,0.0,-0.0033]},"years":{"labels":["1999","2000","2001","2002","2003","2004","2005","2006","2007","2008","2009","2010","2011","2012","2013","2014","2015","2016","2017","2018","2019","2020","2021","2022","2023","2024","2025","2026"],"updraft":[0.515,0.1195,0.0704,-0.08,0.6681,0.8416,0.1574,0.1123,-0.1506,0.1522,0.2302,0.3228,0.0308,0.2587,0.9613,0.3582,0.1052,0.0538,0.5646,-0.0351,0.3223,0.3369,0.102,-0.0318,0.3214,1.5157,0.7941,1.0915],"spy":[0.1122,-0.0973,-0.1175,-0.2159,0.2818,0.107,0.0483,0.1585,0.0514,-0.3681,0.2636,0.1506,0.0189,0.1599,0.3231,0.1346,0.0125,0.12,0.217,-0.0456,0.3122,0.1837,0.2874,-0.1817,0.2619,0.2489,0.1771,0.1271]},"dist12":{"edges":[-0.6,-0.5,-0.4,-0.3,-0.2,-0.1,-0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2.0],"updraft":[0.0,0.0,0.0,0.0095,0.0381,0.0794,0.1841,0.1429,0.1111,0.1016,0.0635,0.054,0.0825,0.0476,0.0254,0.0159,0.0063,0.0,0.0,0.0032,0.0032,0.0063,0.0032,0.0,0.0032,0.019],"spy":[0.0,0.0032,0.0222,0.0381,0.0762,0.0825,0.1968,0.346,0.1587,0.054,0.0127,0.0095,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]},"grid":[{"names":2,"keep":4,"cagr":0.3161934727069124,"sharpe":0.9209382649135608,"max_dd":-0.432998618801497,"y1999_2012":0.2582082546972626,"y2013_2023":0.1962359895888563,"m12_median":0.2475705804554326,"m12_worst":-0.4175093703832446},{"names":2,"keep":6,"cagr":0.3394040117115815,"sharpe":0.977040510665558,"max_dd":-0.4457393583328713,"y1999_2012":0.273486789103974,"y2013_2023":0.240441648591706,"m12_median":0.2945812376795478,"m12_worst":-0.4175093703832443},{"names":3,"keep":4,"cagr":0.2522193565250021,"sharpe":0.8242092785686023,"max_dd":-0.3974651389056879,"y1999_2012":0.1711626304098719,"y2013_2023":0.2355326218927644,"m12_median":0.1872055388718918,"m12_worst":-0.304016674881796},{"names":3,"keep":5,"cagr":0.28664451788674,"sharpe":0.9215341646871223,"max_dd":-0.3427355297078241,"y1999_2012":0.2005609123468017,"y2013_2023":0.2329973692761926,"m12_median":0.2160766277549382,"m12_worst":-0.2844233023336288},{"names":3,"keep":6,"cagr":0.3090458169949988,"sharpe":0.9847741391408324,"max_dd":-0.3398984501089844,"y1999_2012":0.2150787663087499,"y2013_2023":0.2503076842558163,"m12_median":0.2438431839080914,"m12_worst":-0.2583066825381188},{"names":3,"keep":8,"cagr":0.3018736831394175,"sharpe":0.9359923155338654,"max_dd":-0.376054500532278,"y1999_2012":0.1858878480832058,"y2013_2023":0.2461916792200411,"m12_median":0.2495437016176196,"m12_worst":-0.282445119423836},{"names":3,"keep":10,"cagr":0.2837589775531552,"sharpe":0.9172937146130526,"max_dd":-0.3760545005322782,"y1999_2012":0.2131925368631342,"y2013_2023":0.2355347418056463,"m12_median":0.2412255779978267,"m12_worst":-0.2877554194166803},{"names":4,"keep":6,"cagr":0.2691605373359946,"sharpe":0.9129439213051456,"max_dd":-0.2934881291504067,"y1999_2012":0.1986459556654027,"y2013_2023":0.2295836788826881,"m12_median":0.2269408898100704,"m12_worst":-0.1742536940298429},{"names":4,"keep":8,"cagr":0.2697258674525304,"sharpe":0.9120851782307072,"max_dd":-0.2992930560779047,"y1999_2012":0.1821573719880767,"y2013_2023":0.2298786138985067,"m12_median":0.2174726610474355,"m12_worst":-0.2222163279140215},{"names":4,"keep":10,"cagr":0.2489368424323259,"sharpe":0.8690321028809859,"max_dd":-0.3169755467285939,"y1999_2012":0.170364396160624,"y2013_2023":0.2245305116306701,"m12_median":0.2026369055631602,"m12_worst":-0.2285137735444451},{"names":4,"keep":12,"cagr":0.2488020796914363,"sharpe":0.8788330314000167,"max_dd":-0.37673890574299,"y1999_2012":0.1630838239375678,"y2013_2023":0.2121135554038424,"m12_median":0.1802174309902422,"m12_worst":-0.2827267776820087},{"names":5,"keep":7,"cagr":0.2343678250826917,"sharpe":0.8668880780184017,"max_dd":-0.2862165491806545,"y1999_2012":0.1791345864742108,"y2013_2023":0.1975662895155396,"m12_median":0.2207078490167797,"m12_worst":-0.2340187294092119},{"names":5,"keep":10,"cagr":0.2392726448584918,"sharpe":0.8638957957670101,"max_dd":-0.2748486293284885,"y1999_2012":0.1955232365377903,"y2013_2023":0.2022679628600032,"m12_median":0.1990217239287661,"m12_worst":-0.256505433046418},{"names":5,"keep":12,"cagr":0.2495503206806966,"sharpe":0.9073051402861448,"max_dd":-0.2750916686308687,"y1999_2012":0.1888926438931606,"y2013_2023":0.2248469452530741,"m12_median":0.1984350803266026,"m12_worst":-0.273744179103554},{"names":5,"keep":15,"cagr":0.2582504006053931,"sharpe":0.9738762430952778,"max_dd":-0.3133090609784762,"y1999_2012":0.1880482198002597,"y2013_2023":0.2156249709862796,"m12_median":0.2094085755727666,"m12_worst":-0.2735397368926479},{"names":6,"keep":12,"cagr":0.2372760936756699,"sharpe":0.9092095429318582,"max_dd":-0.2762195581878816,"y1999_2012":0.2063759806283513,"y2013_2023":0.1905780435608282,"m12_median":0.2004483590626915,"m12_worst":-0.2376750709369374}],"projections":{"years":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"paths":20000,"block_months":12,"planning_edge_removed":0.1112,"history":{"lump":{"p10":[0.999,0.9773,1.0638,1.2261,1.4496,1.7281,2.0774,2.4883,3.0112,3.7028,4.568,5.5743,6.8843,8.361,10.329,12.8367],"p25":[0.999,1.0601,1.2707,1.5455,1.9033,2.3522,2.9269,3.6402,4.5503,5.6645,7.1395,8.9554,11.1978,14.1813,17.8056,22.3571],"p50":[0.999,1.2435,1.6072,2.0599,2.654,3.4212,4.4275,5.7039,7.4383,9.5277,12.324,15.9274,20.6435,26.5272,34.3668,44.3161],"p75":[0.999,1.5329,2.0567,2.8344,3.8547,5.2121,7.0035,9.405,12.5513,16.7693,22.2817,29.5701,39.2924,51.8926,69.2166,90.9033],"p90":[0.999,1.7858,2.7345,3.9712,5.6635,7.9838,11.0776,15.2401,20.8334,28.5526,39.201,53.514,73.0222,98.1535,133.3292,180.3573],"below_paid":[1.0,0.1214,0.0632,0.0286,0.0132,0.0055,0.0022,0.0013,0.0006,0.0003,0.0003,0.0002,0.0001,0.0001,0.0001,0.0001],"doubled":[0.0,0.0411,0.2724,0.5274,0.7145,0.8361,0.9111,0.9517,0.9749,0.9872,0.993,0.9966,0.998,0.999,0.9993,0.9997],"paid":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]},"sip":{"p10":[0.0,11.4957,24.375,39.0824,56.589,77.9908,102.9952,133.5683,171.3965,217.1066,275.1847,342.3008,425.9916,524.7128,659.7121,818.6992],"p25":[0.0,12.3931,26.8639,44.6147,66.7472,94.3868,128.651,171.2031,224.1609,292.2598,377.8038,480.2567,610.5501,779.4234,996.7392,1245.6705],"p50":[0.0,13.6179,30.9156,53.6753,82.6904,120.6936,169.674,233.0128,315.9598,422.3269,559.1286,737.6898,969.9184,1271.39,1656.6379,2151.834],"p75":[0.0,15.465,36.7313,65.8237,106.2632,160.4655,235.494,333.5373,467.4568,647.7113,882.5153,1206.7001,1626.3993,2183.3466,2958.0572,3920.8374],"p90":[0.0,16.9453,43.2373,82.4759,139.657,220.4449,334.8936,484.7172,695.2621,993.0268,1394.9674,1948.598,2697.7365,3733.129,5085.2174,6957.5634],"below_paid":[null,0.1766,0.0822,0.0442,0.0226,0.0121,0.0056,0.0034,0.0022,0.0014,0.0011,0.0006,0.0004,0.0001,0.0001,0.0001],"doubled":[null,0.0151,0.0561,0.1752,0.3382,0.5063,0.6518,0.7634,0.8448,0.9018,0.9395,0.9626,0.977,0.9869,0.9928,0.9956],"paid":[0.0,12.0,24.0,36.0,48.0,60.0,72.0,84.0,96.0,108.0,120.0,132.0,144.0,156.0,168.0,180.0]}},"planning":{"lump":{"p10":[0.999,0.862,0.8507,0.8771,0.923,0.9775,1.0445,1.1258,1.2332,1.3471,1.4662,1.6328,1.7625,1.9379,2.1321,2.3811],"p25":[0.999,0.9475,1.0139,1.1016,1.2096,1.3304,1.4769,1.6508,1.8518,2.0804,2.3208,2.6274,2.9532,3.3394,3.7694,4.239],"p50":[0.999,1.1125,1.2775,1.4672,1.6959,1.9529,2.2603,2.6165,3.0185,3.5013,4.06,4.654,5.4323,6.2339,7.2364,8.3403],"p75":[0.999,1.3583,1.6481,2.0289,2.4807,2.9811,3.601,4.3348,5.1894,6.2279,7.4482,8.7842,10.4828,12.5722,14.954,17.6699],"p90":[0.999,1.6053,2.1959,2.8935,3.7111,4.6563,5.7451,7.1636,8.9177,10.9155,13.3926,16.4095,20.1134,24.213,29.2195,35.2287],"below_paid":[1.0,0.3594,0.2376,0.1795,0.1365,0.1094,0.0855,0.0688,0.0546,0.0445,0.0365,0.0293,0.0242,0.0183,0.0158,0.0126],"doubled":[0.0,0.0385,0.1353,0.2591,0.3782,0.4851,0.573,0.6475,0.7135,0.766,0.8082,0.8434,0.8723,0.8921,0.9126,0.93],"paid":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]},"sip":{"p10":[0.0,10.7716,21.7392,32.824,44.7119,57.0516,70.8897,86.0133,102.8458,121.3089,141.0145,163.8376,188.413,214.2959,243.0925,277.71],"p25":[0.0,11.6534,23.8667,37.2282,52.1909,68.5856,86.8807,107.8451,131.6683,158.3129,187.9989,221.8563,259.9763,302.1243,352.1714,405.3901],"p50":[0.0,12.7948,27.3174,44.4417,64.2217,86.6313,113.4684,144.3131,180.491,223.4248,271.9796,327.2873,393.2417,471.2109,556.9019,659.816],"p75":[0.0,14.506,32.4162,54.3069,81.5897,114.2784,154.3516,202.345,261.8572,331.4987,416.2265,516.321,639.1886,784.6314,952.2077,1150.1093],"p90":[0.0,15.9199,37.9289,68.0325,106.4542,155.2366,215.521,293.5785,387.5262,509.3263,648.372,822.5497,1030.9987,1290.2291,1609.111,1993.8483],"below_paid":[null,0.3568,0.2624,0.2049,0.1626,0.1325,0.1094,0.088,0.0702,0.0598,0.051,0.0421,0.0345,0.027,0.0223,0.0191],"doubled":[null,0.0097,0.034,0.0786,0.1422,0.2161,0.2963,0.3745,0.4486,0.5243,0.5862,0.6431,0.6898,0.7336,0.7719,0.8056],"paid":[0.0,12.0,24.0,36.0,48.0,60.0,72.0,84.0,96.0,108.0,120.0,132.0,144.0,156.0,168.0,180.0]}},"spy":{"lump":{"p10":[0.999,0.8614,0.8562,0.8529,0.8802,0.9055,0.9343,0.9742,1.012,1.0612,1.1049,1.1506,1.22,1.2925,1.3601,1.434],"p25":[0.999,1.016,1.0304,1.075,1.1242,1.1885,1.2571,1.3342,1.4151,1.5043,1.5992,1.6925,1.8009,1.9117,2.0522,2.1967],"p50":[0.999,1.1307,1.2287,1.3282,1.4296,1.5504,1.6798,1.814,1.965,2.1338,2.3118,2.5025,2.7011,2.9339,3.1683,3.4201],"p75":[0.999,1.1937,1.3751,1.5591,1.7482,1.9488,2.1615,2.3864,2.6537,2.9242,3.2311,3.5761,3.9561,4.3165,4.7394,5.2108],"p90":[0.999,1.2792,1.5148,1.7644,2.0285,2.3267,2.6491,2.9745,3.3702,3.779,4.262,4.7928,5.2998,5.916,6.5616,7.3187],"below_paid":[1.0,0.2245,0.2178,0.1908,0.1676,0.1433,0.1245,0.1087,0.0958,0.0835,0.0752,0.0658,0.0573,0.0493,0.0438,0.0389],"doubled":[0.0,0.0,0.0014,0.0287,0.1124,0.2252,0.3235,0.4112,0.4845,0.5532,0.6058,0.6556,0.6919,0.7277,0.7623,0.7914],"paid":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]},"sip":{"p10":[0.0,11.0483,21.7947,32.5217,43.715,55.1812,67.7434,80.4645,93.6887,107.66,122.2243,137.6922,153.8577,171.8513,190.5917,208.3126],"p25":[0.0,12.0746,24.5434,37.3595,50.901,65.4455,80.8239,97.1067,114.3047,133.2316,152.5312,173.3409,196.2252,220.0604,245.8475,274.0191],"p50":[0.0,12.8351,26.9729,42.357,58.7939,76.7921,96.0341,117.402,140.0342,165.3114,192.3981,221.951,252.8776,288.2204,324.882,365.8025],"p75":[0.0,13.3623,28.6994,46.1958,65.7333,87.557,111.6594,138.3241,168.2089,201.0707,237.772,277.6782,321.444,367.9363,421.3973,480.717],"p90":[0.0,13.9448,30.2872,49.5183,71.8364,97.2422,126.0971,157.8716,194.91,235.2057,282.0422,334.1337,391.405,455.6268,524.2129,606.436],"below_paid":[null,0.2295,0.2147,0.1981,0.1774,0.159,0.14,0.1265,0.1135,0.1014,0.0924,0.0808,0.0748,0.0645,0.059,0.0535],"doubled":[null,0.0,0.0,0.0,0.0006,0.0058,0.0238,0.059,0.1124,0.1741,0.2404,0.3031,0.3603,0.415,0.4667,0.515],"paid":[0.0,12.0,24.0,36.0,48.0,60.0,72.0,84.0,96.0,108.0,120.0,132.0,144.0,156.0,168.0,180.0]}}}}
|
static/index.html
CHANGED
|
@@ -3,209 +3,515 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>Updraft β live</title>
|
| 7 |
-
<meta name="description" content="Updraft
|
|
|
|
|
|
|
|
|
|
| 8 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
| 9 |
<style>
|
| 10 |
-
:root{-
|
| 11 |
-
--
|
| 12 |
-
|
| 13 |
-
--
|
| 14 |
-
|
| 15 |
-
--
|
| 16 |
-
|
| 17 |
-
.
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
.
|
| 26 |
-
|
| 27 |
-
.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
.
|
| 34 |
-
.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
.
|
| 38 |
-
.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
</style>
|
| 44 |
</head>
|
| 45 |
<body>
|
| 46 |
-
<
|
| 47 |
-
<
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
</div>
|
| 55 |
-
<div class="
|
| 56 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</header>
|
| 58 |
|
| 59 |
-
<
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
<div class="
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
</
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
<
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
<div class="
|
| 74 |
-
|
| 75 |
-
<
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
<p class="note" id="
|
| 79 |
-
</
|
| 80 |
-
<section class="card">
|
| 81 |
-
<h2>This month's ranking</h2>
|
| 82 |
-
<div class="scroll"><table id="ranking"><thead><tr><th>#</th><th>Stock</th><th>Sector</th><th class="r">6-month</th></tr></thead><tbody></tbody></table></div>
|
| 83 |
-
<p class="note" id="ranknote"></p>
|
| 84 |
-
</section>
|
| 85 |
-
</div>
|
| 86 |
-
|
| 87 |
-
<section class="card">
|
| 88 |
-
<h2>Trade log</h2>
|
| 89 |
-
<div class="scroll"><table id="orders"><thead><tr><th>Date</th><th>Action</th><th>Stock</th><th class="r">Shares</th><th class="r">Price</th><th>Status</th></tr></thead><tbody></tbody></table></div>
|
| 90 |
</section>
|
| 91 |
|
| 92 |
-
<
|
| 93 |
-
|
| 94 |
-
<
|
| 95 |
-
<
|
| 96 |
-
<
|
| 97 |
-
<
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
<
|
| 103 |
-
<div
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
</div>
|
| 106 |
-
<p class="note">Backtest after 10 bps per trade, point-in-time stock lists, delisted companies included. Backtests flatter β the
|
| 107 |
-
2024-26 AI rally dominates recent years; expect less, with falls of 30β35% or worse. Each stock is a third of the
|
| 108 |
-
money: one stock halving costs about 17%.</p>
|
| 109 |
</section>
|
|
|
|
| 110 |
|
| 111 |
-
<footer>Educational paper-trading demonstration β not investment advice.
|
| 112 |
-
Sharadar fundamentals and prices
|
| 113 |
-
</div>
|
| 114 |
|
| 115 |
<script>
|
|
|
|
| 116 |
const $ = id => document.getElementById(id);
|
| 117 |
-
const
|
| 118 |
-
const
|
| 119 |
-
const
|
| 120 |
-
const
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
| 123 |
|
|
|
|
| 124 |
try { const t = localStorage.getItem("updraft-theme"); if (t) document.documentElement.dataset.theme = t; } catch (e) {}
|
| 125 |
-
$("themeBtn").onclick = () => {
|
| 126 |
-
(matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
| 127 |
const nxt = cur === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = nxt;
|
| 128 |
-
try { localStorage.setItem("updraft-theme", nxt); } catch (e) {}
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
$("
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
tb.innerHTML = p.positions.length ? p.positions.map(r => `<tr><td><span class="sym">${esc(r.symbol)}</span><br><span class="sub">${esc(r.name || "")}</span></td>
|
| 138 |
-
<td class="r">${pct(r.weight, 0).replace("+","")}</td><td class="r">${usd(r.value)}</td>
|
| 139 |
-
<td class="r ${r.pl_pct >= 0 ? "pos" : "neg"}">${pct(r.pl_pct)}</td><td class="r ${r.today >= 0 ? "pos" : "neg"}">${pct(r.today)}</td>
|
| 140 |
-
<td class="r">${r.rank_now ? "#" + r.rank_now : "β"}</td></tr>`).join("")
|
| 141 |
-
: `<tr><td colspan="6" class="empty">No positions yet β the first rebalance buys the top 3.</td></tr>`;
|
| 142 |
-
$("cashnote").textContent = `Cash ${usd(p.cash)}. Prices update every 30 seconds while the page is open.`;
|
| 143 |
-
$("updated").textContent = "Updated " + new Date().toLocaleTimeString();
|
| 144 |
-
$("dot").style.background = "var(--pos)";
|
| 145 |
-
} catch (e) { $("updated").textContent = "Live data unavailable: " + e.message; $("dot").style.background = "var(--neg)"; }
|
| 146 |
}
|
|
|
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
try {
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
$("
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
try {
|
| 169 |
-
const
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
<td class="sym">${esc(x.symbol)}</td><td class="r">${x.qty ? Number(x.qty).toFixed(3) : "β"}</td>
|
| 173 |
-
<td class="r">${x.price ? "$" + Number(x.price).toFixed(2) : "β"}</td><td class="sub">${esc(x.status)}</td></tr>`).join("")
|
| 174 |
-
: `<tr><td colspan="6" class="empty">No trades yet.</td></tr>`;
|
| 175 |
} catch (e) {}
|
| 176 |
}
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
| 191 |
}
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
} catch (e) { $("chartnote").textContent = "Performance unavailable: " + e.message; }
|
| 204 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
setInterval(
|
|
|
|
| 209 |
</script>
|
| 210 |
</body>
|
| 211 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Updraft β live quality-trend fund</title>
|
| 7 |
+
<meta name="description" content="Updraft holds the three quality US stocks with the strongest trends and rebalances itself on the first trading day of every month. Live paper portfolio, strategy and backtest.">
|
| 8 |
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230e2348'/%3E%3Cpath d='M6 25c6-1 11-6 15-14' stroke='%23ea6435' stroke-width='3' fill='none' stroke-linecap='round'/%3E%3Cpath d='M18.5 9.5 27 6l-3.6 8.4-1.9-3.3z' fill='%23fff'/%3E%3C/svg%3E">
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700&display=swap" rel="stylesheet">
|
| 11 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
| 12 |
<style>
|
| 13 |
+
:root{color-scheme:light;
|
| 14 |
+
--page:#f2f5fa;--surface:#fbfcfe;--ink:#0b1a2e;--ink-2:#46566c;--muted:#76849a;--grid:#e3e8f0;--axis:#c6cfdc;
|
| 15 |
+
--border:rgba(11,26,46,.10);--updraft:#e4572e;--plan:#2f6fd0;--spy:#8b867a;--up:#0f7a37;--down:#c2362f;
|
| 16 |
+
--track:#f6dfd6;--chip:#eaf0f8;--shadow:0 1px 2px rgba(11,26,46,.06),0 8px 24px rgba(11,26,46,.06);
|
| 17 |
+
--sans:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;--brandfont:"Sora",var(--sans)}
|
| 18 |
+
@media (prefers-color-scheme: dark){:root:where(:not([data-theme="light"])){color-scheme:dark;
|
| 19 |
+
--page:#0a1322;--surface:#111c2e;--ink:#f2f6fc;--ink-2:#b8c4d6;--muted:#8a97ab;--grid:#22304a;--axis:#34445f;
|
| 20 |
+
--border:rgba(255,255,255,.10);--updraft:#ea6435;--plan:#3e84e0;--spy:#9a968a;--up:#3fbf6b;--down:#f07068;
|
| 21 |
+
--track:#3a2620;--chip:#1a2a44;--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.25)}}
|
| 22 |
+
:root[data-theme="dark"]{color-scheme:dark;
|
| 23 |
+
--page:#0a1322;--surface:#111c2e;--ink:#f2f6fc;--ink-2:#b8c4d6;--muted:#8a97ab;--grid:#22304a;--axis:#34445f;
|
| 24 |
+
--border:rgba(255,255,255,.10);--updraft:#ea6435;--plan:#3e84e0;--spy:#9a968a;--up:#3fbf6b;--down:#f07068;
|
| 25 |
+
--track:#3a2620;--chip:#1a2a44;--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px rgba(0,0,0,.25)}
|
| 26 |
+
*{box-sizing:border-box}
|
| 27 |
+
body{margin:0;background:var(--page);color:var(--ink);font-family:var(--sans);line-height:1.5}
|
| 28 |
+
.wrap{max-width:1120px;margin:0 auto;padding:0 20px}
|
| 29 |
+
/* masthead β the brand band stays night-sky in both themes */
|
| 30 |
+
.masthead{background:radial-gradient(900px 340px at 82% 120%,rgba(234,100,53,.38),transparent 60%),
|
| 31 |
+
radial-gradient(700px 300px at 10% -40%,rgba(62,132,224,.35),transparent 60%),linear-gradient(160deg,#0b1a3a 0%,#11295a 55%,#173a74 100%);
|
| 32 |
+
color:#f2f6fc;padding:18px 0 0}
|
| 33 |
+
.mast{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
| 34 |
+
.brand{display:flex;align-items:center;gap:10px;color:inherit;text-decoration:none}
|
| 35 |
+
.word{font-family:var(--brandfont);font-weight:700;font-size:24px;letter-spacing:-.02em}
|
| 36 |
+
.tagline{font-size:13px;color:#b9c8e2;border-left:1px solid rgba(255,255,255,.2);padding-left:14px}
|
| 37 |
+
.chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:3px 10px;border-radius:999px;
|
| 38 |
+
background:rgba(255,255,255,.12);color:#fff}
|
| 39 |
+
.mast-right{margin-left:auto;display:flex;align-items:center;gap:12px;font-size:12px;color:#b9c8e2}
|
| 40 |
+
.dot{width:8px;height:8px;border-radius:50%;background:#3fbf6b;box-shadow:0 0 0 3px rgba(63,191,107,.2)}
|
| 41 |
+
.theme{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:8px;padding:4px 9px;cursor:pointer;font-size:13px}
|
| 42 |
+
.hero-copy h1{font-family:var(--brandfont);font-weight:700;font-size:clamp(24px,3.4vw,38px);line-height:1.15;letter-spacing:-.02em;margin:26px 0 8px;max-width:760px}
|
| 43 |
+
.hero-copy p{margin:0;color:#c6d3ea;max-width:680px;font-size:16px}
|
| 44 |
+
.tabs{display:flex;gap:6px;margin-top:26px}
|
| 45 |
+
.tabs button{appearance:none;border:0;background:transparent;color:#c6d3ea;font:600 14px var(--sans);padding:11px 16px;border-radius:10px 10px 0 0;cursor:pointer}
|
| 46 |
+
.tabs button[aria-selected="true"]{background:var(--page);color:var(--ink)}
|
| 47 |
+
main{padding:22px 0 40px}
|
| 48 |
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:20px;box-shadow:var(--shadow)}
|
| 49 |
+
.card+.card,.grid2+.card,.card+.grid2{margin-top:16px}
|
| 50 |
+
h2{font-family:var(--brandfont);font-size:19px;letter-spacing:-.01em;margin:0 0 4px}
|
| 51 |
+
h3{font-size:15px;margin:0 0 4px}
|
| 52 |
+
.sub{color:var(--ink-2);font-size:14px;margin:0}
|
| 53 |
+
.muted{color:var(--muted);font-size:12px}
|
| 54 |
+
.kpis{display:grid;grid-template-columns:1.3fr 1fr 1fr 1fr 1fr;gap:12px;margin-bottom:16px}
|
| 55 |
+
.tile{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:14px 16px;box-shadow:var(--shadow)}
|
| 56 |
+
.tile .l{font-size:12px;color:var(--muted)}
|
| 57 |
+
.tile .v{font-size:24px;font-weight:650;margin-top:2px}
|
| 58 |
+
.tile .s{font-size:12px;color:var(--ink-2);margin-top:2px}
|
| 59 |
+
.tile.hero .v{font-size:44px;line-height:1.1;letter-spacing:-.02em}
|
| 60 |
+
.pos{color:var(--up)}.neg{color:var(--down)}
|
| 61 |
+
.chart-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}
|
| 62 |
+
.legend{display:flex;gap:16px;flex-wrap:wrap;font-size:13px;color:var(--ink-2)}
|
| 63 |
+
.legend i{display:inline-block;width:16px;height:2px;border-radius:2px;vertical-align:middle;margin-right:6px}
|
| 64 |
+
.legend i.band{height:10px;border-radius:3px;opacity:.35}
|
| 65 |
+
.seg{display:inline-flex;background:var(--chip);border-radius:10px;padding:3px;gap:2px}
|
| 66 |
+
.seg button{appearance:none;border:0;background:transparent;color:var(--ink-2);font:600 12px var(--sans);padding:6px 11px;border-radius:8px;cursor:pointer}
|
| 67 |
+
.seg button[aria-pressed="true"]{background:var(--surface);color:var(--ink);box-shadow:0 1px 2px rgba(0,0,0,.08)}
|
| 68 |
+
.filters{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 10px}
|
| 69 |
+
.period-read{font-size:13px;color:var(--ink-2)}
|
| 70 |
+
.chartbox{position:relative;height:340px}
|
| 71 |
+
.chartbox.sm{height:260px}
|
| 72 |
+
.hold{width:100%;border-collapse:collapse;font-size:14px}
|
| 73 |
+
.hold th{text-align:left;font-size:12px;font-weight:600;color:var(--muted);padding:8px;border-bottom:1px solid var(--grid)}
|
| 74 |
+
.hold td{padding:12px 8px;border-bottom:1px solid var(--grid);vertical-align:middle}
|
| 75 |
+
.hold td.r,.hold th.r{text-align:right;font-variant-numeric:tabular-nums}
|
| 76 |
+
.tick{font-weight:700;font-size:15px}.co{color:var(--muted);font-size:12px}
|
| 77 |
+
.wbar{display:flex;align-items:center;gap:8px;min-width:120px}
|
| 78 |
+
.wbar span{flex:1;height:6px;border-radius:4px;background:var(--track);overflow:hidden}
|
| 79 |
+
.wbar b{display:block;height:100%;border-radius:4px;background:var(--updraft)}
|
| 80 |
+
.rank{display:inline-block;min-width:34px;text-align:center;font-size:12px;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--chip);color:var(--ink-2)}
|
| 81 |
+
.note{font-size:13px;color:var(--ink-2);margin:10px 0 0}
|
| 82 |
+
.empty{color:var(--muted);padding:12px 0}
|
| 83 |
+
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:14px}
|
| 84 |
+
.step{border:1px solid var(--border);border-radius:14px;padding:14px;position:relative;background:var(--surface)}
|
| 85 |
+
.step .n{font-family:var(--brandfont);font-size:28px;font-weight:700;color:var(--updraft);line-height:1}
|
| 86 |
+
.step .t{font-weight:650;margin-top:6px}.step p{font-size:13px;color:var(--ink-2);margin:6px 0 0}
|
| 87 |
+
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
|
| 88 |
+
.stat6{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin:14px 0 4px}
|
| 89 |
+
.stat6 .tile{box-shadow:none}
|
| 90 |
+
.calc{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;margin:12px 0 4px}
|
| 91 |
+
.calc label{display:flex;flex-direction:column;font-size:12px;color:var(--muted);gap:4px}
|
| 92 |
+
.calc input{font:600 15px var(--sans);color:var(--ink);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:8px 10px;width:150px}
|
| 93 |
+
table.proj{width:100%;border-collapse:collapse;font-size:13px;margin-top:10px}
|
| 94 |
+
table.proj th{text-align:right;font-weight:600;color:var(--muted);font-size:12px;padding:6px;border-bottom:1px solid var(--grid)}
|
| 95 |
+
table.proj th:first-child,table.proj td:first-child{text-align:left}
|
| 96 |
+
table.proj td{text-align:right;padding:8px 6px;border-bottom:1px solid var(--grid);font-variant-numeric:tabular-nums}
|
| 97 |
+
table.proj td.big{font-weight:700;font-size:15px}
|
| 98 |
+
.callout{border-left:3px solid var(--plan);background:var(--chip);border-radius:0 12px 12px 0;padding:12px 14px;font-size:13px;color:var(--ink-2);margin-top:14px}
|
| 99 |
+
details{margin-top:10px}details summary{cursor:pointer;font-size:13px;color:var(--ink-2)}
|
| 100 |
+
details table{width:100%;border-collapse:collapse;font-size:12px;margin-top:8px;font-variant-numeric:tabular-nums}
|
| 101 |
+
details td,details th{padding:4px 6px;border-bottom:1px solid var(--grid);text-align:right}
|
| 102 |
+
details td:first-child,details th:first-child{text-align:left}
|
| 103 |
+
footer{color:var(--muted);font-size:12px;padding:0 0 36px}
|
| 104 |
+
@media (max-width:900px){.kpis{grid-template-columns:1fr 1fr}.tile.hero{grid-column:1/-1}.steps{grid-template-columns:1fr 1fr}
|
| 105 |
+
.grid2{grid-template-columns:1fr}.stat6{grid-template-columns:repeat(2,1fr)}.tagline{display:none}}
|
| 106 |
+
@media (max-width:560px){.hold .hide-s{display:none}.chartbox{height:280px}}
|
| 107 |
</style>
|
| 108 |
</head>
|
| 109 |
<body>
|
| 110 |
+
<header class="masthead">
|
| 111 |
+
<div class="wrap mast">
|
| 112 |
+
<a class="brand" href="/" aria-label="Updraft home">
|
| 113 |
+
<svg width="34" height="34" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="8" fill="#0e2348" stroke="rgba(255,255,255,.18)"/>
|
| 114 |
+
<path d="M6 25c6-1 11-6 15-14" stroke="#ea6435" stroke-width="3" fill="none" stroke-linecap="round"/>
|
| 115 |
+
<path d="M18.5 9.5 27 6l-3.6 8.4-1.9-3.3z" fill="#fff"/></svg>
|
| 116 |
+
<span class="word">Updraft</span>
|
| 117 |
+
</a>
|
| 118 |
+
<span class="tagline">The Quality-Trend 3 strategy, running on its own</span>
|
| 119 |
+
<div class="mast-right"><span class="chip" id="mode">Paper trading</span>
|
| 120 |
+
<span class="dot" id="dot"></span><span id="updated">connectingβ¦</span>
|
| 121 |
+
<button class="theme" id="themeBtn" aria-label="Toggle light or dark">β</button></div>
|
| 122 |
</div>
|
| 123 |
+
<div class="wrap hero-copy">
|
| 124 |
+
<h1>Three quality stocks with the strongest trends β rebalanced automatically every month.</h1>
|
| 125 |
+
<p>A live paper portfolio run by rules, not opinions. Below: what it is worth right now, how it compares with the
|
| 126 |
+
S&P 500, and what it holds.</p>
|
| 127 |
+
</div>
|
| 128 |
+
<nav class="wrap tabs" role="tablist" aria-label="Sections">
|
| 129 |
+
<button role="tab" id="t-live" aria-selected="true" aria-controls="p-live">Live fund</button>
|
| 130 |
+
<button role="tab" id="t-strategy" aria-selected="false" aria-controls="p-strategy">The strategy</button>
|
| 131 |
+
</nav>
|
| 132 |
</header>
|
| 133 |
|
| 134 |
+
<main class="wrap">
|
| 135 |
+
<!-- ===================================================== LIVE FUND -->
|
| 136 |
+
<section id="p-live" role="tabpanel" aria-labelledby="t-live">
|
| 137 |
+
<div class="kpis">
|
| 138 |
+
<div class="tile hero"><div class="l">Portfolio value</div><div class="v" id="k-eq">β</div><div class="s" id="k-today"></div></div>
|
| 139 |
+
<div class="tile"><div class="l">Since start</div><div class="v" id="k-ret">β</div><div class="s" id="k-ret-s"></div></div>
|
| 140 |
+
<div class="tile"><div class="l">S&P 500, same money</div><div class="v" id="k-spy">β</div><div class="s" id="k-spy-s"></div></div>
|
| 141 |
+
<div class="tile"><div class="l">Worst fall</div><div class="v" id="k-dd">β</div><div class="s">peak to trough, since start</div></div>
|
| 142 |
+
<div class="tile"><div class="l">Next rebalance</div><div class="v" id="k-next">β</div><div class="s" id="k-next-s"></div></div>
|
| 143 |
+
</div>
|
| 144 |
|
| 145 |
+
<div class="filters" aria-label="Chart period">
|
| 146 |
+
<div class="seg" id="range">
|
| 147 |
+
<button data-r="1M" aria-pressed="false">1M</button><button data-r="3M" aria-pressed="false">3M</button>
|
| 148 |
+
<button data-r="YTD" aria-pressed="false">YTD</button><button data-r="1Y" aria-pressed="false">1Y</button>
|
| 149 |
+
<button data-r="ALL" aria-pressed="true">All</button></div>
|
| 150 |
+
<span class="period-read" id="period-read"></span>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="card">
|
| 153 |
+
<div class="chart-head">
|
| 154 |
+
<div><h2>Portfolio value</h2><p class="sub">The Alpaca paper account, against the same money invested in the S&P 500 (same start, same deposits).</p></div>
|
| 155 |
+
<div class="legend"><span><i style="background:var(--updraft)"></i>Updraft</span><span><i style="background:var(--spy)"></i>S&P 500</span></div>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="chartbox"><canvas id="c-live" role="img" aria-label="Portfolio value versus the S&P 500"></canvas></div>
|
| 158 |
+
<p class="note" id="live-note">Daily closes from Alpaca plus the live value now. Refreshes every few minutes.</p>
|
| 159 |
+
</div>
|
| 160 |
|
| 161 |
+
<div class="card">
|
| 162 |
+
<div class="chart-head"><div><h2>Current holdings</h2><p class="sub" id="hold-sub">A third each, re-split monthly. A stock stays while it ranks in the top 6.</p></div></div>
|
| 163 |
+
<div style="overflow-x:auto"><table class="hold" id="holdings"><thead><tr>
|
| 164 |
+
<th>Stock</th><th class="hide-s">Weight</th><th class="r">Value</th><th class="r">Gain</th><th class="r hide-s">Today</th><th class="r">Rank now</th>
|
| 165 |
+
</tr></thead><tbody><tr><td colspan="6" class="empty">Loadingβ¦</td></tr></tbody></table></div>
|
| 166 |
+
<p class="note" id="cash-note"></p>
|
| 167 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
</section>
|
| 169 |
|
| 170 |
+
<!-- ===================================================== THE STRATEGY -->
|
| 171 |
+
<section id="p-strategy" role="tabpanel" aria-labelledby="t-strategy" hidden>
|
| 172 |
+
<div class="card">
|
| 173 |
+
<h2>How Updraft picks its three stocks</h2>
|
| 174 |
+
<p class="sub">Two filters that each worked in 27 years of data β quality first, then trend β and a simple monthly routine.</p>
|
| 175 |
+
<div class="steps">
|
| 176 |
+
<div class="step"><div class="n">1,000</div><div class="t">Large US companies</div><p>The 1,000 largest by market value, price β₯ $5, at least $5M traded a day, US-dollar accounts.</p></div>
|
| 177 |
+
<div class="step"><div class="n">~250</div><div class="t">Quality pool</div><p>The top 30% on capital discipline: buybacks, no new share issuance, low dilution, free-cash-flow yield, ROE, margins.</p></div>
|
| 178 |
+
<div class="step"><div class="n">Rank</div><div class="t">Strongest trends</div><p>The pool ranked by its 50- vs 200-day average and its 6-month return (skipping the latest month).</p></div>
|
| 179 |
+
<div class="step"><div class="n">3</div><div class="t">Held, a third each</div><p>On the first trading day of each month: keep a stock while it's still in the top 6, replace the rest from the top.</p></div>
|
| 180 |
+
</div>
|
| 181 |
+
<div class="callout">Why both filters: trend alone picks big winners and big blow-ups; the quality pool kept most of the winners and cut the
|
| 182 |
+
blow-ups by about two-thirds. Holding only three concentrates the money in the strongest trends β fewer names earned more in every
|
| 183 |
+
period tested, at the cost of bigger single-stock swings.</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<div class="card">
|
| 187 |
+
<h2>27 years of backtest</h2>
|
| 188 |
+
<p class="sub" id="bt-sub">Monthly, after trading costs, with companies that later went bust or were delisted included.</p>
|
| 189 |
+
<div class="stat6" id="bt-stats"></div>
|
| 190 |
+
<div class="chart-head" style="margin-top:14px">
|
| 191 |
+
<div><h3>Growth of $10,000</h3><p class="sub">Log scale β equal distances mean equal percentage gains.</p></div>
|
| 192 |
+
<div class="legend"><span><i style="background:var(--updraft)"></i>Updraft</span><span><i style="background:var(--spy)"></i>S&P 500</span></div>
|
| 193 |
+
</div>
|
| 194 |
+
<div class="chartbox"><canvas id="c-growth" role="img" aria-label="Growth of ten thousand dollars, Updraft versus S&P 500"></canvas></div>
|
| 195 |
+
<details><summary>View as table</summary><table id="t-growth"></table></details>
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<div class="grid2">
|
| 199 |
+
<div class="card">
|
| 200 |
+
<div class="chart-head"><div><h3>Every calendar year</h3><p class="sub">First and last years are partial.</p></div>
|
| 201 |
+
<div class="legend"><span><i class="band" style="background:var(--updraft);opacity:1"></i>Updraft</span><span><i class="band" style="background:var(--spy);opacity:1"></i>S&P 500</span></div></div>
|
| 202 |
+
<div class="chartbox sm"><canvas id="c-years" role="img" aria-label="Calendar-year returns"></canvas></div>
|
| 203 |
+
<details><summary>View as table</summary><table id="t-years"></table></details>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="card">
|
| 206 |
+
<div class="chart-head"><div><h3>The falls along the way</h3><p class="sub">Distance below the previous peak at each month-end.</p></div>
|
| 207 |
+
<div class="legend"><span><i style="background:var(--updraft)"></i>Updraft</span><span><i style="background:var(--spy)"></i>S&P 500</span></div></div>
|
| 208 |
+
<div class="chartbox sm"><canvas id="c-dd" role="img" aria-label="Drawdowns"></canvas></div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="card">
|
| 212 |
+
<div class="chart-head"><div><h3>What any 12 months looked like</h3><p class="sub" id="dist-sub">Share of all 12-month periods by return.</p></div>
|
| 213 |
+
<div class="legend"><span><i class="band" style="background:var(--updraft);opacity:1"></i>Updraft</span><span><i class="band" style="background:var(--spy);opacity:1"></i>S&P 500</span></div></div>
|
| 214 |
+
<div class="chartbox sm"><canvas id="c-dist" role="img" aria-label="Distribution of 12-month returns"></canvas></div>
|
| 215 |
+
</div>
|
| 216 |
+
|
| 217 |
+
<div class="card">
|
| 218 |
+
<h2>What it could grow to</h2>
|
| 219 |
+
<p class="sub">20,000 simulated futures built from the real monthly returns (in 12-month blocks, so crashes and rallies stay clustered).
|
| 220 |
+
Enter your amounts; the ranges scale exactly. Charts shade the middle half of outcomes; tables show the wider 10thβ90th range.</p>
|
| 221 |
+
<div class="calc">
|
| 222 |
+
<div><div class="muted" style="margin-bottom:4px">Scenario</div><div class="seg" id="scen">
|
| 223 |
+
<button data-s="planning" aria-pressed="true">Planning case</button><button data-s="history" aria-pressed="false">History repeats</button></div></div>
|
| 224 |
+
<label>Lump sum ($)<input id="in-lump" type="number" min="0" step="1000" value="10000"></label>
|
| 225 |
+
<label>Monthly SIP ($)<input id="in-sip" type="number" min="0" step="100" value="1000"></label>
|
| 226 |
+
</div>
|
| 227 |
+
<p class="muted" id="scen-note"></p>
|
| 228 |
+
<div class="grid2" style="margin-top:6px">
|
| 229 |
+
<div>
|
| 230 |
+
<div class="chart-head" style="margin-bottom:6px"><div><h3>Lump sum</h3><p class="sub" id="lump-sub"></p></div></div>
|
| 231 |
+
<div class="legend" style="margin-bottom:6px" id="leg-lump"></div>
|
| 232 |
+
<div class="chartbox sm"><canvas id="c-lump" role="img" aria-label="Lump-sum projection"></canvas></div>
|
| 233 |
+
<table class="proj" id="tb-lump"></table>
|
| 234 |
+
</div>
|
| 235 |
+
<div>
|
| 236 |
+
<div class="chart-head" style="margin-bottom:6px"><div><h3>Monthly SIP</h3><p class="sub" id="sip-sub"></p></div></div>
|
| 237 |
+
<div class="legend" style="margin-bottom:6px" id="leg-sip"></div>
|
| 238 |
+
<div class="chartbox sm"><canvas id="c-sip" role="img" aria-label="Monthly investment projection"></canvas></div>
|
| 239 |
+
<table class="proj" id="tb-sip"></table>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
<div class="callout">Read this honestly: the backtest's last few years were exceptionally strong (the 2024β26 AI rally). The
|
| 243 |
+
<b>planning case</b> removes half of Updraft's historical edge over the S&P and is the number to plan with; "history repeats" is the
|
| 244 |
+
optimistic end. Single stocks are a third of the money each, so falls of 30β35% will happen along the way.</div>
|
| 245 |
</div>
|
|
|
|
|
|
|
|
|
|
| 246 |
</section>
|
| 247 |
+
</main>
|
| 248 |
|
| 249 |
+
<footer class="wrap">Educational paper-trading demonstration β not investment advice. Live prices and positions from Alpaca (paper account,
|
| 250 |
+
IEX feed); rankings built daily from Sharadar fundamentals and prices; backtest 1999β2026 with point-in-time stock lists.</footer>
|
|
|
|
| 251 |
|
| 252 |
<script>
|
| 253 |
+
"use strict";
|
| 254 |
const $ = id => document.getElementById(id);
|
| 255 |
+
const css = n => getComputedStyle(document.documentElement).getPropertyValue(n).trim();
|
| 256 |
+
const pct = (x, d = 1) => x == null || !isFinite(x) ? "β" : (x >= 0 ? "+" : "β") + Math.abs(x * 100).toFixed(d) + "%";
|
| 257 |
+
const usd = x => x == null || !isFinite(x) ? "β" : (Math.abs(x) >= 1e6 ? "$" + (x / 1e6).toFixed(2) + "M" : "$" + Math.round(x).toLocaleString());
|
| 258 |
+
const usdShort = x => Math.abs(x) >= 1e6 ? "$" + (x / 1e6).toFixed(x >= 1e7 ? 0 : 1) + "M" : Math.abs(x) >= 1e3 ? "$" + Math.round(x / 1e3) + "K" : "$" + Math.round(x);
|
| 259 |
+
const signCls = (el, x) => { el.classList.remove("pos", "neg"); if (x != null && isFinite(x) && x !== 0) el.classList.add(x > 0 ? "pos" : "neg"); };
|
| 260 |
+
const alpha = (hex, a) => { const h = hex.replace("#", ""); const n = parseInt(h, 16); return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`; };
|
| 261 |
+
async function get(u) { const r = await fetch(u); if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || r.status); return r.json(); }
|
| 262 |
+
function setText(el, t) { el.textContent = t; }
|
| 263 |
|
| 264 |
+
/* ---------- theme */
|
| 265 |
try { const t = localStorage.getItem("updraft-theme"); if (t) document.documentElement.dataset.theme = t; } catch (e) {}
|
| 266 |
+
$("themeBtn").onclick = () => {
|
| 267 |
+
const cur = document.documentElement.dataset.theme || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
| 268 |
const nxt = cur === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = nxt;
|
| 269 |
+
try { localStorage.setItem("updraft-theme", nxt); } catch (e) {}
|
| 270 |
+
redrawAll();
|
| 271 |
+
};
|
| 272 |
+
matchMedia("(prefers-color-scheme: dark)").addEventListener?.("change", () => redrawAll());
|
| 273 |
|
| 274 |
+
/* ---------- tabs */
|
| 275 |
+
function showTab(name) {
|
| 276 |
+
for (const t of ["live", "strategy"]) {
|
| 277 |
+
$("t-" + t).setAttribute("aria-selected", String(t === name)); $("p-" + t).hidden = t !== name;
|
| 278 |
+
}
|
| 279 |
+
if (name === "strategy") loadStrategy();
|
| 280 |
+
try { history.replaceState(null, "", name === "strategy" ? "#strategy" : "#"); } catch (e) {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
}
|
| 282 |
+
$("t-live").onclick = () => showTab("live"); $("t-strategy").onclick = () => showTab("strategy");
|
| 283 |
|
| 284 |
+
/* ---------- chart plumbing */
|
| 285 |
+
const charts = {};
|
| 286 |
+
const crosshair = { id: "crosshair", afterDatasetsDraw(ch) {
|
| 287 |
+
const a = ch.tooltip && ch.tooltip.getActiveElements && ch.tooltip.getActiveElements();
|
| 288 |
+
if (!a || !a.length || ch.config.type !== "line") return;
|
| 289 |
+
const x = a[0].element.x, { top, bottom } = ch.chartArea, c = ch.ctx;
|
| 290 |
+
c.save(); c.strokeStyle = css("--axis"); c.lineWidth = 1; c.beginPath(); c.moveTo(x, top); c.lineTo(x, bottom); c.stroke(); c.restore();
|
| 291 |
+
}};
|
| 292 |
+
const endLabels = { id: "endLabels", afterDatasetsDraw(ch, _a, o) {
|
| 293 |
+
if (!o || !o.enabled) return;
|
| 294 |
+
const pts = [];
|
| 295 |
+
ch.data.datasets.forEach((ds, i) => {
|
| 296 |
+
if (ds.endLabel === false || ch.getDatasetMeta(i).hidden) return;
|
| 297 |
+
const els = ch.getDatasetMeta(i).data; let j = ds.data.length - 1;
|
| 298 |
+
while (j >= 0 && (ds.data[j] == null || !els[j])) j--;
|
| 299 |
+
if (j < 0) return;
|
| 300 |
+
pts.push({ x: els[j].x, y: els[j].y, color: ds.borderColor, text: o.format(ds, ds.data[j]) });
|
| 301 |
+
});
|
| 302 |
+
const c = ch.ctx, collide = pts.length === 2 && Math.abs(pts[0].y - pts[1].y) < 18;
|
| 303 |
+
c.save();
|
| 304 |
+
for (const p of pts) {
|
| 305 |
+
c.beginPath(); c.arc(p.x, p.y, 6, 0, 2 * Math.PI); c.fillStyle = css("--surface"); c.fill();
|
| 306 |
+
c.beginPath(); c.arc(p.x, p.y, 4, 0, 2 * Math.PI); c.fillStyle = p.color; c.fill();
|
| 307 |
+
if (!collide) { c.font = "600 12px " + css("--sans"); c.fillStyle = css("--ink"); c.textAlign = "right"; c.fillText(p.text, p.x - 10, p.y - 10); }
|
| 308 |
+
}
|
| 309 |
+
c.restore();
|
| 310 |
+
}};
|
| 311 |
+
Chart.register(crosshair, endLabels);
|
| 312 |
+
function baseOpts(extra = {}) {
|
| 313 |
+
const ink2 = css("--ink-2"), muted = css("--muted"), grid = css("--grid");
|
| 314 |
+
return Object.assign({
|
| 315 |
+
responsive: true, maintainAspectRatio: false, animation: { duration: 250 },
|
| 316 |
+
interaction: { mode: "index", intersect: false },
|
| 317 |
+
layout: { padding: { top: 18, right: 6 } },
|
| 318 |
+
plugins: { legend: { display: false },
|
| 319 |
+
tooltip: { backgroundColor: css("--surface"), borderColor: css("--border"), borderWidth: 1, titleColor: ink2, bodyColor: css("--ink"),
|
| 320 |
+
padding: 10, usePointStyle: true, boxWidth: 14, boxHeight: 2, titleFont: { weight: "600" }, bodyFont: { weight: "600" } } },
|
| 321 |
+
scales: {
|
| 322 |
+
x: { grid: { display: false }, border: { color: css("--axis") }, ticks: { color: muted, maxTicksLimit: 8, maxRotation: 0 } },
|
| 323 |
+
y: { grid: { color: grid }, border: { display: false }, ticks: { color: muted } } }
|
| 324 |
+
}, extra);
|
| 325 |
+
}
|
| 326 |
+
function mk(id, cfg) { if (charts[id]) charts[id].destroy(); charts[id] = new Chart($(id), cfg); return charts[id]; }
|
| 327 |
+
function line(label, data, color, extra = {}) {
|
| 328 |
+
return Object.assign({ label, data, borderColor: color, backgroundColor: color, borderWidth: 2, pointRadius: 0, pointHoverRadius: 4,
|
| 329 |
+
pointStyle: "line", tension: 0.15, borderJoinStyle: "round", borderCapStyle: "round", spanGaps: true }, extra);
|
| 330 |
}
|
| 331 |
|
| 332 |
+
/* ---------- LIVE FUND */
|
| 333 |
+
let perf = null, range = "ALL";
|
| 334 |
+
function cut(dates) {
|
| 335 |
+
if (range === "ALL" || !dates.length) return 0;
|
| 336 |
+
const last = new Date(dates[dates.length - 1] + "T12:00:00"); const d = new Date(last);
|
| 337 |
+
if (range === "1M") d.setMonth(d.getMonth() - 1); else if (range === "3M") d.setMonth(d.getMonth() - 3);
|
| 338 |
+
else if (range === "1Y") d.setFullYear(d.getFullYear() - 1); else if (range === "YTD") d.setMonth(0, 1);
|
| 339 |
+
const iso = d.toISOString().slice(0, 10); const i = dates.findIndex(x => x >= iso); return Math.max(0, i - (range === "YTD" ? 0 : 1));
|
| 340 |
+
}
|
| 341 |
+
function drawLive() {
|
| 342 |
+
if (!perf || !perf.dates || !perf.dates.length) return;
|
| 343 |
+
const i0 = cut(perf.dates);
|
| 344 |
+
const D = perf.dates.slice(i0), V = perf.value.slice(i0), B = perf.bench_value.slice(i0);
|
| 345 |
+
const u = css("--updraft"), s = css("--spy");
|
| 346 |
+
mk("c-live", { type: "line", data: { labels: D, datasets: [
|
| 347 |
+
line("Updraft", V, u, { fill: "start", backgroundColor: alpha(u, 0.10) }),
|
| 348 |
+
line("S&P 500, same money", B, s)] },
|
| 349 |
+
options: baseOpts({ plugins: Object.assign(baseOpts().plugins, { endLabels: { enabled: true, format: (ds, v) => usd(v) },
|
| 350 |
+
tooltip: Object.assign(baseOpts().plugins.tooltip, { callbacks: { label: c => `${usd(c.parsed.y)} ${c.dataset.label}` } }) }),
|
| 351 |
+
scales: Object.assign(baseOpts().scales, { y: Object.assign(baseOpts().scales.y, { ticks: { color: css("--muted"), callback: v => usdShort(v) } }) }) }) });
|
| 352 |
+
const ru = V.length > 1 ? V[V.length - 1] / V[0] - 1 : null, rb = B.length > 1 && B[0] ? B[B.length - 1] / B[0] - 1 : null;
|
| 353 |
+
setText($("period-read"), ru == null ? "" : `This period: Updraft ${pct(ru)} Β· S&P 500 ${pct(rb)}`);
|
| 354 |
+
}
|
| 355 |
+
document.querySelectorAll("#range button").forEach(b => b.onclick = () => {
|
| 356 |
+
range = b.dataset.r; document.querySelectorAll("#range button").forEach(x => x.setAttribute("aria-pressed", String(x === b))); drawLive();
|
| 357 |
+
});
|
| 358 |
+
async function loadPerformance() {
|
| 359 |
try {
|
| 360 |
+
perf = await get("/api/performance"); const s = perf.stats || {};
|
| 361 |
+
setText($("k-ret"), pct(s.return)); signCls($("k-ret"), s.return);
|
| 362 |
+
setText($("k-ret-s"), perf.start ? `since the first trade, ${perf.start}` + (s.annualised != null ? ` Β· ${pct(s.annualised)} a year` : "") : "starts with the first trade");
|
| 363 |
+
const bv = perf.bench_value || [], spyRet = bv.length > 1 && bv[0] ? bv[bv.length - 1] / bv[0] - 1 : s.spy_return;
|
| 364 |
+
setText($("k-spy"), pct(spyRet)); signCls($("k-spy"), spyRet);
|
| 365 |
+
setText($("k-spy-s"), s.return != null && spyRet != null ? `Updraft ${s.return >= spyRet ? "ahead" : "behind"} by ${pct(Math.abs(s.return - spyRet)).replace("+", "")}` : "");
|
| 366 |
+
setText($("k-dd"), s.max_drawdown == null ? "β" : pct(s.max_drawdown)); signCls($("k-dd"), s.max_drawdown < 0 ? -1 : null);
|
| 367 |
+
drawLive();
|
| 368 |
+
if (perf.dates.length < 3) setText($("live-note"), "The fund started on " + (perf.start || "its first trade") + " β the chart fills in day by day. Daily closes from Alpaca plus the live value now.");
|
| 369 |
+
} catch (e) { setText($("live-note"), "Performance unavailable right now: " + e.message); }
|
| 370 |
}
|
| 371 |
+
async function loadPortfolio() {
|
| 372 |
+
try {
|
| 373 |
+
const p = await get("/api/portfolio");
|
| 374 |
+
setText($("mode"), p.paper ? "Paper trading" : "LIVE trading");
|
| 375 |
+
setText($("k-eq"), usd(p.equity));
|
| 376 |
+
setText($("k-today"), p.today_pct == null ? "" : `today ${pct(p.today_pct, 2)} (${p.today_change >= 0 ? "+" : "β"}${usd(Math.abs(p.today_change))})`);
|
| 377 |
+
signCls($("k-today"), p.today_pct);
|
| 378 |
+
const tb = $("holdings").querySelector("tbody"); tb.textContent = "";
|
| 379 |
+
if (!p.positions.length) { const tr = tb.insertRow(); const td = tr.insertCell(); td.colSpan = 6; td.className = "empty"; td.textContent = "No positions yet β the first rebalance buys the top 3."; }
|
| 380 |
+
for (const r of p.positions) {
|
| 381 |
+
const tr = tb.insertRow();
|
| 382 |
+
const a = tr.insertCell(); const t = document.createElement("div"); t.className = "tick"; t.textContent = r.symbol;
|
| 383 |
+
const co = document.createElement("div"); co.className = "co"; co.textContent = [r.name, r.sector].filter(Boolean).join(" Β· "); a.append(t, co);
|
| 384 |
+
const w = tr.insertCell(); w.className = "hide-s"; const wb = document.createElement("div"); wb.className = "wbar";
|
| 385 |
+
const tr2 = document.createElement("span"); const fill = document.createElement("b"); fill.style.width = Math.min(100, (r.weight || 0) * 100 / 0.5) + "%"; tr2.append(fill);
|
| 386 |
+
const wl = document.createElement("small"); wl.textContent = pct(r.weight, 0).replace("+", ""); wb.append(tr2, wl); w.append(wb);
|
| 387 |
+
const v = tr.insertCell(); v.className = "r"; v.textContent = usd(r.value);
|
| 388 |
+
const g = tr.insertCell(); g.className = "r"; g.textContent = pct(r.pl_pct); signCls(g, r.pl_pct);
|
| 389 |
+
const d = tr.insertCell(); d.className = "r hide-s"; d.textContent = pct(r.today); signCls(d, r.today);
|
| 390 |
+
const k = tr.insertCell(); k.className = "r"; const rk = document.createElement("span"); rk.className = "rank"; rk.textContent = r.rank_now ? "#" + r.rank_now : "β"; k.append(rk);
|
| 391 |
+
}
|
| 392 |
+
setText($("cash-note"), `Cash ${usd(p.cash)} (about 1% is kept back for fills). Prices update every 30 seconds while this page is open.`);
|
| 393 |
+
setText($("updated"), "Live Β· " + new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); $("dot").style.background = "#3fbf6b";
|
| 394 |
+
} catch (e) { setText($("updated"), "Live data unavailable"); $("dot").style.background = "#f07068"; }
|
| 395 |
+
}
|
| 396 |
+
async function loadStatus() {
|
| 397 |
try {
|
| 398 |
+
const s = await get("/api/status");
|
| 399 |
+
setText($("k-next"), s.next_rebalance ? new Date(s.next_rebalance + "T12:00:00").toLocaleDateString([], { day: "numeric", month: "short" }) : "β");
|
| 400 |
+
setText($("k-next-s"), s.done_this_month ? "this month's rebalance is done" : (s.market_open ? "market open" : "first trading day of the month"));
|
|
|
|
|
|
|
|
|
|
| 401 |
} catch (e) {}
|
| 402 |
}
|
| 403 |
|
| 404 |
+
/* ---------- THE STRATEGY */
|
| 405 |
+
let strat = null, scen = "planning";
|
| 406 |
+
async function loadStrategy() {
|
| 407 |
+
if (strat) return;
|
| 408 |
+
try { strat = await get("/static/data/strategy.json"); } catch (e) { setText($("bt-sub"), "Backtest data unavailable."); return; }
|
| 409 |
+
const k = strat.stats, sp = strat.spy, y = strat.rolling_12m, box = $("bt-stats"); box.textContent = "";
|
| 410 |
+
const tiles = [["Return per year", pct(k.cagr), `S&P 500 ${pct(sp.cagr)}`], ["Worst fall", pct(k.max_dd, 0), `S&P 500 ${pct(sp.max_dd, 0)}`],
|
| 411 |
+
["Sharpe ratio", k.sharpe.toFixed(2), `S&P 500 ${sp.sharpe.toFixed(2)}`], ["Median 12 months", pct(y.median, 0), `S&P 500 ${pct(y.spy_median, 0)}`],
|
| 412 |
+
["12 months in profit", Math.round(y.share_positive * 100) + "%", `S&P 500 ${Math.round(y.spy_share_positive * 100)}%`],
|
| 413 |
+
["Beat the S&P over 12 months", Math.round(y.share_beat_spy * 100) + "%", "of all start months"]];
|
| 414 |
+
for (const [l, v, s] of tiles) { const t = document.createElement("div"); t.className = "tile";
|
| 415 |
+
for (const [c, x] of [["l", l], ["v", v], ["s", s]]) { const d = document.createElement("div"); d.className = c; d.textContent = x; t.append(d); } box.append(t); }
|
| 416 |
+
setText($("bt-sub"), `${strat.period}, monthly, after trading costs; companies that later went bust or were delisted are included. Before the 2024β26 AI rally (2013β2023): ${pct(k.cagr_2013_2023)} a year vs S&P ${pct(sp.cagr_2013_2023)}.`);
|
| 417 |
+
setText($("dist-sub"), `Share of all ${y.n} rolling 12-month periods by return. Updraft's middle 80% ran from ${pct(y.p10, 0)} to ${pct(y.p90, 0)}.`);
|
| 418 |
+
fillTables(); drawStrategy();
|
| 419 |
}
|
| 420 |
+
function fillTables() {
|
| 421 |
+
const g = strat.growth, tg = $("t-growth"); tg.textContent = "";
|
| 422 |
+
const h = tg.createTHead().insertRow(); for (const x of ["Year-end", "Updraft", "S&P 500"]) { const th = document.createElement("th"); th.textContent = x; h.append(th); }
|
| 423 |
+
const b = tg.createTBody();
|
| 424 |
+
g.dates.forEach((d, i) => { if (d.endsWith("-12") || i === g.dates.length - 1) { const r = b.insertRow(); for (const x of [d, usd(g.updraft[i]), usd(g.spy[i])]) r.insertCell().textContent = x; } });
|
| 425 |
+
const ty = $("t-years"); ty.textContent = ""; const h2 = ty.createTHead().insertRow();
|
| 426 |
+
for (const x of ["Year", "Updraft", "S&P 500"]) { const th = document.createElement("th"); th.textContent = x; h2.append(th); }
|
| 427 |
+
const b2 = ty.createTBody(); strat.years.labels.forEach((yr, i) => { const r = b2.insertRow(); for (const x of [yr, pct(strat.years.updraft[i]), pct(strat.years.spy[i])]) r.insertCell().textContent = x; });
|
| 428 |
+
}
|
| 429 |
+
function bars(label, data, color) {
|
| 430 |
+
return { label, data, backgroundColor: color, borderRadius: 4, borderSkipped: "start", maxBarThickness: 24, categoryPercentage: 0.72, barPercentage: 0.88, pointStyle: "rect" };
|
|
|
|
| 431 |
}
|
| 432 |
+
function drawStrategy() {
|
| 433 |
+
if (!strat) return;
|
| 434 |
+
const u = css("--updraft"), s = css("--spy"), g = strat.growth;
|
| 435 |
+
mk("c-growth", { type: "line", data: { labels: g.dates, datasets: [line("Updraft", g.updraft, u), line("S&P 500", g.spy, s)] },
|
| 436 |
+
options: baseOpts({ plugins: Object.assign(baseOpts().plugins, { endLabels: { enabled: true, format: (ds, v) => usd(v) },
|
| 437 |
+
tooltip: Object.assign(baseOpts().plugins.tooltip, { callbacks: { label: c => `${usd(c.parsed.y)} ${c.dataset.label}` } }) }),
|
| 438 |
+
scales: { x: Object.assign(baseOpts().scales.x, { ticks: { color: css("--muted"), maxTicksLimit: 10, maxRotation: 0, callback: function (v) { return this.getLabelForValue(v).slice(0, 4); } } }),
|
| 439 |
+
y: Object.assign(baseOpts().scales.y, { type: "logarithmic", ticks: { color: css("--muted"), callback: v => [1e4, 3e4, 1e5, 3e5, 1e6, 3e6, 1e7, 3e7].includes(v) ? usdShort(v) : null } }) } }) });
|
| 440 |
+
const Y = strat.years;
|
| 441 |
+
mk("c-years", { type: "bar", data: { labels: Y.labels.map(x => "β" + x.slice(2)), datasets: [bars("Updraft", Y.updraft, u), bars("S&P 500", Y.spy, s)] },
|
| 442 |
+
options: baseOpts({ interaction: { mode: "index", intersect: false }, plugins: Object.assign(baseOpts().plugins, {
|
| 443 |
+
tooltip: Object.assign(baseOpts().plugins.tooltip, { callbacks: { title: c => "20" + c[0].label.slice(1), label: c => `${pct(c.parsed.y)} ${c.dataset.label}` } }) }),
|
| 444 |
+
scales: { x: Object.assign(baseOpts().scales.x, { ticks: { color: css("--muted"), maxTicksLimit: 14, maxRotation: 0 } }),
|
| 445 |
+
y: Object.assign(baseOpts().scales.y, { ticks: { color: css("--muted"), callback: v => pct(v, 0) } }) } }) });
|
| 446 |
+
const DD = strat.drawdown;
|
| 447 |
+
mk("c-dd", { type: "line", data: { labels: DD.dates, datasets: [line("Updraft", DD.updraft, u, { fill: "origin", backgroundColor: alpha(u, 0.10) }), line("S&P 500", DD.spy, s)] },
|
| 448 |
+
options: baseOpts({ plugins: Object.assign(baseOpts().plugins, { tooltip: Object.assign(baseOpts().plugins.tooltip, { callbacks: { label: c => `${pct(c.parsed.y, 0)} ${c.dataset.label}` } }) }),
|
| 449 |
+
scales: { x: Object.assign(baseOpts().scales.x, { ticks: { color: css("--muted"), maxTicksLimit: 7, maxRotation: 0, callback: function (v) { return this.getLabelForValue(v).slice(0, 4); } } }),
|
| 450 |
+
y: Object.assign(baseOpts().scales.y, { max: 0, ticks: { color: css("--muted"), callback: v => pct(v, 0) } }) } }) });
|
| 451 |
+
const E = strat.dist12.edges, lab = E.slice(0, -1).map((e, i) => i === E.length - 2 ? `β₯ ${pct(e, 0)}` : (i === 0 ? `β€ ${pct(E[1], 0)}` : pct(e, 0)));
|
| 452 |
+
mk("c-dist", { type: "bar", data: { labels: lab, datasets: [bars("Updraft", strat.dist12.updraft, u), bars("S&P 500", strat.dist12.spy, s)] },
|
| 453 |
+
options: baseOpts({ plugins: Object.assign(baseOpts().plugins, { tooltip: Object.assign(baseOpts().plugins.tooltip, {
|
| 454 |
+
callbacks: { title: c => { const i = c[0].dataIndex; return i === 0 ? `12-month return β€ ${pct(E[1], 0)}` : i === E.length - 2 ? `12-month return β₯ ${pct(E[i], 0)}` : `12-month return ${pct(E[i], 0)} to ${pct(E[i + 1], 0)}`; },
|
| 455 |
+
label: c => `${Math.round(c.parsed.y * 100)}% of periods ${c.dataset.label}` } }) }),
|
| 456 |
+
scales: { x: Object.assign(baseOpts().scales.x, { ticks: { color: css("--muted"), maxTicksLimit: 9, maxRotation: 0 } }),
|
| 457 |
+
y: Object.assign(baseOpts().scales.y, { ticks: { color: css("--muted"), callback: v => Math.round(v * 100) + "%" } }) } }) });
|
| 458 |
+
drawProjections();
|
| 459 |
+
}
|
| 460 |
+
function num(id, def) { const v = parseFloat($(id).value); return isFinite(v) && v >= 0 ? v : def; }
|
| 461 |
+
function legendRow(el, items) {
|
| 462 |
+
el.textContent = "";
|
| 463 |
+
for (const [color, text, band] of items) { const s = document.createElement("span"); const i = document.createElement("i"); i.style.background = color;
|
| 464 |
+
if (band) i.className = "band"; s.append(i, document.createTextNode(text)); el.append(s); }
|
| 465 |
+
}
|
| 466 |
+
function drawProjections() {
|
| 467 |
+
if (!strat) return;
|
| 468 |
+
const P = strat.projections, S = P[scen], B = P.spy, yrs = P.years;
|
| 469 |
+
const col = scen === "planning" ? css("--plan") : css("--updraft"), spyc = css("--spy"), muted = css("--muted");
|
| 470 |
+
const name = scen === "planning" ? "Updraft, planning case" : "Updraft, history repeats";
|
| 471 |
+
setText($("scen-note"), scen === "planning"
|
| 472 |
+
? `Planning case: the same months with ${pct(P.planning_edge_removed, 1).replace("+", "")} a year of Updraft's edge removed β half of its historical lead over the S&P.`
|
| 473 |
+
: "History repeats: Updraft's 1999β2026 monthly returns as they were β the optimistic end, flattered by the 2024β26 rally.");
|
| 474 |
+
for (const kind of ["lump", "sip"]) {
|
| 475 |
+
const amt = kind === "lump" ? num("in-lump", 10000) : num("in-sip", 1000), d = S[kind], b = B[kind];
|
| 476 |
+
const scale = a => a.map(x => x * amt);
|
| 477 |
+
const ds = [
|
| 478 |
+
line("75th percentile", scale(d.p75), "transparent", { fill: "+1", backgroundColor: alpha(col, 0.16), endLabel: false, pointHoverRadius: 0 }),
|
| 479 |
+
line("25th percentile", scale(d.p25), "transparent", { endLabel: false, pointHoverRadius: 0 }),
|
| 480 |
+
line(name + " (median)", scale(d.p50), col),
|
| 481 |
+
line("S&P 500 (median)", scale(b.p50), spyc)];
|
| 482 |
+
if (kind === "sip") ds.push(line("Paid in", d.paid.map(x => x * amt), muted, { borderWidth: 1, endLabel: false }));
|
| 483 |
+
legendRow($("leg-" + kind), [[col, "Median", false], [col, "Middle half of outcomes", true], [spyc, "S&P 500 median", false]].concat(kind === "sip" ? [[muted, "Paid in", false]] : []));
|
| 484 |
+
mk("c-" + kind, { type: "line", data: { labels: yrs.map(y => y === 0 ? "Start" : y + "y"), datasets: ds },
|
| 485 |
+
options: baseOpts({ plugins: Object.assign(baseOpts().plugins, { endLabels: { enabled: true, format: (x, v) => usdShort(v) },
|
| 486 |
+
tooltip: Object.assign(baseOpts().plugins.tooltip, { filter: it => it.datasetIndex > 1 || it.datasetIndex === 0,
|
| 487 |
+
callbacks: { title: c => c[0].label === "Start" ? "Start" : "After " + c[0].label.replace("y", " years"),
|
| 488 |
+
label: c => c.datasetIndex === 0 ? `${usdShort(scale(d.p25)[c.dataIndex])} β ${usdShort(c.parsed.y)} middle half` : `${usd(c.parsed.y)} ${c.dataset.label}` } }) }),
|
| 489 |
+
scales: { x: baseOpts().scales.x, y: Object.assign(baseOpts().scales.y, { ticks: { color: css("--muted"), callback: v => usdShort(v) } }) } }) });
|
| 490 |
+
const tb = $("tb-" + kind); tb.textContent = "";
|
| 491 |
+
const hr = tb.createTHead().insertRow();
|
| 492 |
+
for (const h of ["After", "Paid in", "Updraft median", "Range (10thβ90th)", "S&P median", "Below paid in"]) { const th = document.createElement("th"); th.textContent = h; hr.append(th); }
|
| 493 |
+
const body = tb.createTBody();
|
| 494 |
+
for (const yr of [5, 10, 15]) {
|
| 495 |
+
const r = body.insertRow(), paid = (kind === "lump" ? amt : amt * 12 * yr);
|
| 496 |
+
const cells = [`${yr} years`, usd(paid), usd(d.p50[yr] * amt), `${usdShort(d.p10[yr] * amt)} β ${usdShort(d.p90[yr] * amt)}`, usd(b.p50[yr] * amt),
|
| 497 |
+
Math.round(d.below_paid[yr] * 100) + "%"];
|
| 498 |
+
cells.forEach((c, i) => { const td = r.insertCell(); td.textContent = c; if (i === 2) td.className = "big"; });
|
| 499 |
+
}
|
| 500 |
+
const m15 = d.p50[15] / (kind === "lump" ? 1 : 180), s15 = b.p50[15] / (kind === "lump" ? 1 : 180);
|
| 501 |
+
setText($(kind + "-sub"), kind === "lump" ? `${usd(amt)} once. Median after 15 years: ${m15.toFixed(1)}Γ the money (S&P ${s15.toFixed(1)}Γ).`
|
| 502 |
+
: `${usd(amt)} every month. Median after 15 years: ${m15.toFixed(1)}Γ what you paid in (S&P ${s15.toFixed(1)}Γ).`);
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
document.querySelectorAll("#scen button").forEach(b => b.onclick = () => { scen = b.dataset.s;
|
| 506 |
+
document.querySelectorAll("#scen button").forEach(x => x.setAttribute("aria-pressed", String(x === b))); drawProjections(); });
|
| 507 |
+
["in-lump", "in-sip"].forEach(id => $(id).addEventListener("input", () => drawProjections()));
|
| 508 |
+
|
| 509 |
+
function redrawAll() { drawLive(); if (strat) drawStrategy(); }
|
| 510 |
|
| 511 |
+
/* ---------- start */
|
| 512 |
+
loadPortfolio(); loadPerformance(); loadStatus();
|
| 513 |
+
setInterval(loadPortfolio, 30000); setInterval(loadStatus, 60000); setInterval(loadPerformance, 300000);
|
| 514 |
+
if (location.hash === "#strategy") showTab("strategy");
|
| 515 |
</script>
|
| 516 |
</body>
|
| 517 |
</html>
|
tests/test_qt3_space.py
CHANGED
|
@@ -241,3 +241,9 @@ def test_scheduler_not_started_without_keys(monkeypatch):
|
|
| 241 |
monkeypatch.setattr(config, "ALPACA_KEY", "")
|
| 242 |
monkeypatch.setattr(scheduler, "STATE", {**scheduler.STATE, "running": False})
|
| 243 |
assert scheduler.start() is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
monkeypatch.setattr(config, "ALPACA_KEY", "")
|
| 242 |
monkeypatch.setattr(scheduler, "STATE", {**scheduler.STATE, "running": False})
|
| 243 |
assert scheduler.start() is False
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def test_bench_value_same_money_and_deposits():
|
| 247 |
+
# S&P +10% on day 2; a $500 deposit on day 3 with S&P flat β benchmark gets the same deposit
|
| 248 |
+
b = performance.bench_value([1000.0, 1050.0, 1600.0], [100.0, 110.0, 110.0], ["d1", "d2", "d3"], {"d3": 500.0})
|
| 249 |
+
assert b == [1000.0, 1100.0, 1600.0]
|