Files
stonks-oracle/docs/stonks_oracle_math_v3_upgrade.md
T

24 KiB

Stonks Oracle — Math Core v3 Upgrade

Purpose: replace the linear weighted-sentiment core with a calibrated evidence engine while preserving the existing service boundaries:

ingestion -> parser -> extractor -> aggregation -> recommendation -> risk -> trading

This version keeps the public concepts already used by Stonks Oracle:

  • WeightedSignal
  • company / macro / competitive layers
  • intraday / 1d / 7d / 30d / 90d windows
  • contradiction detection
  • trend projection
  • recommendation eligibility
  • risk-tiered position sizing
  • circuit breakers

The internal math changes from “weighted sum of vibes” to:

EvidenceUnit -> calibrated reliability -> likelihood ratio -> de-correlated posterior -> return distribution -> EV/risk decision


0. Core Design Change

Old core

W_combined = confidence * recency * credibility * novelty * context
S_avg = sum(W_combined * impact * sentiment) / sum(W_combined * impact)

This is explainable, but it is not probabilistic. It double-counts correlated articles, treats arbitrary weights as likelihood, and turns noisy agreement into false certainty.

New core

raw signal
  -> EvidenceUnit
  -> calibrated reliability p_correct
  -> signal log-likelihood ratio LLR_i
  -> correlation-adjusted cluster evidence LLR_c
  -> posterior probability P_up
  -> expected return distribution
  -> EV-gated recommendation
  -> Kelly/risk-capped position size

The deterministic score remains as an explainability layer only. The trading/recommendation decision should use the posterior and EV layers.


1. Canonical Evidence Unit

Every company, macro, or competitive signal is normalized into the same internal shape before aggregation.

EvidenceUnit_i = {
  symbol,
  layer_i,              # company | macro | competitive
  event_type_i,
  source_id_i,
  source_group_i,
  timestamp_i,
  horizon_i,            # intraday | 1d | 7d | 30d | 90d
  direction_i,          # -1 bearish, 0 neutral, +1 bullish
  sentiment_strength_i, # [0, 1]
  impact_i,             # [0, 1]
  extraction_conf_i,    # [0, 1]
  source_cred_i,        # [0, 1]
  novelty_i,            # [0, 1]
  event_base_rate_i,    # P(event_type)
  cluster_id_i
}

Neutral signals do not vote directionally, but they still count toward quality, coverage, and contradiction context.


2. Replace Combined Weight with Calibrated Reliability

2.1 Extraction reliability

Replace the hard confidence gate and raw sigmoid multiplier with a calibrated reliability term.

q_ext = sigmoid(k_ext * (extraction_conf_i - m_ext))

Defaults:

k_ext = 8.0
m_ext = 0.55

This makes low-confidence extraction fade instead of abruptly disappearing, but still penalizes weak extraction aggressively.


2.2 Source reliability with Bayesian shrinkage

Replace:

F_accuracy = 0.5 + accuracy_ratio

with a posterior source skill estimate.

For each source or source group:

theta_s ~ Beta(alpha_0 + hits_s, beta_0 + misses_s)
E[theta_s] = (alpha_0 + hits_s) / (alpha_0 + beta_0 + hits_s + misses_s)

Defaults:

alpha_0 = 3
beta_0  = 3

This starts every source near 50% until it earns trust.

Convert to usable reliability:

q_source = clamp((E[theta_s] - 0.50) / 0.35, 0.0, 1.0)

A source with 50% realized usefulness contributes little extra skill. A source near 85% realized usefulness approaches full source reliability.


2.3 Recency reliability

Keep exponential half-life decay, but do not clamp stale evidence to a nonzero floor for directional voting.

q_recency = 2^(-age_hours / half_life_hours)

Use a floor only for explainability display, not for posterior voting.

Recommended half-lives:

Horizon Half-life
intraday 2h
1d 12h
7d 72h
30d 240h
90d 720h

Adaptive extension:

tau_adaptive = tau_base * (1 + 0.75 * impact_i + 0.50 * surprise_i)
q_recency = 2^(-age_hours / tau_adaptive)

where:

surprise_i = clamp(-log2(event_base_rate_i) / 5, 0, 1)

This keeps rare/major events alive longer without letting them live forever.


2.4 Novelty as de-duplication, not hype boost

