| 1 | # Global recommendation lifecycle MVP |
| 2 | |
| 3 | ## Architecture |
| 4 | |
| 5 | Java research-service Flyway owns additive migration **V13**. Python shares that schema through |
| 6 | `SqliteResearchPersistence` / `PostgresResearchPersistence`; SQLite mirrors it for deterministic tests. |
| 7 | No acquisition, migration backfill, Rule Engine weight changes, or provider calls were added. |
| 8 | |
| 9 | The explicit `run_global_opportunity_cycle()` job enumerates the canonical ACTIVE EQUITY catalog, |
| 10 | filters NSE, then calls the existing `GlobalOpportunityOrchestrator`. It reuses GlobalScanner, |
| 11 | Stage-B technical/sector enrichment, `STOCK_RULE_ENGINE_V1`, and `GLOBAL_OPPORTUNITY_RANKER_V1`. |
| 12 | Catalog and benchmark metadata reads occur before ranking; portfolio/watchlist/broker holdings |
| 13 | never supply candidates or affect any public score. |
| 14 | |
| 15 | The default deep shortlist is 25 (configurable 1–100). Up to 25 previous public recommendations, |
| 16 | ordered by oldest projection update, have an additional review budget. This does not change |
| 17 | the scanner shortlist or scoring. Reviews that cannot be evaluated remain visible with |
| 18 | `NOT_EVALUATED_THIS_CYCLE`; the application does not claim their thesis was refreshed. |
| 19 | |
| 20 | Within one database transaction, the job inserts immutable opportunity snapshots, **reads those |
| 21 | persisted rows back**, evaluates recommendations, appends changed recommendations, updates current |
| 22 | state, and publishes the Top-N projection. A failure rolls back the publication. The POST route |
| 23 | serializes cycles in the existing single-worker application. Multiple application processes would |
| 24 | need a distributed job coordinator before concurrent scheduling is enabled. |
| 25 | |
| 26 | ## Tables |
| 27 | |
| 28 | * `global_opportunity_snapshot`: one immutable instrument row per cycle; versions, scores, |
| 29 | eligibility/suppression, rank, price/date, reasons, and full technical/sector/rule evidence. |
| 30 | * `stock_recommendation_history`: append-only decisions, snapshot FK, fingerprint, reference price, |
| 31 | short/long levels, reasons, and frozen evidence. |
| 32 | * `recommendation_current_state`: latest history FK, original recommendation anchor, separate |
| 33 | short/long lifecycle/action, price, target distances, and reasons. |
| 34 | * `global_opportunity_top_selection`: immutable cycle publication with independent horizon picks. |
| 35 | * `recommendation_backtest_run`: immutable inputs, metrics, examples, and per-horizon outcome status. |
| 36 | |
| 37 | Both PostgreSQL and SQLite reject UPDATE/DELETE of the immutable tables. Current-state updates |
| 38 | do not mutate history. Complete JSON documents are stored as text alongside queryable columns, |
| 39 | following a shared serialization contract; NULL numeric evidence remains SQL NULL / JSON null. |
| 40 | |
| 41 | ## Recommendation policy: RECOMMENDATION_ENGINE_V1 |
| 42 | |
| 43 | These are explicit starting rules, not calibrated investment forecasts or a second scoring engine. |
| 44 | |
| 45 | * A positive candidate requires existing rank eligibility, opportunity score ≥65, confidence ≥60, |
| 46 | and coverage ≥60. A qualified score ≥80 is a strong-buy candidate. |
| 47 | * Short BUY also requires UPTREND, BREAKOUT, PULLBACK_IN_UPTREND, or REVERSAL_CANDIDATE technical state. |
| 48 | * Long ACCUMULATE additionally requires available quality and valuation dimensions ≥60. |
| 49 | * Unavailable dimensions are not zero and do not constitute deterioration. |
| 50 | * Critical existing risk overrides, governance ≤20, or two available weak core dimensions ≤30 |
| 51 | prompt long EXIT_REVIEW. One weak core dimension prompts REDUCE. |
| 52 | * New investors and holders receive separate actions. Holder actions also reflect partial-profit, |
| 53 | reduction, and exit-review lifecycle conditions. No LLM participates. |
| 54 | |
| 55 | The fingerprint covers instrument/version, all actions, a rounded reference-price bucket, |
| 56 | rounded score/confidence/coverage state, precise stored levels, and deterministic evidence content. |
| 57 | Completion/cache clocks alone do not change it. Comparison with the latest fingerprint avoids |
| 58 | identical history rows while allowing a genuine A→B→A change to be recorded. |
| 59 | |
| 60 | ## Price ranges |
| 61 | |
| 62 | All levels are in the persisted technical price's currency. V1 uses only explicit evidence: |
| 63 | |
| 64 | * When `0 < support ≤ price < resistance`, short entry is support through |
| 65 | `min(price, support × 1.03)` and target 1 is resistance. |
| 66 | * With positive ATR and support greater than ATR, target 2 is resistance + ATR; |
| 67 | invalidation is support − ATR. |
| 68 | * An explicitly persisted positive fair value supports long accumulation at 80–90% of fair value. |
| 69 | Bull target and invalidation require their own valid evidence. A valuation score or analyst |
| 70 | consensus is never relabeled as fair value. The current upstream pipeline does not synthesize |
| 71 | intrinsic value, so these long levels can legitimately remain unavailable. |
| 72 | * Unavailable levels are NULL and produce `PRICE_RANGE_EVIDENCE_INSUFFICIENT`. |
| 73 | |
| 74 | ## Lifecycle |
| 75 | |
| 76 | Original recommendation levels remain anchored independently of later price/score changes. |
| 77 | Within 3% of a target is TARGET_APPROACHING; hitting target 1 triggers SHORT PARTIAL_PROFIT and |
| 78 | risk/reward-compression reasons. Target 2 proximity/reach has a separate reason. A valid long |
| 79 | thesis can remain HOLD simultaneously. Crossing invalidation prompts SHORT EXIT / LONG EXIT_REVIEW; |
| 80 | within 3% above invalidation is INVALIDATION_APPROACHING. Entry bands and proximity have separate |
| 81 | states. Thesis deterioration takes precedence over profit conditions. |
| 82 | |
| 83 | No position membership is inferred from a recommendation's lifecycle: holder actions are a |
| 84 | public recommendation for someone who already holds it, not an instruction executed on an account. |
| 85 | |
| 86 | ## Top-N and dashboard |
| 87 | |
| 88 | `top_n` defaults to 4 and accepts 2–4. Short BUY and long ACCUMULATE/TOP_UP lists are selected |
| 89 | separately after existing suppression gates. Each uses the existing opportunity ordering: |
| 90 | score, confidence, coverage, Rule Engine score, then canonical UUID. Lists may contain fewer |
| 91 | than N when fewer candidates qualify; they are never padded with fabricated picks. |
| 92 | |
| 93 | Dashboard reads persisted selection, snapshot, history, and current-state rows. It does not run |
| 94 | the scanner, Rule Engine, recommendation engine, readiness ensure, or providers. UI membership |
| 95 | annotations use the already-loaded portfolio/watchlist state **after** ranking. The generation |
| 96 | timestamp and missing/stale areas expose the cycle's data status. |
| 97 | |
| 98 | ## APIs and controlled DEV execution |
| 99 | |
| 100 | * `GET /api/v1/research/opportunities/current` |
| 101 | * `GET /api/v1/research/opportunities/history/{instrumentId}` |
| 102 | * `POST /api/v1/research/opportunities/cycles` |
| 103 | * `GET /api/v1/research/backtesting/runs` |
| 104 | * `POST /api/v1/research/backtesting/runs` |
| 105 | |
| 106 | The existing gateway already routes `/api/v1/research/**` to the research engine. GET routes are |
| 107 | read-only. With persistence disabled, reads return empty results and write endpoints return 503. |
| 108 | |
| 109 | After research-service applies V13, submit a small canonical candidate set first: |
| 110 | |
| 111 | ```json |
| 112 | {"top_n": 2, "shortlist_limit": 2, "candidate_ids": ["<existing-canonical-NSE-UUID>"]} |
| 113 | ``` |
| 114 | |
| 115 | Omit `candidate_ids` for a global canonical-universe run. The job does not fetch missing stock data; |
| 116 | it can report no eligible results when the persisted evidence is insufficient. It is intentionally |
| 117 | not invoked from a dashboard effect or GET. No broad live cycle was required for validation. |
| 118 | |
| 119 | Example backtest POST (UTC instants; end must not be in the future): |
| 120 | |
| 121 | ```json |
| 122 | {"start":"2026-01-01T00:00:00Z","end":"2026-09-01T00:00:00Z","market":"NSE","horizon":"SHORT_TERM"} |
| 123 | ``` |
| 124 | |
| 125 | ## Backtesting V1 and temporal guards |
| 126 | |
| 127 | This MVP evaluates **recorded historical recommendations**, not hypothetical recommendations for |
| 128 | dates before the system had history. The selected date range defines the historical recommendation |
| 129 | cohort; each row's generation timestamp is its evaluation date T. No current event/exposure tables |
| 130 | are queried during a backtest. |
| 131 | |
| 132 | Frozen evidence must have publication/public-availability/discovery/retrieval/computation timestamps |
| 133 | no later than T. News features use V12's `latest_known_features` selection and typed model validation, |
| 134 | in addition to the recursive timestamp checks. Persisted price normalization checks both observation |
| 135 | and retrieval timestamps for the entry. Daily closes retain their original retrieval clock, so later |
| 136 | corrections cannot be used as an earlier entry. Future prices are outcome data only. |
| 137 | |
| 138 | Horizons are 7, 30, 91, 182, and 365 calendar days (1W/1M/3M/6M/1Y). The first available persisted |
| 139 | close on/after the horizon, within seven days, supplies the outcome. Missing entry/future prices |
| 140 | have explicit statuses and are excluded from return/hit-rate denominators. Metrics include cohort |
| 141 | count, evaluated/missing counts, hit rate (>0), mean/median/best/worst returns, close-based maximum |
| 142 | adverse excursion, and benchmark excess when explicit benchmark evidence exists. The optional |
| 143 | `benchmark_id` selects a canonical persisted benchmark; no benchmark is guessed. |
| 144 | |
| 145 | Returns are unadjusted price returns, not simulated executions, dividend-adjusted total returns, |
| 146 | or a transaction-cost model. The UI provides dates, market, horizon, run/selection, all five horizon |
| 147 | metrics, and winner/loser examples without complex charts. |
| 148 | |
| 149 | ## Validation notes |
| 150 | |
| 151 | Run focused Python tests in `tests/test_recommendation_lifecycle.py` with existing scanner, |
| 152 | ranker, and orchestration tests. `test_recommendation_postgres.py` is opt-in using |
| 153 | `RECOMMENDATION_TEST_PG_PORT` against an isolated `recommendation_validation` database already |
| 154 | migrated by Flyway. The complete research-engine suite is also required. |
| 155 | |
| 156 | V12 uses PostgreSQL types and PL/pgSQL triggers, which the existing H2 test profile cannot execute. |
| 157 | Run Maven/Flyway validation with disposable PostgreSQL datasource overrides and a second database |
| 158 | for `dailyBarsUpgradeJdbcUrl`. The upgrade assertion now expects V10→V13 (three migrations). |
| 159 | V1–V12 are unchanged. Frontend production builds and rendering tests run in Docker. |
| 160 | |
| 161 | The broader frontend suite has three existing source-assertion failures also reproduced with |
| 162 | unchanged HEAD sources: visible-stock readiness navigation, shareholding drawer company lookup, |
| 163 | and drawer provider/sector/industry projection. The new recommendation rendering tests pass. |
| 164 | |
| 165 | ## Known follow-ups |
| 166 | |
| 167 | * Freshness tuning. |
| 168 | * SHAREHOLDING behavior in global “Find required data”; acquisition policy is unchanged. |
| 169 | * Recommendation threshold calibration. |
| 170 | * Entry/target calibration and explicit fair-value evidence availability. |
| 171 | * Prediction/ML. |
| 172 | * Benchmark calibration, adjusted-return/corporate-action handling, and execution costs. |
| 173 | * Distributed job coordination before scheduling concurrent application instances. |