feat: add autonomous paper-trading and calibration pipeline
This commit is contained in:
@@ -15,6 +15,13 @@ OPEN_ROUTER_API_KEY=
|
|||||||
OPEN_ROUTER_LLM_MODEL=qwen/qwen3-235b-a22b-2507
|
OPEN_ROUTER_LLM_MODEL=qwen/qwen3-235b-a22b-2507
|
||||||
OPEN_ROUTER_EMBED_MODEL=qwen/qwen3-embedding-8b
|
OPEN_ROUTER_EMBED_MODEL=qwen/qwen3-embedding-8b
|
||||||
|
|
||||||
|
# Paper execution is disabled unless AUTONOMY_EXECUTION_MODE=paper.
|
||||||
|
# These credentials are accepted only by the hard-coded Alpaca paper endpoint.
|
||||||
|
ALPACA_PAPER_KEY_ID=
|
||||||
|
ALPACA_PAPER_SECRET_KEY=
|
||||||
|
AUTONOMY_EXECUTION_MODE=shadow
|
||||||
|
AUTONOMY_DEFAULT_NOTIONAL=100
|
||||||
|
|
||||||
GDELT_BQ_PROJECT=
|
GDELT_BQ_PROJECT=
|
||||||
GDELT_BQ_KEY_FILE=./gdelt-credentials.json
|
GDELT_BQ_KEY_FILE=./gdelt-credentials.json
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ Node.js Fastify server that ingests news articles from RSS, GDELT, SEC EDGAR 8-K
|
|||||||
|
|
||||||
The server listens on the host and port defined in `config.json`.
|
The server listens on the host and port defined in `config.json`.
|
||||||
|
|
||||||
|
The bounded autonomy runtime and paper-trading contracts are documented in
|
||||||
|
[`docs/autonomy.md`](docs/autonomy.md). It is opt-in and does not start with
|
||||||
|
the API-only Compose service.
|
||||||
|
|
||||||
## How the data pipeline works
|
## How the data pipeline works
|
||||||
|
|
||||||
On startup the server:
|
On startup the server:
|
||||||
|
|||||||
@@ -214,6 +214,32 @@ def main():
|
|||||||
print(f" 20-day: {a20:.1f}% (n={n20})")
|
print(f" 20-day: {a20:.1f}% (n={n20})")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
# baselines — what would naive strategies have scored on the same set?
|
||||||
|
# this is the most important context for interpreting the model accuracy above
|
||||||
|
eval_df = df[df["correct_10d"].notna()].copy()
|
||||||
|
if len(eval_df) > 0:
|
||||||
|
# 1. always-positive baseline — predict every event as bullish
|
||||||
|
eval_df["always_pos_correct"] = eval_df["10d_return"].apply(lambda r: r > 0 if r is not None else None)
|
||||||
|
always_pos = eval_df["always_pos_correct"].mean() * 100
|
||||||
|
|
||||||
|
# 2. random baseline — flip a coin for each prediction (analytic expectation = 50%)
|
||||||
|
# we report the empirical positive rate of the underlying market over the test window
|
||||||
|
# since random would converge to that for a balanced dataset
|
||||||
|
market_up_rate = (eval_df["10d_return"] > 0).mean() * 100
|
||||||
|
|
||||||
|
# 3. always-negative baseline
|
||||||
|
always_neg = ((eval_df["10d_return"] < 0).sum() / len(eval_df)) * 100
|
||||||
|
|
||||||
|
print("BASELINES (10-day, same evaluation set)")
|
||||||
|
print(f" Always-positive: {always_pos:.1f}% (this is the bar to beat in a bull market)")
|
||||||
|
print(f" Always-negative: {always_neg:.1f}%")
|
||||||
|
print(f" Random (coin): 50.0% (analytic)")
|
||||||
|
print(f" Market up rate: {market_up_rate:.1f}% (% of events where stock rose 10d later)")
|
||||||
|
edge = a10 - always_pos
|
||||||
|
print(f" MODEL EDGE vs always-positive: {edge:+.1f} percentage points")
|
||||||
|
print()
|
||||||
|
|
||||||
# by magnitude
|
# by magnitude
|
||||||
print("BY MAGNITUDE (10-day accuracy)")
|
print("BY MAGNITUDE (10-day accuracy)")
|
||||||
for mag in sorted(df["magnitude"].dropna().unique()):
|
for mag in sorted(df["magnitude"].dropna().unique()):
|
||||||
|
|||||||
@@ -260,8 +260,10 @@ def main():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
sample = df[df["correct_10d"].notna()].head(30)
|
sample = df[df["correct_10d"].notna()].head(30)
|
||||||
|
|
||||||
print("SAMPLE (30 most recent predictions)")
|
print("SAMPLE (30 most recent predictions)")
|
||||||
print(f"{'Ticker':<12} {'Date':<12} {'Dir':<10} {'Mag':<8} {'5d%':>7} {'10d%':>7} {'20d%':>7} @10d")
|
print(f"{'Ticker':<12} {'Date':<12} {'Dir':<10} {'Mag':<8} {'5d%':>7} {'10d%':>7} {'20d%':>7} @10d")
|
||||||
|
|
||||||
print("-" * 72)
|
print("-" * 72)
|
||||||
for _, row in sample.iterrows():
|
for _, row in sample.iterrows():
|
||||||
r5s = f"{row['5d_return']:+.2f}" if row['5d_return'] is not None else "N/A"
|
r5s = f"{row['5d_return']:+.2f}" if row['5d_return'] is not None else "N/A"
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1240,HF,Hugging Face,2025-10-03,negative,medium,medium,OpenAI's valuation surge to $500 billion highlights its dominant market position and ability to attr,20.9692,20.8305,21.0089,20.9791,-0.6615,0.189,0.0472,True,False,False
|
1240,HF,Hugging Face,2025-10-03,negative,medium,medium,OpenAI's valuation surge to $500 billion highlights its dominant market position and ability to attr,20.9692,20.8305,21.0089,20.9791,-0.6615,0.189,0.0472,True,False,False
|
||||||
1304,META,Meta,2025-10-02,positive,medium,medium,"By using AI interaction data to improve recommendation relevance, Meta can increase user engagement ",725.8361,732.2853,710.8811,665.3572,0.8885,-2.0604,-8.3323,True,False,False
|
1304,META,Meta,2025-10-02,positive,medium,medium,"By using AI interaction data to improve recommendation relevance, Meta can increase user engagement ",725.8361,732.2853,710.8811,665.3572,0.8885,-2.0604,-8.3323,True,False,False
|
||||||
1305,META,Meta,2025-10-02,positive,medium,medium,Enhanced personalization using deeper AI signals strengthens Meta's competitive edge in social media,725.8361,732.2853,710.8811,665.3572,0.8885,-2.0604,-8.3323,True,False,False
|
1305,META,Meta,2025-10-02,positive,medium,medium,Enhanced personalization using deeper AI signals strengthens Meta's competitive edge in social media,725.8361,732.2853,710.8811,665.3572,0.8885,-2.0604,-8.3323,True,False,False
|
||||||
935,META,Meta,2025-10-01,positive,medium,short,AI-driven ad personalization strengthens Meta's competitive edge in digital advertising by improving,716.1423,716.6415,716.3519,750.4149,0.0697,0.0293,4.7857,True,True,True
|
935,META,Meta,2025-10-01,positive,medium,short,AI-driven ad personalization strengthens Meta's competitive edge in digital advertising by improving,716.1423,716.6415,716.3519,750.415,0.0697,0.0293,4.7857,True,True,True
|
||||||
937,META,Meta,2025-10-01,positive,medium,medium,"Enhanced ad targeting using AI interactions could increase advertiser ROI, driving higher ad spend s",716.1423,716.6415,716.3519,750.4149,0.0697,0.0293,4.7857,True,True,True
|
937,META,Meta,2025-10-01,positive,medium,medium,"Enhanced ad targeting using AI interactions could increase advertiser ROI, driving higher ad spend s",716.1423,716.6415,716.3519,750.415,0.0697,0.0293,4.7857,True,True,True
|
||||||
1302,SPOT,Spotify,2025-09-27,positive,medium,medium,"By adopting DDEX standards for AI transparency, Spotify positions itself as a responsible leader in ",728.47,680.5,685.29,645.78,-6.585,-5.9275,-11.3512,False,False,False
|
1302,SPOT,Spotify,2025-09-27,positive,medium,medium,"By adopting DDEX standards for AI transparency, Spotify positions itself as a responsible leader in ",728.47,680.5,685.29,645.78,-6.585,-5.9275,-11.3512,False,False,False
|
||||||
926,META,Meta,2025-09-18,positive,medium,medium,Launch of consumer-ready smartglasses with differentiated AI features may increase Meta's presence i,778.4218,747.6595,725.8361,710.8811,-3.9519,-6.7554,-8.6766,False,False,False
|
926,META,Meta,2025-09-18,positive,medium,medium,Launch of consumer-ready smartglasses with differentiated AI features may increase Meta's presence i,778.4218,747.6595,725.8361,710.8811,-3.9519,-6.7554,-8.6766,False,False,False
|
||||||
927,META,Meta,2025-09-18,positive,medium,short,Introduction of AI-integrated smartglasses with partnerships in fitness tech strengthens Meta's posi,778.4218,747.6595,725.8361,710.8811,-3.9519,-6.7554,-8.6766,False,False,False
|
927,META,Meta,2025-09-18,positive,medium,short,Introduction of AI-integrated smartglasses with partnerships in fitness tech strengthens Meta's posi,778.4218,747.6595,725.8361,710.8811,-3.9519,-6.7554,-8.6766,False,False,False
|
||||||
@@ -110,11 +110,11 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
637,AZN,AstraZeneca,2025-09-13,negative,medium,short,"Pausing a major investment may signal reduced confidence in the UK market, potentially affecting inv",154.4894,150.9859,145.9979,167.3157,-2.2678,-5.4965,8.3024,True,True,False
|
637,AZN,AstraZeneca,2025-09-13,negative,medium,short,"Pausing a major investment may signal reduced confidence in the UK market, potentially affecting inv",154.4894,150.9859,145.9979,167.3157,-2.2678,-5.4965,8.3024,True,True,False
|
||||||
638,AZN,AstraZeneca,2025-09-13,negative,medium,medium,Delaying expansion in a key life sciences hub like Cambridge could slow innovation and talent acquis,154.4894,150.9859,145.9979,167.3157,-2.2678,-5.4965,8.3024,True,True,False
|
638,AZN,AstraZeneca,2025-09-13,negative,medium,medium,Delaying expansion in a key life sciences hub like Cambridge could slow innovation and talent acquis,154.4894,150.9859,145.9979,167.3157,-2.2678,-5.4965,8.3024,True,True,False
|
||||||
628,ORCL,Oracle,2025-09-12,positive,medium,short,Bullish options activity suggests increased investor confidence in Oracle's near-term stock performa,289.8924,306.2434,281.2406,291.1707,5.6404,-2.9845,0.441,True,False,True
|
628,ORCL,Oracle,2025-09-12,positive,medium,short,Bullish options activity suggests increased investor confidence in Oracle's near-term stock performa,289.8924,306.2434,281.2406,291.1707,5.6404,-2.9845,0.441,True,False,True
|
||||||
988,AAPL,Apple,2025-09-12,positive,medium,short,"Strong buy-side investor sentiment, particularly from Indian retail investors, combined with histori",233.6247,245.033,254.974,244.8034,4.8832,9.1383,4.7849,True,True,True
|
988,AAPL,Apple,2025-09-12,positive,medium,short,"Strong buy-side investor sentiment, particularly from Indian retail investors, combined with histori",233.6247,245.033,254.974,244.8034,4.8831,9.1383,4.7849,True,True,True
|
||||||
627,ORCL,Oracle,2025-09-11,negative,medium,short,"Shares retreated 6-7% after a record high due to concerns about overreliance on OpenAI for growth, d",305.4496,294.2976,289.049,295.1463,-3.651,-5.3693,-3.3732,True,True,True
|
627,ORCL,Oracle,2025-09-11,negative,medium,short,"Shares retreated 6-7% after a record high due to concerns about overreliance on OpenAI for growth, d",305.4496,294.2976,289.049,295.1462,-3.651,-5.3693,-3.3732,True,True,True
|
||||||
1409,ORCL,Oracle,2025-09-11,positive,high,short,Oracle's stock surged 35.98% following strong cloud revenue forecasts and major AI-related contracts,305.4496,294.2976,289.049,295.1463,-3.651,-5.3693,-3.3732,False,False,False
|
1409,ORCL,Oracle,2025-09-11,positive,high,short,Oracle's stock surged 35.98% following strong cloud revenue forecasts and major AI-related contracts,305.4496,294.2976,289.049,295.1462,-3.651,-5.3693,-3.3732,False,False,False
|
||||||
1410,ORCL,Oracle,2025-09-11,positive,medium,medium,"With a cloud services backlog nearing $500 billion and major contracts in AI infrastructure, Oracle ",305.4496,294.2976,289.049,295.1463,-3.651,-5.3693,-3.3732,False,False,False
|
1410,ORCL,Oracle,2025-09-11,positive,medium,medium,"With a cloud services backlog nearing $500 billion and major contracts in AI infrastructure, Oracle ",305.4496,294.2976,289.049,295.1462,-3.651,-5.3693,-3.3732,False,False,False
|
||||||
1411,ORCL,Oracle,2025-09-11,positive,high,medium,Oracle's strategic positioning as a key AI cloud infrastructure provider through partnerships with l,305.4496,294.2976,289.049,295.1463,-3.651,-5.3693,-3.3732,False,False,False
|
1411,ORCL,Oracle,2025-09-11,positive,high,medium,Oracle's strategic positioning as a key AI cloud infrastructure provider through partnerships with l,305.4496,294.2976,289.049,295.1462,-3.651,-5.3693,-3.3732,False,False,False
|
||||||
611,INTC,Intel,2025-08-28,positive,medium,short,"Direct government investment signals confidence and improves public perception, consistent with prio",24.93,24.61,24.61,33.99,-1.2836,-1.2836,36.3418,False,False,True
|
611,INTC,Intel,2025-08-28,positive,medium,short,"Direct government investment signals confidence and improves public perception, consistent with prio",24.93,24.61,24.61,33.99,-1.2836,-1.2836,36.3418,False,False,True
|
||||||
612,INTC,Intel,2025-08-28,positive,medium,medium,"Government funding and stake may enhance capital investment in manufacturing, supporting domestic pr",24.93,24.61,24.61,33.99,-1.2836,-1.2836,36.3418,False,False,True
|
612,INTC,Intel,2025-08-28,positive,medium,medium,"Government funding and stake may enhance capital investment in manufacturing, supporting domestic pr",24.93,24.61,24.61,33.99,-1.2836,-1.2836,36.3418,False,False,True
|
||||||
608,NVDA,NVIDIA,2025-08-28,positive,high,short,"Extraordinary demand and full-speed ramp-up of Blackwell Ultra platform indicate strong adoption, re",180.1401,171.6315,177.1506,177.6705,-4.7233,-1.6595,-1.3709,False,False,False
|
608,NVDA,NVIDIA,2025-08-28,positive,high,short,"Extraordinary demand and full-speed ramp-up of Blackwell Ultra platform indicate strong adoption, re",180.1401,171.6315,177.1506,177.6705,-4.7233,-1.6595,-1.3709,False,False,False
|
||||||
@@ -135,24 +135,24 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
857,PLTR,Palantir,2025-08-10,positive,medium,medium,Recognition as 'the best story in all of software' and leadership in AI-driven government and enterp,182.68,177.17,158.74,153.11,-3.0162,-13.1049,-16.1868,False,False,False
|
857,PLTR,Palantir,2025-08-10,positive,medium,medium,Recognition as 'the best story in all of software' and leadership in AI-driven government and enterp,182.68,177.17,158.74,153.11,-3.0162,-13.1049,-16.1868,False,False,False
|
||||||
1208,INTC,Intel,2025-08-08,negative,medium,short,Trump's public demand for CEO resignation and allegations of conflict due to ties with Chinese firms,19.95,24.56,24.8,24.49,23.1078,24.3108,22.7569,False,False,False
|
1208,INTC,Intel,2025-08-08,negative,medium,short,Trump's public demand for CEO resignation and allegations of conflict due to ties with Chinese firms,19.95,24.56,24.8,24.49,23.1078,24.3108,22.7569,False,False,False
|
||||||
1209,INTC,Intel,2025-08-08,negative,low,short,"Leadership instability and political scrutiny may delay turnaround plans, giving competitors like Nv",19.95,24.56,24.8,24.49,23.1078,24.3108,22.7569,False,False,False
|
1209,INTC,Intel,2025-08-08,negative,low,short,"Leadership instability and political scrutiny may delay turnaround plans, giving competitors like Nv",19.95,24.56,24.8,24.49,23.1078,24.3108,22.7569,False,False,False
|
||||||
1210,META,Meta,2025-08-08,positive,high,medium,"The $29 billion financing enables accelerated AI infrastructure development, strengthening Meta's ca",767.4975,783.3901,753.0215,750.687,2.0707,-1.8861,-2.1903,True,False,False
|
1210,META,Meta,2025-08-08,positive,high,medium,"The $29 billion financing enables accelerated AI infrastructure development, strengthening Meta's ca",767.4976,783.3902,753.0215,750.6871,2.0707,-1.8861,-2.1903,True,False,False
|
||||||
1289,TSM,TSMC,2025-08-08,negative,medium,short,"The leak of trade secrets, even if not directly TSMC’s fault, could undermine confidence in its IP p",239.7449,236.8203,230.9811,241.3113,-1.2199,-3.6555,0.6534,True,True,False
|
1289,TSM,TSMC,2025-08-08,negative,medium,short,"The leak of trade secrets, even if not directly TSMC’s fault, could undermine confidence in its IP p",239.7449,236.8203,230.9811,241.3112,-1.2199,-3.6555,0.6533,True,True,False
|
||||||
1290,TSM,TSMC,2025-08-08,negative,low,short,"While the incident does not directly implicate TSMC in wrongdoing, associated supply chain instabili",239.7449,236.8203,230.9811,241.3113,-1.2199,-3.6555,0.6534,True,True,False
|
1290,TSM,TSMC,2025-08-08,negative,low,short,"While the incident does not directly implicate TSMC in wrongdoing, associated supply chain instabili",239.7449,236.8203,230.9811,241.3112,-1.2199,-3.6555,0.6533,True,True,False
|
||||||
1218,META,Meta,2025-08-08,positive,high,long,"The $29 billion financing enables large-scale AI data center development, strengthening Meta's infra",767.4975,783.3901,753.0215,750.687,2.0707,-1.8861,-2.1903,True,False,False
|
1218,META,Meta,2025-08-08,positive,high,long,"The $29 billion financing enables large-scale AI data center development, strengthening Meta's infra",767.4976,783.3902,753.0215,750.6871,2.0707,-1.8861,-2.1903,True,False,False
|
||||||
1511,SPOT,Spotify,2025-08-07,positive,medium,short,"The introduction of AI DJ, which provides personalized, context-rich music recommendations using gen",686.74,698.5,689.47,703.85,1.7124,0.3975,2.4915,True,True,True
|
1511,SPOT,Spotify,2025-08-07,positive,medium,short,"The introduction of AI DJ, which provides personalized, context-rich music recommendations using gen",686.74,698.5,689.47,703.85,1.7124,0.3975,2.4915,True,True,True
|
||||||
1513,SPOT,Spotify,2025-08-07,positive,medium,medium,Investment in advanced AI features like AI DJ strengthens Spotify’s differentiation from competitors,686.74,698.5,689.47,703.85,1.7124,0.3975,2.4915,True,True,True
|
1513,SPOT,Spotify,2025-08-07,positive,medium,medium,Investment in advanced AI features like AI DJ strengthens Spotify’s differentiation from competitors,686.74,698.5,689.47,703.85,1.7124,0.3975,2.4915,True,True,True
|
||||||
851,PLTR,Palantir,2025-08-07,negative,medium,medium,"CEO's remarks may deepen skepticism among educated public and academia, contributing to declining pu",182.2,181.02,156.18,156.14,-0.6476,-14.281,-14.303,True,True,True
|
851,PLTR,Palantir,2025-08-07,negative,medium,medium,"CEO's remarks may deepen skepticism among educated public and academia, contributing to declining pu",182.2,181.02,156.18,156.14,-0.6476,-14.281,-14.303,True,True,True
|
||||||
852,PLTR,Palantir,2025-08-07,positive,medium,long,Positioning Palantir as a meritocratic alternative to elite education could strengthen employer bran,182.2,181.02,156.18,156.14,-0.6476,-14.281,-14.303,False,False,False
|
852,PLTR,Palantir,2025-08-07,positive,medium,long,Positioning Palantir as a meritocratic alternative to elite education could strengthen employer bran,182.2,181.02,156.18,156.14,-0.6476,-14.281,-14.303,False,False,False
|
||||||
1288,TSM,TSMC,2025-08-07,negative,medium,short,Increased geopolitical risk and costly overseas expansion could weigh on investor sentiment in the n,240.5281,238.922,225.3699,233.182,-0.6677,-6.302,-3.0541,True,True,True
|
1288,TSM,TSMC,2025-08-07,negative,medium,short,Increased geopolitical risk and costly overseas expansion could weigh on investor sentiment in the n,240.5281,238.922,225.3699,233.182,-0.6677,-6.302,-3.0542,True,True,True
|
||||||
1509,TSM,TSMC,2025-08-07,positive,medium,short,Tariff exemption strengthens TSMC's competitive position relative to non-U.S.-based semiconductor ma,240.5281,238.922,225.3699,233.182,-0.6677,-6.302,-3.0541,False,False,False
|
1509,TSM,TSMC,2025-08-07,positive,medium,short,Tariff exemption strengthens TSMC's competitive position relative to non-U.S.-based semiconductor ma,240.5281,238.922,225.3699,233.182,-0.6677,-6.302,-3.0542,False,False,False
|
||||||
1510,TSM,TSMC,2025-08-07,positive,low,short,"Exemption from high tariffs provides operational certainty and reduces near-term geopolitical risk, ",240.5281,238.922,225.3699,233.182,-0.6677,-6.302,-3.0541,False,False,False
|
1510,TSM,TSMC,2025-08-07,positive,low,short,"Exemption from high tariffs provides operational certainty and reduces near-term geopolitical risk, ",240.5281,238.922,225.3699,233.182,-0.6677,-6.302,-3.0542,False,False,False
|
||||||
873,PLTR,Palantir,2025-08-05,positive,high,short,"Palantir's strong earnings, revenue growth of nearly 50%, net income up 144%, raised guidance, and i",173.27,186.97,157.75,157.09,7.9067,-8.9571,-9.338,True,False,False
|
873,PLTR,Palantir,2025-08-05,positive,high,short,"Palantir's strong earnings, revenue growth of nearly 50%, net income up 144%, raised guidance, and i",173.27,186.97,157.75,157.09,7.9067,-8.9571,-9.338,True,False,False
|
||||||
874,PLTR,Palantir,2025-08-05,positive,medium,medium,Investor confidence in Palantir's AI-powered efficiency and scalability may accelerate adoption in g,173.27,186.97,157.75,157.09,7.9067,-8.9571,-9.338,True,False,False
|
874,PLTR,Palantir,2025-08-05,positive,medium,medium,Investor confidence in Palantir's AI-powered efficiency and scalability may accelerate adoption in g,173.27,186.97,157.75,157.09,7.9067,-8.9571,-9.338,True,False,False
|
||||||
875,PLTR,Palantir,2025-08-05,positive,medium,short,Palantir's demonstrated ability to grow revenue while reducing workforce through AI gives it a perce,173.27,186.97,157.75,157.09,7.9067,-8.9571,-9.338,True,False,False
|
875,PLTR,Palantir,2025-08-05,positive,medium,short,Palantir's demonstrated ability to grow revenue while reducing workforce through AI gives it a perce,173.27,186.97,157.75,157.09,7.9067,-8.9571,-9.338,True,False,False
|
||||||
1463,META,Meta,2025-08-01,positive,high,short,Meta shares jump after strong third-quarter sales forecast and better-than-expected financial perfor,748.2527,767.4975,783.3901,736.9692,2.572,4.6959,-1.508,True,True,False
|
1463,META,Meta,2025-08-01,positive,high,short,Meta shares jump after strong third-quarter sales forecast and better-than-expected financial perfor,748.2527,767.4976,783.3902,736.9692,2.572,4.6959,-1.508,True,True,False
|
||||||
1307,MSFT,Microsoft,2025-08-01,positive,high,short,"Microsoft's stock rose 3.9% on July 31, 2025, following record valuation and strong quarterly result",521.0829,519.0249,517.1657,504.5917,-0.395,-0.7517,-3.1648,False,False,False
|
1307,MSFT,Microsoft,2025-08-01,positive,high,short,"Microsoft's stock rose 3.9% on July 31, 2025, following record valuation and strong quarterly result",521.0829,519.025,517.1657,504.5917,-0.3949,-0.7517,-3.1648,False,False,False
|
||||||
1308,MSFT,Microsoft,2025-08-01,positive,medium,medium,Azure's 39% growth and increasing AI-driven cloud revenue suggest Microsoft is gaining cloud market ,521.0829,519.0249,517.1657,504.5917,-0.395,-0.7517,-3.1648,False,False,False
|
1308,MSFT,Microsoft,2025-08-01,positive,medium,medium,Azure's 39% growth and increasing AI-driven cloud revenue suggest Microsoft is gaining cloud market ,521.0829,519.025,517.1657,504.5917,-0.3949,-0.7517,-3.1648,False,False,False
|
||||||
1309,MSFT,Microsoft,2025-08-01,positive,high,long,"Surpassing four trillion dollars in market cap, second only to Nvidia, reinforces Microsoft's elite ",521.0829,519.0249,517.1657,504.5917,-0.395,-0.7517,-3.1648,False,False,False
|
1309,MSFT,Microsoft,2025-08-01,positive,high,long,"Surpassing four trillion dollars in market cap, second only to Nvidia, reinforces Microsoft's elite ",521.0829,519.025,517.1657,504.5917,-0.3949,-0.7517,-3.1648,False,False,False
|
||||||
1400,ARM,Arm Holdings,2025-07-31,negative,high,short,Arm's shares dropped nearly 13% following disappointing guidance and a strategic shift that risks cu,141.375,135.57,140.55,142.55,-4.1061,-0.5836,0.8311,True,True,False
|
1400,ARM,Arm Holdings,2025-07-31,negative,high,short,Arm's shares dropped nearly 13% following disappointing guidance and a strategic shift that risks cu,141.375,135.57,140.55,142.55,-4.1061,-0.5836,0.8311,True,True,False
|
||||||
1401,ARM,Arm Holdings,2025-07-31,negative,medium,medium,Developing full chip solutions may lead to conflicts of interest with key customers like Nvidia and ,141.375,135.57,140.55,142.55,-4.1061,-0.5836,0.8311,True,True,False
|
1401,ARM,Arm Holdings,2025-07-31,negative,medium,medium,Developing full chip solutions may lead to conflicts of interest with key customers like Nvidia and ,141.375,135.57,140.55,142.55,-4.1061,-0.5836,0.8311,True,True,False
|
||||||
1402,ARM,Arm Holdings,2025-07-31,negative,low,long,Long-term market share could be affected if customers reduce reliance on Arm's IP due to competitive,141.375,135.57,140.55,142.55,-4.1061,-0.5836,0.8311,True,True,False
|
1402,ARM,Arm Holdings,2025-07-31,negative,low,long,Long-term market share could be affected if customers reduce reliance on Arm's IP due to competitive,141.375,135.57,140.55,142.55,-4.1061,-0.5836,0.8311,True,True,False
|
||||||
@@ -163,10 +163,10 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1222,META,Meta,2025-07-31,positive,medium,medium,"Aggressive investment in AI talent and infrastructure positions Meta to better compete with OpenAI, ",771.6278,760.045,780.2974,749.3502,-1.5011,1.1235,-2.8871,False,True,False
|
1222,META,Meta,2025-07-31,positive,medium,medium,"Aggressive investment in AI talent and infrastructure positions Meta to better compete with OpenAI, ",771.6278,760.045,780.2974,749.3502,-1.5011,1.1235,-2.8871,False,True,False
|
||||||
1223,META,Meta,2025-07-31,positive,medium,medium,Strong ad revenue growth and AI-driven product enhancements may increase user engagement and ad mark,771.6278,760.045,780.2974,749.3502,-1.5011,1.1235,-2.8871,False,True,False
|
1223,META,Meta,2025-07-31,positive,medium,medium,Strong ad revenue growth and AI-driven product enhancements may increase user engagement and ad mark,771.6278,760.045,780.2974,749.3502,-1.5011,1.1235,-2.8871,False,True,False
|
||||||
1279,UBS,UBS,2025-07-30,negative,medium,medium,Proposed capital requirements could impair competitiveness if implemented; CEO warns 42 billion doll,36.9761,37.0834,38.6529,39.15,0.29,4.5347,5.8793,False,False,False
|
1279,UBS,UBS,2025-07-30,negative,medium,medium,Proposed capital requirements could impair competitiveness if implemented; CEO warns 42 billion doll,36.9761,37.0834,38.6529,39.15,0.29,4.5347,5.8793,False,False,False
|
||||||
893,META,Meta,2025-07-26,positive,high,medium,Hiring a foundational OpenAI researcher and formalizing leadership under a strong AI executive team ,715.9486,748.2527,767.4975,753.0215,4.5121,7.2001,5.1781,True,True,True
|
893,META,Meta,2025-07-26,positive,high,medium,Hiring a foundational OpenAI researcher and formalizing leadership under a strong AI executive team ,715.9486,748.2527,767.4976,753.0215,4.5121,7.2001,5.1781,True,True,True
|
||||||
894,META,Meta,2025-07-26,positive,medium,long,"Strengthening AI talent base supports future AI product development, potentially increasing market s",715.9486,748.2527,767.4975,753.0215,4.5121,7.2001,5.1781,True,True,True
|
894,META,Meta,2025-07-26,positive,medium,long,"Strengthening AI talent base supports future AI product development, potentially increasing market s",715.9486,748.2527,767.4976,753.0215,4.5121,7.2001,5.1781,True,True,True
|
||||||
1558,VZ,Verizon,2025-07-24,negative,low,short,"The $175 million penalty represents a one-time cost that may slightly pressure earnings, but given V",41.0128,40.7082,40.8891,42.8693,-0.7428,-0.3018,4.5264,True,True,False
|
1558,VZ,Verizon,2025-07-24,negative,low,short,"The $175 million penalty represents a one-time cost that may slightly pressure earnings, but given V",41.0128,40.7082,40.8891,42.8693,-0.7428,-0.3018,4.5265,True,True,False
|
||||||
1559,VZ,Verizon,2025-07-24,negative,medium,short,Increased legal risk from patent litigation could affect investor sentiment and raise concerns about,41.0128,40.7082,40.8891,42.8693,-0.7428,-0.3018,4.5264,True,True,False
|
1559,VZ,Verizon,2025-07-24,negative,medium,short,Increased legal risk from patent litigation could affect investor sentiment and raise concerns about,41.0128,40.7082,40.8891,42.8693,-0.7428,-0.3018,4.5265,True,True,False
|
||||||
799,META,Meta,2025-07-17,negative,medium,short,Public perception may be negatively influenced by the association of top executives with privacy vio,699.7665,713.1252,771.6278,780.2974,1.909,10.2693,11.5083,False,False,False
|
799,META,Meta,2025-07-17,negative,medium,short,Public perception may be negatively influenced by the association of top executives with privacy vio,699.7665,713.1252,771.6278,780.2974,1.909,10.2693,11.5083,False,False,False
|
||||||
1109,NVDA,NVIDIA,2025-07-10,positive,high,short,Nvidia's stock has surged 69% since early April and analysts project an additional 17% rise to $190 ,164.0727,172.9713,173.7111,180.74,5.4235,5.8745,10.1584,True,True,True
|
1109,NVDA,NVIDIA,2025-07-10,positive,high,short,Nvidia's stock has surged 69% since early April and analysts project an additional 17% rise to $190 ,164.0727,172.9713,173.7111,180.74,5.4235,5.8745,10.1584,True,True,True
|
||||||
1110,NVDA,NVIDIA,2025-07-10,positive,medium,medium,Ongoing demand from big tech and AI innovators for high-performance computing chips reinforces Nvidi,164.0727,172.9713,173.7111,180.74,5.4235,5.8745,10.1584,True,True,True
|
1110,NVDA,NVIDIA,2025-07-10,positive,medium,medium,Ongoing demand from big tech and AI innovators for high-performance computing chips reinforces Nvidi,164.0727,172.9713,173.7111,180.74,5.4235,5.8745,10.1584,True,True,True
|
||||||
@@ -182,8 +182,8 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1084,MU,Micron,2025-07-03,positive,low,long,Sustained revenue growth from rising HBM demand and expanded production capacity could support long-,121.998,122.9316,113.0959,108.9819,0.7653,-7.2969,-10.6691,True,False,False
|
1084,MU,Micron,2025-07-03,positive,low,long,Sustained revenue growth from rising HBM demand and expanded production capacity could support long-,121.998,122.9316,113.0959,108.9819,0.7653,-7.2969,-10.6691,True,False,False
|
||||||
1506,BP,BP,2025-07-02,positive,medium,short,"Takeover speculation and activist involvement typically create short-term stock price momentum, even",30.038,30.0093,30.633,30.9497,-0.0958,1.9808,3.0351,False,True,True
|
1506,BP,BP,2025-07-02,positive,medium,short,"Takeover speculation and activist involvement typically create short-term stock price momentum, even",30.038,30.0093,30.633,30.9497,-0.0958,1.9808,3.0351,False,True,True
|
||||||
1507,BP,BP,2025-07-02,negative,medium,medium,Persistent speculation that BP could be acquired may weaken its strategic autonomy and bargaining po,30.038,30.0093,30.633,30.9497,-0.0958,1.9808,3.0351,True,False,False
|
1507,BP,BP,2025-07-02,negative,medium,medium,Persistent speculation that BP could be acquired may weaken its strategic autonomy and bargaining po,30.038,30.0093,30.633,30.9497,-0.0958,1.9808,3.0351,True,False,False
|
||||||
902,META,Meta,2025-06-13,positive,high,medium,Acquiring key AI talent and investing heavily in AI infrastructure through Scale AI strengthens Meta,680.7462,680.7512,731.9111,715.8289,0.0007,7.516,5.1536,True,True,True
|
902,META,Meta,2025-06-13,positive,high,medium,Acquiring key AI talent and investing heavily in AI infrastructure through Scale AI strengthens Meta,680.7463,680.7512,731.9111,715.8289,0.0007,7.516,5.1535,True,True,True
|
||||||
903,META,Meta,2025-06-13,positive,medium,short,Major strategic investment in AI and high-profile talent acquisition may be viewed favorably by inve,680.7462,680.7512,731.9111,715.8289,0.0007,7.516,5.1536,True,True,True
|
903,META,Meta,2025-06-13,positive,medium,short,Major strategic investment in AI and high-profile talent acquisition may be viewed favorably by inve,680.7463,680.7512,731.9111,715.8289,0.0007,7.516,5.1535,True,True,True
|
||||||
1476,NVS,Novartis,2025-06-13,positive,medium,short,"Novo Nordisk's leadership turmoil and stock decline may weaken its market positioning, creating oppo",115.9217,112.3504,116.4652,117.4453,-3.0808,0.4688,1.3144,False,True,True
|
1476,NVS,Novartis,2025-06-13,positive,medium,short,"Novo Nordisk's leadership turmoil and stock decline may weaken its market positioning, creating oppo",115.9217,112.3504,116.4652,117.4453,-3.0808,0.4688,1.3144,False,True,True
|
||||||
1477,NVS,Novartis,2025-06-13,positive,low,short,"While Novartis is not directly affected by Novo Nordisk's CEO dismissal, reduced competitive pressur",115.9217,112.3504,116.4652,117.4453,-3.0808,0.4688,1.3144,False,True,True
|
1477,NVS,Novartis,2025-06-13,positive,low,short,"While Novartis is not directly affected by Novo Nordisk's CEO dismissal, reduced competitive pressur",115.9217,112.3504,116.4652,117.4453,-3.0808,0.4688,1.3144,False,True,True
|
||||||
1085,AMD,AMD,2025-06-13,positive,high,medium,AMD’s launch of the Helios server and partnership with OpenAI strengthen its position in the AI chip,116.16,128.24,143.81,146.42,10.3995,23.8034,26.0503,True,True,True
|
1085,AMD,AMD,2025-06-13,positive,high,medium,AMD’s launch of the Helios server and partnership with OpenAI strengthen its position in the AI chip,116.16,128.24,143.81,146.42,10.3995,23.8034,26.0503,True,True,True
|
||||||
@@ -195,32 +195,32 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1397,UBS,UBS,2025-06-08,positive,high,medium,"The new draft law significantly increases regulatory obligations, including capitalization of foreig",32.1019,31.1758,29.655,33.3107,-2.8849,-7.6222,3.7656,False,False,True
|
1397,UBS,UBS,2025-06-08,positive,high,medium,"The new draft law significantly increases regulatory obligations, including capitalization of foreig",32.1019,31.1758,29.655,33.3107,-2.8849,-7.6222,3.7656,False,False,True
|
||||||
1556,UBS,UBS,2025-06-06,negative,medium,short,"Stricter capital requirements may constrain UBS's ability to deploy capital efficiently, increasing ",32.7745,31.1758,29.655,33.3107,-4.878,-9.5181,1.6359,True,True,False
|
1556,UBS,UBS,2025-06-06,negative,medium,short,"Stricter capital requirements may constrain UBS's ability to deploy capital efficiently, increasing ",32.7745,31.1758,29.655,33.3107,-4.878,-9.5181,1.6359,True,True,False
|
||||||
1557,UBS,UBS,2025-06-06,negative,low,short,"Announcement of higher capital requirements could lead to margin compression concerns, negatively im",32.7745,31.1758,29.655,33.3107,-4.878,-9.5181,1.6359,True,True,False
|
1557,UBS,UBS,2025-06-06,negative,low,short,"Announcement of higher capital requirements could lead to margin compression concerns, negatively im",32.7745,31.1758,29.655,33.3107,-4.878,-9.5181,1.6359,True,True,False
|
||||||
1393,AVGO,Broadcom,2025-06-06,negative,medium,short,"Broadcom's stock fell 2% in extended trading after the forecast missed the loftiest expectations, de",244.9453,246.7011,248.5644,272.6165,0.7168,1.4775,11.2969,False,False,False
|
1393,AVGO,Broadcom,2025-06-06,negative,medium,short,"Broadcom's stock fell 2% in extended trading after the forecast missed the loftiest expectations, de",244.9453,246.7011,248.5644,272.6164,0.7168,1.4775,11.2969,False,False,False
|
||||||
913,META,Meta,2025-06-04,positive,medium,long,"Securing long-term, stable nuclear power enhances Meta's ability to scale AI infrastructure, improvi",685.8104,691.9812,694.1398,711.8981,0.8998,1.2145,3.8039,True,True,True
|
913,META,Meta,2025-06-04,positive,medium,long,"Securing long-term, stable nuclear power enhances Meta's ability to scale AI infrastructure, improvi",685.8105,691.9812,694.1398,711.8981,0.8998,1.2145,3.8039,True,True,True
|
||||||
914,META,Meta,2025-06-04,positive,high,long,The 20-year power purchase agreement with Constellation Energy directly supports Meta's growing AI w,685.8104,691.9812,694.1398,711.8981,0.8998,1.2145,3.8039,True,True,True
|
914,META,Meta,2025-06-04,positive,high,long,The 20-year power purchase agreement with Constellation Energy directly supports Meta's growing AI w,685.8105,691.9812,694.1398,711.8981,0.8998,1.2145,3.8039,True,True,True
|
||||||
1106,AVGO,Broadcom,2025-06-02,negative,medium,short,"Dissatisfied partners may drive customers toward competing private cloud solutions, increasing migra",246.711,242.3166,250.0738,274.0781,-1.7812,1.363,11.0927,True,False,False
|
1106,AVGO,Broadcom,2025-06-02,negative,medium,short,"Dissatisfied partners may drive customers toward competing private cloud solutions, increasing migra",246.711,242.3166,250.0738,274.0781,-1.7812,1.363,11.0928,True,False,False
|
||||||
1107,AVGO,Broadcom,2025-06-02,negative,medium,medium,"Reducing the number of channel partners, especially long-standing ones, could erode VMware's market ",246.711,242.3166,250.0738,274.0781,-1.7812,1.363,11.0927,True,False,False
|
1107,AVGO,Broadcom,2025-06-02,negative,medium,medium,"Reducing the number of channel partners, especially long-standing ones, could erode VMware's market ",246.711,242.3166,250.0738,274.0781,-1.7812,1.363,11.0928,True,False,False
|
||||||
1196,NVDA,NVIDIA,2025-05-29,positive,high,short,Nvidia's revenue surpassing $44bn despite China sales restrictions indicates robust global demand fo,139.1572,139.957,144.9759,154.9942,0.5748,4.1814,11.3807,True,True,True
|
1196,NVDA,NVIDIA,2025-05-29,positive,high,short,Nvidia's revenue surpassing $44bn despite China sales restrictions indicates robust global demand fo,139.1572,139.957,144.9759,154.9942,0.5748,4.1814,11.3807,True,True,True
|
||||||
1197,NVDA,NVIDIA,2025-05-29,positive,medium,medium,Ability to achieve record revenue under geopolitical constraints reinforces Nvidia's competitive adv,139.1572,139.957,144.9759,154.9942,0.5748,4.1814,11.3807,True,True,True
|
1197,NVDA,NVIDIA,2025-05-29,positive,medium,medium,Ability to achieve record revenue under geopolitical constraints reinforces Nvidia's competitive adv,139.1572,139.957,144.9759,154.9942,0.5748,4.1814,11.3807,True,True,True
|
||||||
1198,NVDA,NVIDIA,2025-05-29,positive,medium,short,Strong revenue performance despite headwinds aligns with analyst sentiment supporting stock valuatio,139.1572,139.957,144.9759,154.9942,0.5748,4.1814,11.3807,True,True,True
|
1198,NVDA,NVIDIA,2025-05-29,positive,medium,short,Strong revenue performance despite headwinds aligns with analyst sentiment supporting stock valuatio,139.1572,139.957,144.9759,154.9942,0.5748,4.1814,11.3807,True,True,True
|
||||||
1203,LHX,L3Harris Technologies,2025-05-29,positive,medium,short,Technical breakout above $232 resistance and inclusion in a model portfolio suggest near-term upward,239.3204,239.1635,247.3939,243.8468,-0.0655,3.3735,1.8914,False,True,True
|
1203,LHX,L3Harris Technologies,2025-05-29,positive,medium,short,Technical breakout above $232 resistance and inclusion in a model portfolio suggest near-term upward,239.3204,239.1635,247.3939,243.8468,-0.0655,3.3735,1.8914,False,True,True
|
||||||
1503,META,Meta,2025-05-29,positive,medium,medium,"One billion monthly users of Meta AI strengthens Meta's position in the generative AI race, though i",643.0439,682.4907,691.2036,724.3888,6.1344,7.4893,12.65,True,True,True
|
1503,META,Meta,2025-05-29,positive,medium,medium,"One billion monthly users of Meta AI strengthens Meta's position in the generative AI race, though i",643.0439,682.4907,691.2036,724.3887,6.1344,7.4893,12.65,True,True,True
|
||||||
1504,META,Meta,2025-05-29,positive,low,short,"High user engagement with Meta AI may lead to incremental gains in AI assistant market share, but Go",643.0439,682.4907,691.2036,724.3888,6.1344,7.4893,12.65,True,True,True
|
1504,META,Meta,2025-05-29,positive,low,short,"High user engagement with Meta AI may lead to incremental gains in AI assistant market share, but Go",643.0439,682.4907,691.2036,724.3887,6.1344,7.4893,12.65,True,True,True
|
||||||
1271,NVS,Novartis,2025-05-29,positive,medium,short,"As a competitor in the metabolic and pharmaceutical space, Novartis may benefit from Novo Nordisk's ",109.2546,114.3108,117.2027,116.766,4.6278,7.2748,6.8751,True,True,True
|
1271,NVS,Novartis,2025-05-29,positive,medium,short,"As a competitor in the metabolic and pharmaceutical space, Novartis may benefit from Novo Nordisk's ",109.2546,114.3108,117.2027,116.766,4.6278,7.2748,6.8751,True,True,True
|
||||||
1272,NVS,Novartis,2025-05-29,positive,low,short,"While not directly mentioned, the known positive impact of production investment and divestment on N",109.2546,114.3108,117.2027,116.766,4.6278,7.2748,6.8751,True,True,True
|
1272,NVS,Novartis,2025-05-29,positive,low,short,"While not directly mentioned, the known positive impact of production investment and divestment on N",109.2546,114.3108,117.2027,116.766,4.6278,7.2748,6.8751,True,True,True
|
||||||
1273,META,Meta,2025-05-29,positive,medium,medium,"Increased AI integration in advertising and user content, combined with high advertiser adoption, po",643.0439,682.4907,691.2036,724.3888,6.1344,7.4893,12.65,True,True,True
|
1273,META,Meta,2025-05-29,positive,medium,medium,"Increased AI integration in advertising and user content, combined with high advertiser adoption, po",643.0439,682.4907,691.2036,724.3887,6.1344,7.4893,12.65,True,True,True
|
||||||
1274,META,Meta,2025-05-29,positive,high,medium,"Meta's massive AI investment, global user base, and early lead in AI-powered advertising tools stren",643.0439,682.4907,691.2036,724.3888,6.1344,7.4893,12.65,True,True,True
|
1274,META,Meta,2025-05-29,positive,high,medium,"Meta's massive AI investment, global user base, and early lead in AI-powered advertising tools stren",643.0439,682.4907,691.2036,724.3887,6.1344,7.4893,12.65,True,True,True
|
||||||
1199,META,Meta,2025-05-25,positive,medium,medium,"Expanding AI training with user data may improve Meta AI and Llama models, enhancing competitiveness",640.3224,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,True,True,True
|
1199,META,Meta,2025-05-25,positive,medium,medium,"Expanding AI training with user data may improve Meta AI and Llama models, enhancing competitiveness",640.3223,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,True,True,True
|
||||||
1200,META,Meta,2025-05-25,negative,high,short,"Noyb's challenge and opt-out model may lead to GDPR enforcement actions, increasing regulatory and l",640.3224,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,False,False,False
|
1200,META,Meta,2025-05-25,negative,high,short,"Noyb's challenge and opt-out model may lead to GDPR enforcement actions, increasing regulatory and l",640.3223,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,False,False,False
|
||||||
1201,META,Meta,2025-05-25,negative,medium,short,"Public backlash over data usage for AI without explicit consent could harm user trust, especially in",640.3224,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,False,False,False
|
1201,META,Meta,2025-05-25,negative,medium,short,"Public backlash over data usage for AI without explicit consent could harm user trust, especially in",640.3223,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,False,False,False
|
||||||
1202,META,Meta,2025-05-25,negative,low,short,Short-term investor concerns may arise from increased regulatory scrutiny and reputational risk tied,640.3224,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,False,False,False
|
1202,META,Meta,2025-05-25,negative,low,short,Short-term investor concerns may arise from increased regulatory scrutiny and reputational risk tied,640.3223,645.4763,695.5401,680.7512,0.8049,8.6234,6.3138,False,False,False
|
||||||
1259,TSLA,Tesla,2025-05-23,negative,medium,short,Being outsold in Europe suggests Tesla is losing market share to a strong competitor in a key region,339.34,346.46,295.14,322.16,2.0982,-13.0253,-5.0628,False,True,True
|
1259,TSLA,Tesla,2025-05-23,negative,medium,short,Being outsold in Europe suggests Tesla is losing market share to a strong competitor in a key region,339.34,346.46,295.14,322.16,2.0982,-13.0253,-5.0628,False,True,True
|
||||||
1260,TSLA,Tesla,2025-05-23,negative,medium,short,A competitor labeled as a 'Tesla-killer' gaining sales leadership in Europe undermines Tesla's compe,339.34,346.46,295.14,322.16,2.0982,-13.0253,-5.0628,False,True,True
|
1260,TSLA,Tesla,2025-05-23,negative,medium,short,A competitor labeled as a 'Tesla-killer' gaining sales leadership in Europe undermines Tesla's compe,339.34,346.46,295.14,322.16,2.0982,-13.0253,-5.0628,False,True,True
|
||||||
1261,TSLA,Tesla,2025-05-23,negative,low,short,"Missing sales expectations in a major market can negatively impact investor sentiment, especially am",339.34,346.46,295.14,322.16,2.0982,-13.0253,-5.0628,False,True,True
|
1261,TSLA,Tesla,2025-05-23,negative,low,short,"Missing sales expectations in a major market can negatively impact investor sentiment, especially am",339.34,346.46,295.14,322.16,2.0982,-13.0253,-5.0628,False,True,True
|
||||||
1464,SHOP,Shopify,2025-05-23,positive,medium,short,"The launch of the AI Store Builder enhances Shopify's product offering, differentiating its platform",101.51,107.22,111.41,106.4,5.6251,9.7527,4.8173,True,True,True
|
1464,SHOP,Shopify,2025-05-23,positive,medium,short,"The launch of the AI Store Builder enhances Shopify's product offering, differentiating its platform",101.51,107.22,111.41,106.4,5.6251,9.7527,4.8173,True,True,True
|
||||||
1189,WBD,Warner Bros Discovery,2025-05-14,positive,medium,medium,"By refocusing on HBO's premium brand and distinct adult-oriented content, WBD aims to carve out a cl",9.21,8.95,10.02,10.51,-2.823,8.7948,14.1151,False,True,True
|
1189,WBD,Warner Bros Discovery,2025-05-14,positive,medium,medium,"By refocusing on HBO's premium brand and distinct adult-oriented content, WBD aims to carve out a cl",9.21,8.95,10.02,10.51,-2.823,8.7948,14.1151,False,True,True
|
||||||
1282,META,Meta,2025-05-08,negative,medium,short,Public exposure of widespread scam activity leveraging Meta's platforms may weaken user trust and in,596.1501,641.8775,634.5903,682.4907,7.6704,6.4481,14.483,False,False,False
|
1282,META,Meta,2025-05-08,negative,medium,short,Public exposure of widespread scam activity leveraging Meta's platforms may weaken user trust and in,596.1502,641.8775,634.5902,682.4907,7.6704,6.448,14.483,False,False,False
|
||||||
1283,META,Meta,2025-05-08,negative,low,short,"While the takedown demonstrates proactive moderation, the underlying prevalence of sophisticated sca",596.1501,641.8775,634.5903,682.4907,7.6704,6.4481,14.483,False,False,False
|
1283,META,Meta,2025-05-08,negative,low,short,"While the takedown demonstrates proactive moderation, the underlying prevalence of sophisticated sca",596.1502,641.8775,634.5902,682.4907,7.6704,6.448,14.483,False,False,False
|
||||||
1473,V,Visa,2025-05-08,positive,high,long,"By enabling AI agents to use its payment network, Visa positions itself as a foundational player in ",348.6606,360.2059,355.9009,364.6501,3.3113,2.0766,4.586,True,True,True
|
1473,V,Visa,2025-05-08,positive,high,long,"By enabling AI agents to use its payment network, Visa positions itself as a foundational player in ",348.6606,360.2059,355.9009,364.6501,3.3113,2.0766,4.586,True,True,True
|
||||||
1474,V,Visa,2025-05-08,positive,medium,medium,Opening its network to AI developers and expanding in key markets like Europe by 2025 could increase,348.6606,360.2059,355.9009,364.6501,3.3113,2.0766,4.586,True,True,True
|
1474,V,Visa,2025-05-08,positive,medium,medium,Opening its network to AI developers and expanding in key markets like Europe by 2025 could increase,348.6606,360.2059,355.9009,364.6501,3.3113,2.0766,4.586,True,True,True
|
||||||
1475,V,Visa,2025-05-08,positive,low,short,"The announcement of a forward-looking strategic initiative may generate investor interest, though im",348.6606,360.2059,355.9009,364.6501,3.3113,2.0766,4.586,True,True,True
|
1475,V,Visa,2025-05-08,positive,low,short,"The announcement of a forward-looking strategic initiative may generate investor interest, though im",348.6606,360.2059,355.9009,364.6501,3.3113,2.0766,4.586,True,True,True
|
||||||
@@ -235,22 +235,22 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1022,SPOT,Spotify,2025-05-04,positive,low,long,"While Backstage is gaining traction in the internal developer portal space, its market share impact ",637.65,648.25,656.3,665.14,1.6624,2.9248,4.3111,True,True,True
|
1022,SPOT,Spotify,2025-05-04,positive,low,long,"While Backstage is gaining traction in the internal developer portal space, its market share impact ",637.65,648.25,656.3,665.14,1.6624,2.9248,4.3111,True,True,True
|
||||||
1067,AMZN,Amazon,2025-05-02,positive,medium,short,"Amazon is positioned to gain market share during tariff-related uncertainty, as it did during the pa",189.98,193.06,205.59,205.01,1.6212,8.2167,7.9114,True,True,True
|
1067,AMZN,Amazon,2025-05-02,positive,medium,short,"Amazon is positioned to gain market share during tariff-related uncertainty, as it did during the pa",189.98,193.06,205.59,205.01,1.6212,8.2167,7.9114,True,True,True
|
||||||
1068,AMZN,Amazon,2025-05-02,positive,medium,short,"Diverse seller base and proactive inventory management reduce the risk of price increases, strengthe",189.98,193.06,205.59,205.01,1.6212,8.2167,7.9114,True,True,True
|
1068,AMZN,Amazon,2025-05-02,positive,medium,short,"Diverse seller base and proactive inventory management reduce the risk of price increases, strengthe",189.98,193.06,205.59,205.01,1.6212,8.2167,7.9114,True,True,True
|
||||||
1375,META,Meta,2025-05-02,positive,medium,medium,"Launching a premium AI service positions Meta to compete more effectively with OpenAI, Google, and M",595.1632,590.6473,638.3485,645.4763,-0.7588,7.256,8.4537,False,True,True
|
1375,META,Meta,2025-05-02,positive,medium,medium,"Launching a premium AI service positions Meta to compete more effectively with OpenAI, Google, and M",595.1632,590.6473,638.3484,645.4763,-0.7588,7.256,8.4537,False,True,True
|
||||||
1376,META,Meta,2025-05-02,positive,medium,medium,With nearly a billion users already on Meta AI and a potential premium tier offering enhanced featur,595.1632,590.6473,638.3485,645.4763,-0.7588,7.256,8.4537,False,True,True
|
1376,META,Meta,2025-05-02,positive,medium,medium,With nearly a billion users already on Meta AI and a potential premium tier offering enhanced featur,595.1632,590.6473,638.3484,645.4763,-0.7588,7.256,8.4537,False,True,True
|
||||||
1037,META,Meta,2025-04-29,positive,medium,short,Launching a standalone AI app enhances Meta's visibility and positioning in the competitive AI assis,552.7156,585.4835,653.9897,640.3223,5.9285,18.323,15.8502,True,True,True
|
1037,META,Meta,2025-04-29,positive,medium,short,Launching a standalone AI app enhances Meta's visibility and positioning in the competitive AI assis,552.7156,585.4834,653.9897,640.3223,5.9285,18.323,15.8502,True,True,True
|
||||||
1038,META,Meta,2025-04-29,positive,low,medium,"The app leverages Meta's extensive user data for personalization, which could attract users over tim",552.7156,585.4835,653.9897,640.3223,5.9285,18.323,15.8502,True,True,True
|
1038,META,Meta,2025-04-29,positive,low,medium,"The app leverages Meta's extensive user data for personalization, which could attract users over tim",552.7156,585.4834,653.9897,640.3223,5.9285,18.323,15.8502,True,True,True
|
||||||
1039,PL,Planet Labs,2025-04-29,negative,medium,medium,"The $20M funding enables Near Space Labs to scale its stratospheric imaging operations, increasing c",3.43,3.5,3.78,3.97,2.0408,10.2041,15.7434,False,False,False
|
1039,PL,Planet Labs,2025-04-29,negative,medium,medium,"The $20M funding enables Near Space Labs to scale its stratospheric imaging operations, increasing c",3.43,3.5,3.78,3.97,2.0408,10.2041,15.7434,False,False,False
|
||||||
1291,META,Meta,2025-04-26,negative,medium,short,"High-profile public protest by grieving parents increases reputational damage and litigation risk, w",548.0302,595.1632,590.6473,625.1098,8.6004,7.7764,14.0648,False,False,False
|
1291,META,Meta,2025-04-26,negative,medium,short,"High-profile public protest by grieving parents increases reputational damage and litigation risk, w",548.0302,595.1632,590.6473,625.1097,8.6004,7.7764,14.0648,False,False,False
|
||||||
1292,META,Meta,2025-04-26,negative,medium,medium,Growing civil society and regulatory pressure could force Meta to implement stricter safety measures,548.0302,595.1632,590.6473,625.1098,8.6004,7.7764,14.0648,False,False,False
|
1292,META,Meta,2025-04-26,negative,medium,medium,Growing civil society and regulatory pressure could force Meta to implement stricter safety measures,548.0302,595.1632,590.6473,625.1097,8.6004,7.7764,14.0648,False,False,False
|
||||||
1293,META,Meta,2025-04-26,positive,high,short,"The protest is a direct indicator of escalating civil society pressure, which is likely to attract f",548.0302,595.1632,590.6473,625.1098,8.6004,7.7764,14.0648,True,True,True
|
1293,META,Meta,2025-04-26,positive,high,short,"The protest is a direct indicator of escalating civil society pressure, which is likely to attract f",548.0302,595.1632,590.6473,625.1097,8.6004,7.7764,14.0648,True,True,True
|
||||||
1362,GOOGL,Alphabet,2025-04-24,positive,medium,short,Alphabet exceeded earnings expectations and stock jumped over 7% in after-hours trading despite macr,158.7296,160.7426,153.7469,170.2796,1.2682,-3.1391,7.2765,True,False,True
|
1362,GOOGL,Alphabet,2025-04-24,positive,medium,short,Alphabet exceeded earnings expectations and stock jumped over 7% in after-hours trading despite macr,158.7296,160.7426,153.7469,170.2796,1.2682,-3.1391,7.2765,True,False,True
|
||||||
1047,NFLX,Netflix,2025-04-23,positive,medium,long,"Articulation of a $1 trillion market cap goal by co-CEO suggests strong long-term confidence, which ",104.959,113.172,115.541,119.463,7.825,10.082,13.8187,True,True,True
|
1047,NFLX,Netflix,2025-04-23,positive,medium,long,"Articulation of a $1 trillion market cap goal by co-CEO suggests strong long-term confidence, which ",104.959,113.172,115.541,119.463,7.825,10.082,13.8187,True,True,True
|
||||||
1048,NFLX,Netflix,2025-04-23,positive,medium,long,Ambitious growth targets and diversification into new ventures like theater and retail may strengthe,104.959,113.172,115.541,119.463,7.825,10.082,13.8187,True,True,True
|
1048,NFLX,Netflix,2025-04-23,positive,medium,long,Ambitious growth targets and diversification into new ventures like theater and retail may strengthe,104.959,113.172,115.541,119.463,7.825,10.082,13.8187,True,True,True
|
||||||
1052,META,Meta,2025-04-23,positive,medium,medium,"Expanding ads globally increases Threads' competitiveness against X, leveraging high user engagement",518.652,547.2925,594.9539,633.5235,5.5221,14.7116,22.1481,True,True,True
|
1052,META,Meta,2025-04-23,positive,medium,medium,"Expanding ads globally increases Threads' competitiveness against X, leveraging high user engagement",518.652,547.2925,594.9539,633.5236,5.5221,14.7116,22.1481,True,True,True
|
||||||
1053,META,Meta,2025-04-23,positive,high,short,Opening ad inventory to global advertisers directly expands monetization opportunities across a user,518.652,547.2925,594.9539,633.5235,5.5221,14.7116,22.1481,True,True,True
|
1053,META,Meta,2025-04-23,positive,high,short,Opening ad inventory to global advertisers directly expands monetization opportunities across a user,518.652,547.2925,594.9539,633.5236,5.5221,14.7116,22.1481,True,True,True
|
||||||
1054,META,Meta,2025-04-23,positive,medium,medium,"Meta positions Threads as more advertiser-friendly than X, using Instagram's network effects to stre",518.652,547.2925,594.9539,633.5235,5.5221,14.7116,22.1481,True,True,True
|
1054,META,Meta,2025-04-23,positive,medium,medium,"Meta positions Threads as more advertiser-friendly than X, using Instagram's network effects to stre",518.652,547.2925,594.9539,633.5236,5.5221,14.7116,22.1481,True,True,True
|
||||||
1045,META,Meta,2025-04-20,negative,medium,long,Ongoing struggles with Facebook's cultural relevance may weaken Meta's competitive position over tim,483.1527,545.568,595.1632,638.3485,12.9183,23.1833,32.1215,False,False,False
|
1045,META,Meta,2025-04-20,negative,medium,long,Ongoing struggles with Facebook's cultural relevance may weaken Meta's competitive position over tim,483.1527,545.568,595.1632,638.3484,12.9183,23.1833,32.1215,False,False,False
|
||||||
1046,META,Meta,2025-04-20,negative,medium,long,Declining cultural relevance of Facebook could lead to erosion in user engagement and time spent on ,483.1527,545.568,595.1632,638.3485,12.9183,23.1833,32.1215,False,False,False
|
1046,META,Meta,2025-04-20,negative,medium,long,Declining cultural relevance of Facebook could lead to erosion in user engagement and time spent on ,483.1527,545.568,595.1632,638.3484,12.9183,23.1833,32.1215,False,False,False
|
||||||
1040,META,Meta,2025-04-17,negative,medium,short,TikTok's continued dominance in short-form video has already caused a dramatic slowdown in Meta's gr,499.9204,531.4919,570.4304,641.8775,6.3153,14.1043,28.3959,False,False,False
|
1040,META,Meta,2025-04-17,negative,medium,short,TikTok's continued dominance in short-form video has already caused a dramatic slowdown in Meta's gr,499.9204,531.4919,570.4304,641.8775,6.3153,14.1043,28.3959,False,False,False
|
||||||
1041,META,Meta,2025-04-17,negative,medium,short,Zuckerberg's admission that TikTok directly slowed Meta's growth implies a loss of user engagement a,499.9204,531.4919,570.4304,641.8775,6.3153,14.1043,28.3959,False,False,False
|
1041,META,Meta,2025-04-17,negative,medium,short,Zuckerberg's admission that TikTok directly slowed Meta's growth implies a loss of user engagement a,499.9204,531.4919,570.4304,641.8775,6.3153,14.1043,28.3959,False,False,False
|
||||||
1058,RIVN,Rivian,2025-04-16,positive,medium,medium,"HelloFresh’s adoption of 70 Rivian vans marks the first major commercial customer beyond Amazon, sig",11.49,11.8,13.66,14.82,2.698,18.886,28.9817,True,True,True
|
1058,RIVN,Rivian,2025-04-16,positive,medium,medium,"HelloFresh’s adoption of 70 Rivian vans marks the first major commercial customer beyond Amazon, sig",11.49,11.8,13.66,14.82,2.698,18.886,28.9817,True,True,True
|
||||||
@@ -261,61 +261,61 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1207,NVS,Novartis,2025-04-11,positive,medium,short,The announcement of a major investment in U.S. production facilities led to a 1.6% increase in Novar,104.3441,107.2749,108.8276,105.4892,2.8088,4.2969,1.0975,True,True,True
|
1207,NVS,Novartis,2025-04-11,positive,medium,short,The announcement of a major investment in U.S. production facilities led to a 1.6% increase in Novar,104.3441,107.2749,108.8276,105.4892,2.8088,4.2969,1.0975,True,True,True
|
||||||
1372,HF,Hugging Face,2025-04-11,positive,medium,medium,Increased public legal conflict between OpenAI and Elon Musk may divert focus and resources from Ope,19.9983,20.0924,19.9884,19.9785,0.4707,-0.0495,-0.0991,True,False,False
|
1372,HF,Hugging Face,2025-04-11,positive,medium,medium,Increased public legal conflict between OpenAI and Elon Musk may divert focus and resources from Ope,19.9983,20.0924,19.9884,19.9785,0.4707,-0.0495,-0.0991,True,False,False
|
||||||
1108,SHOP,Shopify,2025-04-10,negative,medium,short,"Growing traction of TikTok Shop in social commerce, especially among Gen Z and legacy brands, may er",84.63,83.65,95.12,94.0,-1.158,12.3951,11.0717,True,False,False
|
1108,SHOP,Shopify,2025-04-10,negative,medium,short,"Growing traction of TikTok Shop in social commerce, especially among Gen Z and legacy brands, may er",84.63,83.65,95.12,94.0,-1.158,12.3951,11.0717,True,False,False
|
||||||
1478,META,Meta,2025-04-10,negative,medium,short,Whistleblower testimony before Congress alleging collusion with the Chinese government on censorship,544.591,499.9204,531.4919,596.1501,-8.2026,-2.4053,9.4675,True,True,False
|
1478,META,Meta,2025-04-10,negative,medium,short,Whistleblower testimony before Congress alleging collusion with the Chinese government on censorship,544.591,499.9204,531.4919,596.1502,-8.2026,-2.4053,9.4675,True,True,False
|
||||||
1479,META,Meta,2025-04-10,negative,medium,medium,"Allegations of aiding Chinese AI development via Llama could damage partnerships, restrict future AI",544.591,499.9204,531.4919,596.1501,-8.2026,-2.4053,9.4675,True,True,False
|
1479,META,Meta,2025-04-10,negative,medium,medium,"Allegations of aiding Chinese AI development via Llama could damage partnerships, restrict future AI",544.591,499.9204,531.4919,596.1502,-8.2026,-2.4053,9.4675,True,True,False
|
||||||
1480,META,Meta,2025-04-10,positive,high,short,Senate testimony alleging Meta’s cooperation with the Chinese Communist Party on censorship and data,544.591,499.9204,531.4919,596.1501,-8.2026,-2.4053,9.4675,False,False,True
|
1480,META,Meta,2025-04-10,positive,high,short,Senate testimony alleging Meta’s cooperation with the Chinese Communist Party on censorship and data,544.591,499.9204,531.4919,596.1502,-8.2026,-2.4053,9.4675,False,False,True
|
||||||
1373,META,Meta,2025-04-10,negative,medium,short,"Allegations of collaboration with the Chinese Communist Party on censorship tools, aired during a U.",544.591,499.9204,531.4919,596.1501,-8.2026,-2.4053,9.4675,True,True,False
|
1373,META,Meta,2025-04-10,negative,medium,short,"Allegations of collaboration with the Chinese Communist Party on censorship tools, aired during a U.",544.591,499.9204,531.4919,596.1502,-8.2026,-2.4053,9.4675,True,True,False
|
||||||
1374,META,Meta,2025-04-10,negative,medium,medium,Increased reputational damage and regulatory scrutiny from U.S. lawmakers may weaken Meta's public t,544.591,499.9204,531.4919,596.1501,-8.2026,-2.4053,9.4675,True,True,False
|
1374,META,Meta,2025-04-10,negative,medium,medium,Increased reputational damage and regulatory scrutiny from U.S. lawmakers may weaken Meta's public t,544.591,499.9204,531.4919,596.1502,-8.2026,-2.4053,9.4675,True,True,False
|
||||||
1175,ORCL,Oracle,2025-03-31,negative,medium,short,"Public criticism over handling of security incidents, particularly involving sensitive patient data,",137.9258,125.4463,133.3026,138.7479,-9.048,-3.3519,0.5961,True,True,False
|
1175,ORCL,Oracle,2025-03-31,negative,medium,short,"Public criticism over handling of security incidents, particularly involving sensitive patient data,",137.9258,125.4463,133.3026,138.748,-9.048,-3.3519,0.5961,True,True,False
|
||||||
1176,ORCL,Oracle,2025-03-31,negative,medium,medium,"Security concerns, especially in healthcare and legacy infrastructure, may weaken trust in Oracle's ",137.9258,125.4463,133.3026,138.7479,-9.048,-3.3519,0.5961,True,True,False
|
1176,ORCL,Oracle,2025-03-31,negative,medium,medium,"Security concerns, especially in healthcare and legacy infrastructure, may weaken trust in Oracle's ",137.9258,125.4463,133.3026,138.748,-9.048,-3.3519,0.5961,True,True,False
|
||||||
1481,TSLA,Tesla,2025-03-27,positive,medium,short,Tesla's 100% US production insulates it from the 25% import tariffs that will burden competitors lik,273.13,267.28,252.4,259.51,-2.1418,-7.5898,-4.9866,False,False,False
|
1481,TSLA,Tesla,2025-03-27,positive,medium,short,Tesla's 100% US production insulates it from the 25% import tariffs that will burden competitors lik,273.13,267.28,252.4,259.51,-2.1418,-7.5898,-4.9866,False,False,False
|
||||||
1482,TSLA,Tesla,2025-03-27,positive,low,medium,"With competitors facing higher costs due to tariffs, Tesla may gain slight market share in the US, t",273.13,267.28,252.4,259.51,-2.1418,-7.5898,-4.9866,False,False,False
|
1482,TSLA,Tesla,2025-03-27,positive,low,medium,"With competitors facing higher costs due to tariffs, Tesla may gain slight market share in the US, t",273.13,267.28,252.4,259.51,-2.1418,-7.5898,-4.9866,False,False,False
|
||||||
1554,RIVN,Rivian,2025-03-26,positive,medium,medium,"By expanding into micromobility through Also, Rivian strengthens its technological brand and diversi",12.1,12.49,11.77,11.8,3.2231,-2.7273,-2.4793,True,False,False
|
1554,RIVN,Rivian,2025-03-26,positive,medium,medium,"By expanding into micromobility through Also, Rivian strengthens its technological brand and diversi",12.1,12.49,11.77,11.8,3.2231,-2.7273,-2.4793,True,False,False
|
||||||
1494,TSM,TSMC,2025-03-25,negative,medium,medium,The substantial capital expenditure with expected margin pressure from U.S. operations may weigh on ,178.6869,166.5769,139.6405,149.5478,-6.7772,-21.8518,-16.3074,True,True,True
|
1494,TSM,TSMC,2025-03-25,negative,medium,medium,The substantial capital expenditure with expected margin pressure from U.S. operations may weigh on ,178.6869,166.5769,139.6405,149.5478,-6.7772,-21.8519,-16.3073,True,True,True
|
||||||
1181,META,Meta,2025-03-24,negative,medium,short,"Meta's failed acquisition of FuriosaAI, an AI chip startup developing competitive chips for reasonin",616.9253,574.5675,514.6444,483.1527,-6.866,-16.5791,-21.6838,True,True,True
|
1181,META,Meta,2025-03-24,negative,medium,short,"Meta's failed acquisition of FuriosaAI, an AI chip startup developing competitive chips for reasonin",616.9254,574.5674,514.6444,483.1526,-6.866,-16.5791,-21.6838,True,True,True
|
||||||
1182,META,Meta,2025-03-24,negative,low,medium,"While FuriosaAI remains independent and may expand its partnerships (e.g., with LG AI Research), Met",616.9253,574.5675,514.6444,483.1527,-6.866,-16.5791,-21.6838,True,True,True
|
1182,META,Meta,2025-03-24,negative,low,medium,"While FuriosaAI remains independent and may expand its partnerships (e.g., with LG AI Research), Met",616.9254,574.5674,514.6444,483.1526,-6.866,-16.5791,-21.6838,True,True,True
|
||||||
1183,FTNT,Fortinet,2025-03-17,negative,medium,short,The public disclosure of active exploitation of Fortinet vulnerabilities by a LockBit-linked group m,96.67,99.79,96.26,96.85,3.2275,-0.4241,0.1862,False,True,False
|
1183,FTNT,Fortinet,2025-03-17,negative,medium,short,The public disclosure of active exploitation of Fortinet vulnerabilities by a LockBit-linked group m,96.67,99.79,96.26,96.85,3.2275,-0.4241,0.1862,False,True,False
|
||||||
1184,FTNT,Fortinet,2025-03-17,negative,medium,medium,Repeated security breaches linked to Fortinet products could weaken its market standing versus compe,96.67,99.79,96.26,96.85,3.2275,-0.4241,0.1862,False,True,False
|
1184,FTNT,Fortinet,2025-03-17,negative,medium,medium,Repeated security breaches linked to Fortinet products could weaken its market standing versus compe,96.67,99.79,96.26,96.85,3.2275,-0.4241,0.1862,False,True,False
|
||||||
1359,RIVN,Rivian,2025-03-05,positive,high,long,Providing core software and architecture to a major automaker like Volkswagen enhances Rivian’s stra,11.42,11.06,11.36,12.49,-3.1524,-0.5254,9.3695,False,False,True
|
1359,RIVN,Rivian,2025-03-05,positive,high,long,Providing core software and architecture to a major automaker like Volkswagen enhances Rivian’s stra,11.42,11.06,11.36,12.49,-3.1524,-0.5254,9.3695,False,False,True
|
||||||
1360,RIVN,Rivian,2025-03-05,positive,medium,short,The influx of capital and validation of Rivian’s technology by Volkswagen may boost investor confide,11.42,11.06,11.36,12.49,-3.1524,-0.5254,9.3695,False,False,True
|
1360,RIVN,Rivian,2025-03-05,positive,medium,short,The influx of capital and validation of Rivian’s technology by Volkswagen may boost investor confide,11.42,11.06,11.36,12.49,-3.1524,-0.5254,9.3695,False,False,True
|
||||||
1354,META,Meta,2025-03-05,positive,medium,medium,Expanding facial recognition tools in regulated markets like the UK and EU strengthens Meta's positi,653.8467,617.084,582.2435,582.1139,-5.6225,-10.9511,-10.9709,False,False,False
|
1354,META,Meta,2025-03-05,positive,medium,medium,Expanding facial recognition tools in regulated markets like the UK and EU strengthens Meta's positi,653.8466,617.0841,582.2435,582.114,-5.6225,-10.9511,-10.9709,False,False,False
|
||||||
1355,META,Meta,2025-03-05,positive,low,medium,"Optional anti-fraud and verification tools may improve user trust and retention, particularly among ",653.8467,617.084,582.2435,582.1139,-5.6225,-10.9511,-10.9709,False,False,False
|
1355,META,Meta,2025-03-05,positive,low,medium,"Optional anti-fraud and verification tools may improve user trust and retention, particularly among ",653.8466,617.0841,582.2435,582.114,-5.6225,-10.9511,-10.9709,False,False,False
|
||||||
1560,BLK,BlackRock,2025-03-02,positive,medium,short,"Record inflows suggest investor confidence remains strong despite the ESG reversal, potentially supp",941.8132,927.7991,909.947,927.5837,-1.488,-3.3835,-1.5109,False,False,False
|
1560,BLK,BlackRock,2025-03-02,positive,medium,short,"Record inflows suggest investor confidence remains strong despite the ESG reversal, potentially supp",941.8132,927.7991,909.947,927.5836,-1.488,-3.3835,-1.5109,False,False,False
|
||||||
1562,BLK,BlackRock,2025-03-02,positive,medium,short,"By aligning with conservative political forces and avoiding regulatory scrutiny, BlackRock may gain ",941.8132,927.7991,909.947,927.5837,-1.488,-3.3835,-1.5109,False,False,False
|
1562,BLK,BlackRock,2025-03-02,positive,medium,short,"By aligning with conservative political forces and avoiding regulatory scrutiny, BlackRock may gain ",941.8132,927.7991,909.947,927.5836,-1.488,-3.3835,-1.5109,False,False,False
|
||||||
1350,META,Meta,2025-02-27,positive,low,short,"Terminating leakers may strengthen internal discipline and protect strategic information, slightly i",655.6096,625.4207,588.2797,600.706,-4.6047,-10.2698,-8.3744,False,False,False
|
1350,META,Meta,2025-02-27,positive,low,short,"Terminating leakers may strengthen internal discipline and protect strategic information, slightly i",655.6095,625.4207,588.2797,600.706,-4.6047,-10.2698,-8.3744,False,False,False
|
||||||
1351,META,Meta,2025-02-27,negative,medium,short,Public disclosure of internal leaks and subsequent firings may amplify perceptions of internal disse,655.6096,625.4207,588.2797,600.706,-4.6047,-10.2698,-8.3744,True,True,True
|
1351,META,Meta,2025-02-27,negative,medium,short,Public disclosure of internal leaks and subsequent firings may amplify perceptions of internal disse,655.6095,625.4207,588.2797,600.706,-4.6047,-10.2698,-8.3744,True,True,True
|
||||||
1142,SAP,SAP,2025-02-27,negative,low,short,"The stock is continuing a downward trend, underperforming the Dax, and trading volume has decreased,",272.1395,276.837,252.9034,265.7473,1.7261,-7.0685,-2.3489,False,True,True
|
1142,SAP,SAP,2025-02-27,negative,low,short,"The stock is continuing a downward trend, underperforming the Dax, and trading volume has decreased,",272.1395,276.837,252.9034,265.7473,1.7261,-7.0685,-2.3489,False,True,True
|
||||||
1114,PLTR,Palantir,2025-02-08,positive,high,short,"Retail investor enthusiasm, dollar-cost averaging, and a significant rise in stock price in 2024 and",116.65,119.16,101.35,84.91,2.1517,-13.1162,-27.2096,True,False,False
|
1114,PLTR,Palantir,2025-02-08,positive,high,short,"Retail investor enthusiasm, dollar-cost averaging, and a significant rise in stock price in 2024 and",116.65,119.16,101.35,84.91,2.1517,-13.1162,-27.2096,True,False,False
|
||||||
1065,AMD,AMD,2025-02-04,positive,medium,medium,"Powering 100 million game consoles indicates strong market penetration in the gaming segment, likely",119.5,111.1,114.28,100.75,-7.0293,-4.3682,-15.6904,False,False,False
|
1065,AMD,AMD,2025-02-04,positive,medium,medium,"Powering 100 million game consoles indicates strong market penetration in the gaming segment, likely",119.5,111.1,114.28,100.75,-7.0293,-4.3682,-15.6904,False,False,False
|
||||||
1066,AMD,AMD,2025-02-04,positive,medium,short,Success in securing design wins across major console platforms strengthens AMD's position against co,119.5,111.1,114.28,100.75,-7.0293,-4.3682,-15.6904,False,False,False
|
1066,AMD,AMD,2025-02-04,positive,medium,short,Success in securing design wins across major console platforms strengthens AMD's position against co,119.5,111.1,114.28,100.75,-7.0293,-4.3682,-15.6904,False,False,False
|
||||||
1125,NVDA,NVIDIA,2025-01-30,negative,medium,short,"Technical indicators suggest further downside toward $110, with momentum shifting negative in the in",124.6092,128.6379,135.2457,120.1106,3.2331,8.5359,-3.6101,False,False,True
|
1125,NVDA,NVIDIA,2025-01-30,negative,medium,short,"Technical indicators suggest further downside toward $110, with momentum shifting negative in the in",124.6092,128.6378,135.2457,120.1106,3.233,8.5359,-3.6101,False,False,True
|
||||||
1244,NVDA,NVIDIA,2025-01-30,positive,medium,short,"The stock rebounded nearly 9% following a sharp decline, indicating strong investor resilience and c",124.6092,128.6379,135.2457,120.1106,3.2331,8.5359,-3.6101,True,True,False
|
1244,NVDA,NVIDIA,2025-01-30,positive,medium,short,"The stock rebounded nearly 9% following a sharp decline, indicating strong investor resilience and c",124.6092,128.6378,135.2457,120.1106,3.233,8.5359,-3.6101,True,True,False
|
||||||
1243,MS,Morgan Stanley,2025-01-24,positive,medium,short,"Morgan Stanley is highlighted as one of the top stocks to buy for strong Q4 earnings, with positive ",133.331,134.8122,136.3217,128.2484,1.1109,2.2431,-3.812,True,True,False
|
1243,MS,Morgan Stanley,2025-01-24,positive,medium,short,"Morgan Stanley is highlighted as one of the top stocks to buy for strong Q4 earnings, with positive ",132.6183,134.0916,135.593,127.5628,1.1109,2.2431,-3.812,True,True,False
|
||||||
1248,STX,Seagate,2025-01-23,positive,medium,short,Solid 2Q25 performance driven by cloud sector recovery and increasing AI storage demand support upwa,106.1709,96.2413,94.5374,100.5108,-9.3525,-10.9574,-5.3311,False,False,False
|
1248,STX,Seagate,2025-01-23,positive,medium,short,Solid 2Q25 performance driven by cloud sector recovery and increasing AI storage demand support upwa,106.1709,96.2413,94.5374,100.5108,-9.3525,-10.9574,-5.3311,False,False,False
|
||||||
1130,META,Meta,2025-01-21,positive,low,short,"Enhanced cross-platform integration improves user engagement and data sharing across Meta's apps, sl",613.9966,671.6353,701.3759,713.5073,9.3875,14.2312,16.207,True,True,True
|
1130,META,Meta,2025-01-21,positive,low,short,"Enhanced cross-platform integration improves user engagement and data sharing across Meta's apps, sl",613.9966,671.6353,701.3759,713.5073,9.3875,14.2312,16.2071,True,True,True
|
||||||
1466,TSM,TSMC,2025-01-16,positive,high,short,Strong profit growth driven by high AI chip demand supports a positive short-term stock price moveme,211.3388,221.0109,204.8055,198.5871,4.5766,-3.0914,-6.0338,True,False,False
|
1466,TSM,TSMC,2025-01-16,positive,high,short,Strong profit growth driven by high AI chip demand supports a positive short-term stock price moveme,211.3388,221.0109,204.8055,198.5871,4.5766,-3.0914,-6.0338,True,False,False
|
||||||
1467,TSM,TSMC,2025-01-16,positive,medium,medium,Continued leadership in advanced semiconductor manufacturing and strong customer relationships reinf,211.3388,221.0109,204.8055,198.5871,4.5766,-3.0914,-6.0338,True,False,False
|
1467,TSM,TSMC,2025-01-16,positive,medium,medium,Continued leadership in advanced semiconductor manufacturing and strong customer relationships reinf,211.3388,221.0109,204.8055,198.5871,4.5766,-3.0914,-6.0338,True,False,False
|
||||||
1468,TSM,TSMC,2025-01-16,negative,medium,medium,Geopolitical uncertainties and export restrictions may negatively affect future business operations ,211.3388,221.0109,204.8055,198.5871,4.5766,-3.0914,-6.0338,False,True,True
|
1468,TSM,TSMC,2025-01-16,negative,medium,medium,Geopolitical uncertainties and export restrictions may negatively affect future business operations ,211.3388,221.0109,204.8055,198.5871,4.5766,-3.0914,-6.0338,False,True,True
|
||||||
1312,META,Meta,2025-01-10,negative,high,short,"Widespread condemnation from 71 fact-checking organizations, including a public open letter, signals",613.3989,610.3214,644.9025,711.6646,-0.5017,5.1359,16.0199,True,False,False
|
1312,META,Meta,2025-01-10,negative,high,short,"Widespread condemnation from 71 fact-checking organizations, including a public open letter, signals",613.3988,610.3212,644.9025,711.6646,-0.5017,5.1359,16.0199,True,False,False
|
||||||
1313,META,Meta,2025-01-10,positive,medium,medium,Strong pushback from civil society groups may prompt increased scrutiny from regulators concerned wi,613.3989,610.3214,644.9025,711.6646,-0.5017,5.1359,16.0199,False,True,True
|
1313,META,Meta,2025-01-10,positive,medium,medium,Strong pushback from civil society groups may prompt increased scrutiny from regulators concerned wi,613.3988,610.3212,644.9025,711.6646,-0.5017,5.1359,16.0199,False,True,True
|
||||||
1159,NVDA,NVIDIA,2025-01-07,positive,medium,short,Analyst reaffirmation of Nvidia as a top pick following a major executive keynote typically boosts i,140.0941,131.7168,140.7839,118.6111,-5.9797,0.4924,-15.3347,False,True,False
|
1159,NVDA,NVIDIA,2025-01-07,positive,medium,short,Analyst reaffirmation of Nvidia as a top pick following a major executive keynote typically boosts i,140.0941,131.7168,140.7839,118.6111,-5.9797,0.4924,-15.3347,False,True,False
|
||||||
1160,NVDA,NVIDIA,2025-01-07,positive,low,medium,Positive analyst sentiment following a strategic keynote may reinforce market perception of Nvidia's,140.0941,131.7168,140.7839,118.6111,-5.9797,0.4924,-15.3347,False,True,False
|
1160,NVDA,NVIDIA,2025-01-07,positive,low,medium,Positive analyst sentiment following a strategic keynote may reinforce market perception of Nvidia's,140.0941,131.7168,140.7839,118.6111,-5.9797,0.4924,-15.3347,False,True,False
|
||||||
1157,RIVN,Rivian,2025-01-03,positive,high,short,"Rivian's stock surged 24.5%, its largest daily increase since going public, after meeting revised pr",16.49,13.85,14.21,12.56,-16.0097,-13.8266,-23.8326,False,False,False
|
1157,RIVN,Rivian,2025-01-03,positive,high,short,"Rivian's stock surged 24.5%, its largest daily increase since going public, after meeting revised pr",16.49,13.85,14.21,12.56,-16.0097,-13.8266,-23.8326,False,False,False
|
||||||
1158,RIVN,Rivian,2025-01-03,positive,medium,medium,Resolving production constraints and delivering above analyst expectations strengthens Rivian's posi,16.49,13.85,14.21,12.56,-16.0097,-13.8266,-23.8326,False,False,False
|
1158,RIVN,Rivian,2025-01-03,positive,medium,medium,Resolving production constraints and delivering above analyst expectations strengthens Rivian's posi,16.49,13.85,14.21,12.56,-16.0097,-13.8266,-23.8326,False,False,False
|
||||||
1162,AVGO,Broadcom,2024-12-31,positive,high,short,Broadcom's stock price rose 111% in 2024 due to strong AI-related demand and market outperformance r,229.2828,226.1181,222.2215,205.0728,-1.3803,-3.0797,-10.559,False,False,False
|
1162,AVGO,Broadcom,2024-12-31,positive,high,short,Broadcom's stock price rose 111% in 2024 due to strong AI-related demand and market outperformance r,229.2828,226.1181,222.2216,205.0728,-1.3803,-3.0797,-10.559,False,False,False
|
||||||
1163,AVGO,Broadcom,2024-12-31,positive,medium,medium,"Broadcom is gaining ground in the AI chip and networking space despite Nvidia's dominance, positioni",229.2828,226.1181,222.2215,205.0728,-1.3803,-3.0797,-10.559,False,False,False
|
1163,AVGO,Broadcom,2024-12-31,positive,medium,medium,"Broadcom is gaining ground in the AI chip and networking space despite Nvidia's dominance, positioni",229.2828,226.1181,222.2216,205.0728,-1.3803,-3.0797,-10.559,False,False,False
|
||||||
1161,AAPL,Apple,2024-12-26,positive,high,medium,"Apple's stock soars to a record high, and JPMorgan has a positive outlook for the company in 2025, s",257.6127,242.5252,235.5632,222.4449,-5.8567,-8.5592,-13.6515,False,False,False
|
1161,AAPL,Apple,2024-12-26,positive,high,medium,"Apple's stock soars to a record high, and JPMorgan has a positive outlook for the company in 2025, s",257.6127,242.5252,235.5632,222.4449,-5.8567,-8.5592,-13.6515,False,False,False
|
||||||
1317,LLY,Eli Lilly,2024-12-23,positive,medium,short,Eli Lilly's potential to extend a winning streak over the broad market for 6 years suggests continue,789.0677,766.8309,758.17,735.6262,-2.8181,-3.9157,-6.7727,False,False,False
|
1317,LLY,Eli Lilly,2024-12-23,positive,medium,short,Eli Lilly's potential to extend a winning streak over the broad market for 6 years suggests continue,789.0677,766.8309,758.1701,735.6262,-2.8181,-3.9157,-6.7727,False,False,False
|
||||||
1427,SMCI,Super Micro Computer,2024-12-16,negative,medium,short,"Removal from Nasdaq 100 triggers passive fund selling, combined with ongoing governance and complian",33.44,32.4,30.68,31.08,-3.11,-8.2536,-7.0574,True,True,True
|
1427,SMCI,Super Micro Computer,2024-12-16,negative,medium,short,"Removal from Nasdaq 100 triggers passive fund selling, combined with ongoing governance and complian",33.44,32.4,30.68,31.08,-3.11,-8.2536,-7.0574,True,True,True
|
||||||
1428,SMCI,Super Micro Computer,2024-12-16,negative,low,medium,Reduced market visibility and investor confidence may impair access to capital and strategic partner,33.44,32.4,30.68,31.08,-3.11,-8.2536,-7.0574,True,True,True
|
1428,SMCI,Super Micro Computer,2024-12-16,negative,low,medium,Reduced market visibility and investor confidence may impair access to capital and strategic partner,33.44,32.4,30.68,31.08,-3.11,-8.2536,-7.0574,True,True,True
|
||||||
1254,META,Meta,2024-12-14,positive,medium,medium,"By challenging OpenAI’s for-profit transition, Meta aims to constrain a key AI competitor’s flexibil",621.7454,582.9113,597.4131,613.3989,-6.246,-3.9135,-1.3424,False,False,False
|
1254,META,Meta,2024-12-14,positive,medium,medium,"By challenging OpenAI’s for-profit transition, Meta aims to constrain a key AI competitor’s flexibil",621.7454,582.9113,597.4131,613.3988,-6.246,-3.9136,-1.3424,False,False,False
|
||||||
1255,META,Meta,2024-12-14,positive,high,short,"Meta’s public call for regulatory intervention increases scrutiny on OpenAI, amplifying broader regu",621.7454,582.9113,597.4131,613.3989,-6.246,-3.9135,-1.3424,False,False,False
|
1255,META,Meta,2024-12-14,positive,high,short,"Meta’s public call for regulatory intervention increases scrutiny on OpenAI, amplifying broader regu",621.7454,582.9113,597.4131,613.3988,-6.246,-3.9136,-1.3424,False,False,False
|
||||||
1424,AAPL,Apple,2024-12-13,positive,medium,medium,"Morgan Stanley naming Apple a top pick for 2025 signals strong institutional confidence, supporting ",246.7819,253.1074,254.2014,235.5632,2.5632,3.0065,-4.546,True,True,False
|
1424,AAPL,Apple,2024-12-13,positive,medium,medium,"Morgan Stanley naming Apple a top pick for 2025 signals strong institutional confidence, supporting ",246.7819,253.1073,254.2014,235.5632,2.5632,3.0065,-4.546,True,True,False
|
||||||
1441,SPOT,Spotify,2024-12-11,positive,medium,short,Public discussion around faking Spotify Wrapped indicates high user interest and emotional investmen,476.91,448.65,457.98,479.73,-5.9256,-3.9693,0.5913,False,False,True
|
1441,SPOT,Spotify,2024-12-11,positive,medium,short,Public discussion around faking Spotify Wrapped indicates high user interest and emotional investmen,476.91,448.65,457.98,479.73,-5.9256,-3.9693,0.5913,False,False,True
|
||||||
1439,RIVN,Rivian,2024-12-09,positive,medium,medium,Rivian's superior charging experience and renewable energy partnerships enhance its differentiation ,14.45,15.34,13.75,15.715,6.1592,-4.8443,8.7543,True,False,True
|
1439,RIVN,Rivian,2024-12-09,positive,medium,medium,Rivian's superior charging experience and renewable energy partnerships enhance its differentiation ,14.45,15.34,13.75,15.715,6.1592,-4.8443,8.7543,True,False,True
|
||||||
1440,RIVN,Rivian,2024-12-09,positive,low,long,Expanded charging infrastructure accessible to all EV users increases Rivian's visibility and brand ,14.45,15.34,13.75,15.715,6.1592,-4.8443,8.7543,True,False,True
|
1440,RIVN,Rivian,2024-12-09,positive,low,long,Expanded charging infrastructure accessible to all EV users increases Rivian's visibility and brand ,14.45,15.34,13.75,15.715,6.1592,-4.8443,8.7543,True,False,True
|
||||||
1431,CVX,Chevron,2024-12-08,positive,medium,short,"Goldman Sachs reiterated a buy rating with a raised price target, citing strong shareholder returns ",148.6666,145.6285,135.1987,139.9309,-2.0435,-9.0591,-5.876,False,False,False
|
1431,CVX,Chevron,2024-12-08,positive,medium,short,"Goldman Sachs reiterated a buy rating with a raised price target, citing strong shareholder returns ",148.6666,145.6285,135.1987,139.931,-2.0435,-9.0591,-5.876,False,False,False
|
||||||
1432,CVX,Chevron,2024-12-08,positive,medium,medium,Recognition by top Wall Street analysts as a top dividend stock enhances Chevron's profile among ene,148.6666,145.6285,135.1987,139.9309,-2.0435,-9.0591,-5.876,False,False,False
|
1432,CVX,Chevron,2024-12-08,positive,medium,medium,Recognition by top Wall Street analysts as a top dividend stock enhances Chevron's profile among ene,148.6666,145.6285,135.1987,139.931,-2.0435,-9.0591,-5.876,False,False,False
|
||||||
1429,SHOP,Shopify,2024-12-06,positive,medium,short,"Analyst upgrade typically leads to improved market sentiment and short-term stock price momentum, es",118.37,114.63,108.95,109.25,-3.1596,-7.9581,-7.7047,False,False,False
|
1429,SHOP,Shopify,2024-12-06,positive,medium,short,"Analyst upgrade typically leads to improved market sentiment and short-term stock price momentum, es",118.37,114.63,108.95,109.25,-3.1596,-7.9581,-7.7047,False,False,False
|
||||||
1430,SHOP,Shopify,2024-12-06,positive,low,medium,Increased recognition of AI capabilities may enhance Shopify's positioning against competitors over ,118.37,114.63,108.95,109.25,-3.1596,-7.9581,-7.7047,False,False,False
|
1430,SHOP,Shopify,2024-12-06,positive,low,medium,Increased recognition of AI capabilities may enhance Shopify's positioning against competitors over ,118.37,114.63,108.95,109.25,-3.1596,-7.9581,-7.7047,False,False,False
|
||||||
1437,AMD,AMD,2024-12-04,positive,medium,short,"The article suggests a potential catch-up trade based on technical charts, indicating upward momentu",143.99,130.15,121.41,120.63,-9.6118,-15.6816,-16.2234,False,False,False
|
1437,AMD,AMD,2024-12-04,positive,medium,short,"The article suggests a potential catch-up trade based on technical charts, indicating upward momentu",143.99,130.15,121.41,120.63,-9.6118,-15.6816,-16.2234,False,False,False
|
||||||
@@ -324,15 +324,15 @@ id,ticker,name,event_date,direction,magnitude,timeframe,rationale,price_0,price_
|
|||||||
1433,NOW,ServiceNow,2024-12-01,positive,high,medium,"Analyst upgraded price target due to strong financials, AI tailwinds, and confidence in near- and me",209.686,224.868,224.22,216.292,7.2403,6.9313,3.1504,True,True,True
|
1433,NOW,ServiceNow,2024-12-01,positive,high,medium,"Analyst upgraded price target due to strong financials, AI tailwinds, and confidence in near- and me",209.686,224.868,224.22,216.292,7.2403,6.9313,3.1504,True,True,True
|
||||||
1434,NOW,ServiceNow,2024-12-01,positive,medium,long,"New Workflow Data Fabric product expected to power new workflows and AI agents, enhancing differenti",209.686,224.868,224.22,216.292,7.2403,6.9313,3.1504,True,True,True
|
1434,NOW,ServiceNow,2024-12-01,positive,medium,long,"New Workflow Data Fabric product expected to power new workflows and AI agents, enhancing differenti",209.686,224.868,224.22,216.292,7.2403,6.9313,3.1504,True,True,True
|
||||||
1435,NOW,ServiceNow,2024-12-01,positive,high,long,"Product innovation expected to double total addressable market to $500 billion, enabling greater mar",209.686,224.868,224.22,216.292,7.2403,6.9313,3.1504,True,True,True
|
1435,NOW,ServiceNow,2024-12-01,positive,high,long,"Product innovation expected to double total addressable market to $500 billion, enabling greater mar",209.686,224.868,224.22,216.292,7.2403,6.9313,3.1504,True,True,True
|
||||||
1442,META,Meta,2024-11-29,positive,high,long,"By building a private, globally spanning subsea cable, Meta gains greater control over data transmis",571.5638,620.7766,617.373,597.4131,8.6102,8.0147,4.5226,True,True,True
|
1442,META,Meta,2024-11-29,positive,high,long,"By building a private, globally spanning subsea cable, Meta gains greater control over data transmis",571.5638,620.7766,617.3729,597.4131,8.6102,8.0147,4.5225,True,True,True
|
||||||
1443,META,Meta,2024-11-29,negative,medium,short,"The $10 billion upfront investment may raise investor concerns about near-term profitability, especi",571.5638,620.7766,617.373,597.4131,8.6102,8.0147,4.5226,False,False,False
|
1443,META,Meta,2024-11-29,negative,medium,short,"The $10 billion upfront investment may raise investor concerns about near-term profitability, especi",571.5638,620.7766,617.3729,597.4131,8.6102,8.0147,4.5225,False,False,False
|
||||||
1444,META,Meta,2024-11-29,positive,medium,long,Exclusive control over high-capacity global data infrastructure will enable Meta to scale AI-driven ,571.5638,620.7766,617.373,597.4131,8.6102,8.0147,4.5226,True,True,True
|
1444,META,Meta,2024-11-29,positive,medium,long,Exclusive control over high-capacity global data infrastructure will enable Meta to scale AI-driven ,571.5638,620.7766,617.3729,597.4131,8.6102,8.0147,4.5225,True,True,True
|
||||||
1446,META,Meta,2024-11-21,positive,low,medium,"Proactive measures against scams may improve platform trustworthiness, slightly enhancing Meta's com",560.3878,571.5639,606.0079,593.1899,1.9943,8.1408,5.8535,True,True,True
|
1446,META,Meta,2024-11-21,positive,low,medium,"Proactive measures against scams may improve platform trustworthiness, slightly enhancing Meta's com",560.3878,571.564,606.0078,593.19,1.9944,8.1408,5.8535,True,True,True
|
||||||
1453,CMCSA,Comcast,2024-11-20,positive,medium,short,"A spin-off of the cable business could unlock shareholder value and streamline operations, potential",37.9328,37.5534,37.5446,33.4063,-1.0002,-1.0235,-11.933,False,False,False
|
1453,CMCSA,Comcast,2024-11-20,positive,medium,short,"A spin-off of the cable business could unlock shareholder value and streamline operations, potential",37.9328,37.5534,37.5446,33.4063,-1.0002,-1.0235,-11.933,False,False,False
|
||||||
1454,CMCSA,Comcast,2024-11-20,positive,medium,long,Separating the cable business may allow Comcast to focus on growth areas like streaming and broadban,37.9328,37.5534,37.5446,33.4063,-1.0002,-1.0235,-11.933,False,False,False
|
1454,CMCSA,Comcast,2024-11-20,positive,medium,long,Separating the cable business may allow Comcast to focus on growth areas like streaming and broadban,37.9328,37.5534,37.5446,33.4063,-1.0002,-1.0235,-11.933,False,False,False
|
||||||
1448,META,Meta,2024-11-14,negative,medium,short,The $840 million fine represents a significant financial penalty and reinforces investor concerns ab,574.3903,560.3878,571.5639,627.7628,-2.4378,-0.4921,9.292,True,True,False
|
1448,META,Meta,2024-11-14,negative,medium,short,The $840 million fine represents a significant financial penalty and reinforces investor concerns ab,574.3902,560.3878,571.564,627.7629,-2.4378,-0.492,9.2921,True,True,False
|
||||||
1449,META,Meta,2024-11-14,negative,medium,medium,"The EU ruling may force Meta to alter how Marketplace is integrated into Facebook, potentially reduc",574.3903,560.3878,571.5639,627.7628,-2.4378,-0.4921,9.292,True,True,False
|
1449,META,Meta,2024-11-14,negative,medium,medium,"The EU ruling may force Meta to alter how Marketplace is integrated into Facebook, potentially reduc",574.3902,560.3878,571.564,627.7629,-2.4378,-0.492,9.2921,True,True,False
|
||||||
1450,META,Meta,2024-11-14,positive,high,long,"This fine adds to a pattern of EU enforcement actions, signaling sustained and increasing regulatory",574.3903,560.3878,571.5639,627.7628,-2.4378,-0.4921,9.292,False,False,True
|
1450,META,Meta,2024-11-14,positive,high,long,"This fine adds to a pattern of EU enforcement actions, signaling sustained and increasing regulatory",574.3902,560.3878,571.564,627.7629,-2.4378,-0.492,9.2921,False,False,True
|
||||||
1527,TSLA,Tesla,2024-11-08,positive,high,short,"Tesla's stock surged 29% in one week following Trump's election, driven by investor optimism over re",321.22,320.72,352.56,389.22,-0.1557,9.7566,21.1693,False,True,True
|
1527,TSLA,Tesla,2024-11-08,positive,high,short,"Tesla's stock surged 29% in one week following Trump's election, driven by investor optimism over re",321.22,320.72,352.56,389.22,-0.1557,9.7566,21.1693,False,True,True
|
||||||
1528,TSLA,Tesla,2024-11-08,positive,medium,medium,Potential higher tariffs on Chinese EVs like BYD could reduce competitive pressure in the U.S. marke,321.22,320.72,352.56,389.22,-0.1557,9.7566,21.1693,False,True,True
|
1528,TSLA,Tesla,2024-11-08,positive,medium,medium,Potential higher tariffs on Chinese EVs like BYD could reduce competitive pressure in the U.S. marke,321.22,320.72,352.56,389.22,-0.1557,9.7566,21.1693,False,True,True
|
||||||
1552,RIVN,Rivian,2024-10-17,positive,low,short,"The novelty of themed updates may attract media attention and increase short-term consumer interest,",10.12,10.43,10.1,10.31,3.0632,-0.1976,1.8775,True,False,True
|
1552,RIVN,Rivian,2024-10-17,positive,low,short,"The novelty of themed updates may attract media attention and increase short-term consumer interest,",10.12,10.43,10.1,10.31,3.0632,-0.1976,1.8775,True,False,True
|
||||||
|
|||||||
|
@@ -10,11 +10,13 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
INTELLIGENCE_DB: /data/intelligence.sqlite
|
INTELLIGENCE_DB: /data/intelligence.sqlite
|
||||||
|
DURIIN_RUN_SCHEDULER: "false"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- nginx_proxy_manager_default
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
intelligence:
|
intelligence:
|
||||||
|
profiles: [legacy]
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
provenance: false
|
provenance: false
|
||||||
@@ -31,6 +33,97 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- nginx_proxy_manager_default
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
|
autonomy:
|
||||||
|
profiles: [autonomy]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
provenance: false
|
||||||
|
command: node workers/autonomy-entrypoint.js
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
- ./data:/data
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DURIIN_DB: /data/archive.sqlite
|
||||||
|
INTELLIGENCE_DB: /data/intelligence.sqlite
|
||||||
|
AUTONOMY_POLL_MS: "1000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
|
coordinator:
|
||||||
|
profiles: [autonomy]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
provenance: false
|
||||||
|
command: node workers/coordinator-entrypoint.js
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
- ./data:/data
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DURIIN_DB: /data/archive.sqlite
|
||||||
|
INTELLIGENCE_DB: /data/intelligence.sqlite
|
||||||
|
AUTONOMY_POLL_MS: "1000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
|
autonomy-outcomes:
|
||||||
|
profiles: [autonomy]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
provenance: false
|
||||||
|
command: node workers/outcome-autonomy-entrypoint.js
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
INTELLIGENCE_DB: /data/intelligence.sqlite
|
||||||
|
AUTONOMY_OUTCOME_POLL_MS: "60000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
|
calibration:
|
||||||
|
profiles: [autonomy]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
provenance: false
|
||||||
|
command: node workers/calibration-entrypoint.js
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
INTELLIGENCE_DB: /data/intelligence.sqlite
|
||||||
|
AUTONOMY_CALIBRATION_POLL_MS: "60000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
|
execution:
|
||||||
|
profiles: [autonomy]
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
provenance: false
|
||||||
|
command: node workers/execution-entrypoint.js
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
INTELLIGENCE_DB: /data/intelligence.sqlite
|
||||||
|
AUTONOMY_EXECUTION_MODE: "shadow"
|
||||||
|
AUTONOMY_DEFAULT_NOTIONAL: "100"
|
||||||
|
AUTONOMY_EXECUTION_POLL_MS: "10000"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- nginx_proxy_manager_default
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
nginx_proxy_manager_default:
|
nginx_proxy_manager_default:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Duriin autonomy runtime
|
||||||
|
|
||||||
|
The autonomy runtime is additive to the existing archive and intelligence
|
||||||
|
tables. It is deliberately disabled until the paper-trading cutover is
|
||||||
|
approved.
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
- `api`: HTTP only; no background scheduler when `DURIIN_RUN_SCHEDULER=false`.
|
||||||
|
- `autonomy`: bounded archive reconciliation and live/historical job creation.
|
||||||
|
- `coordinator`: LLM proposal extraction only. It cannot create an order.
|
||||||
|
- `autonomy-outcomes`: resolves matured predictions against market and benchmark returns.
|
||||||
|
- `calibration`: creates empirical calibration snapshots and deterministic decisions.
|
||||||
|
- `intelligence`: legacy worker, available only under the `legacy` Compose profile.
|
||||||
|
|
||||||
|
Start only the API by default:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d api
|
||||||
|
```
|
||||||
|
|
||||||
|
The new runtime is explicitly opt-in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile autonomy up -d autonomy coordinator autonomy-outcomes calibration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authority rules
|
||||||
|
|
||||||
|
The coordinator may return categorical, evidence-backed proposals. It may not
|
||||||
|
return probabilities, returns, position sizes or trade actions. Proposals must
|
||||||
|
reference existing archive article IDs and instruments in
|
||||||
|
`autonomy_instruments` with `active=1` and `tradable=1`.
|
||||||
|
|
||||||
|
Calibration is computed from resolved market-relative outcomes. The policy
|
||||||
|
engine emits `BUY`, `SELL`, `HOLD` or `ABSTAIN`; an LLM is not in this path.
|
||||||
|
|
||||||
|
Paper order intents use deterministic client IDs and are bounded by the
|
||||||
|
execution validator. A broker adapter must be explicitly configured; the
|
||||||
|
included simulator is the default test adapter.
|
||||||
|
|
||||||
|
## Database initialization
|
||||||
|
|
||||||
|
The autonomy schema is initialized additively by the autonomy workers and
|
||||||
|
maintenance scripts in the intelligence database. To create the initial
|
||||||
|
reconciliation job without starting workers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run autonomy:init
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy predictions remain legacy records and are not silently included in
|
||||||
|
calibration. New prospective predictions are the trusted learning set.
|
||||||
+6
-1
@@ -5,7 +5,12 @@
|
|||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"workers": "node workers/index.js"
|
"workers": "node workers/index.js",
|
||||||
|
"test": "node --test test/**/*.test.js",
|
||||||
|
"autonomy:init": "node scripts/initialize-autonomy.js",
|
||||||
|
"autonomy:allowlist": "node scripts/set-autonomy-instrument.js",
|
||||||
|
"autonomy:sync-assets": "node scripts/sync-paper-assets.js",
|
||||||
|
"autonomy:import-legacy": "node scripts/import-legacy-intelligence.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
|
||||||
|
const sourcePath = process.env.LEGACY_INTELLIGENCE_DB || path.resolve(process.cwd(), 'intelligence.sqlite');
|
||||||
|
const db = new Database(sourcePath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
const insert = db.prepare(`
|
||||||
|
INSERT OR IGNORE INTO autonomy_legacy_records(source_table, source_id, payload)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`);
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
for (const table of ['event_predictions', 'trade_signals', 'company_facts', 'company_relationships']) {
|
||||||
|
const exists = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table);
|
||||||
|
if (!exists) continue;
|
||||||
|
const rows = db.prepare(`SELECT * FROM ${table}`).all();
|
||||||
|
for (const row of rows) insert.run(table, row.id, JSON.stringify(row));
|
||||||
|
console.log(`${table}: ${rows.length}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
db.close();
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Initializes the additive autonomy schema and creates one reconciliation job.
|
||||||
|
// It deliberately does not rewrite legacy intelligence or enqueue millions of
|
||||||
|
// historical records; reconciliation workers discover those in bounded batches.
|
||||||
|
const path = require('path');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { enqueueJob } = require('../src/autonomy/jobs');
|
||||||
|
|
||||||
|
const intelligencePath = process.env.INTELLIGENCE_DB || path.resolve(process.cwd(), 'intelligence.sqlite');
|
||||||
|
const db = new Database(intelligencePath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
const result = enqueueJob(db, {
|
||||||
|
jobType: 'reconcile_archive',
|
||||||
|
lane: 'maintenance',
|
||||||
|
priority: 100,
|
||||||
|
entityType: 'archive',
|
||||||
|
entityId: 'archive',
|
||||||
|
idempotencyKey: 'reconcile_archive:v1',
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify({ intelligencePath, reconciliationJobInserted: result.inserted }));
|
||||||
|
db.close();
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const path = require('path');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
|
||||||
|
const symbol = String(process.argv[2] || '').trim().toUpperCase();
|
||||||
|
if (!/^[A-Z0-9._/-]+$/.test(symbol)) {
|
||||||
|
console.error('usage: node scripts/set-autonomy-instrument.js SYMBOL [broker]');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
const broker = String(process.argv[3] || 'simulator');
|
||||||
|
const db = new Database(process.env.INTELLIGENCE_DB || path.resolve(process.cwd(), 'intelligence.sqlite'));
|
||||||
|
initAutonomySchema(db);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO autonomy_instruments(symbol, broker, active, tradable)
|
||||||
|
VALUES (?, ?, 1, 1)
|
||||||
|
ON CONFLICT(symbol) DO UPDATE SET broker=excluded.broker, active=1, tradable=1, updated_at=datetime('now')
|
||||||
|
`).run(symbol, broker);
|
||||||
|
console.log(JSON.stringify({ symbol, broker, active: true, tradable: true }));
|
||||||
|
db.close();
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { createAlpacaPaperClient } = require('../src/brokers/alpacaPaper');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = createAlpacaPaperClient({
|
||||||
|
keyId: process.env.ALPACA_PAPER_KEY_ID,
|
||||||
|
secretKey: process.env.ALPACA_PAPER_SECRET_KEY,
|
||||||
|
});
|
||||||
|
const assets = await client.getAssets();
|
||||||
|
const db = new Database(process.env.INTELLIGENCE_DB || path.resolve(process.cwd(), 'intelligence.sqlite'));
|
||||||
|
initAutonomySchema(db);
|
||||||
|
const upsert = db.prepare(`
|
||||||
|
INSERT INTO autonomy_instruments(symbol, broker, asset_class, active, tradable, shortable, fractionable, updated_at)
|
||||||
|
VALUES (?, 'alpaca-paper', ?, ?, ?, ?, ?, datetime('now'))
|
||||||
|
ON CONFLICT(symbol) DO UPDATE SET
|
||||||
|
broker=excluded.broker, asset_class=excluded.asset_class, active=excluded.active,
|
||||||
|
tradable=excluded.tradable, shortable=excluded.shortable, fractionable=excluded.fractionable,
|
||||||
|
updated_at=datetime('now')
|
||||||
|
`);
|
||||||
|
const tx = db.transaction(() => assets.forEach((asset) => upsert.run(
|
||||||
|
asset.symbol, asset.class || 'us_equity', asset.status === 'active' ? 1 : 0,
|
||||||
|
asset.tradable ? 1 : 0, asset.shortable ? 1 : 0, asset.fractionable ? 1 : 0
|
||||||
|
)));
|
||||||
|
tx();
|
||||||
|
console.log(JSON.stringify({ synced: assets.length }));
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => { console.error(error.message); process.exit(1); });
|
||||||
@@ -6,6 +6,7 @@ const sourcesRoutes = require('./src/routes/sources');
|
|||||||
const eventRoutes = require('./src/routes/events');
|
const eventRoutes = require('./src/routes/events');
|
||||||
const adminRoutes = require('./src/routes/admin');
|
const adminRoutes = require('./src/routes/admin');
|
||||||
const devRoutes = require('./src/routes/dev');
|
const devRoutes = require('./src/routes/dev');
|
||||||
|
const autonomyRoutes = require('./src/routes/autonomy');
|
||||||
const config = require('./src/config');
|
const config = require('./src/config');
|
||||||
const { startScheduler } = require('./src/scheduler');
|
const { startScheduler } = require('./src/scheduler');
|
||||||
|
|
||||||
@@ -18,13 +19,18 @@ app.register(sourcesRoutes);
|
|||||||
app.register(eventRoutes);
|
app.register(eventRoutes);
|
||||||
app.register(adminRoutes);
|
app.register(adminRoutes);
|
||||||
app.register(devRoutes);
|
app.register(devRoutes);
|
||||||
|
app.register(autonomyRoutes);
|
||||||
|
|
||||||
app.get('/', async () => ({ ok: true }));
|
app.get('/', async () => ({ ok: true }));
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
await app.listen({ port: config.server.port, host: config.server.host });
|
await app.listen({ port: config.server.port, host: config.server.host });
|
||||||
|
|
||||||
|
if (process.env.DURIIN_RUN_SCHEDULER !== 'false') {
|
||||||
startScheduler();
|
startScheduler();
|
||||||
|
} else {
|
||||||
|
app.log.warn('Background ingestion and enrichment scheduler is disabled');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start().catch((error) => {
|
start().catch((error) => {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
|
||||||
|
|
||||||
|
function betaMean(wins, total, priorWins = 1, priorLosses = 1) {
|
||||||
|
return (wins + priorWins) / (total + priorWins + priorLosses);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quantile(values, q) {
|
||||||
|
if (!values.length) return null;
|
||||||
|
const sorted = [...values].sort((a, b) => a - b);
|
||||||
|
const position = (sorted.length - 1) * q;
|
||||||
|
const lower = Math.floor(position);
|
||||||
|
const upper = Math.ceil(position);
|
||||||
|
if (lower === upper) return sorted[lower];
|
||||||
|
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cohortKey({ direction, eventType, horizonDays, sector = 'unknown' }) {
|
||||||
|
return [sector, eventType || 'unknown', horizonDays, direction].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function calibrateOutcomes(rows, parent = null) {
|
||||||
|
const clean = rows.filter((row) => Number.isFinite(Number(row.excess_return)));
|
||||||
|
const wins = clean.filter((row) => Number(row.direction_correct) === 1).length;
|
||||||
|
const total = clean.length;
|
||||||
|
const priorProbability = parent ? parent.directionalProbability : 0.5;
|
||||||
|
const priorStrength = parent ? Math.max(2, Math.min(20, parent.effectiveSampleSize / 10)) : 2;
|
||||||
|
const probability = (wins + priorProbability * priorStrength) / (total + priorStrength);
|
||||||
|
const returns = clean.map((row) => Number(row.excess_return));
|
||||||
|
return {
|
||||||
|
sampleSize: total,
|
||||||
|
effectiveSampleSize: total + priorStrength,
|
||||||
|
directionalProbability: clamp(probability, 0.01, 0.99),
|
||||||
|
expectedExcessReturn: returns.length ? returns.reduce((sum, value) => sum + value, 0) / returns.length : null,
|
||||||
|
lowerReturn: quantile(returns, 0.1),
|
||||||
|
upperReturn: quantile(returns, 0.9),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { betaMean, cohortKey, calibrateOutcomes, quantile };
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
const ALLOWED_DIRECTIONS = new Set(['positive', 'negative']);
|
||||||
|
const ALLOWED_HORIZONS = new Set([1, 5, 10, 20, 30, 60, 90]);
|
||||||
|
|
||||||
|
function normalizeProposal(raw, { informationCutoff, model = 'unknown', promptVersion = 'unknown' } = {}) {
|
||||||
|
if (!raw || typeof raw !== 'object') throw new Error('coordinator output must be an object');
|
||||||
|
const predictions = Array.isArray(raw.predictions) ? raw.predictions : [];
|
||||||
|
const normalized = predictions.map((item) => {
|
||||||
|
const instrument = String(item.instrument || item.ticker || '').trim().toUpperCase();
|
||||||
|
const direction = String(item.direction || '').trim().toLowerCase();
|
||||||
|
const horizonDays = Number(item.horizon_days || item.horizonDays);
|
||||||
|
if (!instrument) throw new Error('prediction instrument is required');
|
||||||
|
if (!ALLOWED_DIRECTIONS.has(direction)) throw new Error(`invalid direction: ${direction}`);
|
||||||
|
if (!ALLOWED_HORIZONS.has(horizonDays)) throw new Error(`invalid horizon_days: ${horizonDays}`);
|
||||||
|
const articleIds = Array.isArray(item.evidence_article_ids)
|
||||||
|
? item.evidence_article_ids.map(Number).filter(Number.isInteger)
|
||||||
|
: [];
|
||||||
|
if (articleIds.length === 0) throw new Error(`prediction for ${instrument} has no evidence`);
|
||||||
|
return {
|
||||||
|
instrument,
|
||||||
|
direction,
|
||||||
|
eventType: String(item.event_type || 'unknown').trim().toLowerCase(),
|
||||||
|
causalChannel: item.causal_channel ? String(item.causal_channel).trim() : null,
|
||||||
|
horizonDays,
|
||||||
|
evidenceArticleIds: [...new Set(articleIds)],
|
||||||
|
invalidationCondition: item.invalidation_condition ? String(item.invalidation_condition).trim() : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
informationCutoff: informationCutoff || new Date().toISOString(),
|
||||||
|
coordinatorModel: model,
|
||||||
|
promptVersion,
|
||||||
|
predictions: normalized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyEvidence(archiveDb, articleIds) {
|
||||||
|
const placeholders = articleIds.map(() => '?').join(',');
|
||||||
|
const rows = archiveDb.prepare(`SELECT id FROM articles WHERE id IN (${placeholders})`).all(...articleIds);
|
||||||
|
const found = new Set(rows.map((row) => row.id));
|
||||||
|
return articleIds.every((id) => found.has(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
|
||||||
|
const proposal = normalizeProposal(raw, metadata);
|
||||||
|
for (const prediction of proposal.predictions) {
|
||||||
|
if (!verifyEvidence(archiveDb, prediction.evidenceArticleIds)) {
|
||||||
|
throw new Error(`proposal references missing evidence for ${prediction.instrument}`);
|
||||||
|
}
|
||||||
|
const instrument = intelligenceDb.prepare(
|
||||||
|
"SELECT tradable FROM autonomy_instruments WHERE symbol = ? AND active = 1 AND tradable = 1"
|
||||||
|
).get(prediction.instrument);
|
||||||
|
if (!instrument) throw new Error(`instrument is not currently allowlisted: ${prediction.instrument}`);
|
||||||
|
}
|
||||||
|
const insert = intelligenceDb.prepare(`
|
||||||
|
INSERT INTO autonomy_proposals
|
||||||
|
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'accepted')
|
||||||
|
`);
|
||||||
|
const insertPrediction = intelligenceDb.prepare(`
|
||||||
|
INSERT INTO autonomy_predictions
|
||||||
|
(proposal_id, event_id, instrument, direction, event_type, causal_channel,
|
||||||
|
horizon_days, information_cutoff, evidence_article_ids, invalidation_condition, learning_eligible, strategy_version)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
const tx = intelligenceDb.transaction(() => {
|
||||||
|
const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(proposal), proposal.informationCutoff,
|
||||||
|
proposal.coordinatorModel, proposal.promptVersion);
|
||||||
|
for (const prediction of proposal.predictions) {
|
||||||
|
insertPrediction.run(proposalResult.lastInsertRowid, metadata.eventId || null, prediction.instrument,
|
||||||
|
prediction.direction, prediction.eventType, prediction.causalChannel, prediction.horizonDays,
|
||||||
|
proposal.informationCutoff, JSON.stringify(prediction.evidenceArticleIds), prediction.invalidationCondition,
|
||||||
|
metadata.learningEligible ? 1 : 0, metadata.strategyVersion || 'autonomy-1');
|
||||||
|
}
|
||||||
|
return Number(proposalResult.lastInsertRowid);
|
||||||
|
});
|
||||||
|
return { proposalId: tx(), predictionCount: proposal.predictions.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordRejectedProposal(intelligenceDb, raw, metadata = {}, reason = 'validation failed') {
|
||||||
|
const payload = raw && typeof raw === 'object' ? raw : { raw: String(raw) };
|
||||||
|
return intelligenceDb.prepare(`
|
||||||
|
INSERT INTO autonomy_proposals
|
||||||
|
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status, rejection_reason, reviewed_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'rejected', ?, datetime('now'))
|
||||||
|
`).run(metadata.eventId || null, JSON.stringify(payload), metadata.informationCutoff || new Date().toISOString(),
|
||||||
|
metadata.model || 'unknown', metadata.promptVersion || 'unknown', String(reason).slice(0, 1000)).lastInsertRowid;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { normalizeProposal, verifyEvidence, acceptProposal, recordRejectedProposal };
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
function makeClientOrderId(decisionId) {
|
||||||
|
return `duriin-${String(decisionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePaperIntent(intent, constraints = {}) {
|
||||||
|
if (!intent || !intent.decisionId || !intent.instrument) throw new Error('decisionId and instrument are required');
|
||||||
|
if (!['BUY', 'SELL'].includes(intent.action)) throw new Error('only BUY and SELL create order intents');
|
||||||
|
if (!Number.isFinite(Number(intent.notional)) || Number(intent.notional) <= 0) throw new Error('notional must be positive');
|
||||||
|
const maxNotional = Number(constraints.maxNotional ?? 1000);
|
||||||
|
if (Number(intent.notional) > maxNotional) throw new Error('notional exceeds paper risk limit');
|
||||||
|
if (constraints.tradable !== true) throw new Error('instrument is not confirmed tradable');
|
||||||
|
return {
|
||||||
|
clientOrderId: makeClientOrderId(intent.decisionId),
|
||||||
|
instrument: String(intent.instrument).toUpperCase(),
|
||||||
|
side: intent.action === 'BUY' ? 'buy' : 'sell',
|
||||||
|
notional: Number(intent.notional),
|
||||||
|
mode: 'paper',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSimulator() {
|
||||||
|
const orders = new Map();
|
||||||
|
return {
|
||||||
|
submit(intent) {
|
||||||
|
if (orders.has(intent.clientOrderId)) return orders.get(intent.clientOrderId);
|
||||||
|
const order = { ...intent, brokerOrderId: `sim-${intent.clientOrderId}`, status: 'accepted' };
|
||||||
|
orders.set(intent.clientOrderId, order);
|
||||||
|
return order;
|
||||||
|
},
|
||||||
|
get(clientOrderId) { return orders.get(clientOrderId) || null; },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { makeClientOrderId, validatePaperIntent, createSimulator };
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const { initAutonomySchema } = require('./schema');
|
||||||
|
const jobs = require('./jobs');
|
||||||
|
const coordinator = require('./coordinator');
|
||||||
|
const calibration = require('./calibration');
|
||||||
|
const policy = require('./policy');
|
||||||
|
const execution = require('./execution');
|
||||||
|
const orderIntents = require('./orderIntents');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initAutonomySchema,
|
||||||
|
...jobs,
|
||||||
|
...coordinator,
|
||||||
|
...calibration,
|
||||||
|
...policy,
|
||||||
|
...execution,
|
||||||
|
...orderIntents,
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
function enqueueJob(db, { jobType, lane = 'historical', priority = 0, entityType, entityId, idempotencyKey }) {
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT OR IGNORE INTO autonomy_jobs
|
||||||
|
(job_type, lane, priority, entity_type, entity_id, idempotency_key)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(jobType, lane, priority, entityType, String(entityId), idempotencyKey);
|
||||||
|
return { inserted: result.changes > 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaseNextJob(db, workerId, leaseSeconds = 60, jobTypes = null) {
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
const typeClause = Array.isArray(jobTypes) && jobTypes.length
|
||||||
|
? `AND job_type IN (${jobTypes.map(() => '?').join(',')})`
|
||||||
|
: '';
|
||||||
|
const typeParams = Array.isArray(jobTypes) && jobTypes.length ? jobTypes : [];
|
||||||
|
const job = db.prepare(`
|
||||||
|
SELECT * FROM autonomy_jobs
|
||||||
|
WHERE ((status = 'pending' AND datetime(available_at) <= datetime('now'))
|
||||||
|
OR (status = 'leased' AND datetime(lease_expires_at) <= datetime('now')))
|
||||||
|
${typeClause}
|
||||||
|
ORDER BY CASE lane WHEN 'live' THEN 3 WHEN 'maintenance' THEN 2 ELSE 1 END DESC,
|
||||||
|
priority DESC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
`).get(...typeParams);
|
||||||
|
if (!job) return null;
|
||||||
|
const updated = db.prepare(`
|
||||||
|
UPDATE autonomy_jobs
|
||||||
|
SET status = 'leased', leased_by = ?, lease_expires_at = datetime('now', ?),
|
||||||
|
attempts = attempts + 1
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(workerId, `+${Math.max(1, Math.floor(leaseSeconds))} seconds`, job.id);
|
||||||
|
return updated.changes ? { ...job, status: 'leased', leased_by: workerId } : null;
|
||||||
|
});
|
||||||
|
return tx();
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeJob(db, id, workerId) {
|
||||||
|
return db.prepare(`
|
||||||
|
UPDATE autonomy_jobs
|
||||||
|
SET status = 'complete', completed_at = datetime('now'),
|
||||||
|
leased_by = NULL, lease_expires_at = NULL
|
||||||
|
WHERE id = ? AND leased_by = ?
|
||||||
|
`).run(id, workerId).changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function failJob(db, id, workerId, error, maxAttempts = 5) {
|
||||||
|
return db.prepare(`
|
||||||
|
UPDATE autonomy_jobs
|
||||||
|
SET status = CASE WHEN attempts >= ? THEN 'dead_letter' ELSE 'pending' END,
|
||||||
|
available_at = datetime('now', '+60 seconds'), last_error = ?,
|
||||||
|
leased_by = NULL, lease_expires_at = NULL
|
||||||
|
WHERE id = ? AND leased_by = ?
|
||||||
|
`).run(maxAttempts, String(error || 'unknown error').slice(0, 2000), id, workerId).changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { enqueueJob, leaseNextJob, completeJob, failJob };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
function extractJson(text) {
|
||||||
|
const value = String(text || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
||||||
|
try { return JSON.parse(value); } catch (_) {
|
||||||
|
const start = value.indexOf('{');
|
||||||
|
const end = value.lastIndexOf('}');
|
||||||
|
if (start >= 0 && end > start) return JSON.parse(value.slice(start, end + 1));
|
||||||
|
throw new Error('LLM response did not contain valid JSON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callCoordinator(config, prompt) {
|
||||||
|
const apiKey = String(config?.openRouter?.apiKey || '').trim();
|
||||||
|
if (!apiKey) throw new Error('OpenRouter API key is not configured');
|
||||||
|
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: config.openRouter.llmModel,
|
||||||
|
temperature: 0,
|
||||||
|
response_format: { type: 'json_object' },
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are a coordinator. Extract only evidence-backed categorical hypotheses. Never output probabilities, expected returns, confidence scores, position sizes, or trade actions.' },
|
||||||
|
{ role: 'user', content: prompt },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`coordinator request failed with ${response.status}`);
|
||||||
|
const body = await response.json();
|
||||||
|
return extractJson(body?.choices?.[0]?.message?.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { extractJson, callCoordinator };
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const { validatePaperIntent } = require('./execution');
|
||||||
|
|
||||||
|
function createOrderIntent(db, decisionId, notional, constraints = {}) {
|
||||||
|
const row = db.prepare(`
|
||||||
|
SELECT d.id AS decision_id, d.action, p.instrument,
|
||||||
|
COALESCE(ai.active, 0) AS active, COALESCE(ai.tradable, 0) AS tradable
|
||||||
|
FROM autonomy_decisions d
|
||||||
|
JOIN autonomy_predictions p ON p.id = d.prediction_id
|
||||||
|
LEFT JOIN autonomy_instruments ai ON ai.symbol = p.instrument
|
||||||
|
WHERE d.id = ?
|
||||||
|
`).get(decisionId);
|
||||||
|
if (!row) throw new Error(`decision ${decisionId} does not exist`);
|
||||||
|
const intent = validatePaperIntent({ decisionId, instrument: row.instrument, action: row.action, notional }, {
|
||||||
|
...constraints,
|
||||||
|
tradable: row.active === 1 && row.tradable === 1,
|
||||||
|
});
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT OR IGNORE INTO autonomy_order_intents
|
||||||
|
(decision_id, client_order_id, instrument, side, notional, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'shadow')
|
||||||
|
`).run(decisionId, intent.clientOrderId, intent.instrument, intent.side, intent.notional);
|
||||||
|
return { ...intent, inserted: result.changes > 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createOrderIntent };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
function addTradingDays(date, days) {
|
||||||
|
const value = new Date(`${date}T00:00:00Z`);
|
||||||
|
let remaining = Math.max(0, Number(days) || 0);
|
||||||
|
while (remaining > 0) {
|
||||||
|
value.setUTCDate(value.getUTCDate() + 1);
|
||||||
|
const weekday = value.getUTCDay();
|
||||||
|
if (weekday !== 0 && weekday !== 6) remaining -= 1;
|
||||||
|
}
|
||||||
|
return value.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestOnOrAfter(history, date) {
|
||||||
|
return history.find((row) => row.date >= date)?.close ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOutcome(prediction, instrumentHistory, benchmarkHistory) {
|
||||||
|
const eventDate = String(prediction.information_cutoff).slice(0, 10);
|
||||||
|
const horizonDate = addTradingDays(eventDate, prediction.horizon_days);
|
||||||
|
const price0 = nearestOnOrAfter(instrumentHistory, eventDate);
|
||||||
|
const priceHorizon = nearestOnOrAfter(instrumentHistory, horizonDate);
|
||||||
|
const benchmark0 = nearestOnOrAfter(benchmarkHistory, eventDate);
|
||||||
|
const benchmarkHorizon = nearestOnOrAfter(benchmarkHistory, horizonDate);
|
||||||
|
if (![price0, priceHorizon, benchmark0, benchmarkHorizon].every(Number.isFinite)) return null;
|
||||||
|
const instrumentReturn = (priceHorizon - price0) / price0;
|
||||||
|
const benchmarkReturn = (benchmarkHorizon - benchmark0) / benchmark0;
|
||||||
|
const excessReturn = instrumentReturn - benchmarkReturn;
|
||||||
|
const directionCorrect = prediction.direction === 'positive' ? excessReturn > 0 : excessReturn < 0;
|
||||||
|
return {
|
||||||
|
price0, priceHorizon, benchmark0, benchmarkHorizon,
|
||||||
|
excessReturn, directionCorrect: directionCorrect ? 1 : 0,
|
||||||
|
eventDate, horizonDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { addTradingDays, nearestOnOrAfter, calculateOutcome };
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
function decide({ direction = 'positive', probability, expectedExcessReturn, lowerReturn, upperReturn, sampleSize }, rules = {}) {
|
||||||
|
const minSampleSize = Number(rules.minSampleSize ?? 30);
|
||||||
|
const minProbability = Number(rules.minProbability ?? 0.58);
|
||||||
|
const minExpectedReturn = Number(rules.minExpectedReturn ?? 0.005);
|
||||||
|
const maxDownside = Number(rules.maxDownside ?? -0.08);
|
||||||
|
|
||||||
|
if (![probability, expectedExcessReturn].every(Number.isFinite)) {
|
||||||
|
return { action: 'ABSTAIN', rationale: 'calibration unavailable' };
|
||||||
|
}
|
||||||
|
if (sampleSize < minSampleSize) {
|
||||||
|
return { action: 'ABSTAIN', rationale: `insufficient calibration sample (${sampleSize}/${minSampleSize})` };
|
||||||
|
}
|
||||||
|
const signedExpectedReturn = direction === 'negative' ? -expectedExcessReturn : expectedExcessReturn;
|
||||||
|
const signedLowerReturn = direction === 'negative'
|
||||||
|
? (Number.isFinite(upperReturn) ? -upperReturn : null)
|
||||||
|
: (Number.isFinite(lowerReturn) ? lowerReturn : null);
|
||||||
|
if (Number.isFinite(signedLowerReturn) && signedLowerReturn < maxDownside) {
|
||||||
|
return { action: 'HOLD', rationale: 'calibrated downside exceeds policy limit' };
|
||||||
|
}
|
||||||
|
if (probability >= minProbability && signedExpectedReturn >= minExpectedReturn) {
|
||||||
|
return { action: direction === 'negative' ? 'SELL' : 'BUY', rationale: 'calibrated edge clears policy thresholds' };
|
||||||
|
}
|
||||||
|
return { action: 'HOLD', rationale: 'calibrated edge does not clear policy thresholds' };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { decide };
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
const AUTONOMY_SCHEMA_VERSION = 1;
|
||||||
|
|
||||||
|
function initAutonomySchema(db) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_schema (
|
||||||
|
version INTEGER PRIMARY KEY,
|
||||||
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_jobs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_type TEXT NOT NULL,
|
||||||
|
lane TEXT NOT NULL CHECK (lane IN ('live', 'historical', 'maintenance')),
|
||||||
|
priority INTEGER NOT NULL DEFAULT 0,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
entity_id TEXT NOT NULL,
|
||||||
|
idempotency_key TEXT NOT NULL UNIQUE,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK (status IN ('pending', 'leased', 'complete', 'failed', 'dead_letter')),
|
||||||
|
attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
available_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
leased_by TEXT,
|
||||||
|
lease_expires_at TEXT,
|
||||||
|
last_error TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
completed_at TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_autonomy_jobs_claim
|
||||||
|
ON autonomy_jobs(status, lane, priority DESC, available_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_cursors (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_proposals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
event_id INTEGER,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
information_cutoff TEXT NOT NULL,
|
||||||
|
coordinator_model TEXT,
|
||||||
|
prompt_version TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'candidate'
|
||||||
|
CHECK (status IN ('candidate', 'accepted', 'rejected', 'superseded')),
|
||||||
|
rejection_reason TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
reviewed_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_instruments (
|
||||||
|
symbol TEXT PRIMARY KEY,
|
||||||
|
broker TEXT NOT NULL,
|
||||||
|
asset_class TEXT NOT NULL DEFAULT 'us_equity',
|
||||||
|
active INTEGER NOT NULL DEFAULT 0,
|
||||||
|
tradable INTEGER NOT NULL DEFAULT 0,
|
||||||
|
shortable INTEGER NOT NULL DEFAULT 0,
|
||||||
|
fractionable INTEGER NOT NULL DEFAULT 0,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_legacy_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
source_table TEXT NOT NULL,
|
||||||
|
source_id INTEGER NOT NULL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
calibration_eligible INTEGER NOT NULL DEFAULT 0,
|
||||||
|
imported_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(source_table, source_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_predictions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
proposal_id INTEGER NOT NULL REFERENCES autonomy_proposals(id),
|
||||||
|
event_id INTEGER,
|
||||||
|
instrument TEXT NOT NULL,
|
||||||
|
direction TEXT NOT NULL CHECK (direction IN ('positive', 'negative')),
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
causal_channel TEXT,
|
||||||
|
horizon_days INTEGER NOT NULL,
|
||||||
|
information_cutoff TEXT NOT NULL,
|
||||||
|
evidence_article_ids TEXT NOT NULL,
|
||||||
|
invalidation_condition TEXT,
|
||||||
|
learning_eligible INTEGER NOT NULL DEFAULT 0,
|
||||||
|
strategy_version TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'open'
|
||||||
|
CHECK (status IN ('open', 'resolved', 'unresolvable', 'invalidated')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_autonomy_predictions_resolution
|
||||||
|
ON autonomy_predictions(status, information_cutoff, instrument);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_outcomes (
|
||||||
|
prediction_id INTEGER PRIMARY KEY REFERENCES autonomy_predictions(id),
|
||||||
|
price_0 REAL,
|
||||||
|
price_horizon REAL,
|
||||||
|
benchmark_0 REAL,
|
||||||
|
benchmark_horizon REAL,
|
||||||
|
excess_return REAL,
|
||||||
|
direction_correct INTEGER,
|
||||||
|
error_type TEXT,
|
||||||
|
evaluated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_calibration_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
cohort_key TEXT NOT NULL,
|
||||||
|
sample_size INTEGER NOT NULL,
|
||||||
|
effective_sample_size REAL NOT NULL,
|
||||||
|
directional_probability REAL NOT NULL,
|
||||||
|
expected_excess_return REAL,
|
||||||
|
lower_return REAL,
|
||||||
|
upper_return REAL,
|
||||||
|
parent_cohort_key TEXT,
|
||||||
|
version TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_autonomy_calibration_lookup
|
||||||
|
ON autonomy_calibration_snapshots(cohort_key, created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_decisions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
prediction_id INTEGER NOT NULL REFERENCES autonomy_predictions(id),
|
||||||
|
action TEXT NOT NULL CHECK (action IN ('BUY', 'SELL', 'HOLD', 'ABSTAIN')),
|
||||||
|
calibrated_probability REAL,
|
||||||
|
expected_excess_return REAL,
|
||||||
|
rationale TEXT NOT NULL,
|
||||||
|
strategy_version TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_order_intents (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
decision_id INTEGER NOT NULL REFERENCES autonomy_decisions(id),
|
||||||
|
client_order_id TEXT NOT NULL UNIQUE,
|
||||||
|
instrument TEXT NOT NULL,
|
||||||
|
side TEXT NOT NULL CHECK (side IN ('buy', 'sell')),
|
||||||
|
notional REAL NOT NULL CHECK (notional > 0),
|
||||||
|
status TEXT NOT NULL DEFAULT 'shadow'
|
||||||
|
CHECK (status IN ('shadow', 'pending', 'submitted', 'filled', 'partially_filled', 'rejected', 'cancelled')),
|
||||||
|
broker_order_id TEXT,
|
||||||
|
attempts INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_error TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_broker_events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
broker TEXT NOT NULL,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
broker_id TEXT,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(broker, event_type, broker_id, occurred_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_account_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
broker TEXT NOT NULL,
|
||||||
|
account_id TEXT,
|
||||||
|
equity REAL,
|
||||||
|
cash REAL,
|
||||||
|
buying_power REAL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
captured_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS autonomy_position_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
broker TEXT NOT NULL,
|
||||||
|
instrument TEXT NOT NULL,
|
||||||
|
quantity REAL,
|
||||||
|
market_value REAL,
|
||||||
|
unrealized_pl REAL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
captured_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT OR IGNORE INTO autonomy_schema(version) VALUES (${AUTONOMY_SCHEMA_VERSION});
|
||||||
|
`);
|
||||||
|
for (const statement of [
|
||||||
|
'ALTER TABLE autonomy_order_intents ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0',
|
||||||
|
'ALTER TABLE autonomy_order_intents ADD COLUMN last_error TEXT',
|
||||||
|
'ALTER TABLE autonomy_predictions ADD COLUMN learning_eligible INTEGER NOT NULL DEFAULT 0',
|
||||||
|
]) {
|
||||||
|
try { db.exec(statement); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { AUTONOMY_SCHEMA_VERSION, initAutonomySchema };
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const PAPER_BASE_URL = 'https://paper-api.alpaca.markets';
|
||||||
|
|
||||||
|
function createAlpacaPaperClient({ keyId, secretKey } = {}) {
|
||||||
|
if (!keyId || !secretKey) throw new Error('Alpaca paper credentials are required');
|
||||||
|
async function request(path, options = {}) {
|
||||||
|
const response = await fetch(`${PAPER_BASE_URL}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'APCA-API-KEY-ID': keyId,
|
||||||
|
'APCA-API-SECRET-KEY': secretKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
let body = null;
|
||||||
|
try { body = text ? JSON.parse(text) : null; } catch (_) { body = { raw: text }; }
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(body?.message || `Alpaca paper API returned ${response.status}`);
|
||||||
|
error.status = response.status;
|
||||||
|
error.body = body;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
getAccount: () => request('/v2/account'),
|
||||||
|
getAssets: () => request('/v2/assets?status=active&asset_class=us_equity'),
|
||||||
|
getOrders: () => request('/v2/orders?status=all&limit=500&direction=desc'),
|
||||||
|
getPositions: () => request('/v2/positions'),
|
||||||
|
getOrderByClientId: (clientOrderId) => request(`/v2/orders:by_client_order_id?client_order_id=${encodeURIComponent(clientOrderId)}`),
|
||||||
|
submitOrder: (order) => request('/v2/orders', { method: 'POST', body: JSON.stringify(order) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { PAPER_BASE_URL, createAlpacaPaperClient };
|
||||||
@@ -48,6 +48,12 @@ db.exec(`
|
|||||||
CREATE INDEX IF NOT EXISTS idx_articles_event_id ON articles(event_id);
|
CREATE INDEX IF NOT EXISTS idx_articles_event_id ON articles(event_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_articles_has_embedding ON articles(has_embedding);
|
CREATE INDEX IF NOT EXISTS idx_articles_has_embedding ON articles(has_embedding);
|
||||||
CREATE INDEX IF NOT EXISTS idx_articles_pub_date_effective ON articles(pub_date_effective DESC);
|
CREATE INDEX IF NOT EXISTS idx_articles_pub_date_effective ON articles(pub_date_effective DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_articles_usable_pub_date
|
||||||
|
ON articles(pub_date_effective DESC, id DESC)
|
||||||
|
WHERE content IS NOT NULL
|
||||||
|
AND content != ''
|
||||||
|
AND is_index_page = 0
|
||||||
|
AND has_embedding = 1;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function buildArticlesQuery(query) {
|
|||||||
return {
|
return {
|
||||||
sql: `
|
sql: `
|
||||||
SELECT id, title, description, content, ${includeEmbedding ? 'embedding,' : ''} url, normalized_title, source, pub_date, ingested_at
|
SELECT id, title, description, content, ${includeEmbedding ? 'embedding,' : ''} url, normalized_title, source, pub_date, ingested_at
|
||||||
FROM articles
|
FROM articles INDEXED BY idx_articles_usable_pub_date
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY ${orderBy}
|
ORDER BY ${orderBy}
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
|
||||||
|
const intelligencePath = process.env.INTELLIGENCE_DB || path.resolve(process.cwd(), 'intelligence.sqlite');
|
||||||
|
const db = new Database(intelligencePath, { readonly: true });
|
||||||
|
|
||||||
|
async function autonomyRoutes(fastify) {
|
||||||
|
fastify.get('/health', async () => ({ ok: true, service: 'duriin-api' }));
|
||||||
|
|
||||||
|
fastify.get('/autonomy/status', async () => {
|
||||||
|
const exists = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='autonomy_jobs'").get();
|
||||||
|
if (!exists) return { enabled: false, reason: 'autonomy schema is not initialized' };
|
||||||
|
const jobs = db.prepare(`
|
||||||
|
SELECT lane, status, COUNT(*) AS count
|
||||||
|
FROM autonomy_jobs
|
||||||
|
GROUP BY lane, status
|
||||||
|
ORDER BY lane, status
|
||||||
|
`).all();
|
||||||
|
const predictions = db.prepare(`
|
||||||
|
SELECT status, COUNT(*) AS count
|
||||||
|
FROM autonomy_predictions
|
||||||
|
GROUP BY status
|
||||||
|
ORDER BY status
|
||||||
|
`).all();
|
||||||
|
const decisions = db.prepare(`
|
||||||
|
SELECT action, COUNT(*) AS count
|
||||||
|
FROM autonomy_decisions
|
||||||
|
GROUP BY action
|
||||||
|
ORDER BY action
|
||||||
|
`).all();
|
||||||
|
const outcomes = db.prepare(`
|
||||||
|
SELECT COUNT(*) AS total,
|
||||||
|
SUM(direction_correct) AS correct,
|
||||||
|
AVG(excess_return) AS average_excess_return
|
||||||
|
FROM autonomy_outcomes
|
||||||
|
`).get();
|
||||||
|
const legacy = db.prepare('SELECT COUNT(*) AS count FROM autonomy_legacy_records').get();
|
||||||
|
const instruments = db.prepare('SELECT COUNT(*) AS count FROM autonomy_instruments WHERE active=1 AND tradable=1').get();
|
||||||
|
return { enabled: true, jobs, predictions, decisions, outcomes, legacyRecords: legacy.count, allowlistedInstruments: instruments.count };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = autonomyRoutes;
|
||||||
+14
-1
@@ -6,7 +6,20 @@ let statusCacheAt = 0;
|
|||||||
const STATUS_CACHE_TTL_MS = 30 * 1000;
|
const STATUS_CACHE_TTL_MS = 30 * 1000;
|
||||||
|
|
||||||
async function statusRoutes(fastify) {
|
async function statusRoutes(fastify) {
|
||||||
fastify.get('/status', async () => {
|
fastify.get('/status', async (request) => {
|
||||||
|
const deep = String(request.query?.deep || '').toLowerCase() === 'true';
|
||||||
|
if (!deep) {
|
||||||
|
const sequence = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'articles'").get();
|
||||||
|
return {
|
||||||
|
total: sequence ? sequence.seq : 0,
|
||||||
|
usable: null,
|
||||||
|
lastIngestionBySource: getLastIngestionBySource(),
|
||||||
|
bySource: null,
|
||||||
|
embeddingModels: null,
|
||||||
|
mode: 'lightweight',
|
||||||
|
deep_status_url: '/status?deep=true',
|
||||||
|
};
|
||||||
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (statusCache && now - statusCacheAt < STATUS_CACHE_TTL_MS) {
|
if (statusCache && now - statusCacheAt < STATUS_CACHE_TTL_MS) {
|
||||||
return statusCache;
|
return statusCache;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { enqueueJob, leaseNextJob, completeJob } = require('../src/autonomy/jobs');
|
||||||
|
const { normalizeProposal, acceptProposal } = require('../src/autonomy/coordinator');
|
||||||
|
const { calibrateOutcomes, cohortKey } = require('../src/autonomy/calibration');
|
||||||
|
const { decide } = require('../src/autonomy/policy');
|
||||||
|
const { validatePaperIntent, createSimulator } = require('../src/autonomy/execution');
|
||||||
|
const { calculateOutcome } = require('../src/autonomy/outcomes');
|
||||||
|
const { createOrderIntent } = require('../src/autonomy/orderIntents');
|
||||||
|
const { reconcileArchiveBatch, reconcileLiveBatch } = require('../workers/autonomyWorker');
|
||||||
|
|
||||||
|
test('autonomy schema and leased jobs are restart-safe', () => {
|
||||||
|
const db = new Database(':memory:');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
assert.equal(enqueueJob(db, {
|
||||||
|
jobType: 'enrich', lane: 'live', priority: 10, entityType: 'article', entityId: 42,
|
||||||
|
idempotencyKey: 'enrich:42',
|
||||||
|
}).inserted, true);
|
||||||
|
assert.equal(enqueueJob(db, {
|
||||||
|
jobType: 'enrich', lane: 'live', priority: 10, entityType: 'article', entityId: 42,
|
||||||
|
idempotencyKey: 'enrich:42',
|
||||||
|
}).inserted, false);
|
||||||
|
const job = leaseNextJob(db, 'test-worker');
|
||||||
|
assert.equal(job.lane, 'live');
|
||||||
|
assert.equal(completeJob(db, job.id, 'test-worker'), true);
|
||||||
|
assert.equal(db.prepare("SELECT status FROM autonomy_jobs WHERE id = ?").get(job.id).status, 'complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('coordinator proposals require evidence and contain no arbitrary numeric confidence', () => {
|
||||||
|
const normalized = normalizeProposal({ predictions: [{
|
||||||
|
instrument: 'nvda', direction: 'positive', event_type: 'supply_constraint',
|
||||||
|
horizon_days: 10, evidence_article_ids: [7],
|
||||||
|
}] }, { informationCutoff: '2026-01-01T00:00:00Z', model: 'test-model' });
|
||||||
|
assert.equal(normalized.predictions[0].instrument, 'NVDA');
|
||||||
|
assert.equal('probability' in normalized.predictions[0], false);
|
||||||
|
assert.throws(() => normalizeProposal({ predictions: [{ instrument: 'NVDA', direction: 'positive', horizon_days: 10 }] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepted proposal preserves evidence and creates immutable prediction', () => {
|
||||||
|
const archive = new Database(':memory:');
|
||||||
|
archive.exec('CREATE TABLE articles (id INTEGER PRIMARY KEY)');
|
||||||
|
archive.prepare('INSERT INTO articles (id) VALUES (?)').run(7);
|
||||||
|
const intelligence = new Database(':memory:');
|
||||||
|
initAutonomySchema(intelligence);
|
||||||
|
intelligence.prepare("INSERT INTO autonomy_instruments(symbol, broker, active, tradable) VALUES ('NVDA', 'test', 1, 1)").run();
|
||||||
|
const result = acceptProposal(intelligence, archive, {
|
||||||
|
predictions: [{ instrument: 'NVDA', direction: 'positive', event_type: 'earnings', horizon_days: 10, evidence_article_ids: [7] }],
|
||||||
|
}, { informationCutoff: '2026-01-01T00:00:00Z', strategyVersion: 'test' });
|
||||||
|
assert.equal(result.predictionCount, 1);
|
||||||
|
assert.deepEqual(JSON.parse(intelligence.prepare('SELECT evidence_article_ids FROM autonomy_predictions').get().evidence_article_ids), [7]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calibration and policy abstain on insufficient evidence', () => {
|
||||||
|
const calibration = calibrateOutcomes([
|
||||||
|
{ excess_return: 0.02, direction_correct: 1 },
|
||||||
|
{ excess_return: -0.01, direction_correct: 0 },
|
||||||
|
]);
|
||||||
|
assert.equal(calibration.sampleSize, 2);
|
||||||
|
const result = decide({ ...calibration }, { minSampleSize: 30 });
|
||||||
|
assert.equal(result.action, 'ABSTAIN');
|
||||||
|
assert.equal(cohortKey({ sector: 'tech', eventType: 'earnings', horizonDays: 10, direction: 'positive' }), 'tech|earnings|10|positive');
|
||||||
|
assert.equal(decide({ direction: 'negative', probability: 0.8, expectedExcessReturn: -0.02, lowerReturn: -0.04, sampleSize: 40 }).action, 'SELL');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('paper execution is allowlisted, bounded and idempotent', () => {
|
||||||
|
const intent = validatePaperIntent({ decisionId: 12, instrument: 'NVDA', action: 'BUY', notional: 100 }, { tradable: true, maxNotional: 500 });
|
||||||
|
const broker = createSimulator();
|
||||||
|
assert.deepEqual(broker.submit(intent), broker.submit(intent));
|
||||||
|
assert.throws(() => validatePaperIntent({ decisionId: 13, instrument: 'PRIVATE', action: 'BUY', notional: 100 }, { tradable: false }));
|
||||||
|
assert.throws(() => validatePaperIntent({ decisionId: 14, instrument: 'NVDA', action: 'BUY', notional: 501 }, { tradable: true, maxNotional: 500 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('archive reconciliation prioritizes recent usable events and is bounded', () => {
|
||||||
|
const archive = new Database(':memory:');
|
||||||
|
archive.exec('CREATE TABLE articles (id INTEGER PRIMARY KEY, event_id INTEGER, ingested_at TEXT, content TEXT, has_embedding INTEGER)');
|
||||||
|
archive.prepare('INSERT INTO articles VALUES (1, 99, ?, ?, 1)').run(new Date().toISOString(), 'content');
|
||||||
|
const intelligence = new Database(':memory:');
|
||||||
|
initAutonomySchema(intelligence);
|
||||||
|
const result = reconcileArchiveBatch(archive, intelligence, 1);
|
||||||
|
assert.equal(result.scanned, 1);
|
||||||
|
const job = intelligence.prepare("SELECT lane, priority FROM autonomy_jobs WHERE job_type='coordinator_event'").get();
|
||||||
|
assert.equal(job.lane, 'live');
|
||||||
|
assert.equal(job.priority, 100);
|
||||||
|
const live = reconcileLiveBatch(archive, intelligence, 1);
|
||||||
|
assert.equal(live.scanned, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('outcome calculation uses benchmark-relative return', () => {
|
||||||
|
const outcome = calculateOutcome(
|
||||||
|
{ information_cutoff: '2026-01-02T00:00:00Z', horizon_days: 5, direction: 'positive' },
|
||||||
|
[{ date: '2026-01-02', close: 100 }, { date: '2026-01-09', close: 110 }],
|
||||||
|
[{ date: '2026-01-02', close: 100 }, { date: '2026-01-09', close: 105 }]
|
||||||
|
);
|
||||||
|
assert.equal(outcome.directionCorrect, 1);
|
||||||
|
assert.equal(outcome.excessReturn, 0.05);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('order intents require the explicit instrument allowlist', () => {
|
||||||
|
const db = new Database(':memory:');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
db.prepare("INSERT INTO autonomy_instruments(symbol, broker, active, tradable) VALUES ('NVDA', 'sim', 1, 1)").run();
|
||||||
|
const proposal = db.prepare(`INSERT INTO autonomy_proposals(payload, information_cutoff, status) VALUES ('{}', datetime('now'), 'accepted')`).run();
|
||||||
|
const prediction = db.prepare(`INSERT INTO autonomy_predictions(proposal_id, instrument, direction, event_type, horizon_days, information_cutoff, evidence_article_ids, learning_eligible, strategy_version) VALUES (?, 'NVDA', 'positive', 'test', 10, datetime('now'), '[1]', 1, 'test')`).run(proposal.lastInsertRowid);
|
||||||
|
const decision = db.prepare(`INSERT INTO autonomy_decisions(prediction_id, action, rationale, strategy_version) VALUES (?, 'BUY', 'test', 'test')`).run(prediction.lastInsertRowid);
|
||||||
|
const intent = createOrderIntent(db, decision.lastInsertRowid, 100, { maxNotional: 100 });
|
||||||
|
assert.equal(intent.side, 'buy');
|
||||||
|
assert.equal(db.prepare('SELECT status FROM autonomy_order_intents').get().status, 'shadow');
|
||||||
|
});
|
||||||
+111
-12
@@ -2,6 +2,11 @@ const https = require("https");
|
|||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
|
||||||
const { findMatchedCompaniesByEmbedding } = require("./embeddings");
|
const { findMatchedCompaniesByEmbedding } = require("./embeddings");
|
||||||
|
const { getPriceContext, formatPriceContext } = require("./priceContext");
|
||||||
|
|
||||||
|
const REPROCESS_MIN_NEW_ARTICLES = 3;
|
||||||
|
const REPROCESS_COOLDOWN_HOURS = 6;
|
||||||
|
|
||||||
|
|
||||||
async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||||
const loopDelay = config.workers?.augorLoopDelayMs ?? 1500;
|
const loopDelay = config.workers?.augorLoopDelayMs ?? 1500;
|
||||||
@@ -11,6 +16,26 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
SELECT * FROM article_queue WHERE status = 'pending' LIMIT 1
|
SELECT * FROM article_queue WHERE status = 'pending' LIMIT 1
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const getProcessingState = intelligenceDb.prepare(
|
||||||
|
"SELECT last_processed_at, articles_at_last_run FROM event_processing_state WHERE event_id = ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
const upsertProcessingState = intelligenceDb.prepare(`
|
||||||
|
INSERT INTO event_processing_state (event_id, last_processed_at, articles_at_last_run)
|
||||||
|
VALUES (?, CURRENT_TIMESTAMP, ?)
|
||||||
|
ON CONFLICT(event_id) DO UPDATE SET
|
||||||
|
last_processed_at = CURRENT_TIMESTAMP,
|
||||||
|
articles_at_last_run = excluded.articles_at_last_run
|
||||||
|
`);
|
||||||
|
|
||||||
|
const getCompanyAccuracy = intelligenceDb.prepare(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(correct_10d) as correct
|
||||||
|
FROM prediction_outcomes
|
||||||
|
WHERE company_id = ? AND correct_10d IS NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
const recordEvent = intelligenceDb.prepare(
|
const recordEvent = intelligenceDb.prepare(
|
||||||
`INSERT INTO worker_events (worker) VALUES ('augor')`
|
`INSERT INTO worker_events (worker) VALUES ('augor')`
|
||||||
);
|
);
|
||||||
@@ -45,8 +70,8 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
const insertPrediction = intelligenceDb.prepare(`
|
const insertPrediction = intelligenceDb.prepare(`
|
||||||
INSERT INTO event_predictions (event_id, company_id, type, direction, magnitude, timeframe, rationale, event_date)
|
INSERT INTO event_predictions (event_id, company_id, type, direction, magnitude, timeframe, rationale, probability, event_date)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const getEventDate = archiveDb.prepare(`
|
const getEventDate = archiveDb.prepare(`
|
||||||
@@ -66,7 +91,6 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
const queueRow = getPending.get();
|
const queueRow = getPending.get();
|
||||||
|
|
||||||
if (!queueRow) {
|
if (!queueRow) {
|
||||||
await sleep(loopDelay);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +122,29 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
|
|
||||||
const eventArticleIds = eventArticles.map(a => a.id);
|
const eventArticleIds = eventArticles.map(a => a.id);
|
||||||
|
|
||||||
|
|
||||||
|
// event-level batching guard — skip re-processing if we already ran on this event recently
|
||||||
|
// and not enough new articles have arrived to justify another LLM call
|
||||||
|
const state = getProcessingState.get(eventId);
|
||||||
|
if (state && state.last_processed_at) {
|
||||||
|
const newArticles = eventArticles.length - state.articles_at_last_run;
|
||||||
|
const lastRunMs = new Date(state.last_processed_at + "Z").getTime();
|
||||||
|
const hoursSince = (Date.now() - lastRunMs) / 3_600_000;
|
||||||
|
|
||||||
|
if (newArticles < REPROCESS_MIN_NEW_ARTICLES && hoursSince < REPROCESS_COOLDOWN_HOURS) {
|
||||||
|
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
||||||
|
console.log(`[augor] event ${eventId} — only ${newArticles} new articles in ${hoursSince.toFixed(1)}h, skipping re-process`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const matchedCompanies = findMatchedCompaniesByEmbedding(
|
const matchedCompanies = findMatchedCompaniesByEmbedding(
|
||||||
eventArticleIds, archiveDb, intelligenceDb, config
|
eventArticleIds, archiveDb, intelligenceDb, config
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matchedCompanies.length === 0) {
|
if (matchedCompanies.length === 0) {
|
||||||
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
||||||
|
upsertProcessingState.run(eventId, eventArticles.length);
|
||||||
console.log(`[augor] event ${eventId} — no company match, skipped`);
|
console.log(`[augor] event ${eventId} — no company match, skipped`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -114,6 +155,7 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
|
|
||||||
const eventDateRow = getEventDate.get(eventId);
|
const eventDateRow = getEventDate.get(eventId);
|
||||||
const eventDate = eventDateRow ? eventDateRow.pub_date_effective : null;
|
const eventDate = eventDateRow ? eventDateRow.pub_date_effective : null;
|
||||||
|
const eventDateOnly = eventDate ? eventDate.slice(0, 10) : null;
|
||||||
|
|
||||||
const articleText = eventArticles.map((a, i) => {
|
const articleText = eventArticles.map((a, i) => {
|
||||||
const body = (a.content || a.description || "").slice(0, 2000);
|
const body = (a.content || a.description || "").slice(0, 2000);
|
||||||
@@ -130,9 +172,30 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
factsBlock = `Known facts about ${company.name}:\n${lines}`;
|
factsBlock = `Known facts about ${company.name}:\n${lines}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await callLlm(llmConfig, buildPrompt(company.name, event.title, articleText, factsBlock));
|
|
||||||
|
// pull live market context for the company at the time of the event
|
||||||
|
let priceBlock = null;
|
||||||
|
if (company.ticker && eventDateOnly) {
|
||||||
|
try {
|
||||||
|
const snapshot = await getPriceContext(intelligenceDb, company.ticker, eventDateOnly);
|
||||||
|
priceBlock = formatPriceContext(snapshot, company.ticker);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// historical accuracy of past predictions for this company
|
||||||
|
let accuracyBlock = null;
|
||||||
|
const acc = getCompanyAccuracy.get(company.id);
|
||||||
|
if (acc && acc.total >= 5) {
|
||||||
|
const pct = (acc.correct / acc.total * 100).toFixed(0);
|
||||||
|
accuracyBlock = `Past prediction accuracy for ${company.name}: ${pct}% over ${acc.total} evaluated calls.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await callLlm(llmConfig, buildPrompt(company.name, event.title, articleText, factsBlock, priceBlock, accuracyBlock));
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
const seenPreds = new Set();
|
||||||
|
|
||||||
const writeAll = intelligenceDb.transaction(() => {
|
const writeAll = intelligenceDb.transaction(() => {
|
||||||
for (const r of (result.knowledge?.relationships || [])) {
|
for (const r of (result.knowledge?.relationships || [])) {
|
||||||
insertKnowledge.run(eventId, company.id, "relationship", JSON.stringify(r), eventDate);
|
insertKnowledge.run(eventId, company.id, "relationship", JSON.stringify(r), eventDate);
|
||||||
@@ -145,7 +208,21 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const p of (result.predictions || [])) {
|
for (const p of (result.predictions || [])) {
|
||||||
insertPrediction.run(eventId, company.id, p.type, p.direction, p.magnitude, p.timeframe, p.rationale, eventDate);
|
// hard filter — neutral predictions are dead weight, skip them
|
||||||
|
if (p.direction === "neutral") continue;
|
||||||
|
if (p.direction !== "positive" && p.direction !== "negative") continue;
|
||||||
|
|
||||||
|
const key = `${p.type}|${p.direction}|${p.magnitude}|${p.timeframe}`;
|
||||||
|
if (seenPreds.has(key)) continue;
|
||||||
|
seenPreds.add(key);
|
||||||
|
|
||||||
|
const prob = typeof p.probability === "number" && p.probability >= 0 && p.probability <= 1
|
||||||
|
? p.probability
|
||||||
|
: null;
|
||||||
|
|
||||||
|
insertPrediction.run(
|
||||||
|
eventId, company.id, p.type, p.direction, p.magnitude, p.timeframe, p.rationale, prob, eventDate
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,6 +235,7 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
||||||
|
upsertProcessingState.run(eventId, eventArticles.length);
|
||||||
recordEvent.run();
|
recordEvent.run();
|
||||||
pruneCounter++;
|
pruneCounter++;
|
||||||
if (pruneCounter >= 100) { pruneEvents.run(); pruneCounter = 0; }
|
if (pruneCounter >= 100) { pruneEvents.run(); pruneCounter = 0; }
|
||||||
@@ -165,23 +243,44 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[augor] error:", err.message);
|
console.error("[augor] error:", err.message);
|
||||||
|
} finally {
|
||||||
|
// Enforce pacing on every path, including the many early `continue`
|
||||||
|
// branches for skipped or already-processed events.
|
||||||
await sleep(loopDelay);
|
await sleep(loopDelay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPrompt(companyName, eventTitle, articleText, factsBlock) {
|
function buildPrompt(companyName, eventTitle, articleText, factsBlock, priceBlock, accuracyBlock) {
|
||||||
const factsPart = factsBlock ? `${factsBlock}\n\n` : "";
|
const factsPart = factsBlock ? `${factsBlock}\n\n` : "";
|
||||||
|
const pricePart = priceBlock ? `Market context for ${companyName}:\n${priceBlock}\n\n` : "";
|
||||||
|
const accPart = accuracyBlock ? `${accuracyBlock}\n\n` : "";
|
||||||
|
|
||||||
return `You are a financial intelligence analyst focused on ${companyName}. Always respond in English regardless of the language of the input articles.
|
return `You are a financial intelligence analyst. Always respond in English.
|
||||||
|
|
||||||
${factsPart}Assess the impact of the following news event on ${companyName} given what you already know about the company.
|
${factsPart}${pricePart}${accPart}Event: ${eventTitle}
|
||||||
|
|
||||||
Event: ${eventTitle}
|
|
||||||
|
|
||||||
${articleText}
|
${articleText}
|
||||||
|
|
||||||
Return JSON only — no explanation. Shape:
|
Analyze the impact of this event on ${companyName}. Return JSON only — no explanation.
|
||||||
|
|
||||||
|
Strict rules — read carefully:
|
||||||
|
- predictions must be directly caused by THIS specific event — no speculation, no priced-in narrative
|
||||||
|
- only emit a prediction if the evidence is unambiguous AND the effect on ${companyName} is concrete and quantifiable
|
||||||
|
- the default answer is no prediction. an empty predictions array is the correct output for most news. only emit one when the event clearly moves the needle
|
||||||
|
- never emit two predictions that share type+direction+magnitude+timeframe — collapse them into one
|
||||||
|
- direction must be "positive" or "negative" only — no neutral predictions, ever. if the impact is unclear, emit nothing
|
||||||
|
- magnitude:
|
||||||
|
"high" = major revenue/market-share shift, expected >5% stock move
|
||||||
|
"medium" = measurable but limited, expected 1-5% stock move
|
||||||
|
omit any prediction that doesnt clear the medium bar
|
||||||
|
- timeframe:
|
||||||
|
"short" = days to 2 weeks (use sparingly — short-horizon predictions are unreliable)
|
||||||
|
"medium" = 2 weeks to 3 months
|
||||||
|
"long" = 3+ months — preferred when the thesis is structural
|
||||||
|
- probability: your honest calibrated probability that the directional call is correct over the stated timeframe, as a number between 0.5 and 0.95. if you cant honestly assign >= 0.6, dont emit the prediction
|
||||||
|
- if the market context shows the stock has already moved sharply (>10% in 30 days), be sceptical that this event adds new information — the move may already be priced in
|
||||||
|
|
||||||
{
|
{
|
||||||
"knowledge": {
|
"knowledge": {
|
||||||
"relationships": [
|
"relationships": [
|
||||||
@@ -195,7 +294,7 @@ Return JSON only — no explanation. Shape:
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"predictions": [
|
"predictions": [
|
||||||
{ "type": "market_share|stock_price|competitive_position|other", "direction": "positive|negative|neutral", "magnitude": "high|medium|low", "timeframe": "short|medium|long", "rationale": "string" }
|
{ "type": "market_share|stock_price|competitive_position|other", "direction": "positive|negative", "magnitude": "high|medium", "timeframe": "short|medium|long", "probability": 0.0, "rationale": "string" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { runAutonomyWorker } = require('./autonomyWorker');
|
||||||
|
|
||||||
|
runAutonomyWorker({
|
||||||
|
archivePath: process.env.DURIIN_DB || path.resolve('/data/archive.sqlite'),
|
||||||
|
intelligencePath: process.env.INTELLIGENCE_DB || path.resolve('/data/intelligence.sqlite'),
|
||||||
|
pollMs: Number(process.env.AUTONOMY_POLL_MS) || 1000,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[autonomy] fatal:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { enqueueJob, leaseNextJob, completeJob, failJob } = require('../src/autonomy/jobs');
|
||||||
|
|
||||||
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
||||||
|
|
||||||
|
function reconcileArchiveBatch(archiveDb, intelligenceDb, batchSize = 250) {
|
||||||
|
const cursor = intelligenceDb.prepare("SELECT value FROM autonomy_cursors WHERE key = 'archive_reconcile'").get();
|
||||||
|
const afterId = cursor ? cursor.value : 0;
|
||||||
|
const rows = archiveDb.prepare(`
|
||||||
|
SELECT id, event_id, ingested_at, content, has_embedding
|
||||||
|
FROM articles
|
||||||
|
WHERE id > ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(afterId, batchSize);
|
||||||
|
if (!rows.length) {
|
||||||
|
intelligenceDb.prepare(`
|
||||||
|
INSERT INTO autonomy_cursors(key, value) VALUES ('archive_reconcile', 0)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = 0, updated_at = datetime('now')
|
||||||
|
`).run();
|
||||||
|
return { scanned: 0, nextCursor: 0, reset: true };
|
||||||
|
}
|
||||||
|
const enqueue = intelligenceDb.transaction(() => {
|
||||||
|
for (const row of rows) {
|
||||||
|
const isLive = row.ingested_at && Date.now() - Date.parse(row.ingested_at) <= 48 * 60 * 60 * 1000;
|
||||||
|
const readyForIntelligence = row.event_id && row.content && row.has_embedding;
|
||||||
|
if (readyForIntelligence) {
|
||||||
|
enqueueJob(intelligenceDb, {
|
||||||
|
jobType: 'coordinator_event',
|
||||||
|
lane: isLive ? 'live' : 'historical',
|
||||||
|
priority: isLive ? 100 : 10,
|
||||||
|
entityType: 'event',
|
||||||
|
entityId: row.event_id,
|
||||||
|
idempotencyKey: `coordinator_event:${row.event_id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intelligenceDb.prepare(`
|
||||||
|
INSERT INTO autonomy_cursors(key, value) VALUES ('archive_reconcile', ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')
|
||||||
|
`).run(rows[rows.length - 1].id);
|
||||||
|
});
|
||||||
|
enqueue();
|
||||||
|
return { scanned: rows.length, nextCursor: rows[rows.length - 1].id, reset: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileLiveBatch(archiveDb, intelligenceDb, batchSize = 250) {
|
||||||
|
const rows = archiveDb.prepare(`
|
||||||
|
SELECT id, event_id, ingested_at, content, has_embedding
|
||||||
|
FROM articles
|
||||||
|
WHERE ingested_at >= datetime('now', '-48 hours')
|
||||||
|
ORDER BY ingested_at DESC, id DESC
|
||||||
|
LIMIT ?
|
||||||
|
`).all(batchSize);
|
||||||
|
let queued = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!row.event_id || !row.content || !row.has_embedding) continue;
|
||||||
|
const result = enqueueJob(intelligenceDb, {
|
||||||
|
jobType: 'coordinator_event', lane: 'live', priority: 100,
|
||||||
|
entityType: 'event', entityId: row.event_id,
|
||||||
|
idempotencyKey: `coordinator_event:${row.event_id}`,
|
||||||
|
});
|
||||||
|
if (result.inserted) queued++;
|
||||||
|
}
|
||||||
|
return { scanned: rows.length, queued };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAutonomyWorker({ archivePath, intelligencePath, workerId = `autonomy-${os.hostname()}-${process.pid}`, pollMs = 1000 } = {}) {
|
||||||
|
const archiveDb = new Database(archivePath, { readonly: true });
|
||||||
|
const intelligenceDb = new Database(intelligencePath);
|
||||||
|
intelligenceDb.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(intelligenceDb);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const job = leaseNextJob(intelligenceDb, workerId, 120);
|
||||||
|
if (!job) { await sleep(pollMs); continue; }
|
||||||
|
try {
|
||||||
|
if (job.job_type === 'reconcile_archive') {
|
||||||
|
reconcileLiveBatch(archiveDb, intelligenceDb);
|
||||||
|
reconcileArchiveBatch(archiveDb, intelligenceDb);
|
||||||
|
// Keep the reconciler alive as a bounded maintenance loop.
|
||||||
|
enqueueJob(intelligenceDb, {
|
||||||
|
jobType: 'reconcile_archive', lane: 'maintenance', priority: 100,
|
||||||
|
entityType: 'archive', entityId: 'archive',
|
||||||
|
idempotencyKey: `reconcile_archive:${Date.now()}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
completeJob(intelligenceDb, job.id, workerId);
|
||||||
|
} catch (error) {
|
||||||
|
failJob(intelligenceDb, job.id, workerId, error);
|
||||||
|
}
|
||||||
|
await sleep(pollMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { reconcileArchiveBatch, reconcileLiveBatch, runAutonomyWorker };
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { runCalibrationWorker } = require('./calibrationWorker');
|
||||||
|
|
||||||
|
runCalibrationWorker({
|
||||||
|
intelligencePath: process.env.INTELLIGENCE_DB || path.resolve('/data/intelligence.sqlite'),
|
||||||
|
pollMs: Number(process.env.AUTONOMY_CALIBRATION_POLL_MS) || 60000,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[calibration] fatal:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { calibrateOutcomes, cohortKey } = require('../src/autonomy/calibration');
|
||||||
|
const { decide } = require('../src/autonomy/policy');
|
||||||
|
|
||||||
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
||||||
|
|
||||||
|
function refreshCalibration(db, version = `cal-${Date.now()}`) {
|
||||||
|
const groups = db.prepare(`
|
||||||
|
SELECT p.direction, p.event_type, p.horizon_days, o.*
|
||||||
|
FROM autonomy_predictions p
|
||||||
|
JOIN autonomy_outcomes o ON o.prediction_id = p.id
|
||||||
|
WHERE p.status = 'resolved' AND p.learning_eligible = 1
|
||||||
|
`).all().reduce((map, row) => {
|
||||||
|
const key = cohortKey({ direction: row.direction, eventType: row.event_type, horizonDays: row.horizon_days });
|
||||||
|
if (!map.has(key)) map.set(key, []);
|
||||||
|
map.get(key).push(row);
|
||||||
|
return map;
|
||||||
|
}, new Map());
|
||||||
|
const insert = db.prepare(`
|
||||||
|
INSERT INTO autonomy_calibration_snapshots
|
||||||
|
(cohort_key, sample_size, effective_sample_size, directional_probability,
|
||||||
|
expected_excess_return, lower_return, upper_return, parent_cohort_key, version)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
for (const [key, rows] of groups) {
|
||||||
|
if (db.prepare("SELECT 1 FROM autonomy_calibration_snapshots WHERE cohort_key = ? AND version = ?").get(key, version)) continue;
|
||||||
|
const result = calibrateOutcomes(rows);
|
||||||
|
insert.run(key, result.sampleSize, result.effectiveSampleSize, result.directionalProbability,
|
||||||
|
result.expectedExcessReturn, result.lowerReturn, result.upperReturn, null, version);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
return groups.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDecisions(db, strategyVersion = 'autonomy-1') {
|
||||||
|
const predictions = db.prepare(`
|
||||||
|
SELECT p.* FROM autonomy_predictions p
|
||||||
|
LEFT JOIN autonomy_decisions d ON d.prediction_id = p.id
|
||||||
|
WHERE d.prediction_id IS NULL AND p.status = 'open'
|
||||||
|
`).all();
|
||||||
|
const latest = db.prepare(`
|
||||||
|
SELECT * FROM autonomy_calibration_snapshots
|
||||||
|
WHERE cohort_key = ? ORDER BY created_at DESC, id DESC LIMIT 1
|
||||||
|
`);
|
||||||
|
const insert = db.prepare(`
|
||||||
|
INSERT INTO autonomy_decisions
|
||||||
|
(prediction_id, action, calibrated_probability, expected_excess_return, rationale, strategy_version)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
let created = 0;
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
for (const prediction of predictions) {
|
||||||
|
const key = cohortKey({ direction: prediction.direction, eventType: prediction.event_type, horizonDays: prediction.horizon_days });
|
||||||
|
const calibration = latest.get(key);
|
||||||
|
const decision = calibration
|
||||||
|
? decide({ ...calibration, direction: prediction.direction }, { minSampleSize: 30 })
|
||||||
|
: { action: 'ABSTAIN', rationale: 'calibration unavailable' };
|
||||||
|
insert.run(prediction.id, decision.action, calibration?.directional_probability || null,
|
||||||
|
calibration?.expected_excess_return || null, decision.rationale, strategyVersion);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCalibrationWorker({ intelligencePath, pollMs = 60000, workerId = `calibration-${os.hostname()}-${process.pid}` } = {}) {
|
||||||
|
const db = new Database(intelligencePath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const state = db.prepare('SELECT COUNT(*) AS count, COALESCE(MAX(prediction_id), 0) AS max_id FROM autonomy_outcomes').get();
|
||||||
|
const groups = refreshCalibration(db, `cal-${state.count}-${state.max_id}`);
|
||||||
|
const decisions = createDecisions(db);
|
||||||
|
if (groups || decisions) console.log(`[${workerId}] calibration groups=${groups} decisions=${decisions}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${workerId}] calibration error:`, error.message);
|
||||||
|
}
|
||||||
|
await sleep(pollMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { refreshCalibration, createDecisions, runCalibrationWorker };
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { runCoordinatorWorker } = require('./coordinatorWorker');
|
||||||
|
|
||||||
|
runCoordinatorWorker({
|
||||||
|
archivePath: process.env.DURIIN_DB || path.resolve('/data/archive.sqlite'),
|
||||||
|
intelligencePath: process.env.INTELLIGENCE_DB || path.resolve('/data/intelligence.sqlite'),
|
||||||
|
pollMs: Number(process.env.AUTONOMY_POLL_MS) || 1000,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[coordinator] fatal:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { leaseNextJob, completeJob, failJob } = require('../src/autonomy/jobs');
|
||||||
|
const { callCoordinator } = require('../src/autonomy/llm');
|
||||||
|
const { acceptProposal, recordRejectedProposal } = require('../src/autonomy/coordinator');
|
||||||
|
|
||||||
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
||||||
|
|
||||||
|
function loadConfig() {
|
||||||
|
const configPath = path.resolve(process.env.DURIIN_CONFIG || path.join(__dirname, '..', 'config.json'));
|
||||||
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
require('dotenv').config({ path: path.resolve(path.dirname(configPath), '.env') });
|
||||||
|
raw.openRouter = { ...(raw.openRouter || {}) };
|
||||||
|
if (process.env.OPEN_ROUTER_API_KEY) raw.openRouter.apiKey = process.env.OPEN_ROUTER_API_KEY;
|
||||||
|
if (process.env.OPEN_ROUTER_LLM_MODEL) raw.openRouter.llmModel = process.env.OPEN_ROUTER_LLM_MODEL;
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrompt(event, articles) {
|
||||||
|
const evidence = articles.map((article, index) =>
|
||||||
|
`[Evidence ${index + 1}] article_id=${article.id}\nTitle: ${article.title}\n${String(article.content || article.description || '').slice(0, 4000)}`
|
||||||
|
).join('\n\n---\n\n');
|
||||||
|
return `Event title: ${event.title}\n\n${evidence}\n\nReturn JSON only in this shape:\n${JSON.stringify({ predictions: [{
|
||||||
|
instrument: 'NVDA', direction: 'positive|negative', event_type: 'stable_enum',
|
||||||
|
causal_channel: 'short description', horizon_days: 10,
|
||||||
|
evidence_article_ids: [123], invalidation_condition: 'condition',
|
||||||
|
}] }, null, 2)}\n\nUse only instruments and evidence directly supported by the articles. Return an empty predictions array when there is no clear, tradable hypothesis. Never include probabilities, returns, confidence, position sizes, or actions.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCoordinatorWorker({ archivePath, intelligencePath, workerId = `coordinator-${os.hostname()}-${process.pid}`, pollMs = 1000 } = {}) {
|
||||||
|
const archiveDb = new Database(archivePath, { readonly: true });
|
||||||
|
const intelligenceDb = new Database(intelligencePath);
|
||||||
|
intelligenceDb.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(intelligenceDb);
|
||||||
|
const config = loadConfig();
|
||||||
|
while (true) {
|
||||||
|
const job = leaseNextJob(intelligenceDb, workerId, 180, ['coordinator_event']);
|
||||||
|
if (!job) { await sleep(pollMs); continue; }
|
||||||
|
try {
|
||||||
|
const event = archiveDb.prepare('SELECT id, title FROM events WHERE id = ?').get(job.entity_id);
|
||||||
|
if (!event) throw new Error(`event ${job.entity_id} does not exist`);
|
||||||
|
const articles = archiveDb.prepare(`
|
||||||
|
SELECT id, title, description, content, pub_date_effective
|
||||||
|
FROM articles
|
||||||
|
WHERE event_id = ? AND content IS NOT NULL AND content != '' AND is_index_page = 0
|
||||||
|
ORDER BY pub_date_effective ASC, id ASC LIMIT 25
|
||||||
|
`).all(job.entity_id);
|
||||||
|
const allowlisted = intelligenceDb.prepare(
|
||||||
|
"SELECT 1 FROM autonomy_instruments WHERE active=1 AND tradable=1 LIMIT 1"
|
||||||
|
).get();
|
||||||
|
if (!allowlisted) throw new Error('no tradable instruments are allowlisted');
|
||||||
|
const historical = job.lane === 'historical';
|
||||||
|
const informationCutoff = historical
|
||||||
|
? (articles.map((article) => article.pub_date_effective).filter(Boolean).sort().pop() || new Date().toISOString())
|
||||||
|
: new Date().toISOString();
|
||||||
|
const raw = await callCoordinator(config, buildPrompt(event, articles));
|
||||||
|
try {
|
||||||
|
acceptProposal(intelligenceDb, archiveDb, raw, {
|
||||||
|
eventId: event.id,
|
||||||
|
informationCutoff,
|
||||||
|
model: config.openRouter.llmModel || 'unknown',
|
||||||
|
promptVersion: 'coordinator-1',
|
||||||
|
strategyVersion: 'autonomy-1',
|
||||||
|
learningEligible: !historical,
|
||||||
|
});
|
||||||
|
} catch (validationError) {
|
||||||
|
recordRejectedProposal(intelligenceDb, raw, {
|
||||||
|
eventId: event.id,
|
||||||
|
informationCutoff,
|
||||||
|
model: config.openRouter.llmModel || 'unknown',
|
||||||
|
promptVersion: 'coordinator-1',
|
||||||
|
learningEligible: !historical,
|
||||||
|
}, validationError.message);
|
||||||
|
}
|
||||||
|
completeJob(intelligenceDb, job.id, workerId);
|
||||||
|
} catch (error) {
|
||||||
|
failJob(intelligenceDb, job.id, workerId, error);
|
||||||
|
}
|
||||||
|
await sleep(pollMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildPrompt, runCoordinatorWorker };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const Database = require("better-sqlite3");
|
const Database = require("better-sqlite3");
|
||||||
const sqliteVec = require("sqlite-vec");
|
const sqliteVec = require("sqlite-vec");
|
||||||
|
const { initAutonomySchema } = require("../src/autonomy/schema");
|
||||||
|
|
||||||
let archiveDb = null;
|
let archiveDb = null;
|
||||||
let intelligenceDb = null;
|
let intelligenceDb = null;
|
||||||
@@ -17,6 +18,7 @@ function getIntelligenceDb(dbPath) {
|
|||||||
if (!intelligenceDb) {
|
if (!intelligenceDb) {
|
||||||
intelligenceDb = new Database(dbPath);
|
intelligenceDb = new Database(dbPath);
|
||||||
intelligenceDb.pragma("journal_mode = WAL");
|
intelligenceDb.pragma("journal_mode = WAL");
|
||||||
|
initAutonomySchema(intelligenceDb);
|
||||||
}
|
}
|
||||||
return intelligenceDb;
|
return intelligenceDb;
|
||||||
}
|
}
|
||||||
@@ -109,6 +111,7 @@ function runMigrations(db) {
|
|||||||
function runColumnMigrations(db) {
|
function runColumnMigrations(db) {
|
||||||
try { db.exec("ALTER TABLE event_predictions ADD COLUMN event_date TEXT"); } catch (_) {}
|
try { db.exec("ALTER TABLE event_predictions ADD COLUMN event_date TEXT"); } catch (_) {}
|
||||||
try { db.exec("ALTER TABLE event_knowledge ADD COLUMN event_date TEXT"); } catch (_) {}
|
try { db.exec("ALTER TABLE event_knowledge ADD COLUMN event_date TEXT"); } catch (_) {}
|
||||||
|
try { db.exec("ALTER TABLE event_predictions ADD COLUMN probability REAL"); } catch (_) {}
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS worker_events (
|
CREATE TABLE IF NOT EXISTS worker_events (
|
||||||
@@ -137,6 +140,49 @@ function runColumnMigrations(db) {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// tracks last-processed state per event so augor doesnt redundantly re-run on every new article
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS event_processing_state (
|
||||||
|
event_id INTEGER PRIMARY KEY,
|
||||||
|
last_processed_at DATETIME,
|
||||||
|
articles_at_last_run INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// cached daily price snapshots so the augor prompt can include real market context
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS price_snapshots (
|
||||||
|
ticker TEXT NOT NULL,
|
||||||
|
as_of TEXT NOT NULL,
|
||||||
|
price REAL,
|
||||||
|
price_30d_ago REAL,
|
||||||
|
price_90d_ago REAL,
|
||||||
|
vol_30d REAL,
|
||||||
|
fetched_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (ticker, as_of)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// outcomes — actual realized returns for each prediction, populated by the outcome worker
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS prediction_outcomes (
|
||||||
|
prediction_id INTEGER PRIMARY KEY,
|
||||||
|
company_id INTEGER,
|
||||||
|
ticker TEXT,
|
||||||
|
event_date TEXT,
|
||||||
|
price_0 REAL,
|
||||||
|
price_5d REAL,
|
||||||
|
price_10d REAL,
|
||||||
|
r5 REAL,
|
||||||
|
r10 REAL,
|
||||||
|
correct_5d INTEGER,
|
||||||
|
correct_10d INTEGER,
|
||||||
|
evaluated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_prediction_outcomes_company ON prediction_outcomes (company_id);
|
||||||
|
`);
|
||||||
|
|
||||||
// prune rows older than 1 hour so the table doesnt grow unbounded
|
// prune rows older than 1 hour so the table doesnt grow unbounded
|
||||||
db.exec(`DELETE FROM worker_events WHERE completed_at < datetime('now', '-1 hour')`);
|
db.exec(`DELETE FROM worker_events WHERE completed_at < datetime('now', '-1 hour')`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { runExecutionWorker } = require('./executionWorker');
|
||||||
|
|
||||||
|
runExecutionWorker({
|
||||||
|
intelligencePath: process.env.INTELLIGENCE_DB || path.resolve('/data/intelligence.sqlite'),
|
||||||
|
pollMs: Number(process.env.AUTONOMY_EXECUTION_POLL_MS) || 10000,
|
||||||
|
mode: process.env.AUTONOMY_EXECUTION_MODE || 'shadow',
|
||||||
|
notional: Number(process.env.AUTONOMY_DEFAULT_NOTIONAL) || 100,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[execution] fatal:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { createOrderIntent } = require('../src/autonomy/orderIntents');
|
||||||
|
const { createAlpacaPaperClient } = require('../src/brokers/alpacaPaper');
|
||||||
|
|
||||||
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
||||||
|
|
||||||
|
async function runExecutionWorker({ intelligencePath, pollMs = 10000, mode = 'shadow', notional = 100, workerId = `execution-${os.hostname()}-${process.pid}` } = {}) {
|
||||||
|
if (!['shadow', 'paper'].includes(mode)) throw new Error(`unsupported execution mode: ${mode}`);
|
||||||
|
const db = new Database(intelligencePath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
const paperClient = mode === 'paper'
|
||||||
|
? createAlpacaPaperClient({ keyId: process.env.ALPACA_PAPER_KEY_ID, secretKey: process.env.ALPACA_PAPER_SECRET_KEY })
|
||||||
|
: null;
|
||||||
|
while (true) {
|
||||||
|
if (paperClient) {
|
||||||
|
try {
|
||||||
|
const [account, positions, orders] = await Promise.all([
|
||||||
|
paperClient.getAccount(), paperClient.getPositions(), paperClient.getOrders(),
|
||||||
|
]);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO autonomy_account_snapshots(broker, account_id, equity, cash, buying_power, payload)
|
||||||
|
VALUES ('alpaca-paper', ?, ?, ?, ?, ?)
|
||||||
|
`).run(account.id || null, Number(account.equity), Number(account.cash), Number(account.buying_power), JSON.stringify(account));
|
||||||
|
const insertPosition = db.prepare(`
|
||||||
|
INSERT INTO autonomy_position_snapshots(broker, instrument, quantity, market_value, unrealized_pl, payload)
|
||||||
|
VALUES ('alpaca-paper', ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
for (const position of positions || []) {
|
||||||
|
insertPosition.run(position.symbol, Number(position.qty), Number(position.market_value), Number(position.unrealized_pl), JSON.stringify(position));
|
||||||
|
}
|
||||||
|
for (const order of orders || []) {
|
||||||
|
if (!order.client_order_id) continue;
|
||||||
|
const mapped = { accepted: 'submitted', new: 'submitted', pending_new: 'submitted', partially_filled: 'partially_filled', filled: 'filled', canceled: 'cancelled', cancelled: 'cancelled', rejected: 'rejected' }[order.status];
|
||||||
|
if (!mapped) continue;
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE autonomy_order_intents SET status=?, broker_order_id=?, updated_at=datetime('now')
|
||||||
|
WHERE client_order_id=?
|
||||||
|
`).run(mapped, order.id || null, order.client_order_id);
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO autonomy_broker_events(broker, event_type, broker_id, payload)
|
||||||
|
VALUES ('alpaca-paper', ?, ?, ?)
|
||||||
|
`).run(order.status, order.id || null, JSON.stringify(order));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${workerId}] broker reconciliation:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const decisions = db.prepare(`
|
||||||
|
SELECT d.id FROM autonomy_decisions d
|
||||||
|
LEFT JOIN autonomy_order_intents oi ON oi.decision_id = d.id
|
||||||
|
WHERE oi.id IS NULL AND d.action IN ('BUY', 'SELL')
|
||||||
|
ORDER BY d.created_at ASC LIMIT 25
|
||||||
|
`).all();
|
||||||
|
for (const decision of decisions) {
|
||||||
|
try {
|
||||||
|
const intent = createOrderIntent(db, decision.id, notional, { tradable: true, maxNotional: notional });
|
||||||
|
if (mode === 'paper') db.prepare("UPDATE autonomy_order_intents SET status='pending', updated_at=datetime('now') WHERE client_order_id=?").run(intent.clientOrderId);
|
||||||
|
console.log(`[${workerId}] ${mode} intent ${intent.clientOrderId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${workerId}] decision ${decision.id}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (paperClient) {
|
||||||
|
const pending = db.prepare("SELECT * FROM autonomy_order_intents WHERE status='pending' ORDER BY created_at ASC LIMIT 25").all();
|
||||||
|
for (const intent of pending) {
|
||||||
|
try {
|
||||||
|
let order;
|
||||||
|
try { order = await paperClient.getOrderByClientId(intent.client_order_id); } catch (error) {
|
||||||
|
if (error.status !== 404) throw error;
|
||||||
|
}
|
||||||
|
if (!order) {
|
||||||
|
order = await paperClient.submitOrder({
|
||||||
|
symbol: intent.instrument,
|
||||||
|
notional: String(intent.notional),
|
||||||
|
side: intent.side,
|
||||||
|
type: 'market',
|
||||||
|
time_in_force: 'day',
|
||||||
|
client_order_id: intent.client_order_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE autonomy_order_intents
|
||||||
|
SET status = ?, broker_order_id = ?, updated_at = datetime('now')
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(order.status === 'filled' ? 'filled' : 'submitted', order.id || null, intent.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${workerId}] paper order ${intent.client_order_id}:`, error.message);
|
||||||
|
db.prepare("UPDATE autonomy_order_intents SET attempts=attempts+1, last_error=?, updated_at=datetime('now') WHERE id=?")
|
||||||
|
.run(String(error.message).slice(0, 1000), intent.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await sleep(pollMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runExecutionWorker };
|
||||||
@@ -8,6 +8,7 @@ const { ensureCompanyEmbeddings } = require("./embeddings");
|
|||||||
const { runConsolidationWorker } = require("./consolidationWorker");
|
const { runConsolidationWorker } = require("./consolidationWorker");
|
||||||
const { runGraphWorker } = require("./graphWorker");
|
const { runGraphWorker } = require("./graphWorker");
|
||||||
const { runSignalWorker } = require("./signalWorker");
|
const { runSignalWorker } = require("./signalWorker");
|
||||||
|
const { runOutcomeWorker } = require("./outcomeWorker");
|
||||||
|
|
||||||
|
|
||||||
require("dotenv").config({ path: path.resolve(__dirname, "../.env") });
|
require("dotenv").config({ path: path.resolve(__dirname, "../.env") });
|
||||||
@@ -77,6 +78,11 @@ runSignalWorker(archiveDb, intelligenceDb, config).catch(err => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
runOutcomeWorker(archiveDb, intelligenceDb, config).catch(err => {
|
||||||
|
console.error("[outcome] fatal:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
console.log("[intelligence] shutting down");
|
console.log("[intelligence] shutting down");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const { resolveAutonomyOutcomes } = require('./outcomeAutonomyWorker');
|
||||||
|
|
||||||
|
resolveAutonomyOutcomes({
|
||||||
|
intelligencePath: process.env.INTELLIGENCE_DB || path.resolve('/data/intelligence.sqlite'),
|
||||||
|
pollMs: Number(process.env.AUTONOMY_OUTCOME_POLL_MS) || 60000,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('[autonomy-outcome] fatal:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
const os = require('os');
|
||||||
|
const https = require('https');
|
||||||
|
const Database = require('better-sqlite3');
|
||||||
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
||||||
|
const { calculateOutcome } = require('../src/autonomy/outcomes');
|
||||||
|
|
||||||
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
||||||
|
function httpGet(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = https.get(url, { headers: { 'User-Agent': 'duriin-autonomy/1.0' } }, (response) => {
|
||||||
|
let body = '';
|
||||||
|
response.setEncoding('utf8');
|
||||||
|
response.on('data', (chunk) => { body += chunk; });
|
||||||
|
response.on('end', () => response.statusCode >= 200 && response.statusCode < 300
|
||||||
|
? resolve(body) : reject(new Error(`market data returned ${response.statusCode}`)));
|
||||||
|
});
|
||||||
|
request.setTimeout(15000, () => request.destroy(new Error('market data timeout')));
|
||||||
|
request.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function history(symbol) {
|
||||||
|
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?range=10y&interval=1d`;
|
||||||
|
const body = JSON.parse(await httpGet(url));
|
||||||
|
const result = body?.chart?.result?.[0];
|
||||||
|
if (!result) return [];
|
||||||
|
return (result.timestamp || []).map((timestamp, index) => ({
|
||||||
|
date: new Date(timestamp * 1000).toISOString().slice(0, 10),
|
||||||
|
close: result.indicators?.quote?.[0]?.close?.[index],
|
||||||
|
})).filter((row) => Number.isFinite(row.close));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAutonomyOutcomes({ intelligencePath, workerId = `outcome-${os.hostname()}-${process.pid}`, pollMs = 60000 } = {}) {
|
||||||
|
const db = new Database(intelligencePath);
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
initAutonomySchema(db);
|
||||||
|
const cache = new Map();
|
||||||
|
while (true) {
|
||||||
|
const predictions = db.prepare(`
|
||||||
|
SELECT p.* FROM autonomy_predictions p
|
||||||
|
LEFT JOIN autonomy_outcomes o ON o.prediction_id = p.id
|
||||||
|
WHERE p.status = 'open' AND o.prediction_id IS NULL
|
||||||
|
AND datetime(p.information_cutoff, '+' || p.horizon_days || ' days') <= datetime('now')
|
||||||
|
ORDER BY p.information_cutoff ASC LIMIT 25
|
||||||
|
`).all();
|
||||||
|
for (const prediction of predictions) {
|
||||||
|
try {
|
||||||
|
if (!cache.has(prediction.instrument)) cache.set(prediction.instrument, await history(prediction.instrument));
|
||||||
|
if (!cache.has('SPY')) cache.set('SPY', await history('SPY'));
|
||||||
|
const result = calculateOutcome(prediction, cache.get(prediction.instrument), cache.get('SPY'));
|
||||||
|
if (!result) {
|
||||||
|
db.prepare("UPDATE autonomy_predictions SET status = 'unresolvable' WHERE id = ?").run(prediction.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO autonomy_outcomes
|
||||||
|
(prediction_id, price_0, price_horizon, benchmark_0, benchmark_horizon, excess_return, direction_correct, error_type)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(prediction.id, result.price0, result.priceHorizon, result.benchmark0, result.benchmarkHorizon,
|
||||||
|
result.excessReturn, result.directionCorrect, result.directionCorrect ? null : 'direction_error');
|
||||||
|
db.prepare("UPDATE autonomy_predictions SET status = 'resolved' WHERE id = ?").run(prediction.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[autonomy-outcome] ${workerId} prediction ${prediction.id}:`, error.message);
|
||||||
|
}
|
||||||
|
await sleep(800);
|
||||||
|
}
|
||||||
|
await sleep(pollMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { calculateOutcome, resolveAutonomyOutcomes };
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// evaluates predictions older than 11 days against realized stock returns
|
||||||
|
// runs continuously, batching by ticker so we hit yahoo once per company per cycle
|
||||||
|
|
||||||
|
const { getPriceContext } = require("./priceContext");
|
||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
|
||||||
|
async function runOutcomeWorker(archiveDb, intelligenceDb, config) {
|
||||||
|
const loopDelay = config.workers?.outcomeLoopDelayMs ?? 60000;
|
||||||
|
|
||||||
|
// pull predictions that are old enough to evaluate (>= 11 calendar days) and dont have an outcome yet
|
||||||
|
const getPending = intelligenceDb.prepare(`
|
||||||
|
SELECT ep.id, ep.company_id, ep.event_date, ep.direction, tc.ticker
|
||||||
|
FROM event_predictions ep
|
||||||
|
JOIN tracked_companies tc ON ep.company_id = tc.id
|
||||||
|
LEFT JOIN prediction_outcomes po ON po.prediction_id = ep.id
|
||||||
|
WHERE po.prediction_id IS NULL
|
||||||
|
AND ep.event_date IS NOT NULL
|
||||||
|
AND date(ep.event_date) <= date('now', '-11 days')
|
||||||
|
AND ep.direction IN ('positive', 'negative')
|
||||||
|
AND tc.ticker IS NOT NULL
|
||||||
|
AND tc.ticker NOT LIKE '%.%'
|
||||||
|
AND length(tc.ticker) <= 5
|
||||||
|
ORDER BY ep.event_date ASC
|
||||||
|
LIMIT 50
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertOutcome = intelligenceDb.prepare(`
|
||||||
|
INSERT OR REPLACE INTO prediction_outcomes
|
||||||
|
(prediction_id, company_id, ticker, event_date, price_0, price_5d, price_10d, r5, r10, correct_5d, correct_10d)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const pending = getPending.all();
|
||||||
|
|
||||||
|
if (pending.length === 0) {
|
||||||
|
await sleep(loopDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// group by ticker so we only fetch each company's history once per cycle
|
||||||
|
const byTicker = new Map();
|
||||||
|
for (const p of pending) {
|
||||||
|
if (!byTicker.has(p.ticker)) byTicker.set(p.ticker, []);
|
||||||
|
byTicker.get(p.ticker).push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
let evaluated = 0;
|
||||||
|
for (const [ticker, preds] of byTicker.entries()) {
|
||||||
|
let history;
|
||||||
|
try {
|
||||||
|
history = await fetchYahooHistory(ticker, "1y");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[outcome] yahoo error for ${ticker}: ${err.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!history || history.length === 0) continue;
|
||||||
|
|
||||||
|
for (const pred of preds) {
|
||||||
|
const eventDate = pred.event_date.slice(0, 10);
|
||||||
|
const price0 = nearestOnOrAfter(history, eventDate);
|
||||||
|
if (price0 == null) continue;
|
||||||
|
|
||||||
|
const date5 = addTradingDays(eventDate, 5);
|
||||||
|
const date10 = addTradingDays(eventDate, 10);
|
||||||
|
const price5 = nearestOnOrAfter(history, date5);
|
||||||
|
const price10 = nearestOnOrAfter(history, date10);
|
||||||
|
|
||||||
|
const r5 = price5 != null ? (price5 - price0) / price0 * 100 : null;
|
||||||
|
const r10 = price10 != null ? (price10 - price0) / price0 * 100 : null;
|
||||||
|
|
||||||
|
const correct5 = r5 == null ? null : (pred.direction === "positive" ? (r5 > 0 ? 1 : 0) : (r5 < 0 ? 1 : 0));
|
||||||
|
const correct10 = r10 == null ? null : (pred.direction === "positive" ? (r10 > 0 ? 1 : 0) : (r10 < 0 ? 1 : 0));
|
||||||
|
|
||||||
|
insertOutcome.run(
|
||||||
|
pred.id, pred.company_id, ticker, eventDate,
|
||||||
|
price0, price5, price10, r5, r10, correct5, correct10
|
||||||
|
);
|
||||||
|
evaluated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// small delay between tickers so we dont hammer yahoo
|
||||||
|
await sleep(800);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluated > 0) {
|
||||||
|
console.log(`[outcome] evaluated ${evaluated} predictions across ${byTicker.size} tickers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(loopDelay);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[outcome] cycle error:", err.message);
|
||||||
|
await sleep(loopDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchYahooHistory(ticker, range) {
|
||||||
|
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?range=${range}&interval=1d`;
|
||||||
|
const body = await httpGet(url, { "User-Agent": "Mozilla/5.0 (compatible; duriin-intelligence/1.0)" });
|
||||||
|
|
||||||
|
const parsed = JSON.parse(body);
|
||||||
|
const result = parsed?.chart?.result?.[0];
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
const ts = result.timestamp || [];
|
||||||
|
const closes = result.indicators?.quote?.[0]?.close || [];
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < ts.length; i++) {
|
||||||
|
if (closes[i] == null) continue;
|
||||||
|
out.push({
|
||||||
|
date: new Date(ts[i] * 1000).toISOString().slice(0, 10),
|
||||||
|
close: closes[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function nearestOnOrAfter(history, dateStr) {
|
||||||
|
for (const row of history) {
|
||||||
|
if (row.date >= dateStr) return row.close;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addTradingDays(dateStr, n) {
|
||||||
|
const dt = new Date(dateStr);
|
||||||
|
let count = 0;
|
||||||
|
while (count < n) {
|
||||||
|
dt.setDate(dt.getDate() + 1);
|
||||||
|
const dow = dt.getDay();
|
||||||
|
if (dow >= 1 && dow <= 5) count++;
|
||||||
|
}
|
||||||
|
return dt.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function httpGet(url, headers) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const u = new URL(url);
|
||||||
|
const req = https.request({
|
||||||
|
hostname: u.hostname,
|
||||||
|
path: u.pathname + u.search,
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
}, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", chunk => data += chunk);
|
||||||
|
res.on("end", () => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) resolve(data);
|
||||||
|
else reject(new Error(`yahoo ${res.statusCode}: ${data.slice(0, 200)}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(r => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { runOutcomeWorker };
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
// fetches daily OHLC from yahoo finance v8 chart api
|
||||||
|
// no api key needed but rate limited so we cache aggressively
|
||||||
|
async function fetchYahooHistory(ticker, range = "6mo") {
|
||||||
|
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?range=${range}&interval=1d`;
|
||||||
|
|
||||||
|
const body = await httpGet(url, {
|
||||||
|
"User-Agent": "Mozilla/5.0 (compatible; duriin-intelligence/1.0)",
|
||||||
|
});
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(body);
|
||||||
|
} catch (_) {
|
||||||
|
throw new Error(`yahoo response not JSON: ${body.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = parsed?.chart?.result?.[0];
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
const ts = result.timestamp || [];
|
||||||
|
const closes = result.indicators?.quote?.[0]?.close || [];
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < ts.length; i++) {
|
||||||
|
if (closes[i] == null) continue;
|
||||||
|
out.push({
|
||||||
|
date: new Date(ts[i] * 1000).toISOString().slice(0, 10),
|
||||||
|
close: closes[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function nearestPriceOnOrBefore(history, dateStr) {
|
||||||
|
// history is sorted ascending by date
|
||||||
|
let last = null;
|
||||||
|
for (const row of history) {
|
||||||
|
if (row.date <= dateStr) last = row;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
return last ? last.close : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function computeStdev(values) {
|
||||||
|
if (values.length < 2) return 0;
|
||||||
|
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
|
const sq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0);
|
||||||
|
return Math.sqrt(sq / (values.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function computeReturns(history) {
|
||||||
|
const rets = [];
|
||||||
|
for (let i = 1; i < history.length; i++) {
|
||||||
|
const prev = history[i - 1].close;
|
||||||
|
const cur = history[i].close;
|
||||||
|
if (prev > 0) rets.push((cur - prev) / prev);
|
||||||
|
}
|
||||||
|
return rets;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// returns { price, price_30d_ago, price_90d_ago, vol_30d } as_of a given date
|
||||||
|
async function getPriceContext(intelligenceDb, ticker, asOfDate) {
|
||||||
|
if (!ticker || !asOfDate) return null;
|
||||||
|
|
||||||
|
// skip private/synthetic tickers — yahoo wont know them
|
||||||
|
if (/^(OPENAI|ANTHROPIC|XAI|HUAWEI|BYTEDANCE|DEEPSEEK|MISTRAL|COHERE|GROQ|SCALEAI|MCKINSEY|DELOITTE|STABILITY|INFLECTION|SPACEX|BLUEORIGIN)$/i.test(ticker)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheRow = intelligenceDb.prepare(
|
||||||
|
"SELECT price, price_30d_ago, price_90d_ago, vol_30d FROM price_snapshots WHERE ticker = ? AND as_of = ?"
|
||||||
|
).get(ticker, asOfDate);
|
||||||
|
|
||||||
|
if (cacheRow) return cacheRow;
|
||||||
|
|
||||||
|
let history;
|
||||||
|
try {
|
||||||
|
history = await fetchYahooHistory(ticker, "6mo");
|
||||||
|
} catch (err) {
|
||||||
|
// dont blow up the worker on a single bad ticker
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!history || history.length === 0) return null;
|
||||||
|
|
||||||
|
const price = nearestPriceOnOrBefore(history, asOfDate);
|
||||||
|
if (price == null) return null;
|
||||||
|
|
||||||
|
const date30 = new Date(asOfDate);
|
||||||
|
date30.setDate(date30.getDate() - 30);
|
||||||
|
const price30 = nearestPriceOnOrBefore(history, date30.toISOString().slice(0, 10));
|
||||||
|
|
||||||
|
const date90 = new Date(asOfDate);
|
||||||
|
date90.setDate(date90.getDate() - 90);
|
||||||
|
const price90 = nearestPriceOnOrBefore(history, date90.toISOString().slice(0, 10));
|
||||||
|
|
||||||
|
// 30-day annualized vol from daily returns
|
||||||
|
const recent = history.filter(h => h.date <= asOfDate).slice(-30);
|
||||||
|
const vol30 = computeStdev(computeReturns(recent)) * Math.sqrt(252);
|
||||||
|
|
||||||
|
const snapshot = {
|
||||||
|
price,
|
||||||
|
price_30d_ago: price30,
|
||||||
|
price_90d_ago: price90,
|
||||||
|
vol_30d: vol30,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
intelligenceDb.prepare(
|
||||||
|
"INSERT OR REPLACE INTO price_snapshots (ticker, as_of, price, price_30d_ago, price_90d_ago, vol_30d) VALUES (?, ?, ?, ?, ?, ?)"
|
||||||
|
).run(ticker, asOfDate, snapshot.price, snapshot.price_30d_ago, snapshot.price_90d_ago, snapshot.vol_30d);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// formats the snapshot for inclusion in the LLM prompt
|
||||||
|
function formatPriceContext(snapshot, ticker) {
|
||||||
|
if (!snapshot || snapshot.price == null) return null;
|
||||||
|
|
||||||
|
const lines = [`${ticker} price as of event: $${snapshot.price.toFixed(2)}`];
|
||||||
|
|
||||||
|
if (snapshot.price_30d_ago) {
|
||||||
|
const ret30 = (snapshot.price - snapshot.price_30d_ago) / snapshot.price_30d_ago * 100;
|
||||||
|
lines.push(`30-day return: ${ret30 >= 0 ? "+" : ""}${ret30.toFixed(1)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.price_90d_ago) {
|
||||||
|
const ret90 = (snapshot.price - snapshot.price_90d_ago) / snapshot.price_90d_ago * 100;
|
||||||
|
lines.push(`90-day return: ${ret90 >= 0 ? "+" : ""}${ret90.toFixed(1)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.vol_30d) {
|
||||||
|
lines.push(`30-day annualized volatility: ${(snapshot.vol_30d * 100).toFixed(1)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function httpGet(url, headers) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const u = new URL(url);
|
||||||
|
const req = https.request({
|
||||||
|
hostname: u.hostname,
|
||||||
|
path: u.pathname + u.search,
|
||||||
|
method: "GET",
|
||||||
|
headers,
|
||||||
|
}, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", chunk => data += chunk);
|
||||||
|
res.on("end", () => {
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) resolve(data);
|
||||||
|
else reject(new Error(`yahoo ${res.statusCode}: ${data.slice(0, 200)}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { getPriceContext, formatPriceContext };
|
||||||
+19
-1
@@ -4,6 +4,10 @@
|
|||||||
async function runQueueFeeder(archiveDb, intelligenceDb, config) {
|
async function runQueueFeeder(archiveDb, intelligenceDb, config) {
|
||||||
const batchSize = config.workers?.queueFeederBatchSize ?? 100;
|
const batchSize = config.workers?.queueFeederBatchSize ?? 100;
|
||||||
const loopDelay = config.workers?.queueFeederLoopDelayMs ?? 3000;
|
const loopDelay = config.workers?.queueFeederLoopDelayMs ?? 3000;
|
||||||
|
const maxPending = Math.max(
|
||||||
|
batchSize,
|
||||||
|
config.workers?.queueFeederMaxPending ?? 250
|
||||||
|
);
|
||||||
|
|
||||||
const getCursor = intelligenceDb.prepare(
|
const getCursor = intelligenceDb.prepare(
|
||||||
"SELECT value FROM cursors WHERE key = 'queue_feeder'"
|
"SELECT value FROM cursors WHERE key = 'queue_feeder'"
|
||||||
@@ -16,11 +20,21 @@ async function runQueueFeeder(archiveDb, intelligenceDb, config) {
|
|||||||
INSERT OR IGNORE INTO article_queue (article_id, status, created_at)
|
INSERT OR IGNORE INTO article_queue (article_id, status, created_at)
|
||||||
VALUES (?, 'pending', CURRENT_TIMESTAMP)
|
VALUES (?, 'pending', CURRENT_TIMESTAMP)
|
||||||
`);
|
`);
|
||||||
|
const getPendingCount = intelligenceDb.prepare(
|
||||||
|
"SELECT COUNT(*) AS count FROM article_queue WHERE status = 'pending'"
|
||||||
|
);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
|
const pending = getPendingCount.get().count;
|
||||||
|
if (pending >= maxPending) {
|
||||||
|
await sleep(loopDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const cursorRow = getCursor.get();
|
const cursorRow = getCursor.get();
|
||||||
const cursor = cursorRow ? cursorRow.value : 0;
|
const cursor = cursorRow ? cursorRow.value : 0;
|
||||||
|
const availableSlots = Math.min(batchSize, maxPending - pending);
|
||||||
|
|
||||||
const articles = archiveDb.prepare(`
|
const articles = archiveDb.prepare(`
|
||||||
SELECT id FROM articles
|
SELECT id FROM articles
|
||||||
@@ -31,7 +45,7 @@ async function runQueueFeeder(archiveDb, intelligenceDb, config) {
|
|||||||
AND event_id IS NOT NULL
|
AND event_id IS NOT NULL
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(cursor, batchSize);
|
`).all(cursor, availableSlots);
|
||||||
|
|
||||||
if (articles.length === 0) {
|
if (articles.length === 0) {
|
||||||
await sleep(loopDelay);
|
await sleep(loopDelay);
|
||||||
@@ -53,6 +67,10 @@ async function runQueueFeeder(archiveDb, intelligenceDb, config) {
|
|||||||
console.log(`[feeder] queued ${inserted} articles, cursor now ${newCursor}`);
|
console.log(`[feeder] queued ${inserted} articles, cursor now ${newCursor}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always yield between archive scans. The query is synchronous and can
|
||||||
|
// otherwise monopolise the event loop while catching up a large archive.
|
||||||
|
await sleep(loopDelay);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[feeder] error:", err.message);
|
console.error("[feeder] error:", err.message);
|
||||||
await sleep(loopDelay);
|
await sleep(loopDelay);
|
||||||
|
|||||||
+76
-9
@@ -1,10 +1,14 @@
|
|||||||
const https = require("https");
|
const https = require("https");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
|
||||||
|
const { getPriceContext, formatPriceContext } = require("./priceContext");
|
||||||
|
|
||||||
const CONCURRENCY = 4;
|
const CONCURRENCY = 4;
|
||||||
|
const PREDICTION_WINDOW_DAYS = 21;
|
||||||
|
|
||||||
|
|
||||||
async function runSignalWorker(archiveDb, intelligenceDb, config) {
|
async function runSignalWorker(archiveDb, intelligenceDb, config) {
|
||||||
|
const loopDelay = config.workers?.signalLoopDelayMs ?? 1000;
|
||||||
const llmConfig = config.openRouter || {};
|
const llmConfig = config.openRouter || {};
|
||||||
|
|
||||||
// add as_of column if it doesnt exist yet
|
// add as_of column if it doesnt exist yet
|
||||||
@@ -28,15 +32,28 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// decay window — only feed recent predictions into the signal prompt.
|
||||||
|
// backtest showed signal degrades sharply after ~10 days, so use 21d as a soft window
|
||||||
const getPredictions = intelligenceDb.prepare(`
|
const getPredictions = intelligenceDb.prepare(`
|
||||||
SELECT type, direction, magnitude, timeframe, rationale, event_date, id
|
SELECT type, direction, magnitude, timeframe, rationale, probability, event_date, id
|
||||||
FROM event_predictions
|
FROM event_predictions
|
||||||
WHERE company_id = ?
|
WHERE company_id = ?
|
||||||
AND substr(event_date, 1, 10) <= ?
|
AND substr(event_date, 1, 10) <= ?
|
||||||
|
AND date(substr(event_date, 1, 10)) >= date(?, '-${PREDICTION_WINDOW_DAYS} days')
|
||||||
|
AND timeframe != 'short'
|
||||||
|
AND direction IN ('positive', 'negative')
|
||||||
ORDER BY event_date DESC
|
ORDER BY event_date DESC
|
||||||
LIMIT 50
|
LIMIT 50
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const getCompanyAccuracy = intelligenceDb.prepare(`
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total,
|
||||||
|
SUM(correct_10d) as correct
|
||||||
|
FROM prediction_outcomes
|
||||||
|
WHERE company_id = ? AND correct_10d IS NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
const getFacts = intelligenceDb.prepare(`
|
const getFacts = intelligenceDb.prepare(`
|
||||||
SELECT claim, type, confidence, confirmation_count
|
SELECT claim, type, confidence, confirmation_count
|
||||||
FROM company_facts
|
FROM company_facts
|
||||||
@@ -118,11 +135,31 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const predictions = getPredictions.all(company_id, checkpoint_date);
|
const predictions = getPredictions.all(company_id, checkpoint_date, checkpoint_date);
|
||||||
const facts = getFacts.all(company_id, checkpoint_date);
|
const facts = getFacts.all(company_id, checkpoint_date);
|
||||||
const relationships = getRelationships.all(company_id, checkpoint_date);
|
const relationships = getRelationships.all(company_id, checkpoint_date);
|
||||||
|
|
||||||
const prompt = buildPrompt(company.name, facts, relationships, predictions, checkpoint_date);
|
// skip if the decay window left us with nothing useful
|
||||||
|
if (predictions.length === 0) {
|
||||||
|
inFlight.delete(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pull market context + historical accuracy for this company
|
||||||
|
let priceBlock = null;
|
||||||
|
if (company.ticker) {
|
||||||
|
try {
|
||||||
|
const snapshot = await getPriceContext(intelligenceDb, company.ticker, checkpoint_date);
|
||||||
|
priceBlock = formatPriceContext(snapshot, company.ticker);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const acc = getCompanyAccuracy.get(company_id);
|
||||||
|
const accuracyBlock = (acc && acc.total >= 5)
|
||||||
|
? `Past prediction accuracy for ${company.name}: ${(acc.correct / acc.total * 100).toFixed(0)}% over ${acc.total} evaluated calls.`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const prompt = buildPrompt(company.name, facts, relationships, predictions, checkpoint_date, priceBlock, accuracyBlock);
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
@@ -165,6 +202,10 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[signal:${id}] cycle error:`, err.message);
|
console.error(`[signal:${id}] cycle error:`, err.message);
|
||||||
|
} finally {
|
||||||
|
// Successful and early-exit paths must yield too; otherwise an invalid
|
||||||
|
// checkpoint can turn this into a tight synchronous SQLite loop.
|
||||||
|
await sleep(loopDelay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +220,7 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildPrompt(companyName, facts, relationships, predictions, asOf) {
|
function buildPrompt(companyName, facts, relationships, predictions, asOf, priceBlock, accuracyBlock) {
|
||||||
const factsBlock = facts.length > 0
|
const factsBlock = facts.length > 0
|
||||||
? facts.map(f => `- ${f.claim} (confirmed ${f.confirmation_count}x)`).join("\n")
|
? facts.map(f => `- ${f.claim} (confirmed ${f.confirmation_count}x)`).join("\n")
|
||||||
: "No known facts yet.";
|
: "No known facts yet.";
|
||||||
@@ -188,9 +229,30 @@ function buildPrompt(companyName, facts, relationships, predictions, asOf) {
|
|||||||
? relationships.map(r => `- ${r.relationship_type}: ${r.to_entity} (${r.confidence})`).join("\n")
|
? relationships.map(r => `- ${r.relationship_type}: ${r.to_entity} (${r.confidence})`).join("\n")
|
||||||
: "No known relationships.";
|
: "No known relationships.";
|
||||||
|
|
||||||
const predBlock = predictions.map((p, i) =>
|
|
||||||
`${i + 1}. [${p.type}] ${p.direction} / ${p.magnitude} / ${p.timeframe} — ${p.rationale || "no rationale"}`
|
// recency-weighted prediction block — newer predictions get a [RECENT] tag,
|
||||||
).join("\n");
|
// and high-magnitude + long-timeframe gets [HIGH CONFIDENCE].
|
||||||
|
// probability is surfaced when present so the LLM can weight by it.
|
||||||
|
const asOfMs = new Date(asOf + "T00:00:00Z").getTime();
|
||||||
|
|
||||||
|
const predBlock = predictions.map((p, i) => {
|
||||||
|
const tags = [];
|
||||||
|
if (p.magnitude === "high" && p.timeframe === "long") tags.push("HIGH CONFIDENCE");
|
||||||
|
|
||||||
|
if (p.event_date) {
|
||||||
|
const ageDays = Math.round((asOfMs - new Date(p.event_date.slice(0, 10) + "T00:00:00Z").getTime()) / 86_400_000);
|
||||||
|
if (ageDays <= 7) tags.push(`RECENT ${ageDays}d`);
|
||||||
|
else tags.push(`${ageDays}d old`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const probStr = (typeof p.probability === "number") ? ` p=${p.probability.toFixed(2)}` : "";
|
||||||
|
const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
|
||||||
|
|
||||||
|
return `${i + 1}. [${p.type}]${tagStr}${probStr} ${p.direction} / ${p.magnitude} / ${p.timeframe} — ${p.rationale || "no rationale"}`;
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
|
const pricePart = priceBlock ? `\nMarket context for ${companyName}:\n${priceBlock}\n` : "";
|
||||||
|
const accPart = accuracyBlock ? `\n${accuracyBlock}\n` : "";
|
||||||
|
|
||||||
return `You are a financial intelligence analyst generating a trade signal for ${companyName} as of ${asOf}.
|
return `You are a financial intelligence analyst generating a trade signal for ${companyName} as of ${asOf}.
|
||||||
|
|
||||||
@@ -199,10 +261,12 @@ ${factsBlock}
|
|||||||
|
|
||||||
Known relationships:
|
Known relationships:
|
||||||
${relBlock}
|
${relBlock}
|
||||||
|
${pricePart}${accPart}
|
||||||
Event predictions up to ${asOf}:
|
Recent event predictions (last 21 days):
|
||||||
${predBlock}
|
${predBlock}
|
||||||
|
|
||||||
|
Weight RECENT and HIGH CONFIDENCE predictions more heavily. Discount older predictions and any that lack a probability score. Predictions that disagree with the recent price trajectory are weaker — be sceptical of bullish predictions on a name that has already rallied 20% in 30 days, and vice versa.
|
||||||
|
|
||||||
Generate a trade signal as JSON with this exact shape:
|
Generate a trade signal as JSON with this exact shape:
|
||||||
{
|
{
|
||||||
"signal": "BUY | HOLD | SELL",
|
"signal": "BUY | HOLD | SELL",
|
||||||
@@ -214,11 +278,14 @@ Generate a trade signal as JSON with this exact shape:
|
|||||||
"summary": "2-3 sentence plain English summary"
|
"summary": "2-3 sentence plain English summary"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Default to HOLD when the predictions are mixed, stale, or low-probability. Reserve BUY/SELL for cases where the weight of high-confidence recent evidence is unambiguous.
|
||||||
|
|
||||||
Risk factors should be derived from:
|
Risk factors should be derived from:
|
||||||
- Supply chain concentration (heavy dependence on single suppliers)
|
- Supply chain concentration (heavy dependence on single suppliers)
|
||||||
- Geopolitical exposure (relationships with entities in sensitive regions)
|
- Geopolitical exposure (relationships with entities in sensitive regions)
|
||||||
- Competitive threats (strong competitors gaining ground)
|
- Competitive threats (strong competitors gaining ground)
|
||||||
- Regulatory exposure (themes mentioning regulation or export controls)
|
- Regulatory exposure (themes mentioning regulation or export controls)
|
||||||
|
- Stretched valuation given recent price moves
|
||||||
- Negative prediction patterns in recent events
|
- Negative prediction patterns in recent events
|
||||||
|
|
||||||
Only output valid JSON. Always respond in English.`;
|
Only output valid JSON. Always respond in English.`;
|
||||||
|
|||||||
Reference in New Issue
Block a user