Replace direct novelty multiplication:

1 + 0.25 * novelty

with a saturation term inside each event cluster.

For signal i inside cluster c:

q_novelty_i = 1 / sqrt(1 + duplicate_count_before_i)

Then:

q_uniqueness_i = clamp(0.50 + 0.50 * novelty_i, 0.50, 1.00) * q_novelty_i

This prevents 12 near-identical articles from becoming 12 units of evidence.


2.5 Final signal reliability

q_i = q_ext * q_source * source_cred_i * q_recency * q_uniqueness_i

Clamp only at the very end:

q_i = clamp(q_i, 0, 1)

3. Convert Signals to Log-Likelihood Ratios

The posterior engine should not sum arbitrary sentiment weights. It should sum calibrated evidence.

3.1 Directional correctness probability

p_correct_i = 0.50 + p_edge_max * q_i * impact_i * sentiment_strength_i

Defaults:

p_edge_max = 0.35
p_correct_i = clamp(p_correct_i, 0.501, 0.85)

Why 0.85 max? Because even very good text signals should not become near-certain by themselves.


3.2 Signal likelihood ratio

LLR_i = direction_i * log(p_correct_i / (1 - p_correct_i))

Examples:

p_correct abs(LLR)
0.55 0.20
0.60 0.41
0.70 0.85
0.80 1.39
0.85 1.73

This makes one strong signal meaningful but not magical.


4. Correlation-Aware Evidence Clustering

4.1 Cluster signals

Cluster by:

(symbol, horizon, event_type, normalized_event_key, source_group, time_bucket)

Also include embedding/content similarity when available.

Near-duplicates go into the same cluster even if they come from different URLs.


4.2 Effective evidence count

For a cluster c with signal weights w_i = abs(LLR_i):

n_eff_c = (sum_i w_i)^2 / (sum_i w_i^2 + 2 * sum_{i<j}(rho_ij * w_i * w_j))

Defaults:

rho_ij = 0.80  same wire/story/source group
rho_ij = 0.50  same event, different publisher
rho_ij = 0.25  same theme, different event
rho_ij = 0.00  independent event

This is the anti-bullshit lever. Correlated evidence gets discounted before it reaches the posterior.


4.3 Cluster likelihood ratio

LLR_mean_c = sum_i(LLR_i * abs(LLR_i)) / sum_i(abs(LLR_i))
LLR_c = LLR_mean_c * sqrt(n_eff_c)

Clamp cluster evidence:

LLR_c = clamp(LLR_c, -2.5, 2.5)

One event cluster cannot dominate the whole decision by itself.


5. Market and Regime Prior

5.1 Base prior

Use a neutral prior unless there is validated symbol/sector history.

P_prior = 0.50

Optional calibrated prior:

logit(P_prior) = beta_0
               + beta_symbol * symbol_drift_z
               + beta_sector * sector_relative_strength_z
               + beta_market * index_trend_z

Clamp:

P_prior = clamp(P_prior, 0.40, 0.60)

The prior should nudge, not decide.


5.2 Regime detection

Keep the current regimes, but change how they are used. Do not boost raw evidence during panic. Panic should usually reduce confidence, not make the model louder.

Inputs:

trend_z = (EMA_20 - EMA_100) / ATR_20
vol_ratio = sigma_20 / sigma_100
volume_z = (log(volume_t) - mean_log_volume_60) / std_log_volume_60

Regime mapping:

Regime Condition Evidence multiplier Confidence multiplier Min edge
panic vol_ratio > 1.5 or abs(trend_z) > 2.5 0.70 0.70 high
trend_following abs(trend_z) >= 0.75 and vol_ratio < 1.3 1.10 1.00 normal
mean_reversion abs(trend_z) < 0.50 and vol_ratio < 1.0 0.90 0.95 normal
uncertainty otherwise 0.80 0.85 high

6. Posterior Trend Assembly

6.1 Posterior log-odds

logit(P_up) = logit(P_prior) + sum_c(gamma_regime * LLR_c)

where:

gamma_regime = evidence multiplier from regime table

Then:

P_up = sigmoid(logit(P_up))
P_down = 1 - P_up

6.2 Direction

Use dynamic thresholds by regime.

