npm package: @nguyenquang1986/ai-core
Description: AI-core shared library: pure indicator functions, domain types, and utilities.
Latest version: 0.1.26


# @linhquang/ai-core

A shared library for building systematic FX/CFD strategies on top of multi‑timeframe (**M5 → D1**) signals.
It provides:

- **Pure indicator functions** (BB, ATR, EMA, …)
- **Domain types & constants** (timeframes, strategies, weights, thresholds)
- **A decision engine** that blends **Momentum** & **Mean‑Reversion** logic across multiple timeframes
- **UI‑friendly scoring** for transparent decisions

> All logic is designed to work directly with the dataset shape you’re producing (`candle`, `indicators[]`, `signal.*`).

---

## Install

```bash
npm install @linhquang/ai-core
```

---

## Quick Start

```ts
// Indicators
import { calcBB } from '@linhquang/ai-core/core/indicators/bb';
import { calcATR } from '@linhquang/ai-core/core/indicators/atr';
import { calcEMA } from '@linhquang/ai-core/core/indicators/ema';

// Utils & Domain
import { getIndicatorFromFrame } from '@linhquang/ai-core/core/utils';
import { TimeFrame } from '@linhquang/ai-core/core/domain/timeframe';

// Engine (decision & scoring for UI)
import { decideAuto, decideAllWithScores } from '@linhquang/ai-core/core/engine/decision';
```

```ts
// Given "allFrames" is a map of timeframe -> analyzed frame (your Redis payloads)
import type { AllFrames } from '@linhquang/ai-core/core/engine/decision';

const result = decideAllWithScores(allFrames);
console.log(result.best);           // => best { timeframe, decision }
console.log(result.byFrame.M15);    // => per‑TF decision + score breakdown
```

---

## Data Model (what the engine expects)

```ts
type FramePacket = {
  candle: {
    timestamp: string;
    timeframe: '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
    open: number; high: number; low: number; close: number;
    confirmed: boolean;
  };
  indicators: Array<{
    indicatorType: 'BB'|'ATR'|'EMA50'|'EMA200'|'ADX'|'TSADX'|'PSAR'|'SUPPORT_RESISTANCE'|'STOCH'|'STOCH_RSI'|'SUPER_TREND'|'RSI'|string;
    // … indicator-specific fields (value, upper/lower/middle, adx/pdi/mdi, signal1/2, bandwidth, position, …)
  }>;
  signal?: {
    // optional, if your upstream already computes single‑TF momentum/MR scores
    score?: {
      buy?:    { total: number };
      sell?:   { total: number };
      buyMR?:  { total: number };
      sellMR?: { total: number };
    };
    candlePattern?: { direction?: 'BULLISH'|'BEARISH'|'NEUTRAL', category?: string };
    trend?: any;
  };
};
```

Minimum fields used by the engine:
- **EMA50 / EMA200**: `.value`
- **ADX / TSADX**: `.adx`, `.pdi`, `.mdi`
- **BB**: `.middle`, `.upper`, `.lower`, `.position (0..1)`
- **ATR**: `.value`
- **SUPER_TREND**: `.signal1`, `.signal2`
- **PSAR**: `.value` *(optional)*
- **SUPPORT_RESISTANCE** *(optional for R/R idea)*
- **STOCH_RSI**: `.k`, `.d` *(for MR hooks)*
- **candlePattern** *(optional, only a small confirmation weight)*

---

## Strategy Model

Two complementary strategies are supported on **each TF**:

1. **Momentum** (follow trend / buy pullback in uptrend, sell pullback in downtrend)
2. **Mean‑Reversion (MR)** (fade extremes when volatility/trend strength are low)

The engine can run both and return the **best actionable decision** per TF, then **rank TFs** to pick the best overall entry window for execution.

### Multi‑Timeframe (MTF) Context

Each entry TF blends higher TF context via a convex combination:

```
m5 : 0.60*m5 + 0.25*m15 + 0.15*m30
m15: 0.60*m15 + 0.25*m30 + 0.15*H1
m30: 0.60*m30 + 0.25*H1  + 0.15*H4
H1 : 0.75*H1  + 0.25*H4
H4 : 1.00*H4
D1 : 1.00*D1
```

