From c4028cc394035bea61223178fe8c85157355e046 Mon Sep 17 00:00:00 2001 From: ImBenji Date: Mon, 3 Aug 2026 14:03:27 +0100 Subject: [PATCH] feat: add autonomous paper-trading and calibration pipeline --- .env.example | 7 + README.md | 4 + backtest/backtest.py | 26 ++++ backtest/backtest_full.py | 2 + backtest/backtest_results.csv | 176 +++++++++++------------ docker-compose.yml | 93 ++++++++++++ docs/autonomy.md | 53 +++++++ package.json | 7 +- scripts/import-legacy-intelligence.js | 25 ++++ scripts/initialize-autonomy.js | 24 ++++ scripts/set-autonomy-instrument.js | 21 +++ scripts/sync-paper-assets.js | 33 +++++ server.js | 8 +- src/autonomy/calibration.js | 39 +++++ src/autonomy/coordinator.js | 90 ++++++++++++ src/autonomy/execution.js | 34 +++++ src/autonomy/index.js | 17 +++ src/autonomy/jobs.js | 56 ++++++++ src/autonomy/llm.js | 32 +++++ src/autonomy/orderIntents.js | 25 ++++ src/autonomy/outcomes.js | 35 +++++ src/autonomy/policy.js | 26 ++++ src/autonomy/schema.js | 192 +++++++++++++++++++++++++ src/brokers/alpacaPaper.js | 36 +++++ src/db/index.js | 8 +- src/routes/articles.js | 2 +- src/routes/autonomy.js | 43 ++++++ src/routes/status.js | 15 +- test/autonomy.test.js | 111 ++++++++++++++ workers/augorWorker.js | 123 ++++++++++++++-- workers/autonomy-entrypoint.js | 11 ++ workers/autonomyWorker.js | 98 +++++++++++++ workers/calibration-entrypoint.js | 10 ++ workers/calibrationWorker.js | 88 ++++++++++++ workers/coordinator-entrypoint.js | 11 ++ workers/coordinatorWorker.js | 86 +++++++++++ workers/db.js | 46 ++++++ workers/execution-entrypoint.js | 12 ++ workers/executionWorker.js | 100 +++++++++++++ workers/index.js | 6 + workers/outcome-autonomy-entrypoint.js | 10 ++ workers/outcomeAutonomyWorker.js | 71 +++++++++ workers/outcomeWorker.js | 173 ++++++++++++++++++++++ workers/priceContext.js | 171 ++++++++++++++++++++++ workers/queueFeeder.js | 20 ++- workers/signalWorker.js | 85 +++++++++-- 46 files changed, 2246 insertions(+), 115 deletions(-) create mode 100644 docs/autonomy.md create mode 100644 scripts/import-legacy-intelligence.js create mode 100644 scripts/initialize-autonomy.js create mode 100644 scripts/set-autonomy-instrument.js create mode 100644 scripts/sync-paper-assets.js create mode 100644 src/autonomy/calibration.js create mode 100644 src/autonomy/coordinator.js create mode 100644 src/autonomy/execution.js create mode 100644 src/autonomy/index.js create mode 100644 src/autonomy/jobs.js create mode 100644 src/autonomy/llm.js create mode 100644 src/autonomy/orderIntents.js create mode 100644 src/autonomy/outcomes.js create mode 100644 src/autonomy/policy.js create mode 100644 src/autonomy/schema.js create mode 100644 src/brokers/alpacaPaper.js create mode 100644 src/routes/autonomy.js create mode 100644 test/autonomy.test.js create mode 100644 workers/autonomy-entrypoint.js create mode 100644 workers/autonomyWorker.js create mode 100644 workers/calibration-entrypoint.js create mode 100644 workers/calibrationWorker.js create mode 100644 workers/coordinator-entrypoint.js create mode 100644 workers/coordinatorWorker.js create mode 100644 workers/execution-entrypoint.js create mode 100644 workers/executionWorker.js create mode 100644 workers/outcome-autonomy-entrypoint.js create mode 100644 workers/outcomeAutonomyWorker.js create mode 100644 workers/outcomeWorker.js create mode 100644 workers/priceContext.js diff --git a/.env.example b/.env.example index c74237d..b8786d2 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,13 @@ OPEN_ROUTER_API_KEY= OPEN_ROUTER_LLM_MODEL=qwen/qwen3-235b-a22b-2507 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_KEY_FILE=./gdelt-credentials.json diff --git a/README.md b/README.md index cf5a264..0264006 100644 --- a/README.md +++ b/README.md @@ -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 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 On startup the server: diff --git a/backtest/backtest.py b/backtest/backtest.py index a76d84e..a4c95c6 100644 --- a/backtest/backtest.py +++ b/backtest/backtest.py @@ -214,6 +214,32 @@ def main(): print(f" 20-day: {a20:.1f}% (n={n20})") 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 print("BY MAGNITUDE (10-day accuracy)") for mag in sorted(df["magnitude"].dropna().unique()): diff --git a/backtest/backtest_full.py b/backtest/backtest_full.py index 2ba332b..fa2a82c 100644 --- a/backtest/backtest_full.py +++ b/backtest/backtest_full.py @@ -260,8 +260,10 @@ def main(): print() sample = df[df["correct_10d"].notna()].head(30) + print("SAMPLE (30 most recent predictions)") print(f"{'Ticker':<12} {'Date':<12} {'Dir':<10} {'Mag':<8} {'5d%':>7} {'10d%':>7} {'20d%':>7} @10d") + print("-" * 72) for _, row in sample.iterrows(): r5s = f"{row['5d_return']:+.2f}" if row['5d_return'] is not None else "N/A" diff --git a/backtest/backtest_results.csv b/backtest/backtest_results.csv index 91eee3c..e5d5ba8 100644 --- a/backtest/backtest_results.csv +++ b/backtest/backtest_results.csv @@ -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 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 -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 -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 +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.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 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 @@ -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 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 -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 -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 -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 -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 -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 +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.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.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.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.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 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 @@ -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 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 -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 -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 -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 -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 +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.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.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.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 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 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 -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 -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 +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.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.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 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 -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 -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 -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 -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 +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.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.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.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 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 @@ -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 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 -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 -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 -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 -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 +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.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.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.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 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 @@ -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 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 -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 -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 +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.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 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 @@ -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 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 -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 -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 -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 -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 -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 +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.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.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.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.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 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 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 -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 +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.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 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 -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 -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 -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 -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 -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 +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.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.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.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.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.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 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 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 -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 -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 +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.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 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 @@ -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 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 -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 -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 -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 -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 +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.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.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.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 -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 -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 -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 +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.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.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 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 -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 -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 -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 -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 -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 +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.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.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.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.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 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 @@ -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 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 -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 -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 -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 -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 -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 -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 -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 +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.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.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.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.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.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.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 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 -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 -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 -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 +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.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.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 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 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 -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 -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 -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 -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 -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 +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.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.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.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.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.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 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 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 -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 -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 +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.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 ",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 -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 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 -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 -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 +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.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 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 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 -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 +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.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 -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 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 -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 -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 +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.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.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 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 -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 -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 +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.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 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 @@ -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 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 -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 -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 -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 -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 +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.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.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.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 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 -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 -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 +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.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.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 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 diff --git a/docker-compose.yml b/docker-compose.yml index 80e3f4b..a160084 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,13 @@ services: environment: NODE_ENV: production INTELLIGENCE_DB: /data/intelligence.sqlite + DURIIN_RUN_SCHEDULER: "false" restart: unless-stopped networks: - nginx_proxy_manager_default intelligence: + profiles: [legacy] build: context: . provenance: false @@ -31,6 +33,97 @@ services: networks: - 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: nginx_proxy_manager_default: external: true diff --git a/docs/autonomy.md b/docs/autonomy.md new file mode 100644 index 0000000..ed3a5c4 --- /dev/null +++ b/docs/autonomy.md @@ -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. diff --git a/package.json b/package.json index df68bdb..13a24d9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,12 @@ "main": "server.js", "scripts": { "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": [], "author": "", diff --git a/scripts/import-legacy-intelligence.js b/scripts/import-legacy-intelligence.js new file mode 100644 index 0000000..6430eda --- /dev/null +++ b/scripts/import-legacy-intelligence.js @@ -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(); diff --git a/scripts/initialize-autonomy.js b/scripts/initialize-autonomy.js new file mode 100644 index 0000000..b45e424 --- /dev/null +++ b/scripts/initialize-autonomy.js @@ -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(); diff --git a/scripts/set-autonomy-instrument.js b/scripts/set-autonomy-instrument.js new file mode 100644 index 0000000..81f9eaa --- /dev/null +++ b/scripts/set-autonomy-instrument.js @@ -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(); diff --git a/scripts/sync-paper-assets.js b/scripts/sync-paper-assets.js new file mode 100644 index 0000000..0bdfaa4 --- /dev/null +++ b/scripts/sync-paper-assets.js @@ -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); }); diff --git a/server.js b/server.js index e43eb89..0fdd573 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ const sourcesRoutes = require('./src/routes/sources'); const eventRoutes = require('./src/routes/events'); const adminRoutes = require('./src/routes/admin'); const devRoutes = require('./src/routes/dev'); +const autonomyRoutes = require('./src/routes/autonomy'); const config = require('./src/config'); const { startScheduler } = require('./src/scheduler'); @@ -18,13 +19,18 @@ app.register(sourcesRoutes); app.register(eventRoutes); app.register(adminRoutes); app.register(devRoutes); +app.register(autonomyRoutes); app.get('/', async () => ({ ok: true })); async function start() { await app.listen({ port: config.server.port, host: config.server.host }); - startScheduler(); + if (process.env.DURIIN_RUN_SCHEDULER !== 'false') { + startScheduler(); + } else { + app.log.warn('Background ingestion and enrichment scheduler is disabled'); + } } start().catch((error) => { diff --git a/src/autonomy/calibration.js b/src/autonomy/calibration.js new file mode 100644 index 0000000..c3c0a67 --- /dev/null +++ b/src/autonomy/calibration.js @@ -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 }; diff --git a/src/autonomy/coordinator.js b/src/autonomy/coordinator.js new file mode 100644 index 0000000..576b740 --- /dev/null +++ b/src/autonomy/coordinator.js @@ -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 }; diff --git a/src/autonomy/execution.js b/src/autonomy/execution.js new file mode 100644 index 0000000..74aac44 --- /dev/null +++ b/src/autonomy/execution.js @@ -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 }; diff --git a/src/autonomy/index.js b/src/autonomy/index.js new file mode 100644 index 0000000..233c319 --- /dev/null +++ b/src/autonomy/index.js @@ -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, +}; diff --git a/src/autonomy/jobs.js b/src/autonomy/jobs.js new file mode 100644 index 0000000..028d619 --- /dev/null +++ b/src/autonomy/jobs.js @@ -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 }; diff --git a/src/autonomy/llm.js b/src/autonomy/llm.js new file mode 100644 index 0000000..7796e4e --- /dev/null +++ b/src/autonomy/llm.js @@ -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 }; diff --git a/src/autonomy/orderIntents.js b/src/autonomy/orderIntents.js new file mode 100644 index 0000000..aece9a3 --- /dev/null +++ b/src/autonomy/orderIntents.js @@ -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 }; diff --git a/src/autonomy/outcomes.js b/src/autonomy/outcomes.js new file mode 100644 index 0000000..e0a01e4 --- /dev/null +++ b/src/autonomy/outcomes.js @@ -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 }; diff --git a/src/autonomy/policy.js b/src/autonomy/policy.js new file mode 100644 index 0000000..a41727f --- /dev/null +++ b/src/autonomy/policy.js @@ -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 }; diff --git a/src/autonomy/schema.js b/src/autonomy/schema.js new file mode 100644 index 0000000..2fe593a --- /dev/null +++ b/src/autonomy/schema.js @@ -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 }; diff --git a/src/brokers/alpacaPaper.js b/src/brokers/alpacaPaper.js new file mode 100644 index 0000000..29796ac --- /dev/null +++ b/src/brokers/alpacaPaper.js @@ -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 }; diff --git a/src/db/index.js b/src/db/index.js index 7df2834..9ea1cb2 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -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_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_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(` @@ -149,4 +155,4 @@ db.exec(` ); `); -module.exports = db; \ No newline at end of file +module.exports = db; diff --git a/src/routes/articles.js b/src/routes/articles.js index c119f5b..2da4e01 100644 --- a/src/routes/articles.js +++ b/src/routes/articles.js @@ -60,7 +60,7 @@ function buildArticlesQuery(query) { return { sql: ` 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} ORDER BY ${orderBy} LIMIT ? OFFSET ? diff --git a/src/routes/autonomy.js b/src/routes/autonomy.js new file mode 100644 index 0000000..b502913 --- /dev/null +++ b/src/routes/autonomy.js @@ -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; diff --git a/src/routes/status.js b/src/routes/status.js index 9509575..1a3ebed 100644 --- a/src/routes/status.js +++ b/src/routes/status.js @@ -6,7 +6,20 @@ let statusCacheAt = 0; const STATUS_CACHE_TTL_MS = 30 * 1000; 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(); if (statusCache && now - statusCacheAt < STATUS_CACHE_TTL_MS) { return statusCache; diff --git a/test/autonomy.test.js b/test/autonomy.test.js new file mode 100644 index 0000000..bcb2b7f --- /dev/null +++ b/test/autonomy.test.js @@ -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'); +}); diff --git a/workers/augorWorker.js b/workers/augorWorker.js index eeff7d0..2eb424b 100644 --- a/workers/augorWorker.js +++ b/workers/augorWorker.js @@ -2,6 +2,11 @@ const https = require("https"); const http = require("http"); 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) { 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 `); + 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( `INSERT INTO worker_events (worker) VALUES ('augor')` ); @@ -45,8 +70,8 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) { VALUES (?, ?, ?, ?, ?) `); const insertPrediction = intelligenceDb.prepare(` - INSERT INTO event_predictions (event_id, company_id, type, direction, magnitude, timeframe, rationale, event_date) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO event_predictions (event_id, company_id, type, direction, magnitude, timeframe, rationale, probability, event_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); const getEventDate = archiveDb.prepare(` @@ -66,7 +91,6 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) { const queueRow = getPending.get(); if (!queueRow) { - await sleep(loopDelay); continue; } @@ -98,12 +122,29 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) { 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( eventArticleIds, archiveDb, intelligenceDb, config ); if (matchedCompanies.length === 0) { 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`); continue; } @@ -114,6 +155,7 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) { const eventDateRow = getEventDate.get(eventId); const eventDate = eventDateRow ? eventDateRow.pub_date_effective : null; + const eventDateOnly = eventDate ? eventDate.slice(0, 10) : null; const articleText = eventArticles.map((a, i) => { 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}`; } - 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) { + const seenPreds = new Set(); + const writeAll = intelligenceDb.transaction(() => { for (const r of (result.knowledge?.relationships || [])) { 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 || [])) { - 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); + upsertProcessingState.run(eventId, eventArticles.length); recordEvent.run(); pruneCounter++; if (pruneCounter >= 100) { pruneEvents.run(); pruneCounter = 0; } @@ -165,23 +243,44 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) { } catch (err) { 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); } } } -function buildPrompt(companyName, eventTitle, articleText, factsBlock) { +function buildPrompt(companyName, eventTitle, articleText, factsBlock, priceBlock, accuracyBlock) { 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. - -Event: ${eventTitle} +${factsPart}${pricePart}${accPart}Event: ${eventTitle} ${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": { "relationships": [ @@ -195,7 +294,7 @@ Return JSON only — no explanation. Shape: ] }, "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" } ] } diff --git a/workers/autonomy-entrypoint.js b/workers/autonomy-entrypoint.js new file mode 100644 index 0000000..d612ca7 --- /dev/null +++ b/workers/autonomy-entrypoint.js @@ -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); +}); diff --git a/workers/autonomyWorker.js b/workers/autonomyWorker.js new file mode 100644 index 0000000..b327695 --- /dev/null +++ b/workers/autonomyWorker.js @@ -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 }; diff --git a/workers/calibration-entrypoint.js b/workers/calibration-entrypoint.js new file mode 100644 index 0000000..9f4991e --- /dev/null +++ b/workers/calibration-entrypoint.js @@ -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); +}); diff --git a/workers/calibrationWorker.js b/workers/calibrationWorker.js new file mode 100644 index 0000000..220fc5d --- /dev/null +++ b/workers/calibrationWorker.js @@ -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 }; diff --git a/workers/coordinator-entrypoint.js b/workers/coordinator-entrypoint.js new file mode 100644 index 0000000..e9296cc --- /dev/null +++ b/workers/coordinator-entrypoint.js @@ -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); +}); diff --git a/workers/coordinatorWorker.js b/workers/coordinatorWorker.js new file mode 100644 index 0000000..74de840 --- /dev/null +++ b/workers/coordinatorWorker.js @@ -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 }; diff --git a/workers/db.js b/workers/db.js index ec5e089..83ce7be 100644 --- a/workers/db.js +++ b/workers/db.js @@ -1,5 +1,6 @@ const Database = require("better-sqlite3"); const sqliteVec = require("sqlite-vec"); +const { initAutonomySchema } = require("../src/autonomy/schema"); let archiveDb = null; let intelligenceDb = null; @@ -17,6 +18,7 @@ function getIntelligenceDb(dbPath) { if (!intelligenceDb) { intelligenceDb = new Database(dbPath); intelligenceDb.pragma("journal_mode = WAL"); + initAutonomySchema(intelligenceDb); } return intelligenceDb; } @@ -109,6 +111,7 @@ function runMigrations(db) { function runColumnMigrations(db) { 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_predictions ADD COLUMN probability REAL"); } catch (_) {} db.exec(` 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 db.exec(`DELETE FROM worker_events WHERE completed_at < datetime('now', '-1 hour')`); } diff --git a/workers/execution-entrypoint.js b/workers/execution-entrypoint.js new file mode 100644 index 0000000..8ef7eb8 --- /dev/null +++ b/workers/execution-entrypoint.js @@ -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); +}); diff --git a/workers/executionWorker.js b/workers/executionWorker.js new file mode 100644 index 0000000..2e677dd --- /dev/null +++ b/workers/executionWorker.js @@ -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 }; diff --git a/workers/index.js b/workers/index.js index fb0aff1..769a406 100644 --- a/workers/index.js +++ b/workers/index.js @@ -8,6 +8,7 @@ const { ensureCompanyEmbeddings } = require("./embeddings"); const { runConsolidationWorker } = require("./consolidationWorker"); const { runGraphWorker } = require("./graphWorker"); const { runSignalWorker } = require("./signalWorker"); +const { runOutcomeWorker } = require("./outcomeWorker"); require("dotenv").config({ path: path.resolve(__dirname, "../.env") }); @@ -77,6 +78,11 @@ runSignalWorker(archiveDb, intelligenceDb, config).catch(err => { process.exit(1); }); +runOutcomeWorker(archiveDb, intelligenceDb, config).catch(err => { + console.error("[outcome] fatal:", err); + process.exit(1); +}); + process.on("SIGINT", () => { console.log("[intelligence] shutting down"); process.exit(0); diff --git a/workers/outcome-autonomy-entrypoint.js b/workers/outcome-autonomy-entrypoint.js new file mode 100644 index 0000000..ea94996 --- /dev/null +++ b/workers/outcome-autonomy-entrypoint.js @@ -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); +}); diff --git a/workers/outcomeAutonomyWorker.js b/workers/outcomeAutonomyWorker.js new file mode 100644 index 0000000..deb0737 --- /dev/null +++ b/workers/outcomeAutonomyWorker.js @@ -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 }; diff --git a/workers/outcomeWorker.js b/workers/outcomeWorker.js new file mode 100644 index 0000000..1afe55b --- /dev/null +++ b/workers/outcomeWorker.js @@ -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 }; diff --git a/workers/priceContext.js b/workers/priceContext.js new file mode 100644 index 0000000..b37ad1e --- /dev/null +++ b/workers/priceContext.js @@ -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 }; diff --git a/workers/queueFeeder.js b/workers/queueFeeder.js index 8f04975..c22ea33 100644 --- a/workers/queueFeeder.js +++ b/workers/queueFeeder.js @@ -4,6 +4,10 @@ async function runQueueFeeder(archiveDb, intelligenceDb, config) { const batchSize = config.workers?.queueFeederBatchSize ?? 100; const loopDelay = config.workers?.queueFeederLoopDelayMs ?? 3000; + const maxPending = Math.max( + batchSize, + config.workers?.queueFeederMaxPending ?? 250 + ); const getCursor = intelligenceDb.prepare( "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) VALUES (?, 'pending', CURRENT_TIMESTAMP) `); + const getPendingCount = intelligenceDb.prepare( + "SELECT COUNT(*) AS count FROM article_queue WHERE status = 'pending'" + ); while (true) { try { + const pending = getPendingCount.get().count; + if (pending >= maxPending) { + await sleep(loopDelay); + continue; + } + const cursorRow = getCursor.get(); const cursor = cursorRow ? cursorRow.value : 0; + const availableSlots = Math.min(batchSize, maxPending - pending); const articles = archiveDb.prepare(` SELECT id FROM articles @@ -31,7 +45,7 @@ async function runQueueFeeder(archiveDb, intelligenceDb, config) { AND event_id IS NOT NULL ORDER BY id ASC LIMIT ? - `).all(cursor, batchSize); + `).all(cursor, availableSlots); if (articles.length === 0) { await sleep(loopDelay); @@ -53,6 +67,10 @@ async function runQueueFeeder(archiveDb, intelligenceDb, config) { 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) { console.error("[feeder] error:", err.message); await sleep(loopDelay); diff --git a/workers/signalWorker.js b/workers/signalWorker.js index ecb41a0..78aaf61 100644 --- a/workers/signalWorker.js +++ b/workers/signalWorker.js @@ -1,10 +1,14 @@ const https = require("https"); const http = require("http"); +const { getPriceContext, formatPriceContext } = require("./priceContext"); + const CONCURRENCY = 4; +const PREDICTION_WINDOW_DAYS = 21; async function runSignalWorker(archiveDb, intelligenceDb, config) { + const loopDelay = config.workers?.signalLoopDelayMs ?? 1000; const llmConfig = config.openRouter || {}; // add as_of column if it doesnt exist yet @@ -28,15 +32,28 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) { 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(` - SELECT type, direction, magnitude, timeframe, rationale, event_date, id + SELECT type, direction, magnitude, timeframe, rationale, probability, event_date, id FROM event_predictions WHERE company_id = ? 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 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(` SELECT claim, type, confidence, confirmation_count FROM company_facts @@ -118,11 +135,31 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) { 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 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; try { @@ -165,6 +202,10 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) { } catch (err) { 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 ? facts.map(f => `- ${f.claim} (confirmed ${f.confirmation_count}x)`).join("\n") : "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") : "No known relationships."; - const predBlock = predictions.map((p, i) => - `${i + 1}. [${p.type}] ${p.direction} / ${p.magnitude} / ${p.timeframe} — ${p.rationale || "no rationale"}` - ).join("\n"); + + // recency-weighted prediction block — newer predictions get a [RECENT] tag, + // 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}. @@ -199,10 +261,12 @@ ${factsBlock} Known relationships: ${relBlock} - -Event predictions up to ${asOf}: +${pricePart}${accPart} +Recent event predictions (last 21 days): ${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: { "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" } +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: - Supply chain concentration (heavy dependence on single suppliers) - Geopolitical exposure (relationships with entities in sensitive regions) - Competitive threats (strong competitors gaining ground) - Regulatory exposure (themes mentioning regulation or export controls) +- Stretched valuation given recent price moves - Negative prediction patterns in recent events Only output valid JSON. Always respond in English.`;