Regime Bullish if Bearish if
panic P_up >= 0.68 P_up <= 0.32
trend_following P_up >= 0.60 P_up <= 0.40
mean_reversion P_up >= 0.63 P_up <= 0.37
uncertainty P_up >= 0.65 P_up <= 0.35

Otherwise: neutral or mixed.


6.3 Strength

Replace:

strength = abs(S_avg)

with:

strength = abs(2 * P_up - 1)

This keeps strength in [0, 1], but now it actually means posterior directional separation.


7. Contradiction Score v3

Contradiction should measure meaningful opposing evidence, not just minority weight.

E_pos = sum_c(max(LLR_c, 0))
E_neg = sum_c(max(-LLR_c, 0))
E_total = E_pos + E_neg

If E_total = 0:

contradiction = 0

Otherwise:

f_pos = E_pos / E_total
f_neg = E_neg / E_total
H_conflict = -f_pos * log2(f_pos) - f_neg * log2(f_neg)
volume_factor = 1 - exp(-E_total / E_conflict_scale)
contradiction = H_conflict * volume_factor

Default:

E_conflict_scale = 3.0

This avoids screaming “contradiction” when there are only two tiny weak signals.


8. Confidence v3

Confidence must not be the same thing as bullishness. A model can be confidently mixed, weakly bullish, or confidently bearish.

n_eff_total = sum_c(n_eff_c)
C_evidence = 1 - exp(-n_eff_total / k_evidence)
C_direction = abs(2 * P_up - 1)
C_quality = weighted_mean(q_i, weight=abs(LLR_i))
C_regime = regime confidence multiplier
C_contradiction = 1 - contradiction
C_data = data_quality_score

Default:

k_evidence = 5.0

Final confidence:

confidence = clamp(
  C_evidence
  * sqrt(C_quality)
  * sqrt(max(C_direction, 0.05))
  * C_regime
  * C_contradiction
  * C_data,
  0,
  1
)

Why multiplicative? Because one bad dimension should actually suppress the trade instead of being averaged away.


9. Macro Layer v3

9.1 Exposure as noisy-OR

Keep the multiplicative macro exposure idea, but normalize it so full exposure can actually reach 1.

E_macro_raw = 1 - product_k(1 - w_k * O_k)
E_macro_max = 1 - product_k(1 - w_k)
E_macro = E_macro_raw / E_macro_max

Default weights:

w_geo       = 0.35
w_supply    = 0.25
w_commodity = 0.25
w_sector    = 0.15

Now full overlap maps to 1.0, not 0.689.


9.2 Resilience as effect dampener

Do not multiply the whole score blindly. Apply resilience to impact.

resilience_dampener = {
  global_leader: 0.70,
  multinational: 0.85,
  regional: 1.00,
  domestic: 1.20
}

For international events:

macro_impact = clamp(severity_weight * E_macro * resilience_dampener, 0, 1)

For domestic events:

macro_impact = clamp(severity_weight * E_macro, 0, 1)

9.3 Macro likelihood ratio

p_macro = 0.50 + 0.30 * macro_impact * event_confidence * q_recency
p_macro = clamp(p_macro, 0.501, 0.80)
LLR_macro = macro_direction * log(p_macro / (1 - p_macro))

Macro enters the same posterior engine as company evidence. No special post-hoc modifier is needed.


10. Competitive Layer v3

10.1 Correlation shrinkage

Replace raw rolling correlation with shrunk correlation.

rho_shrunk = (n / (n + k_rho)) * rho_rolling + (k_rho / (n + k_rho)) * rho_prior

Defaults:

k_rho = 30
rho_prior_same_sector = 0.30
rho_prior_cross_sector = 0.10

Use only positive propagation unless the relationship is explicitly inverse.

rho_effective = max(rho_shrunk, 0)

10.2 Graph attenuation

attenuation = rho_effective * exp(-lambda_graph * d_network)

Default:

lambda_graph = 0.85
max_distance = 3

No propagation when d_network > 3.


10.3 Competitive likelihood ratio

LLR_competitive = LLR_source * attenuation * pattern_confidence

Clamp:

LLR_competitive = clamp(LLR_competitive, -1.25, 1.25)

Competitive signals can support a case. They should not dominate a case.


11. Trend Projection v3

Replace simple momentum difference with a posterior state.

11.1 Evidence state

A_t = phi_regime * A_{t-1} + sum_c(LLR_c)
P_up_t = sigmoid(logit(P_prior_t) + A_t)

Regime decay:

Regime phi_regime
panic 0.35
trend_following 0.80
mean_reversion 0.55
uncertainty 0.50

This makes trend-following evidence persist and panic evidence decay quickly.


11.2 Projected alpha

For horizon h:

A_projected_h = phi_regime^h * A_t + expected_known_catalyst_LLR_h
P_up_projected_h = sigmoid(logit(P_prior_h) + A_projected_h)

Projected strength:

strength_projected_h = abs(2 * P_up_projected_h - 1)

Divergence flag:

divergence = sign(P_up_projected_h - 0.5) != sign(P_up_t - 0.5)

12. Return Distribution and Expected Value Gate

The existing EV formula uses strength as if it were payoff size. Replace it with a return distribution.

12.1 Horizon volatility

sigma_h = realized_vol_20d * sqrt(horizon_days / 252)

For intraday, use intraday realized volatility when available.


12.2 Expected return

edge_z = tanh(A_projected_h / z_scale)
mu_h = edge_z * confidence * sigma_h

Default:

z_scale = 3.0

12.3 Trading costs

cost = spread_cost + slippage_estimate + commission_estimate

12.4 Long EV

Simple approximation:

EV_long = mu_h - cost

Risk-adjusted approximation:

EV_long_risk_adjusted = mu_h - cost - lambda_tail * CVaR_5_loss

Default:

lambda_tail = 0.10

For a sell/exit decision on an existing long:

EV_hold = mu_h - expected_drawdown_penalty - cost_to_exit_later
EV_exit = -exit_cost
sell_if EV_exit > EV_hold

12.5 EV gate

min_edge = max(0.0025, 0.25 * cost, regime_min_edge)

Suggested regime minimum edge:

Regime min_edge
panic 0.0100
trend_following 0.0035
mean_reversion 0.0050
uncertainty 0.0075

Trade-eligible only when:

EV_long > min_edge
confidence >= confidence_min
contradiction <= contradiction_max
n_eff_total >= evidence_min

13. Recommendation Mapping v3

13.1 Eligibility gates

eligible = (
  data_quality_score >= 0.50
  and confidence >= confidence_min_regime
  and n_eff_total >= 2.0
  and contradiction <= contradiction_max_regime
  and abs(2 * P_up - 1) >= strength_min_regime
)

Defaults:

Regime confidence_min contradiction_max strength_min
panic 0.70 0.25 0.36
trend_following 0.55 0.40 0.20
mean_reversion 0.60 0.35 0.26
uncertainty 0.65 0.30 0.30

13.2 Action mapping

if not eligible:
    action = WATCH or INFORMATIONAL
elif P_up >= bullish_threshold and EV_long > min_edge:
    action = BUY
elif existing_position and EV_exit > EV_hold:
    action = SELL
elif existing_position:
    action = HOLD
else:
    action = WATCH

Do not emit SELL as a short recommendation unless shorting is explicitly enabled in config.


13.3 Mode escalation

live_eligible = (
  action in {BUY, SELL}
  and confidence >= 0.75
  and contradiction <= 0.20
  and n_eff_total >= 5
  and EV_long > 2 * min_edge
  and risk_engine_passed
)

paper_eligible = (
  action in {BUY, SELL}
  and confidence >= 0.60
  and EV_long > min_edge
  and risk_engine_passed
)

informational = otherwise

14. Position Sizing v3

Replace the two separate sizing formulas with one core sizing equation plus hard risk caps.

14.1 Stop distance

stop_distance_pct = max(
  ATR_pct * ATR_multiplier_regime,
  sigma_h * z_stop,
  min_stop_pct
)

Defaults:

z_stop = 1.25
min_stop_pct = 0.005

Regime ATR multiplier:

Regime ATR multiplier
panic 2.5
trend_following 1.8
mean_reversion 1.4
uncertainty 2.0

14.2 Win probability

p_win = P(R_h > 0)

Approximation:

p_win = P_up

Better version when distribution is available:

p_win = 1 - CDF_return_distribution(0)

14.3 Fractional Kelly

b = take_profit_distance_pct / stop_distance_pct
f_kelly = (p_win * b - (1 - p_win)) / b

Conservative final sizing:

f_raw = max(0, f_kelly)
      * kelly_fraction
      * confidence
      * data_quality_score
      * (1 - contradiction)

Defaults:

kelly_fraction = 0.25

14.4 Hard caps

portfolio_pct = clamp(f_raw, 0, max_position_pct)
portfolio_pct = min(portfolio_pct, available_sector_capacity_pct)
portfolio_pct = min(portfolio_pct, available_correlation_capacity_pct)
portfolio_pct = min(portfolio_pct, available_heat_capacity_pct)

If portfolio_pct < min_trade_pct, downgrade to WATCH.


15. Correlation and Portfolio Heat v3

15.1 Position correlation penalty

rho_portfolio = weighted_mean(abs(rho(symbol, held_symbol)), weight=position_weight)
correlation_capacity = clamp(1 - ((rho_portfolio - 0.40) / 0.40), 0, 1)

Reject if:

rho_portfolio > 0.80

15.2 Heat

Risk dollars, not position dollars:

risk_dollars_new = position_value * stop_distance_pct
portfolio_heat_new = current_open_risk + risk_dollars_new

Reject if:

portfolio_heat_new > max_portfolio_heat * portfolio_value

This is better than dollar_amount * atr_multiplier * 0.02, because it measures actual stop-defined loss exposure.


16. Stop Loss and Take Profit v3

For a long position:

stop_loss = entry_price * (1 - stop_distance_pct)
take_profit = entry_price * (1 + b * stop_distance_pct)

Dynamic reward ratio:

b = clamp(1.2 + 2.0 * confidence + 1.0 * strength - contradiction, 1.2, 3.0)

Trailing activation:

activate_trailing = unrealized_gain_pct >= 0.50 * take_profit_distance_pct

Trailing stop:

trailing_stop = max(existing_stop, current_price * (1 - trailing_distance_pct))
trailing_distance_pct = max(ATR_pct * trailing_ATR_mult, sigma_h * 0.75)

17. Data Quality v3

Replace additive quality with fail-closed multiplicative quality.

Q_parse = 1 - extraction_failure_rate
Q_conf = weighted_mean(extraction_conf_i, weight=impact_i)
Q_fresh = exp(-age_newest_hours / freshness_tau)
Q_coverage = 1 - exp(-N_valid / coverage_scale)
Q_diversity = min(1, log2(1 + N_source_types) / log2(4))

Defaults:

freshness_tau = 168h
coverage_scale = 5

Final:

data_quality_score = clamp(
  Q_parse
  * sqrt(Q_conf)
  * Q_fresh
  * Q_coverage
  * Q_diversity,
  0,
  1
)

Suppression:

if data_quality_score < 0.50: informational
if N_valid < 2: informational
if Q_parse < 0.50: informational
if company_evidence_abs == 0 and macro_evidence_abs > 0: informational unless macro_only_enabled
if company_evidence_abs == 0 and competitive_evidence_abs > 0: informational

18. Risk Tier Auto-Adjustment v3

Replace raw win rate with risk-adjusted performance.

Track:

win_rate_30d
profit_factor_30d = gross_profit / abs(gross_loss)
max_drawdown_30d
calibration_error = mean(abs(predicted_probability - realized_outcome))
realized_sharpe_30d

Downgrade one tier if any:

profit_factor_30d < 1.0
max_drawdown_30d > 0.12
calibration_error > 0.20
realized_sharpe_30d < 0

Upgrade one tier only if all:

profit_factor_30d > 1.35
max_drawdown_30d < 0.05
calibration_error < 0.12
reserve_pool > 0.20
N_trades_30d >= 20

19. Mapping to Existing Services

Existing service Keep Replace / add
services/aggregation/scoring.py input normalization, recency windows replace W_combined with q_i, p_correct_i, LLR_i
services/aggregation/contradiction.py contradiction output field replace minority-weight formula with LLR entropy contradiction
services/aggregation/bayesian.py feature-flagged probabilistic path make this the primary posterior engine
services/aggregation/regime.py EMA/volatility regime concept stop boosting evidence in panic; use regime multipliers and thresholds
services/aggregation/interpolation.py macro/company overlap model normalize noisy-OR exposure; emit macro LLR as normal evidence
services/aggregation/signal_propagation.py competitor graph add correlation shrinkage and cap propagated LLR
services/aggregation/projection.py projected trend API replace momentum delta with posterior state A_t
services/recommendation/suppression.py suppression layer replace additive quality with multiplicative fail-closed quality
services/recommendation/eligibility.py mode/action output use posterior, confidence, contradiction, and EV gates
services/trading/position_sizer.py risk caps, sector/correlation checks replace allocation formula with fractional Kelly under caps
services/trading/stop_loss_manager.py ATR stops/trailing stops make stops volatility/regime/distribution aware
services/risk/engine.py hard limits calculate heat from stop-defined risk dollars
services/trading/risk_tier_controller.py tier state machine use profit factor, drawdown, calibration error, Sharpe

20. Implementation Order

Recommended order so the system stays testable:

  1. Add EvidenceUnit and LLR conversion behind a feature flag.
  2. Add evidence clustering and n_eff.
  3. Replace S_avg trend assembly with posterior P_up.
  4. Replace contradiction with LLR entropy contradiction.
  5. Replace confidence formula.
  6. Convert macro and competitive layers to emit LLR-compatible evidence.
  7. Replace EV gate with expected-return distribution.
  8. Replace sizing with fractional Kelly under existing risk caps.
  9. Replace portfolio heat with stop-defined risk dollars.
  10. Retire heuristic scoring to explainability-only mode.

21. Output Contract

Aggregation output should expose both machine decision fields and explainability fields.

{
  "symbol": "string",
  "horizon": "7d",
  "regime": "trend_following",
  "posterior": {
    "p_up": 0.64,
    "p_down": 0.36,
    "log_odds": 0.58,
    "strength": 0.28,
    "confidence": 0.61,
    "contradiction": 0.18,
    "n_eff": 5.7,
    "data_quality": 0.82
  },
  "return_model": {
    "mu_h": 0.012,
    "sigma_h": 0.041,
    "ev_long": 0.007,
    "min_edge": 0.0035
  },
  "recommendation": {
    "action": "BUY",
    "mode": "paper_eligible",
    "reason": "Positive posterior, sufficient effective evidence, EV clears threshold, risk engine passed."
  },
  "explainability": {
    "top_positive_clusters": [],
    "top_negative_clusters": [],
    "suppression_reasons": [],
    "risk_adjustments": []
  }
}

22. The New Core in One Block

q_i = q_ext * q_source * source_cred_i * q_recency * q_uniqueness_i

p_correct_i = clamp(
  0.50 + 0.35 * q_i * impact_i * sentiment_strength_i,
  0.501,
  0.85
)

LLR_i = direction_i * log(p_correct_i / (1 - p_correct_i))

n_eff_c = (sum_i w_i)^2 / (sum_i w_i^2 + 2 * sum_{i<j}(rho_ij * w_i * w_j))

LLR_c = clamp(weighted_mean(LLR_i, abs(LLR_i)) * sqrt(n_eff_c), -2.5, 2.5)

logit(P_up) = logit(P_prior) + sum_c(gamma_regime * LLR_c)
P_up = sigmoid(logit(P_up))
strength = abs(2 * P_up - 1)

contradiction = entropy(pos_LLR_share, neg_LLR_share) * (1 - exp(-E_total / 3.0))

confidence = clamp(
  (1 - exp(-n_eff_total / 5.0))
  * sqrt(weighted_mean(q_i))
  * sqrt(max(strength, 0.05))
  * regime_confidence_multiplier
  * (1 - contradiction)
  * data_quality_score,
  0,
  1
)

A_t = phi_regime * A_{t-1} + sum_c(LLR_c)
P_up_projected_h = sigmoid(logit(P_prior_h) + phi_regime^h * A_t + known_catalyst_LLR_h)

sigma_h = realized_vol_20d * sqrt(horizon_days / 252)
mu_h = tanh(A_projected_h / 3.0) * confidence * sigma_h
EV_long = mu_h - costs - 0.10 * CVaR_5_loss

eligible = data_quality >= 0.50
        and confidence >= regime_confidence_min
        and contradiction <= regime_contradiction_max
        and n_eff_total >= 2.0
        and EV_long > min_edge

f_kelly = (p_win * b - (1 - p_win)) / b
portfolio_pct = clamp(
  max(0, f_kelly)
  * 0.25
  * confidence
  * data_quality
  * (1 - contradiction),
  0,
  max_position_pct
)