> Higher TFs act as **context** (trend, structure). Entry logic (zones/SL/TP) is taken from the selected TF.

---

## Thresholds (`TH`) — dynamic & per‑TF

We provide **global defaults** and **per‑TF overrides** for both strategies:

```ts
const DEFAULT_THRESHOLDS = {
  // global
  momentum: { entry: 75, total: 80 },
  mr:       { entry: 70, total: 75 },

  // per‑TF overrides
  momentum_m5:  { entry: 80, total: 85 },
  momentum_m15: { entry: 78, total: 83 },
  momentum_m30: { entry: 75, total: 80 },
  momentum_h1:  { entry: 74, total: 79 },
  momentum_h4:  { entry: 72, total: 78 },
  momentum_d1:  { entry: 75, total: 80 },

  mr_m5:  { entry: 75, total: 80 },
  mr_m15: { entry: 72, total: 78 },
  mr_m30: { entry: 70, total: 75 },
  mr_h1:  { entry: 68, total: 74 },
  mr_h4:  { entry: 66, total: 72 },
  mr_d1:  { entry: 70, total: 75 },
};
```

They are normalized at runtime to a flat map `TH`:

```
MOM_entry, MOM_total,
MOM_entry_m5, MOM_total_m5, … MOM_entry_d1, MOM_total_d1,
MR_entry, MR_total,
MR_entry_m5, MR_total_m5, … MR_entry_d1, MR_total_d1
```

> In code: `const entryTh = TH[\`MOM_entry_${tf}\] ?? TH.MOM_entry;` (or `MR_…`), where `tf ∈ {'5m','15m','30m','1h','4h','1d'}`.

---

## Decision Rules (per TF)

### 1) Trend‑Following SELL (symmetric BUY)

SELL is constructed when all of these hold:

- **Structure**: `close < EMA50 < EMA200`
- **Momentum strength**: `ADX ≥ ADX_TREND_MIN` and `-DI > +DI`
- **PSAR** *(optional)*: value above price (bearish)
- **Momentum score gap**: `scores.sell ≥ SCORE_SELL_MIN` and `scores.sell - max(scores.buy, scores.buyMR) ≥ SCORE_GAP_MIN`
- **HTF Bias** not contradicting (i.e. not BULL)

**Entry Zones** (pullback references): `BB.middle`, `SUPER_TREND.signal2`, `SUPER_TREND.signal1`, `EMA50`.  
Pick the **nearest reference above price** (or nearest overall if none above).

**Stops/Targets**:
- `SL = entryRef + ATR * ATR_SL_MULT`, bounded by `EMA50 + ATR * SL_EMA50_BUFFER_ATR_MULT`
- `TP1 = entryRef - ATR * ATR_TP1_MULT`
- `TP2 = entryRef - ATR * ATR_TP2_MULT`

Order type:
- **MARKET** if `|close - entryRef| ≤ MARKET_PROXIMITY_ATR * ATR`
- else **SELL_LIMIT** at `entryRef`

BUY rules mirror SELL with direction swapped.

### 2) Mean‑Reversion (MR)

Active only when **trend is weak**: `ADX < MR_ADX_MAX`.

- **MR SELL**: `BB.position > 0.9` and `StochRSI k<d` (rolling over from overbought).  
  Entry at `BB.upper`, `SL = +ATR*ATR_SL_MULT`, `TP1 = BB.middle`, `TP2 = entryRef - ATR*ATR_TP1_MULT`

- **MR BUY**: `BB.position < 0.1` and `StochRSI k>d` (hook from oversold).  
  Entry at `BB.lower`, `SL = -ATR*ATR_SL_MULT`, `TP1 = BB.middle`, `TP2 = entryRef + ATR*ATR_TP1_MULT`

> **BUY_DIP** is the momentum variant that buys **pullback** in uptrend. In practice it selects the nearest anchor below price among `BB.middle`, `ST.s2/s1`, `EMA50`, then applies the momentum stop/targets above.

### 3) Penalties / Gates

- **Unconfirmed candle**: multiply `entryScore` & MTF `total` by `UNCONFIRMED_FACTOR` (default 0.85).
- **Big bar**: if `(high-low)/ATR > BIG_BAR_ATR` then subtract `BIG_BAR_PENALTY` from entry score.
- (Optional) **MR guard in strong trend**: if `ADX>` threshold but **no BB overshoot**, down‑weight MR entries.

---

## UI Scoring (for transparent ranking)

Function `computeScore(decision)` produces a **0..1** score and a **breakdown**:

- **Trend**: normalized momentum score for the chosen direction  
  `pTrend = clamp01(trendScore / 100)`
- **Gap**: advantage vs opposite side  
  `pGap = clamp01(max(0, trendScore - oppScore) / 30)`
- **ADX**: trend strength  
  `pAdx = clamp01((ADX - ADX_TREND_MIN) / 20)`
- **R/R**: best of TP1/TP2 vs SL (if an order is formed)  
  `pRR = clamp01(rrBest / 2)`
- **Proximity**: closeness of current price to the entry reference  
  `pProx = clamp01(1 - |close-entry|/(0.5*ATR))`
- **Candle**: 1 if `candlePattern.direction` agrees with the chosen side (else 0)
- **Align**: agreement with HTF bias (BULL with BUY or BEAR with SELL → 1; opposite → 0; neutral → 0.5)

**Total score** (weights chosen for UX):

```
score =
  0.30*Trend +
  0.15*Gap   +
  0.15*ADX   +
  0.20*R/R   +
  0.10*Prox  +
  0.10*Candle+
  0.20*Align
```

> “BB pos” shown in snapshots is **informational**; it only feeds MR detection (pos > 0.9 or < 0.1), not the breakdown bars.

---

## Configuration

```ts
const CFG = {
  BIAS_TF: ['1h','4h'],    // timeframes to compute HTF bias

  ADX_TREND_MIN: 30,       // momentum require at least this ADX
  ADX_STRONG: 45,          // very strong trend (rarely used by default)
  MR_ADX_MAX: 25,          // MR allowed only when ADX below this

  SCORE_SELL_MIN: 55,      // minimal raw momentum score for trend‑follow
  SCORE_GAP_MIN: 15,       // min gap vs opposite side

  ATR_SL_MULT: 1.3,        // stop distance
  ATR_TP1_MULT: 1.0,       // first target
  ATR_TP2_MULT: 1.5,       // second target
  SL_EMA50_BUFFER_ATR_MULT: 0.2, // ensure SL beyond EMA50 a bit

  MARKET_PROXIMITY_ATR: 0.10, // switch to MARKET if close to reference
};
```

You can override these per deployment or symbol profile.

---

## API

### `decideForTf(all: AllFrames, entryTf: TimeFrame): Decision`
Builds a specific decision for a single TF (BUY/SELL/LIMIT/MARKET/SL/TP) or returns `WAIT` with reasons & diagnostics.

### `decideAll(all: AllFrames, frames?: TimeFrame[])`
Runs `decideForTf` on multiple TFs and returns `{ [tf]: Decision }`.

### `decideAllWithScores(all: AllFrames, frames?: TimeFrame[])`
Returns decisions + UI scores and a ranked list. Shape:
```ts
{
  byFrame: Partial<Record<TimeFrame, DecisionWithScore>>;
  ranked: Array<{ timeframe: TimeFrame, decision: DecisionWithScore }>;
  best: { timeframe: TimeFrame|null, decision: DecisionWithScore|null };
}
```

### `decideAuto(all: AllFrames): Decision`
Picks the best TF using `decideAllWithScores` (ties broken by a preferred TF order).

---

## Extending

- Add candle‑pattern weighting per TF (e.g. give 2–6 points when reversal appears at BB edge).
- Calibrate thresholds (`TH`) per symbol or session (Asia/EU/US) without touching engine code.
- Swap anchors for BUY_DIP/Sell‑rally (e.g. prefer `EMA50` over `BB.middle`) via config.

---

## License

MIT — © linhquang