PrimeThai
PrimeThai is a research, daily scanner and historical backtest toolkit for Thai listed equities. It does not connect to a broker and cannot place live orders. Stock price downloads use Yahoo Finance through yfinance; symbols use Yahoo's .BK suffix. The SET benchmark can be imported from SiamChart's daily EOD ZIP files as ^SET.csv.
Architecture
Yahoo Finance stocks / SiamChart SET archive
↓
Local CSV cache → dated SET universe → indicators / RS / breadth
↓
SET + SET100EW + MCO / MCSI
↓
deterministic market regime
↓ ↓
leader scan event backtest
↓
metrics / experiments / JSON
MarketDataProviderisolates downloads from the rest of the engine.YFinanceProviderstores Yahoo OHLC,Adj Close, derived adjusted OHLC, dividends and splits locally. Yahoo is called withauto_adjust=False,actions=True, andrepair=True. The simulator default assumes repaired Yahoo OHLC is on a split-adjusted share basis, and credits dividends as cash. Yahoo saysrepair=Trueattempts price and split repairs; inspect split dates and unusual corporate actions before trusting a run. Other providers can set the backtest'syahoo_split_adjusted=Falseoption to apply split ratios explicitly.- Indicators, regimes, candidate scores, risk sizing and backtest transitions are deterministic Python code. LLM output is a JSON handoff for later explanation only.
data/cache/holds provider cache;data/raw/contains the working input files;reports/receives generated results.
Windows setup
Use Python 3.11 or later. From PowerShell in this folder:
py -3.14 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
If PowerShell blocks activation, call the environment's interpreter directly:
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
The core dependencies are pinned in requirements.txt and pyproject.toml. scipy and scikit-learn are included because yfinance's optional price-repair path uses them when repair=True.
Download data
The default command downloads a ten-symbol smoke universe and attempts to download ^SET.BK:
python -m primethai download
Import the SET index from SiamChart
SiamChart's Stock page publishes two ZIP archives under “SET INDEX Historical (*.csv)”: one through the previous year and one for the current year. Each archive contains one daily CSV per trading date. Import the SET index rows into the standard local input file with:
python -m primethai import-siamchart --archives data/raw/siamchart/set-archive_EOD_1970-LAST.zip data/raw/siamchart/set-archive_EOD.zip --symbols ^SET
The importer accepts additional SiamChart tickers, for example --symbols ^SET ADVANC AOT, and writes one normalized CSV per selected ticker. The EOD archives contain raw OHLCV without action fields. Import the separate stock-info action history with:
python -m primethai import-siamchart-actions --symbols ADVANC AOT CPALL DELTA PTT KBANK KTB TRUE GULF SCB
This writes data/raw/actions/SYMBOL_dividends.csv and SYMBOL_par_changes.csv. Dividend dates are treated as XD dates. Backtests load those dividend sidecars automatically, credit cash only for shares held before the XD date, and include an action date even when that symbol has no OHLC row. Par-value history is saved for review and is not converted into a stock split automatically. Missing or unavailable stock-info pages are listed per ticker; a partial import exits with a nonzero status.
To compare imported events against a user-reviewed SET CSV, use columns symbol,action_type,date,value,source_url, where action_type is dividend or par_value:
python -m primethai verify-siamchart-actions --symbols AP INTUCH --set-reference data/reference/siamchart_actions_set_reference_sample.csv
The command writes reports/siamchart_actions_audit.csv, including matches, missing events and value mismatches. The included reference is a two-row example, not a complete action history. A reliable audit needs a complete SET reference for the same symbols and date range. The SiamChart archives and imported daily price files are stored under data/raw/ and included in this repository so a deployment can start with the same research history without downloading and rebuilding it first. The app can still refresh the latest session automatically.
SET cash-dividend reference and action-adjusted backtests
The official SET Rights & Benefits history is the reference for cash XD events within its available date range. Import the same dated PIT price symbols, plus any source ticker aliases, and keep the generated coverage manifest beside the CSV:
$symbols = Import-Csv data/universes/set100_pit_2024_2026_h1.csv | Select-Object -ExpandProperty symbol -Unique
python -m primethai import-set-actions --symbols $symbols --start 2025-01-01 --end 2026-07-01 --symbol-aliases data/reference/set_symbol_aliases.csv
python -m primethai backtest --universe SET100 --mode accurate --membership-file data/universes/set100_pit_2024_2026_h1.csv --start 2025-01-01 --end 2026-07-01 --data-dir data/backtest_2025_2026_accurate --set-reference data/reference/set_actions_2025_2026_h1.csv --output-dir reports/backtest_2025_2026_accurate_set_actions
The importer writes a .manifest.json file with requested price symbols, the inclusive/exclusive coverage dates, source ticker aliases, and any failed ticker requests. A backtest refuses a missing, incomplete, or symbol-incomplete manifest. Inside that verified date range it credits the SET cash-action list and suppresses other dividend fields; SiamChart-only events remain flagged in the audit and are not credited there. Outside that date range, imported SiamChart dividend sidecars remain available. Alias rows map the SET action ticker to the historical price-series ticker for cash-action reconciliation only; they do not relabel OHLCV data. Par-value rows remain review-only.
Choose symbols and a date range explicitly:
python -m primethai download --symbols ADVANC AOT CPALL DELTA PTT KBANK KTB TRUE GULF SCB --start 2020-01-01 --end 2026-09-29
The end date is exclusive, as in yfinance. Repeated requests use the local cache; use --refresh to fetch again. Symbols that fail are reported individually, and successful symbols are kept. The Yahoo data terms, adjustments and historical availability can change; inspect downloaded data before relying on a result. A failed or delisted symbol is never silently filled with a surviving symbol. A real-data download can be run with one command after installing dependencies:
python -m primethai download
SET100, SET50, SET and mai universes
Membership is supplied as CSV with symbol,start_date,end_date,sector. Dated membership intervals are used for breadth and the equal-weight series. The consolidated data/universes/set100_pit_2024_2026_h1.csv contains official SET snapshots from 2024-01-02 through 2026-06-30, including dated SET100 changes during 1H 2025. Its 2024-2025 sector cells are blank; sector labels are included only for H1 2026. The original data/universes/set100_2026_h1.csv remains as the official H1 2026 fixture.
The SET constituent archive lists H2 2026, but its full constituent file is not available in the public materials used here. The PIT CSV stops at June 30, 2026 and does not carry the H1 roster forward. SET50/SET/mai and custom lists use the same interface and need their own membership files for accurate historical breadth.
Fast versus accurate mode
- Fast takes the latest member snapshot available in the file and holds it fixed through the requested price history. Every command prints a
SURVIVORSHIP BIASwarning. Use it for development and smoke runs only, never to claim historical index performance. - Accurate resolves members by effective date, clips a longer index file to the covered PIT interval and requested end, and retains covered pre-start history for indicator warm-up. It errors when the requested evaluation dates or any market-context session lack PIT membership coverage. For SET100EW / breadth / MCO history to be accurate, its PIT file must cover the warm-up window too. The consolidated SET100 file covers 2024-01-02 through 2026-06-30.
For an accurate run from 2025 through H1 2026 (including a year of price warm-up and PIT breadth coverage):
python -m primethai download --universe SET100 --mode accurate --membership-file data/universes/set100_pit_2024_2026_h1.csv --start 2024-01-02 --end 2026-07-01
Accurate mode correctly stops for dates after June 30, 2026 until a complete H2 2026 constituent history is supplied. See data/universes/README.md for the official sources, dated 2025 interim updates, sector coverage, and known boundaries.
Indicators
Per-symbol and SET-index features include EMA21 High / Close / Low, UP/DOWN/NEUTRAL EMA structure, ABOVE/INSIDE/BELOW price state, ATR14 (14-session simple mean of true range), ADR20 (mean daily range as percent of previous close), SMA50/SMA200, fully warmed rolling 252-session high/low, 20-session average volume/value turnover, and weekly 10-period EMA High/Close/Low with weekly trend state. The ATR extension is (close - EMA21 high) / ATR14; the default filter blocks new entries beyond +1 ATR.
RS is a project-owned cross-sectional percentile from 0–100. It combines 1-, 3- and 12-month excess return versus SET100EW and close proximity to the rolling 252-session high. The starting weights are 20/35/30/15 percent. Period excess-return comparisons versus both SET and SET100EW, and the raw 52-week proximity percentage, are retained for inspection. This is a PrimeThai research assumption, not TradersLab's proprietary RS rank or formula.
Breadth uses advancing, declining, unchanged and net advances from effective constituents. MCO is defined here as EMA19(net advances) minus EMA39(net advances), with a rolling mean, standard deviation and z-score; MCSI is the cumulative MCO with a 10-session average and crossing/curl states. These are transparent implementation choices; breadth counts are not ratio-adjusted for universe size. MCO is timing context, not a standalone buy rule; MCSI is confirmation context.
SET100EW is a daily-rebalanced equal-weight price-return proxy with base 100, computed from available member returns. It is intended as a breadth/participation companion to cap-weighted ^SET.BK, not an official SET index and not a total-return series.
Market regime and scanner
The regime state machine emits CORRECTION, REPAIR, EARLY_UPTREND, CONFIRMED_UPTREND, UPTREND_PULLBACK, OVERBOUGHT or BREAKDOWN, plus deterministic reasons. It combines SET structure/50DMA, SET100EW structure, MCO/MCSI and breadth. Its cutoffs and MCO repair threshold can be changed in config/default.json. No LLM participates in the decision.
The leader scanner filters by price and 20-session average turnover (default THB 20m), then ranks RS, market alignment, EMA structure, confirmed higher low, ATR distance, volume behavior, tightness, available sector strength and weekly confluence. In dated mode it uses only members with a bar on the latest market session; blocked market regimes show candidates as Watch with zero proposed risk. Weights start at 25/20/15/10/10/5/5/5/5 and are configurable under focus_weights in config/default.json. Indicator warm-ups, pivot confirmation width, pullback volume window, tightness window and the 20-session base breakout window are configurable. Missing sector data is omitted and remaining weights are renormalized; no sector is guessed. Candidate entry references are research levels from the latest close; Watch-only rows show zero shares; backtest fills are at the next open.
python -m primethai market --universe SET100 --mode fast
python -m primethai scan --universe SET100 --mode fast --top 10 --json-out reports\qwen_candidates.json
Backtester
Signals from a daily close are submitted for the next session's open. The engine charges configurable commission, fees, sell tax and slippage on every fill; defaults are research assumptions, not broker quotes. Entry risk starts at 0.25% equity for weakness and 0.50% for confirmation, with a 20% notional cap per position. Quantity is rounded down by configured board lot. The default Thai lot is 100; a 50-share lot is available for eligible high-priced securities, but eligibility needs to be supplied/verified for the symbol and period.
The engine records initial R once, takes one third at +2R (odd-lot remainder is permitted for the trim), and follows the runner with a daily close below EMA21 Low → next-open exit by default. Position sizing uses the initial EMA21 Low distance, while the actual exit follows the moving EMA21 Low at the next open; therefore initial R is a sizing reference, not a guaranteed maximum loss, and overnight gaps can exceed it. The 8% emergency max-loss check is a separate research trigger and cannot cap a gap through its price. The alternative exit mode records a structure break, then a close back into structure as a reclaim attempt, and exits next open if price breaks structure again. Portfolio heat is the remaining shares' initial risk divided by equity; the regime heat limits and portfolio cushion are saved daily. The default heat map and position cap are PrimeThai research assumptions.
Backtest output includes total return, CAGR, drawdown, win rate, average win/loss, expectancy in cash and R, profit factor, average/median R, MAE/MFE (including actual fill prices), holding time, average invested exposure, trades/month, 2R hit rate, separate 2R trim/runner contribution, a fill ledger, and skipped-entry reasons. See reports/backtest_2025_2026_accurate_set_actions/strategy_diagnosis.md for the current loss analysis.
The opt-in strategy-overlays research command compares the baseline with no-progress exits for weak 21DMA retests and a break-even runner floor after a 2R trim. It requires explicit dates and saves each run separately; none of these settings change the production defaults.
python -m primethai strategy-overlays --universe SET100 --mode accurate --membership-file data/universes/set100_pit_2024_2026_h1.csv --start 2025-01-01 --end 2026-07-01 --data-dir data/backtest_2025_2026_accurate --set-reference data/reference/set_actions_2025_2026_h1.csv --output-dir reports/strategy_overlays_2025_2026_accurate_set_actions
The result summary and research interpretation are in reports/strategy_overlays_2025_2026_accurate_set_actions/strategy_overlays/README.md.
python -m primethai backtest --universe SET100 --mode fast --start 2021-01-01 --end 2026-01-01
Outputs are saved under reports/. Fast-mode results are survivorship biased. Accurate mode also requires continuous PIT membership across all market-context warm-up dates.
Plain EMA crossover benchmark
Compare a no-frills trend-following rule against the research strategy. A fast EMA crossing above a slow EMA at the daily close schedules a buy for the next open; a cross below schedules a full exit for the next open. The benchmark has no market-regime, RS, stop-loss, or profit-taking rule. By default it compares EMA 10/30, 20/50 and 50/200, caps each stock at 20% of equity, spreads available cash equally across simultaneous signals, reallocates budgets that cannot buy a minimum board lot, applies only the configured minimum-price and 20-session average-turnover filters, and uses the same Thai board lots, costs, PIT membership and cash-dividend handling as the main backtest.
python -m primethai ema-crossover --universe SET100 --mode accurate --membership-file data/universes/set100_pit_2024_2026_h1.csv --start 2025-01-01 --end 2026-07-01 --data-dir data/backtest_2025_2026_accurate --set-reference data/reference/set_actions_2025_2026_h1.csv --split-by-year --output-dir reports/ema_crossover_2025_2026_accurate_set_actions
The command saves a summary and separate fills, trades, equity curves and skipped-entry reasons for each EMA pair and period. Year slices reset capital and liquidate independently; they are diagnostics and should not be added together to reproduce the full-period run. EMA spans and allocation can be changed with --ema-pairs FAST:SLOW ... and --max-position-pct.
To test overlays, pass one or more --market-filters (none, ew_trend_up, ew_above_ema200), --exit-modes (cross_under, ema21_low) and --max-loss-pcts (0 disables the hard stop; values such as 0.08 mean 8%). The command tests the Cartesian combination of the selected options and saves each combination separately.
Long-history model lab
The separate model-lab command screens five fixed rule families on the SiamChart market-wide archive: 12–1 month momentum rotation, the same momentum ranked by realized volatility, EMA 50/200 crossovers, RSI(2) pullbacks in an uptrend, and 55/20 Donchian breakouts. Each uses a daily as-of-date top-100 liquidity universe, next-open execution, dividend/split adjustments where the local action history supports them, and both base and stressed transaction costs. Reports include annual and early/middle/recent-period returns. The low-volatility momentum, EMA 50/200 and Donchian candidates also get separate 100-share board-lot simulations with raw prices and cash dividends, plus a sensitivity that writes off a position after 60 consecutive sessions without a bar. Remaining limitations include incomplete action history and no market-impact model. It does not change PrimeThai defaults or count the previously identified EMA 20/50 + SET100EW candidate.
python -m primethai model-lab --start 2005-01-01 --end 2026-07-01 --output-dir reports/model_lab_2005_2026_h1
The defaults expect both data/raw/siamchart/set-archive_EOD_1970-LAST.zip and data/raw/siamchart/set-archive_EOD.zip. Review summary.csv, period_metrics.csv, annual_returns.csv, symbol_attribution.csv, board_lot_validation.csv, board_lot_period_metrics.csv, board_lot_stale60_validation.csv, board_lot_stale60_period_metrics.csv, and data_notes.json together; a positive full-period return alone is not enough to promote a strategy.
Experiments and walk-forward
The cumulative A–H ablation table adds RS, regime, MCO, MCSI, higher lows, weekly structure and the ATR extension filter in order. The experiment command also reports chronological train/validation/out-of-sample slices and rolling fixed-parameter windows; it does not optimize on the full sample. The slices are only defensible OOS evidence when the historical membership and price/action data are valid.
python -m primethai experiment --universe SET100 --mode accurate --start 2015-01-01 --end 2026-01-01
Tests
The suite uses Python's standard unittest test cases and synthetic/fixture data, so it runs without Yahoo access or pytest:
python -m unittest discover -s tests -v
Run the end-to-end offline smoke pipeline on generated data:
python -m primethai smoke
This is only a wiring check; generated returns are not market evidence. For a real-data pipeline smoke run, download the ten default symbols and the SET index, then run:
python -m primethai market --universe custom --symbols ADVANC AOT CPALL DELTA PTT KBANK KTB TRUE GULF SCB
python -m primethai scan --universe custom --symbols ADVANC AOT CPALL DELTA PTT KBANK KTB TRUE GULF SCB --top 5
python -m primethai backtest --universe custom --symbols ADVANC AOT CPALL DELTA PTT KBANK KTB TRUE GULF SCB
Custom fast runs also show SURVIVORSHIP BIAS; these ten tickers are only a pipeline smoke set, not a historical SET100 universe.
Original PrimeTrading concepts
The reference framework describes market-first positioning, the EMA21 high/close/low structure, buying pullbacks near rising structure, relative strength, liquid leaders, breadth timing with MCO and confirmation with MCSI, weekly 10-period confluence, avoiding entries more than about 1 ATR above the structure, trimming near 2R, and managing the remainder around EMA21 low. The author explicitly describes the system as discretionary and tailored to their own risk tolerance. PrimeThai models a subset as deterministic research rules; it does not reproduce discretionary judgment or claim the same performance.
PrimeThai research assumptions
The precise EMA slope lookback, ATR/ADR formulas, RS periods/weights and percentile method, MCO normalization/Z-score window, regime transition conditions, scanner score, pivot confirmation window, volume contraction metric, entry triggers, initial risk, one-third trim, max-loss stop, portfolio heat, position cap, transaction cost defaults and daily-rebalanced SET100EW calculation are implementation assumptions. They are configurable where practical and must be assessed with out-of-sample research. They are not asserted to be original TradersLab formulas.
Known limitations
- Yahoo Finance is a convenience source, not an exchange-grade historical database. Check gaps, ticker changes, corporate-action behavior, suspensions, delistings and licensing/availability for each symbol. The package reports failures per symbol, but cannot recover missing delisted securities automatically.
- In a live probe on 2026-09-28, Yahoo returned only one current quote for
^SET.BK, so the provider refuses incomplete benchmark history. The SiamChart archives now provide a direct path to populatedata/raw/^SET.csv; the imported series must still be checked for missing/corrupt daily records. The current archive has one unreadable entry dated 2018-09-14, and some older daily files omit the SET row. The importer reports unreadable entries and the CLI requires at least 260 valid index bars before market context or scanning. SET's official historical index data is also available as a paid data request. - The included SET100 PIT file covers 2024-01-02 through 2026-06-30 only. Accurate research before or after that range needs dated constituent files for every tested period and warm-up; there is no fabricated backfill.
- yfinance prices can be revised and Yahoo corporate-action semantics may change. SiamChart's EOD archive omits its separately listed actions; import the stock-info sidecars and check them against a complete SET reference before relying on dividend-adjusted results. Par-value history is audit-only pending manual confirmation of actual share splits. Rights offerings, tender offers, spin-offs, mergers and symbol migrations are not fully modeled.
- The simulator uses daily OHLC. If both emergency risk and target levels occur in one bar, it assumes the adverse stop occurs first. Gaps, limit-up/down, halts, queue priority, partial liquidity fills, lot-size history, odd-lot execution, broker-specific costs, settlement and earnings exposure are not fully modeled.
- The stock 50-share board lot requires six months at/above THB 500 and an exchange notice; historical eligibility is not inferred. Index constituents may include products with different trading units.
- SET100EW is a reconstructed proxy, not a licensed exchange-calculated index. Breadth uses price changes among observed bars; suspended/no-print shares are excluded on a date.
- Earnings/calendar risk, sector mapping outside the supplied SET membership file, downtrend-line breakouts, discretionary character-change assessments and actual paper-trading order lifecycle are not implemented.
Phase 3 direction
Keep the scanner and deterministic JSON payload as the interface. Add a local Qwen copilot that explains setups, points out conflicting fields, lists missing confirmations, and states invalidation conditions. Validate every explanation against the payload; do not allow it to compute indicators, alter a regime, size a position or submit a trade.
Primary model focus (2026-09-29)
The selected primary research models are donchian_55_20 and ema50_200_cross. Their combined “either model signals” portfolio uses the latest daily archive, currently through 2026-09-28. The current fresh-start run is in reports/primary_union_current/primary_union_donchian_ema/; the earlier separate-model comparison through 2026-09-25 remains in reports/model_lab_2026_fresh_start_through_2026_09_25/ytd_summary.md.
The combined primary signal is the union of both model target lists: a symbol is held when either model selects it, overlapping selections count once, and distinct active symbols are equal-weighted up to 20 positions. For 2026-01-05 through 2026-09-28 (179 sessions), its 100-share lot simulation returned 37.9% at base costs and 35.1% under stressed costs, with maximum drawdowns of 11.0% and 11.2%; turnover was about 10.2 times starting capital. This is a short historical simulation, not a live track record or a promise of future returns. The daily table is a target basket, not a buy/sell delta against a brokerage account; reconcile it with actual holdings and current quotes before placing any orders. The current scan and backtest commands still use the PrimeThai setup described above; this project has no broker connection or live order execution.
Local Web UI and daily signals
Run the strategy dashboard from the project folder:
python -m primethai webui
The app requires a local .env file for its login. The project has a generated .env for this workspace; for another installation, copy .env.example to .env and set a username, a unique password of at least 12 characters, and a random session secret of at least 32 bytes. The .env file is excluded from Git. PRIMETHAI_SESSION_TTL_HOURS controls how long a login lasts (default 168 hours); PRIMETHAI_COOKIE_SECURE=auto enables secure cookies when an HTTPS reverse proxy sends X-Forwarded-Proto: https. The app accepts the current public host when the proxy preserves the original Host or X-Forwarded-Host and X-Forwarded-Proto headers. If the proxy uses a fixed internal host, list the accepted full public origins in PRIMETHAI_ALLOWED_ORIGINS, separated by commas, for example https://trade.example.com,https://stocks.example.net. Include a non-default port in the origin. Restart the server after changing these settings.
Then open http://127.0.0.1:18900 and sign in. The same login protects the portfolio page, backtest page, and API; there is also an ออกจากระบบ button. Sessions use signed, HTTP-only, same-site cookies and expire after the configured lifetime. The home page is the portfolio book; the historical chart and simulation are at /backtest. At startup, the service checks SiamChart and downloads the latest EOD ZIP unless a successful after-close refresh has already happened that day, then recalculates signals. It merges each dated CSV into a cumulative local archive instead of replacing earlier sessions; data/raw/siamchart/set-archive_EOD_LAST.sources.json records the provider and coverage for each stored session. It repeats automatically on Thai weekdays after 20:30 Asia/Bangkok and retries network failures every 30 minutes. If SiamChart fails after the close, Yahoo Finance through yfinance is the secondary feed. The fallback must include a SET index bar and at least 95% of the previous session's top-100 liquid model universe, plus current signal/held symbols, before its data is accepted. If it cannot meet that check, the app keeps the prior complete price history and shows its date; there is no upload step. The อัปเดตสัญญาณ button remains as an optional immediate refresh. The service process must remain running for scheduled refreshes. The calculation runs on the server and can take a few minutes.
Yahoo Finance is an independent fallback, not an exchange-licensed feed or a guarantee of complete coverage. yfinance describes itself as an open-source interface to Yahoo's public APIs, intended for research/personal use; use the downloaded data only for the user's private app and review Yahoo's terms. For an official SET market-data API source, SET's specification requires an assigned account and API key and documents latest stock/index quotations; it is not configured here. See the SET Market Data API specification.
For a server deployment, bind to the network interface with --host 0.0.0.0 and place the app behind an HTTPS reverse proxy that forwards the original Host and X-Forwarded-Proto headers. The app's login protects its pages and APIs, but the app itself uses HTTP; do not expose its port directly to the public internet. Example: python -m primethai webui --host 0.0.0.0 --port 18900.
The cumulative daily ZIP is saved automatically as data/raw/siamchart/set-archive_EOD_LAST.zip; the two historical ZIPs remain the warm-up and backtest sources. The dashboard displays the stored session range and the source used for its latest date. ลงทุนครั้งแรก records the initial cash contribution; later contributions increase cash and recalculate target holdings from current portfolio equity and the model weights. The app shows current shares, target shares and proposed BUY/SELL differences. Click บันทึกซื้อ or บันทึกขาย beside a daily signal to add its proposed share quantity to the local portfolio ledger at the latest EOD close. This only updates the app; it does not send a broker order. Use แก้ in the recent history after your order fills to update the quantity, actual price and fees, or ยกเลิก to remove an estimated signal entry from portfolio calculations while retaining its history. Record dividends and withdrawals in the cash ledger too; share splits and rights entitlements still need manual reconciliation. Account history is persisted in data/portfolio/prime_thai_portfolio.sqlite3, which is included as the initial database snapshot. Keep this file on persistent server storage so portfolio edits survive redeploys; later server-side changes do not sync back to Git automatically.
When the available cash cannot fund the whole target basket, the dashboard uses a capital-aware lot allocator. It ranks names selected by both models first, then uses a combined momentum and volatility-adjusted momentum score, and only recommends whole 100-share lots that fit the current cash. Names that do not fit remain visible as งบไม่พอ instead of being silently chosen by ticker order. A recommendation that was recorded as a one-lot purchase remains held while its signal is still active, even if equal-weight sizing would round below one lot on the next refresh. The budget is based on currently recorded cash; expected sale proceeds become available after the sale is recorded.
The proposed limit reference is the latest EOD close, not a live quote or a guaranteed fill. Signal entries use that close as an estimate until you edit the ledger with the actual fill. The app does not connect to a broker, transmit orders, or automatically reconcile broker holdings. The backtest page is a research simulation, not a validated live record, and does not model market impact, bid/ask spreads, every corporate action, or broker-specific buying power. Verify current quotes, liquidity, order size and cash with the broker before each order.
The default dashboard report is written to reports/primary_union_current. You can change the local port, date range, starting cash, report folder, or input archives with --port, --start, --initial-equity, --report-dir, and --archives.
Docker deployment
The Docker image includes the existing data/ tree (SiamChart archives, imported daily prices, backtest inputs and the initial portfolio database) and reports/. It does not need to download the historical archives again. The app still checks for the latest daily EOD session and refreshes it automatically when needed.
Create .env from .env.example and set the login credentials and session secret as described above. Then build and start the app:
docker compose up --build -d
Compose uses named volumes for /app/data and /app/reports. On first creation, Docker seeds them from the files included in the image; later restarts and image rebuilds keep the server's updated price archive, portfolio database and reports. Use docker compose down to stop the app while keeping its data. The port is bound to localhost so an HTTPS reverse proxy can sit in front of it.
For automatic refresh diagnostics, view the container log with docker compose logs -f primethai. Failed-source entries include the provider, failed stage, exception type and message; retry entries summarize SiamChart, Yahoo Finance, archive reading and signal calculation status.
Login origin troubleshooting
The login form requires Referrer-Policy: same-origin. The old no-referrer policy makes browsers send Origin: null for native form POSTs, which the app rejects even if the public domain is listed in PRIMETHAI_ALLOWED_ORIGINS. The app now sets same-origin in its response header and login page metadata. After updating, rebuild/recreate the container and reload /login before submitting; an already open page retains its old policy:
git pull --ff-only
docker compose up -d --build --force-recreate primethai
If login still returns 403, look for [auth] entries with event: origin_rejected in docker compose logs --tail=100 primethai. These show the reason, received Origin, expected origin, configured origins and selected proxy headers. null_origin_check_page_referrer_policy indicates a hidden/opaque origin; reload the page and check browser/privacy or proxy referrer-policy settings. origin_mismatch indicates a public scheme/host mismatch; compare the logged headers with the public URL and the configured allowed origins. Logs exclude passwords, session cookies and request bodies. Missing, null and unrelated origins remain rejected.
The home page shows the refresh stage at the top. A 409 from the refresh button means a refresh is already running (usually the automatic startup refresh); the page follows that existing job and keeps showing its current stage. If the report files are missing, the page explicitly says that signals are not ready yet. A failed calculation shows the failed stage and exception in the page and in the container log.