Commit 02b92e82 authored by xeron56's avatar xeron56

Refactor repository docstring, add DGT dependencies, and implement tests for DGT forecasting

- Updated the docstring in repository.py for clarity.
- Added new dependencies for DGT in pyproject.toml.
- Created a new test file for DGT forecasting with comprehensive tests covering market graph alignment, graph prior normalization, model predictions, and end-to-end run validation.
parent 1a941ef5
...@@ -412,6 +412,30 @@ and skill versus a last-price baseline. A positive-looking headline metric does ...@@ -412,6 +412,30 @@ and skill versus a last-price baseline. A positive-looking headline metric does
not prove useful predictive skill; compare the forecast with the reported naive not prove useful predictive skill; compare the forecast with the reported naive
baseline before relying on it. baseline before relying on it.
### Differential Graph Transformer forecasting
The DGT command uses the same authenticated DSE candle service and the same
prediction dashboard. Unlike TimesFM, it trains a small model for each run and
can learn from a target stock plus related DSE securities. It saves a real vs.
predicted holdout CSV, a future CSV, a chart, and a reusable PyTorch checkpoint.
```bash
pip install -e '.[dgt]'
dohasecuritiesstockai-dgt-predict GP \
--resolution 1d \
--lookback 2y \
--peers DSEX,BRACBANK,SQURPHARMA \
--future-steps 12 \
--open-ui
```
The evaluation graph, scaler, and model use only data before the validation and
holdout periods. The holdout forecast is recursive, so its real-price comparison
has no future-price leakage. After scoring, a separate deployment model is refit
on all available real bars for the future forecast; use `--no-refit` to disable
that final step.
## Important limitations ## Important limitations
- LLM output is non-deterministic and can differ between otherwise identical - LLM output is non-deterministic and can differ between otherwise identical
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
**Recommendation**: Buy
**Rationale**: প্রিয় ট্রেডার, এই বিতর্কটা সত্যিই দুটি শিবিরের মধ্যে তীক্ষ্ণ এবং উভয় পক্ষই শক্ত যুক্তি দিয়েছে — তাই আমি সতর্কভাবে দুই পক্ষের দিকে তাকিয়ে সিদ্ধান্তে পৌঁছেছি।
বুলিশ শিবিরের যুক্তি সংখ্যার দিক থেকে খুবই শক্তিশালী এবং আমার কাছে সেটা বেশি গুরুত্বপূর্ণ: SQURPHARMA-র P/E মাত্র 7.73 — যা কোম্পানির ইতিহাসের সর্বনিম্ন অঞ্চলের কাছাকাছি, যেখানে 2019 সালে ছিল 14.68। EPS টানা ৭ বছর ধরে 16.03 থেকে 27.04-এ বেড়েছে (প্রায় 69% বৃদ্ধি), কিন্তু দাম সেই সময়ের চেয়েও নিচে। NAV/share 169.79, P/B 1.30, ROE 17%+, নগদ ৫২ বিলিয়ন টাকা এবং ঋণ মাত্র 0.82 বিলিয়ন — অর্থাৎ নিট নগদ প্রায় ৫১ বিলিয়ন। লভ্যাংশ 42% থেকে ক্রমাগত বেড়ে 120% হয়েছে, মানে 5.46% নগদ ফলন। আর সবচেয়ে গুরুত্বপূর্ণ — স্পনসর/পরিচালক হোল্ডিং 34.67% থেকে বেড়ে 44.15% হয়েছে। যারা কোম্পানির ভেতরের খবর সবচেয়ে ভালো জানেন, তারা ক্রমাগত নিজেদের অংশ বাড়াচ্ছেন — এটা একটি প্রকৃত আস্থার সংকেত।
বেয়িশ শিবিরের যুক্তিও অবহেলা করার মতো নয় — বিশেষ করে Q3-এ YoY ভিত্তিতে EPS প্রায় -5.2% কমেছে, এবং প্রবৃদ্ধির গতি 14.5% থেকে 10%-এ নেমে এসেছে। MACD নেতিবাচক, RSI 50-এর নিচে (48.73), দাম বোলিঞ্জার মধ্যমার নিচে, এবং 7 বছর ধরে দাম 198-236-এর রেঞ্জে বন্দি — বেয়িশের "ভ্যালু ট্র্যাপ" উদ্বেগটির একটা বাস্তব ভিত্তি আছে। লিকুইডিটিও কম (ফ্রি ফ্লোট মাত্র ~27%)।
কিন্তু আমি যখন সামগ্রিক ছবিটা দেখি, বুলিশ শিবিরের পক্ষে ভারের পার্থক্যটা বেশি। বেয়িশ যে "ক্যাটালিস্ট নেই" বলেছে — সেটা পুরোপুরি ঠিক নয়; Q4 ফলাফল, লভ্যাংশ ঘোষণা, এবং রপ্তানি সম্প্রসারণ — এগুলো বাস্তব সম্ভাব্য ট্রিগার। আর সবচেয়ে বড় কথা, 7.73 P/E-তে ঝুঁকি-রিটার্ন উপরের দিকে অনুকূল: নিচে 200-SMA 213.44 (প্রায় 3%) এবং 52-সপ্তাহের লো 198 (প্রায় 10%), যেখানে উপরে বোলিঞ্জার আপার 224.88, রেঞ্জ-হাই 236, এবং P/E 9-10-এ রিটার্ন হলে 240-280 পর্যন্ত সম্ভাবনা। Q3-এর দুর্বলতা ঋতুভিত্তিক প্যাটার্নের (প্রতি বছর Q3-Q4 দুর্বল) সঙ্গেও সামঞ্জস্যপূর্ণ।
সুতরাং বেয়িশের টেকনিক্যাল সতর্কতাকে আমি সম্মান করি এবং দামের গতিবিধির প্রতি সতর্ক থাকব, কিন্তু ফান্ডামেন্টাল ও মূল্যায়নের দিক থেকে বুলিশ শিবির জয়ী। এটি "ভ্যালু ট্র্যাপ" নয় — এটি বাংলাদেশের সেরা ফার্মাসিউটিক্যাল ফ্র্যাঞ্চাইজকে গভীর ছাড়ে কেনার সুযোগ। আমার সিদ্ধান্ত: Buy — কিন্তু কিস্তিতে (staged), যাতে টেকনিক্যাল দুর্বলতা থাকা সত্ত্বেও এন্ট্রি পয়েন্টগুলো সামঞ্জস্যপূর্ণ হয়।
**Strategic Actions**: SQURPHARMA-তে Buy রেটিং অনুযায়ী নিচের ধাপগুলো বাস্তবায়ন করুন:
১. পজিশন সাইজিং: দীর্ঘমেয়াদি মূল পোর্টফোলিওর কোর-হোল্ডিং হিসেবে মাঝারি-বড় সাইজের পজিশন (যেমন সম্পূর্ণ প্ল্যানড পজিশনের ১০০%-এর লক্ষ্য) নিন, কিন্তু মাসিক-মেয়াদি জুড়ে তা ৩টি কিস্তিতে ভাগ করুন। "Buy" রেটিং অনুযায়ী ৭০-১০০%-এর মধ্যে সম্পূর্ণ পজিশন নেওয়ার ইচ্ছা রাখুন, তবে টেকনিক্যাল অস্থিরতা মাথায় রেখে প্রথম কিস্তি ৪০-৫০% দিয়ে শুরু করুন।
২. কিস্তিভিত্তিক এন্ট্রি (Staged entry):
- প্রথম কিস্তি (৪০-৫০%): বর্তমান জোন 216-220 BDT-তে (200-SMA 213.44-এর উপরে থাকা অবস্থায়) নিন।
- দ্বিতীয় কিস্তি (৩০%): 213-216 BDT (200-SMA-এর কাছাকাছি) বা 221+ ভলিউমসহ ব্রেকআউট নিশ্চিত হলে যোগ করুন — যেটা আগে ঘটে।
- তৃতীয় কিস্তি (বাকি): দামে 224.88 (বোলিঞ্জার UB) ভাঙলে বা কনফার্মড ব্রেকআউটের পর সম্পূর্ণ করুন।
৩. স্টপ-লস ও ঝুঁকি ব্যবস্থাপনা: যারা নতুন পজিশন নিচ্ছেন তাদের জন্য কঠোর স্টপ-লস 205 BDT-এর নিচে ক্লোজে রাখুন (P/E 6.8-এর কাছাকাছি — ঝুঁকি-রিটার্ন সুরক্ষিত)। যাদের আগে থেকে পজিশন আছে, তারা 213.44 (200-SMA) নিচে ক্লোজে স্টপ রাখুন। ঝুঁকিও ৩-১০% এর মধ্যে সীমাবদ্ধ রাখতে প্রতি বিনিয়োগে বরাদ্দ এমনভাবে করুন।
৪. টার্গেট: স্বল্প-মেয়াদি টার্গেট 224-232 (বোলিঞ্জার UB ও রেঞ্জ-হাই); মাঝারি-মেয়াদি (৬-১২ মাস) 250-280 (P/E 9-10-এ); দীর্ঘমেয়াদি (২-৩ বছর) 300+ ধারাবাহিক EPS ও লভ্যাংশ প্রবৃদ্ধির ওপর ভিত্তি করে।
৫. মনিটরিং ট্রিগার: Q4 FY26 ফলাফলে EPS >7.5 এলে ও MACD পজিটিভ ক্রস দিলে বুলিশ থিসিস শক্তিশালী হবে — পজিশন সম্পূর্ণ করার সুযোগও। বিপরীতে Q4-এ EPS <6.0 বা দাম 213.44-এর নিচে ক্লোজ করলে থিসিস দুর্বল হবে — তখন সম্পূর্ণ পজিশন গ্রহণ বন্ধ করুন ও স্টপ সম্মান করুন।
৬. ভারসাম্য: এটি একটি ডিফেন্সিভ ব্লু-চিপ — তাই পোর্টফোলিওর ঝুঁকিপূর্ণ/গ্রোথ অংশের বিপরীতে এটিকে স্থিতিশীল অ্যাংকর পজিশন হিসেবে রাখুন।
\ No newline at end of file
**Action**: Buy
**Reasoning**: SQURPHARMA-র ফান্ডামেন্টাল ও মূল্যায়ন উভয়ই বুলিশ শিবিরের পক্ষে ভারসাম্য ঝুঁকেছে। মাত্র 7.73 P/E (ইতিহাসের সর্বনিম্ন অঞ্চলের কাছাকাছি), ধারাবাহিক EPS বৃদ্ধি (৭ বছরে 16.03 থেকে 27.04), NAV/share 169.79, P/B 1.30, ROE 17%+, নিট নগদ ~৫১ বিলিয়ন টাকা এবং লভ্যাংশ 120% (5.46% নগদ ফলন) — এই সবই শক্তিশালী ফান্ডামেন্টাল। সর্বোপরি স্পনসর/পরিচালক হোল্ডিং 34.67% থেকে 44.15%-এ বৃদ্ধি ভেতরের আস্থার শক্তিশালী সংকেত। Q3-এর সাময়িক দুর্বলতা ঋতুভিত্তিক প্যাটার্নের সঙ্গে সামঞ্জস্যপূর্ণ এবং 'ভ্যালু ট্র্যাপ' নয়, বরং গভীর ছাড়ে কেনার সুযোগ। টেকনিক্যাল সতর্কতা থাকা সত্ত্বেও কিস্তিভিত্তিক (staged) এন্ট্রিতে ঝুঁকি-রিটার্ন উপরের দিকে অনুকূল।
**Entry Price**: 216.0
**Stop Loss**: 205.0
**Position Sizing**: মাঝারি-বড় কোর-হোল্ডিং; ৩টি কিস্তিতে বিভক্ত, প্রথম কিস্তি ৪০-৫০%
FINAL TRANSACTION PROPOSAL: **BUY**
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
**Rating**: Buy
**Executive Summary**: SQURPHARMA-তে কিস্তিভিত্তিক (staged) BUY নেওয়ার সুপারিশ। ফান্ডামেন্টাল ও মূল্যায়ন অত্যন্ত আকর্ষণীয় (P/E 7.73, নিট নগদ ~৫১ বিলিয়ন, স্পনসর হোল্ডিং 44.15%), কিন্তু স্বল্পমেয়াদি টেকনিক্যাল দুর্বলতা (নেতিবাচক MACD, কম ভলিউম, প্রাইস কনফ্লুয়েন্স পয়েন্টে) থাকায় প্রথম কিস্তি সীমিত ২৫-৩০%-এ রাখা হবে। ধাপে ধাপে কনফার্মেশন সিগন্যালে পজিশন বাড়িয়ে মোট ৫০% পর্যন্ত যাওয়া। স্টপ-লস ২০৫ BDT, প্রাথমিক টার্গেট ২৩২-২৩৬, সময়-দিগন্ত ৬-১২ মাস।
**Investment Thesis**: বুলিশ শিবিরের সংখ্যাগত যুক্তি জোরালো: P/E মাত্র 7.73 (ইতিহাসের নিম্ন অঞ্চল), EPS সাত বছরে 16.03 থেকে 27.04-তে ৬৯% বৃদ্ধি, NAV/share 169.79, P/B 1.30, ROE 17%+, নিট নগদ ~৫১ বিলিয়ন টাকা, এবং লভ্যাংশ 42% থেকে 120%-তে উত্থান। সবচেয়ে গুরুত্বপূর্ণ সংকেত হলো স্পনসর/পরিচালক হোল্ডিং এক বছরে 34.67% থেকে 44.15%-এ বেড়ে যাওয়া — যা ভেতরের খেলোয়াড়দের আস্থার শক্তিশালী প্রমাণ। বেয়ারিশ শিবিরের সতর্কতাও বৈধ: Q3-এ EPS YoY -5.2%, MACD নেতিবাচক, RSI 48.73, ভলিউম 140,479 (দুর্বল), এবং 7 বছর ধরে প্রাইস 198-236 রেঞ্জে বন্দি। তবে Q3-এর দুর্বলতা ঋতুভিত্তিক প্যাটার্নের সাথে সামঞ্জস্যপূর্ণ, ৯ মাসের EPS (23.29) আগের বছরের (21.15) চেয়ে বেশি। 2019-2024-এ lud প্রাইস 200-220-এ থাকলেও সেই সময় EPS 16-23 আর NAV 86-142-তে ছিল — এখন ফান্ডামেন্টাল দ্বিগুণ হলেও দাম একই আছে, এটি বাজারের গুরুতর অদক্ষতা যা বিনিয়োগ সুযোগ তৈরি করে। ভ্যালু-ট্র্যাপ উদ্বেগ সত্ত্বেও, 52 বিলিয়ন নগদ আর নগণ্য ঋণের (0.82 বিলিয়ন) কারণে downside সীমিত। ঝুঁকি-রিটার্ন অসমমিত: নিচে 205-এ স্টপ (জন­রিস্ক ~5-6%), উপরে 232-236 পর্যন্ত প্রথম টার্গেট অবিবেচনা থেকে ~14% আপসাইড, এবং P/E 9-10-এ রিরেট হলে 250-280 সম্ভাবনা। তবে টেকনিক্যাল মোমেন্টাম দুর্বল ও কনফ্লুয়েন্স পয়েন্ট (50-SMA 219.69, 10-EMA 219.86) না ভাঙা পর্যন্ত পুরো পজিশন নেওয়া ঝুঁকিপূর্ণ — তাই প্রথম কিস্তি ২৫-৩০%-এ সীমিত রেখে কনফার্মেশন সিগন্যালে ধাপে ধাপে পজিশন বাড়ানোই সর্বোত্তম ভারসাম্য।
**Price Target**: 236.0
**Time Horizon**: ৬-১২ মাস
\ No newline at end of file
This diff is collapsed.
<main class="prediction-page"> <main class="prediction-page">
<header class="prediction-toolbar"> <header class="prediction-toolbar">
<a class="brand" href="/?view=timesfm" aria-label="TimesFM forecast home"> <a class="brand" href="/?view=timesfm" aria-label="Forecast home">
<span class="brand-mark">T</span> <span class="brand-mark">T</span>
<span>TRADINGAGENTS <strong>FORECAST LAB</strong></span> <span>TRADINGAGENTS <strong>FORECAST LAB</strong></span>
</a> </a>
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
@if (loading()) { @if (loading()) {
<section class="loading-state" aria-live="polite"> <section class="loading-state" aria-live="polite">
<span class="loader"></span> <span class="loader"></span>
<h1>Loading TimesFM prediction</h1> <h1>Loading market prediction</h1>
<p>Reading the saved backtest and chart series…</p> <p>Reading the saved backtest and chart series…</p>
</section> </section>
} }
...@@ -23,14 +23,14 @@ ...@@ -23,14 +23,14 @@
<span class="eyebrow">NO PREDICTION RUN</span> <span class="eyebrow">NO PREDICTION RUN</span>
<h1>Generate the forecast first.</h1> <h1>Generate the forecast first.</h1>
<p>{{ errorMessage() }}</p> <p>{{ errorMessage() }}</p>
<code>tradingagents-predict "BXPHARMA'PB" --resolution 1d --open-ui</code> <code>dohasecuritiesstockai-dgt-predict GP --resolution 1d --open-ui</code>
</section> </section>
} }
@if (result(); as report) { @if (result(); as report) {
<section class="hero"> <section class="hero">
<div> <div>
<p class="eyebrow">TIMESFM {{ report.model.version }} · ZERO-SHOT PRICE FORECAST</p> <p class="eyebrow">{{ report.model.name | uppercase }} {{ report.model.version }} · PRICE FORECAST</p>
<div class="symbol-line"> <div class="symbol-line">
<h1>{{ report.symbol }}</h1> <h1>{{ report.symbol }}</h1>
<span>{{ report.data.resolution_label }}</span> <span>{{ report.data.resolution_label }}</span>
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
<article class="accuracy-card"> <article class="accuracy-card">
<span>Holdout accuracy</span> <span>Holdout accuracy</span>
<strong>{{ report.metrics.accuracy_score | number: '1.1-2' }}<small>%</small></strong> <strong>{{ report.metrics.accuracy_score | number: '1.1-2' }}<small>%</small></strong>
<p>100 − sMAPE across the full hidden half</p> <p>100 − sMAPE across the full hidden holdout</p>
</article> </article>
</section> </section>
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
<header> <header>
<div> <div>
<p class="eyebrow">HISTORICAL BACKTEST + FORWARD PATH</p> <p class="eyebrow">HISTORICAL BACKTEST + FORWARD PATH</p>
<h2>Actual candles against TimesFM</h2> <h2>Actual candles against {{ report.model.name }}</h2>
</div> </div>
<div class="legend" aria-label="Chart legend"> <div class="legend" aria-label="Chart legend">
<span><i class="candle"></i>Actual OHLC</span> <span><i class="candle"></i>Actual OHLC</span>
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
<span><i class="live"></i>Live actual</span> <span><i class="live"></i>Live actual</span>
</div> </div>
</header> </header>
<div #predictionChart class="prediction-chart" role="img" aria-label="DSE actual and TimesFM forecast chart"></div> <div #predictionChart class="prediction-chart" role="img" [attr.aria-label]="'DSE actual and ' + report.model.name + ' forecast chart'"></div>
<footer> <footer>
<span>Context</span> <span>Context</span>
<strong>{{ report.data.first_timestamp | date: 'mediumDate' }} → {{ report.history[report.data.context_points - 1].time | date: 'mediumDate' }}</strong> <strong>{{ report.data.first_timestamp | date: 'mediumDate' }} → {{ report.history[report.data.context_points - 1].time | date: 'mediumDate' }}</strong>
......
...@@ -70,7 +70,7 @@ export class TimesFmPredictionComponent ...@@ -70,7 +70,7 @@ export class TimesFmPredictionComponent
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.titleService.setTitle('TimesFM Stock Forecast'); this.titleService.setTitle('DSE Stock Forecast');
const query = new URLSearchParams(window.location.search); const query = new URLSearchParams(window.location.search);
const runId = query.get('run')?.trim(); const runId = query.get('run')?.trim();
const symbol = query.get('symbol')?.trim().toUpperCase(); const symbol = query.get('symbol')?.trim().toUpperCase();
...@@ -82,6 +82,7 @@ export class TimesFmPredictionComponent ...@@ -82,6 +82,7 @@ export class TimesFmPredictionComponent
request.subscribe({ request.subscribe({
next: (result) => { next: (result) => {
this.result.set(result); this.result.set(result);
this.titleService.setTitle(`${result.model.name} · ${result.symbol}`);
this.loading.set(false); this.loading.set(false);
this.errorMessage.set(''); this.errorMessage.set('');
setTimeout(() => this.renderChart()); setTimeout(() => this.renderChart());
...@@ -90,7 +91,7 @@ export class TimesFmPredictionComponent ...@@ -90,7 +91,7 @@ export class TimesFmPredictionComponent
error: () => { error: () => {
this.loading.set(false); this.loading.set(false);
this.errorMessage.set( this.errorMessage.set(
'Prediction data is not available. Run tradingagents-predict first.', 'Prediction data is not available. Run a forecasting command first.',
); );
this.liveStatus.set('offline'); this.liveStatus.set('offline');
}, },
......
...@@ -114,7 +114,7 @@ def create_app() -> FastAPI: ...@@ -114,7 +114,7 @@ def create_app() -> FastAPI:
if result is None: if result is None:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="No TimesFM prediction run matches this request.", detail="No prediction run matches this request.",
) )
return result return result
......
...@@ -116,7 +116,7 @@ def dashboard_url(host: str, port: int, symbol: str, analysis_date: date | str) ...@@ -116,7 +116,7 @@ def dashboard_url(host: str, port: int, symbol: str, analysis_date: date | str)
def prediction_dashboard_url(host: str, port: int, run_id: str) -> str: def prediction_dashboard_url(host: str, port: int, run_id: str) -> str:
"""Return the standalone Angular URL for one TimesFM prediction run.""" """Return the standalone Angular URL for one prediction run."""
browser_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host browser_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
query = urlencode({"view": "timesfm", "run": run_id}) query = urlencode({"view": "timesfm", "run": run_id})
...@@ -188,7 +188,7 @@ def launch_prediction_dashboard( ...@@ -188,7 +188,7 @@ def launch_prediction_dashboard(
host: str | None = None, host: str | None = None,
port: int | None = None, port: int | None = None,
) -> str: ) -> str:
"""Build, serve, and open the standalone TimesFM Angular result screen.""" """Build, serve, and open the standalone forecast result screen."""
resolved_host = host or os.environ.get("TRADINGAGENTS_API_HOST", "127.0.0.1") resolved_host = host or os.environ.get("TRADINGAGENTS_API_HOST", "127.0.0.1")
resolved_port = port or int(os.environ.get("TRADINGAGENTS_API_PORT", "8000")) resolved_port = port or int(os.environ.get("TRADINGAGENTS_API_PORT", "8000"))
......
"""Differential Graph Transformer forecasting for Doha Securities DSE data."""
from .core import (
DGTConfig,
DifferentialGraphTransformer,
ForecastRun,
MarketGraphData,
TrainingSummary,
build_graph_prior,
load_dse_market_graph,
run_dgt_prediction,
save_forecast_run,
)
__all__ = [
"DGTConfig",
"DifferentialGraphTransformer",
"ForecastRun",
"MarketGraphData",
"TrainingSummary",
"build_graph_prior",
"load_dse_market_graph",
"run_dgt_prediction",
"save_forecast_run",
]
"""Run DGT forecasting with ``python -m dohasecuritiesstockai.dgt_forecasting``."""
from .cli import main
if __name__ == "__main__":
raise SystemExit(main())
"""Dedicated command-line entry point for DGT forecasts."""
from .core import main
__all__ = ["main"]
if __name__ == "__main__":
raise SystemExit(main())
This diff is collapsed.
"""Filesystem persistence for chart-ready TimesFM prediction runs.""" """Filesystem persistence for chart-ready prediction runs."""
from __future__ import annotations from __future__ import annotations
......
...@@ -51,6 +51,10 @@ timesfm = [ ...@@ -51,6 +51,10 @@ timesfm = [
"einshape>=1.0.0", "einshape>=1.0.0",
"utilsforecast>=0.1.10", "utilsforecast>=0.1.10",
] ]
dgt = [
"torch>=2.2.0",
"matplotlib>=3.8.0",
]
[project.scripts] [project.scripts]
# Product commands plus legacy executable aliases for existing scripts. # Product commands plus legacy executable aliases for existing scripts.
...@@ -61,6 +65,7 @@ dohasecuritiesstockai-api = "dohasecuritiesstockai.api.__main__:main" ...@@ -61,6 +65,7 @@ dohasecuritiesstockai-api = "dohasecuritiesstockai.api.__main__:main"
tradingagents-api = "dohasecuritiesstockai.api.__main__:main" tradingagents-api = "dohasecuritiesstockai.api.__main__:main"
dohasecuritiesstockai-predict = "dohasecuritiesstockai.timesfm_forecasting.cli:main" dohasecuritiesstockai-predict = "dohasecuritiesstockai.timesfm_forecasting.cli:main"
tradingagents-predict = "dohasecuritiesstockai.timesfm_forecasting.cli:main" tradingagents-predict = "dohasecuritiesstockai.timesfm_forecasting.cli:main"
dohasecuritiesstockai-dgt-predict = "dohasecuritiesstockai.dgt_forecasting.cli:main"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["dohasecuritiesstockai*", "cli*"] include = ["dohasecuritiesstockai*", "cli*"]
......
from __future__ import annotations
from datetime import datetime, timedelta, timezone
import numpy as np
import torch
import dohasecuritiesstockai.dgt_forecasting as dgt
from dohasecuritiesstockai.timesfm_forecasting.market_data import (
MarketCandle,
normalize_resolution,
)
def _candles(values: list[float], *, start_day: int = 0) -> list[MarketCandle]:
start = datetime(2025, 1, 1, tzinfo=timezone.utc)
return [
MarketCandle(
time=start + timedelta(days=start_day + index),
open=value - 0.5,
high=value + 1,
low=value - 1,
close=value,
volume=1000 + index,
)
for index, value in enumerate(values)
]
def test_market_graph_alignment_forward_fills_without_back_filling():
payloads = {
"GP": _candles([10, 11, 12, 13, 14]),
"DSEX": _candles([100, 102, 104], start_day=2),
}
def fake_fetcher(symbol, resolution, **kwargs):
del kwargs
return normalize_resolution(resolution), payloads[symbol]
graph = dgt.load_dse_market_graph(
"GP",
("DSEX",),
"1d",
fetcher=fake_fetcher,
)
# The first two target dates are dropped, not back-filled from DSEX day 3.
assert [candle.close for candle in graph.target_candles] == [12, 13, 14]
np.testing.assert_array_equal(graph.closes[:, 1], [100, 102, 104])
def test_graph_prior_uses_top_correlations_and_is_row_normalized():
prices = np.array(
[
[10, 20, 30],
[11, 22, 29],
[12, 24, 31],
[13, 26, 28],
[14, 28, 32],
],
dtype=np.float32,
)
prior = dgt.build_graph_prior(prices, top_k=2)
assert prior.shape == (3, 3)
assert np.all(prior >= 0)
np.testing.assert_allclose(prior.sum(axis=1), 1.0, atol=1e-6)
assert np.all(np.diag(prior) > 0)
def test_dgt_forward_returns_one_prediction_per_graph_node():
model = dgt.DifferentialGraphTransformer(
node_count=3,
window=8,
hidden_size=8,
num_heads=1,
num_layers=1,
dropout=0,
)
features = torch.randn(4, 3, 8, 1)
prior = torch.eye(3)
prediction = model(features, prior)
assert prediction.shape == (4, 3)
assert torch.isfinite(prediction).all()
def test_end_to_end_run_builds_real_comparison_and_future_forecast(tmp_path):
point_count = 55
step = np.arange(point_count, dtype=np.float32)
target = 100 + 0.2 * step + np.sin(step / 5)
benchmark = 6000 + 2 * step + 10 * np.sin(step / 6)
market = dgt.MarketGraphData(
symbol="GP",
symbols=("GP", "DSEX"),
resolution=normalize_resolution("1d"),
target_candles=tuple(_candles(target.tolist())),
closes=np.column_stack((target, benchmark)).astype(np.float32),
)
config = dgt.DGTConfig(
window=6,
split_ratio=0.8,
validation_ratio=0.2,
future_steps=3,
epochs=1,
patience=1,
batch_size=16,
hidden_size=8,
num_heads=1,
num_layers=1,
dropout=0,
)
run = dgt.run_dgt_prediction(
"GP", config=config, device="cpu", market_data=market, progress=None
)
assert len(run.result.history) == point_count
assert len(run.result.backtest) == 11
assert len(run.result.future) == 3
assert run.result.backtest[0].actual == market.target_candles[44].close
assert run.result.model.name == "Differential Graph Transformer"
assert run.summary.graph_symbols == ("GP", "DSEX")
assert run.summary.future_refit_epochs == 1
paths = dgt.save_forecast_run(run, tmp_path, make_plot=False)
assert set(paths) == {"json", "checkpoint", "comparison", "future"}
assert all(path.is_file() for path in paths.values())
checkpoint = torch.load(paths["checkpoint"], map_location="cpu", weights_only=True)
assert checkpoint["run_id"] == run.result.run_id
assert checkpoint["model_config"]["symbols"] == ["GP", "DSEX"]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment