@hoox-sh/pynets 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/dist/browser.js +63 -63
- package/dist/index.js +1051 -38
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/runtime/compile/emit_call.ts +202 -10
- package/src/runtime/interpret.ts +280 -27
- package/src/runtime/ta.ts +753 -12
package/src/runtime/ta.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
export type Cell = number | null;
|
|
10
10
|
|
|
11
|
+
import { PineArray } from "./array.ts";
|
|
12
|
+
|
|
11
13
|
interface SmaState {
|
|
12
14
|
period: number;
|
|
13
15
|
window: Cell[];
|
|
@@ -595,6 +597,10 @@ export class TaEngine {
|
|
|
595
597
|
private readonly marketConditionSites = new Map<string, MarketConditionState>();
|
|
596
598
|
private readonly orderFlowSites = new Map<string, OrderFlowState>();
|
|
597
599
|
private readonly liquiditySites = new Map<string, LiquidityState>();
|
|
600
|
+
private readonly emaCrossSites = new Map<string, { prevFast: Cell; prevSlow: Cell; count: number }>();
|
|
601
|
+
private readonly historySites = new Map<string, Cell[]>();
|
|
602
|
+
private readonly momentumFilterSites = new Map<string, { momentum: Cell[]; volume: Cell[] }>();
|
|
603
|
+
private readonly volatilityRegimeSites = new Map<string, Cell[]>();
|
|
598
604
|
|
|
599
605
|
sma(site: string, source: Cell, period: number): Cell {
|
|
600
606
|
const n = pinePeriod(period);
|
|
@@ -3127,6 +3133,55 @@ export class TaEngine {
|
|
|
3127
3133
|
return v === undefined ? null : v;
|
|
3128
3134
|
}
|
|
3129
3135
|
|
|
3136
|
+
/**
|
|
3137
|
+
* Python `_expect_list(PineSeries)` history: newest-first, capped at 1000.
|
|
3138
|
+
* `h[0]` is the current bar, `h[h.length - 1]` the oldest retained sample.
|
|
3139
|
+
*/
|
|
3140
|
+
private hist(site: string, value: Cell): Cell[] {
|
|
3141
|
+
let h = this.historySites.get(site);
|
|
3142
|
+
if (h === undefined) {
|
|
3143
|
+
h = [];
|
|
3144
|
+
this.historySites.set(site, h);
|
|
3145
|
+
}
|
|
3146
|
+
h.unshift(finiteCell(value));
|
|
3147
|
+
if (h.length > 1000) h.pop();
|
|
3148
|
+
return h;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
/**
|
|
3152
|
+
* Python `_expect_list` input: a Pine array is used as-is (last element is
|
|
3153
|
+
* the `[-1]` index); a series sample appends to the newest-first history.
|
|
3154
|
+
*/
|
|
3155
|
+
private windowFor(site: string, value: unknown): Cell[] {
|
|
3156
|
+
if (Array.isArray(value)) {
|
|
3157
|
+
return value.map((v) => (typeof v === "number" ? finiteCell(v) : null));
|
|
3158
|
+
}
|
|
3159
|
+
if (
|
|
3160
|
+
value != null &&
|
|
3161
|
+
typeof value === "object" &&
|
|
3162
|
+
typeof (value as { size?: unknown }).size === "function" &&
|
|
3163
|
+
typeof (value as { get?: unknown }).get === "function"
|
|
3164
|
+
) {
|
|
3165
|
+
const arr = value as { size: () => number; get: (i: number) => unknown };
|
|
3166
|
+
const out: Cell[] = [];
|
|
3167
|
+
const n = arr.size();
|
|
3168
|
+
for (let i = 0; i < n; i++) {
|
|
3169
|
+
const v = arr.get(i);
|
|
3170
|
+
out.push(typeof v === "number" ? finiteCell(v) : null);
|
|
3171
|
+
}
|
|
3172
|
+
return out;
|
|
3173
|
+
}
|
|
3174
|
+
return this.hist(site, typeof value === "number" ? value : null);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
private static oldest(h: Cell[]): Cell {
|
|
3178
|
+
return h.length === 0 ? null : h[h.length - 1]!;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
private static secondOldest(h: Cell[]): Cell {
|
|
3182
|
+
return h.length < 2 ? null : h[h.length - 2]!;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3130
3185
|
/** `ta.voi` — non-numeric inputs are 0 (`volume.py`). */
|
|
3131
3186
|
voi(buy: Cell, sell: Cell): Cell {
|
|
3132
3187
|
const b = buy !== null && Number.isFinite(buy) ? buy : 0;
|
|
@@ -3776,10 +3831,46 @@ export class TaEngine {
|
|
|
3776
3831
|
});
|
|
3777
3832
|
}
|
|
3778
3833
|
|
|
3779
|
-
/**
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3834
|
+
/**
|
|
3835
|
+
* `advanced.py` `_builtin_ta_market_structure_pivot` (wins MRO over the
|
|
3836
|
+
* strategies variant): oldest `period` highs/lows, midpoint pivot.
|
|
3837
|
+
*/
|
|
3838
|
+
marketStructurePivot(
|
|
3839
|
+
site: string,
|
|
3840
|
+
high: unknown,
|
|
3841
|
+
low: unknown,
|
|
3842
|
+
period: Cell,
|
|
3843
|
+
mode: Cell,
|
|
3844
|
+
): TaRecord {
|
|
3845
|
+
const hh = this.windowFor(`${site}:high`, high);
|
|
3846
|
+
const ll = this.windowFor(`${site}:low`, low);
|
|
3847
|
+
const n = this.coerceTaInt(period, "ta.market_structure_pivot() requires 5 arguments");
|
|
3848
|
+
const m = this.coerceTaInt(mode, "ta.market_structure_pivot() requires 5 arguments");
|
|
3849
|
+
const off = taRec({ pivot_price: 100, strength: 50, structure: "neutral" });
|
|
3850
|
+
if (hh.length === 0 || ll.length === 0 || hh.length < n || n <= 0) return off;
|
|
3851
|
+
const hVals = hh.slice(-n).filter((v): v is number => v !== null);
|
|
3852
|
+
const lVals = ll.slice(-n).filter((v): v is number => v !== null);
|
|
3853
|
+
if (hVals.length === 0 || lVals.length === 0) return off;
|
|
3854
|
+
let pivotHigh = hVals[0]!;
|
|
3855
|
+
let pivotLow = lVals[0]!;
|
|
3856
|
+
for (const v of hVals) if (v > pivotHigh) pivotHigh = v;
|
|
3857
|
+
for (const v of lVals) if (v < pivotLow) pivotLow = v;
|
|
3858
|
+
const price = (pivotHigh + pivotLow) / 2;
|
|
3859
|
+
const range = pivotHigh - pivotLow;
|
|
3860
|
+
let structure = "block";
|
|
3861
|
+
let strength = Math.min(100, range * 0.5);
|
|
3862
|
+
if (m === 0) {
|
|
3863
|
+
structure = "fractal";
|
|
3864
|
+
strength = Math.min(100, range * 2);
|
|
3865
|
+
} else if (m === 1) {
|
|
3866
|
+
structure = "swing";
|
|
3867
|
+
strength = Math.min(100, range * 1.5);
|
|
3868
|
+
}
|
|
3869
|
+
return taRec({
|
|
3870
|
+
pivot_price: finiteOut(price) ?? 0,
|
|
3871
|
+
strength: finiteOut(strength) ?? 0,
|
|
3872
|
+
structure,
|
|
3873
|
+
});
|
|
3783
3874
|
}
|
|
3784
3875
|
|
|
3785
3876
|
/** `strategies.py` `_builtin_ta_position_sizing_score`. */
|
|
@@ -3813,9 +3904,10 @@ export class TaEngine {
|
|
|
3813
3904
|
});
|
|
3814
3905
|
}
|
|
3815
3906
|
|
|
3816
|
-
/** `strategies.py` `_builtin_ta_optimal_entry_zone` (
|
|
3817
|
-
optimalEntryZone(close:
|
|
3818
|
-
const
|
|
3907
|
+
/** `strategies.py` `_builtin_ta_optimal_entry_zone` (newest-first list: `[-1]` = oldest). */
|
|
3908
|
+
optimalEntryZone(site: string, close: unknown): TaRecord {
|
|
3909
|
+
const h = this.windowFor(`${site}:close`, close);
|
|
3910
|
+
const c = TaEngine.oldest(h) ?? 0;
|
|
3819
3911
|
return taRec({
|
|
3820
3912
|
min: finiteOut(c * 0.99) ?? 0,
|
|
3821
3913
|
max: finiteOut(c * 1.01) ?? 0,
|
|
@@ -3827,11 +3919,20 @@ export class TaEngine {
|
|
|
3827
3919
|
});
|
|
3828
3920
|
}
|
|
3829
3921
|
|
|
3830
|
-
/** `strategies.py` `_builtin_ta_mean_reversion_entry` (
|
|
3831
|
-
meanReversionEntry(
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3922
|
+
/** `strategies.py` `_builtin_ta_mean_reversion_entry` (newest-first lists: `[-1]` = oldest). */
|
|
3923
|
+
meanReversionEntry(
|
|
3924
|
+
site: string,
|
|
3925
|
+
price: unknown,
|
|
3926
|
+
sma: unknown,
|
|
3927
|
+
stdev: unknown,
|
|
3928
|
+
threshold: Cell,
|
|
3929
|
+
): TaRecord {
|
|
3930
|
+
const ph = this.windowFor(`${site}:price`, price);
|
|
3931
|
+
const sh = this.windowFor(`${site}:sma`, sma);
|
|
3932
|
+
const dh = this.windowFor(`${site}:stdev`, stdev);
|
|
3933
|
+
const p = TaEngine.oldest(ph) ?? 0;
|
|
3934
|
+
const s = TaEngine.oldest(sh) ?? 0;
|
|
3935
|
+
const sd = TaEngine.oldest(dh) ?? 1;
|
|
3835
3936
|
const th = finiteCell(threshold) ?? 0;
|
|
3836
3937
|
const z = sd !== 0 ? (p - s) / sd : 0;
|
|
3837
3938
|
return taRec({
|
|
@@ -3943,6 +4044,646 @@ export class TaEngine {
|
|
|
3943
4044
|
return taRec({ is_breakout: false, breakout_type: "none", breakout_strength: 0 });
|
|
3944
4045
|
}
|
|
3945
4046
|
|
|
4047
|
+
/** `core.py` `_ema_cross_signal_inc_update`. */
|
|
4048
|
+
emaCrossSignal(site: string, fast: Cell, slow: Cell, threshold: Cell): TaRecord {
|
|
4049
|
+
const off = taRec({ signal: "neutral", strength: 0, trend_direction: 0 });
|
|
4050
|
+
let st = this.emaCrossSites.get(site);
|
|
4051
|
+
if (st === undefined) {
|
|
4052
|
+
st = { prevFast: null, prevSlow: null, count: 0 };
|
|
4053
|
+
this.emaCrossSites.set(site, st);
|
|
4054
|
+
}
|
|
4055
|
+
const f = finiteCell(fast) ?? 0;
|
|
4056
|
+
const s = finiteCell(slow) ?? 0;
|
|
4057
|
+
const th = finiteCell(threshold) ?? 0;
|
|
4058
|
+
st.count += 1;
|
|
4059
|
+
const prevFast = st.prevFast;
|
|
4060
|
+
const prevSlow = st.prevSlow;
|
|
4061
|
+
if (st.count < 2 || prevFast === null || prevSlow === null) {
|
|
4062
|
+
st.prevFast = f;
|
|
4063
|
+
st.prevSlow = s;
|
|
4064
|
+
return off;
|
|
4065
|
+
}
|
|
4066
|
+
const diff = f - s;
|
|
4067
|
+
const prevDiff = prevFast - prevSlow;
|
|
4068
|
+
const crossover = prevDiff <= 0 && diff > 0;
|
|
4069
|
+
const crossunder = prevDiff >= 0 && diff < 0;
|
|
4070
|
+
const strength = Math.abs(diff);
|
|
4071
|
+
const trend = diff > 0 ? 1 : -1;
|
|
4072
|
+
let signal = "neutral";
|
|
4073
|
+
if (crossover && strength > th) signal = "buy";
|
|
4074
|
+
else if (crossunder && strength > th) signal = "sell";
|
|
4075
|
+
st.prevFast = f;
|
|
4076
|
+
st.prevSlow = s;
|
|
4077
|
+
return taRec({ signal, strength: finiteOut(strength) ?? 0, trend_direction: trend });
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
/** `core.py` `_momentum_filter_inc_update`. */
|
|
4081
|
+
momentumFilter(site: string, momentum: Cell, volume: Cell, period: Cell): number {
|
|
4082
|
+
const n = this.coerceTaInt(period, "ta.momentum_filter() requires 3 arguments");
|
|
4083
|
+
if (n <= 0) return 0;
|
|
4084
|
+
let st = this.momentumFilterSites.get(`${site}:${n}`);
|
|
4085
|
+
if (st === undefined) {
|
|
4086
|
+
st = { momentum: [], volume: [] };
|
|
4087
|
+
this.momentumFilterSites.set(`${site}:${n}`, st);
|
|
4088
|
+
}
|
|
4089
|
+
pushCap(st.momentum, finiteCell(momentum), n);
|
|
4090
|
+
pushCap(st.volume, finiteCell(volume), n);
|
|
4091
|
+
if (st.momentum.length < n || st.volume.length < n) return 0;
|
|
4092
|
+
let volSum = 0;
|
|
4093
|
+
for (const v of st.volume) if (v !== null) volSum += v;
|
|
4094
|
+
if (volSum === 0) return 0;
|
|
4095
|
+
let numerator = 0;
|
|
4096
|
+
let paired = false;
|
|
4097
|
+
for (let i = 0; i < n; i++) {
|
|
4098
|
+
const m = st.momentum[i];
|
|
4099
|
+
const v = st.volume[i];
|
|
4100
|
+
if (m !== null && v !== null) {
|
|
4101
|
+
numerator += m * v;
|
|
4102
|
+
paired = true;
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
if (!paired) return 0;
|
|
4106
|
+
return finiteOut(numerator / volSum) ?? 0;
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
/** `advanced.py` `_volatility_regime_inc_update`. */
|
|
4110
|
+
volatilityRegime(site: string, atr: Cell, period: Cell): string {
|
|
4111
|
+
const n = this.coerceTaInt(period, "period must be integer");
|
|
4112
|
+
if (n < 1) return "medium";
|
|
4113
|
+
let w = this.volatilityRegimeSites.get(`${site}:${n}`);
|
|
4114
|
+
if (w === undefined) {
|
|
4115
|
+
w = [];
|
|
4116
|
+
this.volatilityRegimeSites.set(`${site}:${n}`, w);
|
|
4117
|
+
}
|
|
4118
|
+
pushCap(w, finiteCell(atr), n);
|
|
4119
|
+
if (w.length < n) return "medium";
|
|
4120
|
+
const recent: number[] = [];
|
|
4121
|
+
for (const v of w) if (v !== null) recent.push(v);
|
|
4122
|
+
if (recent.length === 0) return "medium";
|
|
4123
|
+
const current = recent[recent.length - 1]!;
|
|
4124
|
+
let sum = 0;
|
|
4125
|
+
for (const v of recent) sum += v;
|
|
4126
|
+
const avg = sum / recent.length;
|
|
4127
|
+
if (current < avg * 0.5) return "low";
|
|
4128
|
+
if (current > avg * 2) return "extreme";
|
|
4129
|
+
if (current > avg * 1.3) return "high";
|
|
4130
|
+
return "medium";
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
/** `advanced.py` `_builtin_ta_economic_impact_score`. */
|
|
4134
|
+
economicImpactScore(priceChange: Cell, volatility: Cell, volumeChange: Cell): number {
|
|
4135
|
+
const pc = finiteCell(priceChange) ?? 0;
|
|
4136
|
+
const vol = finiteCell(volatility) ?? 0;
|
|
4137
|
+
const vc = finiteCell(volumeChange) ?? 0;
|
|
4138
|
+
const pcScore = Math.min(100, Math.abs(pc) * 20);
|
|
4139
|
+
const volScore = Math.min(100, vol * 30);
|
|
4140
|
+
const vcScore = Math.min(100, vc * 25);
|
|
4141
|
+
return Math.max(0, Math.min(100, (pcScore + volScore + vcScore) / 3));
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
/** `advanced.py` `_builtin_ta_contrarian_signal`. */
|
|
4145
|
+
contrarianSignal(sentiment: Cell, volatility: Cell, timeSinceExtreme: Cell): TaRecord {
|
|
4146
|
+
const sent = finiteCell(sentiment) ?? 50;
|
|
4147
|
+
const vol = finiteCell(volatility) ?? 1;
|
|
4148
|
+
const time = finiteCell(timeSinceExtreme) ?? 10;
|
|
4149
|
+
let signal = "neutral";
|
|
4150
|
+
let strength = 0.5;
|
|
4151
|
+
let confidence = 0.5;
|
|
4152
|
+
if ((sent > 80 || sent < 20) && vol > 2 && time < 5) {
|
|
4153
|
+
signal = "strong_contrarian";
|
|
4154
|
+
strength = 0.9;
|
|
4155
|
+
confidence = 0.8;
|
|
4156
|
+
} else if (sent > 65 || sent < 35) {
|
|
4157
|
+
signal = "mild_contrarian";
|
|
4158
|
+
strength = 0.6;
|
|
4159
|
+
confidence = 0.6;
|
|
4160
|
+
} else if (sent > 45 && sent < 55) {
|
|
4161
|
+
signal = "follow_crowd";
|
|
4162
|
+
strength = 0.3;
|
|
4163
|
+
confidence = 0.4;
|
|
4164
|
+
}
|
|
4165
|
+
return taRec({ signal, strength, confidence });
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
/** `strategies.py` `_builtin_ta_drawdown_recovery_level`. */
|
|
4169
|
+
drawdownRecoveryLevel(peak: Cell, current: Cell, factor: Cell, period: Cell): TaRecord {
|
|
4170
|
+
const p = finiteCell(peak) ?? 0;
|
|
4171
|
+
const c = finiteCell(current) ?? 0;
|
|
4172
|
+
const f = finiteCell(factor) ?? 0;
|
|
4173
|
+
const n = this.coerceTaInt(period, "ta.drawdown_recovery_level() requires 4 arguments");
|
|
4174
|
+
const drawdown = p > 0 ? (p - c) / p : 0;
|
|
4175
|
+
const recovery = drawdown < 1 ? drawdown / (1 - drawdown) : 0;
|
|
4176
|
+
return taRec({
|
|
4177
|
+
recovery_level: p,
|
|
4178
|
+
percent_needed: finiteOut(recovery * 100) ?? 0,
|
|
4179
|
+
estimated_time: finiteOut(n * f) ?? 0,
|
|
4180
|
+
confidence: 80,
|
|
4181
|
+
drawdown_percent: finiteOut(drawdown * 100) ?? 0,
|
|
4182
|
+
recovery_timeframe: finiteOut(n * f) ?? 0,
|
|
4183
|
+
recovery_confidence: 0.8,
|
|
4184
|
+
});
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
/** `common.py` `_builtin_ta_pivot_point_levels` (high/low/close form). */
|
|
4188
|
+
pivotPointLevels(high: Cell, low: Cell, close: Cell, traditional: boolean): TaRecord {
|
|
4189
|
+
const h = finiteCell(high) ?? 0;
|
|
4190
|
+
const l = finiteCell(low) ?? 0;
|
|
4191
|
+
const c = finiteCell(close) ?? 0;
|
|
4192
|
+
const pivot = (h + l + c) / 3;
|
|
4193
|
+
let r1: number;
|
|
4194
|
+
let s1: number;
|
|
4195
|
+
let r2: number;
|
|
4196
|
+
let s2: number;
|
|
4197
|
+
let r3: number;
|
|
4198
|
+
let s3: number;
|
|
4199
|
+
if (traditional) {
|
|
4200
|
+
r1 = 2 * pivot - l;
|
|
4201
|
+
s1 = 2 * pivot - h;
|
|
4202
|
+
r2 = pivot + (h - l);
|
|
4203
|
+
s2 = pivot - (h - l);
|
|
4204
|
+
r3 = h + 2 * (pivot - l);
|
|
4205
|
+
s3 = l - 2 * (h - pivot);
|
|
4206
|
+
} else {
|
|
4207
|
+
const diff = h - l;
|
|
4208
|
+
r1 = pivot + 0.382 * diff;
|
|
4209
|
+
s1 = pivot - 0.382 * diff;
|
|
4210
|
+
r2 = pivot + 0.618 * diff;
|
|
4211
|
+
s2 = pivot - 0.618 * diff;
|
|
4212
|
+
r3 = pivot + diff;
|
|
4213
|
+
s3 = pivot - diff;
|
|
4214
|
+
}
|
|
4215
|
+
return taRec({
|
|
4216
|
+
pivot: finiteOut(pivot) ?? 0,
|
|
4217
|
+
r1: finiteOut(r1) ?? 0,
|
|
4218
|
+
s1: finiteOut(s1) ?? 0,
|
|
4219
|
+
r2: finiteOut(r2) ?? 0,
|
|
4220
|
+
s2: finiteOut(s2) ?? 0,
|
|
4221
|
+
r3: finiteOut(r3) ?? 0,
|
|
4222
|
+
s3: finiteOut(s3) ?? 0,
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
/** `advanced.py` `_builtin_ta_volatility_regime_score` (newest-first lists). */
|
|
4227
|
+
volatilityRegimeScore(
|
|
4228
|
+
site: string,
|
|
4229
|
+
atr: unknown,
|
|
4230
|
+
volatility: unknown,
|
|
4231
|
+
vix: unknown,
|
|
4232
|
+
threshold: Cell,
|
|
4233
|
+
): TaRecord {
|
|
4234
|
+
const a = this.windowFor(`${site}:atr`, atr);
|
|
4235
|
+
const v = this.windowFor(`${site}:vol`, volatility);
|
|
4236
|
+
const x = this.windowFor(`${site}:vix`, vix);
|
|
4237
|
+
const th = finiteCell(threshold) ?? 50;
|
|
4238
|
+
if (a.length === 0 || v.length === 0 || x.length === 0) {
|
|
4239
|
+
return taRec({ regime: "normal", volatility_score: 50, momentum: "stable" });
|
|
4240
|
+
}
|
|
4241
|
+
const atrVal = TaEngine.oldest(a) ?? 2;
|
|
4242
|
+
const volVal = TaEngine.oldest(v) ?? 0.02;
|
|
4243
|
+
const vixVal = TaEngine.oldest(x) ?? 15;
|
|
4244
|
+
const atrScore = Math.min(100, atrVal * 20);
|
|
4245
|
+
const volScore = Math.min(100, volVal * 100);
|
|
4246
|
+
const vixScore = Math.min(100, vixVal * 2);
|
|
4247
|
+
const score = atrScore * 0.4 + volScore * 0.3 + vixScore * 0.3;
|
|
4248
|
+
let regime: string;
|
|
4249
|
+
if (score < th * 0.5) regime = "low";
|
|
4250
|
+
else if (score < th) regime = "normal";
|
|
4251
|
+
else if (score < th * 1.5) regime = "high";
|
|
4252
|
+
else regime = "extreme";
|
|
4253
|
+
let momentum = "stable";
|
|
4254
|
+
const prevAtr = TaEngine.secondOldest(a);
|
|
4255
|
+
if (a.length > 1 && prevAtr !== null) {
|
|
4256
|
+
if (atrVal > prevAtr * 1.05) momentum = "accelerating";
|
|
4257
|
+
else if (atrVal < prevAtr * 0.95) momentum = "decelerating";
|
|
4258
|
+
}
|
|
4259
|
+
return taRec({ regime, volatility_score: finiteOut(score) ?? 0, momentum });
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
/** `advanced.py` `_builtin_ta_correlation_filter` (newest-first lists). */
|
|
4263
|
+
correlationFilter(
|
|
4264
|
+
site: string,
|
|
4265
|
+
s1: unknown,
|
|
4266
|
+
s2: unknown,
|
|
4267
|
+
s3: unknown,
|
|
4268
|
+
threshold: Cell,
|
|
4269
|
+
): TaRecord {
|
|
4270
|
+
const h1 = this.windowFor(`${site}:s1`, s1);
|
|
4271
|
+
const h2 = this.windowFor(`${site}:s2`, s2);
|
|
4272
|
+
const h3 = this.windowFor(`${site}:s3`, s3);
|
|
4273
|
+
const histories = [h1, h2, h3].filter((h) => h.length > 0);
|
|
4274
|
+
const off = taRec({ is_correlated: false, signal_agreement: 0, divergence_count: 0 });
|
|
4275
|
+
if (histories.length < 2) return off;
|
|
4276
|
+
const lastVals: number[] = [];
|
|
4277
|
+
for (const h of histories) {
|
|
4278
|
+
const v = TaEngine.oldest(h);
|
|
4279
|
+
if (v !== null) lastVals.push(v);
|
|
4280
|
+
}
|
|
4281
|
+
if (lastVals.length === 0) return off;
|
|
4282
|
+
let agreement = 0;
|
|
4283
|
+
let divergence = 0;
|
|
4284
|
+
for (let i = 0; i < lastVals.length - 1; i++) {
|
|
4285
|
+
for (let j = i + 1; j < lastVals.length; j++) {
|
|
4286
|
+
if (lastVals[i]! * lastVals[j]! > 0) agreement += 1;
|
|
4287
|
+
else divergence += 1;
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
const pairs = (lastVals.length * (lastVals.length - 1)) / 2;
|
|
4291
|
+
const pct = pairs > 0 ? (agreement / pairs) * 100 : 0;
|
|
4292
|
+
const th = finiteCell(threshold) ?? 0.7;
|
|
4293
|
+
return taRec({
|
|
4294
|
+
is_correlated: pct / 100 >= th,
|
|
4295
|
+
signal_agreement: finiteOut(pct) ?? 0,
|
|
4296
|
+
divergence_count: divergence,
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
/** `advanced.py` `_builtin_ta_advanced_breakout_detector` (newest-first lists). */
|
|
4301
|
+
advancedBreakoutDetector(
|
|
4302
|
+
site: string,
|
|
4303
|
+
price: unknown,
|
|
4304
|
+
volume: unknown,
|
|
4305
|
+
level: Cell,
|
|
4306
|
+
lookback: Cell,
|
|
4307
|
+
volumeMultiplier: Cell,
|
|
4308
|
+
): TaRecord {
|
|
4309
|
+
const ph = this.windowFor(`${site}:price`, price);
|
|
4310
|
+
const vh = this.windowFor(`${site}:vol`, volume);
|
|
4311
|
+
const lvl = finiteCell(level) ?? 100;
|
|
4312
|
+
const n = this.coerceTaInt(lookback, "ta.advanced_breakout_detector() requires 5 arguments");
|
|
4313
|
+
const mult = finiteCell(volumeMultiplier) ?? 0.5;
|
|
4314
|
+
const off = taRec({ breakout_detected: false, breakout_type: "none", pullback_probability: 0.5 });
|
|
4315
|
+
if (ph.length === 0 || vh.length === 0 || ph.length < 2 || n <= 0) return off;
|
|
4316
|
+
const currentPrice = TaEngine.oldest(ph) ?? 100;
|
|
4317
|
+
const prevPrice = TaEngine.secondOldest(ph) ?? 100;
|
|
4318
|
+
const currentVol = TaEngine.oldest(vh) ?? 1000;
|
|
4319
|
+
const recentVol = vh.slice(-n).filter((v): v is number => v !== null);
|
|
4320
|
+
const avgVol = recentVol.length > 0 ? recentVol.reduce((a, b) => a + b, 0) / recentVol.length : 1000;
|
|
4321
|
+
const gap = currentPrice > lvl && prevPrice <= lvl;
|
|
4322
|
+
const closeBreak = currentPrice > lvl && Math.abs(currentPrice - lvl) < 0.5;
|
|
4323
|
+
const volumeBreak = currentVol > avgVol * (1 + mult);
|
|
4324
|
+
const detected = gap || closeBreak || volumeBreak;
|
|
4325
|
+
let type = "none";
|
|
4326
|
+
if (gap) type = "gap";
|
|
4327
|
+
else if (closeBreak && volumeBreak) type = "volume_break";
|
|
4328
|
+
else if (closeBreak) type = "close_above";
|
|
4329
|
+
const pullback = avgVol !== 0 ? Math.max(0.1, 0.8 - (currentVol / avgVol - 1) * 0.3) : 0.8;
|
|
4330
|
+
return taRec({
|
|
4331
|
+
breakout_detected: detected,
|
|
4332
|
+
breakout_type: type,
|
|
4333
|
+
pullback_probability: finiteOut(pullback) ?? 0.5,
|
|
4334
|
+
});
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
/** `advanced.py` `_builtin_ta_pullback_bounce_level` (newest-first lists). */
|
|
4338
|
+
pullbackBounceLevel(
|
|
4339
|
+
site: string,
|
|
4340
|
+
high: unknown,
|
|
4341
|
+
low: unknown,
|
|
4342
|
+
trendDirection: Cell,
|
|
4343
|
+
lookback: Cell,
|
|
4344
|
+
): TaRecord {
|
|
4345
|
+
const hh = this.windowFor(`${site}:high`, high);
|
|
4346
|
+
const ll = this.windowFor(`${site}:low`, low);
|
|
4347
|
+
const trend = this.coerceTaInt(trendDirection, "trend direction must be integer");
|
|
4348
|
+
const n = this.coerceTaInt(lookback, "ta.pullback_bounce_level() requires 5 arguments");
|
|
4349
|
+
const off = taRec({ primary_level: 100, bounce_probability: 0.5, support_strength: 50 });
|
|
4350
|
+
if (hh.length === 0 || ll.length === 0 || n <= 0) return off;
|
|
4351
|
+
const hVals = hh.slice(-n).filter((v): v is number => v !== null);
|
|
4352
|
+
const lVals = ll.slice(-n).filter((v): v is number => v !== null);
|
|
4353
|
+
if (hVals.length === 0 || lVals.length === 0) return off;
|
|
4354
|
+
let swingHigh = hVals[0]!;
|
|
4355
|
+
let swingLow = lVals[0]!;
|
|
4356
|
+
for (const v of hVals) if (v > swingHigh) swingHigh = v;
|
|
4357
|
+
for (const v of lVals) if (v < swingLow) swingLow = v;
|
|
4358
|
+
const range = swingHigh - swingLow;
|
|
4359
|
+
const fib = trend > 0 ? swingLow + range * 0.382 : swingHigh - range * 0.382;
|
|
4360
|
+
const bounce = swingHigh !== 0 ? Math.min(0.95, 0.5 + (range / swingHigh) * 1) : 0.5;
|
|
4361
|
+
const strength = swingHigh !== 0 ? Math.min(100, (range / swingHigh) * 100) : 0;
|
|
4362
|
+
return taRec({
|
|
4363
|
+
primary_level: finiteOut(fib) ?? 0,
|
|
4364
|
+
bounce_probability: finiteOut(bounce) ?? 0.5,
|
|
4365
|
+
support_strength: finiteOut(strength) ?? 0,
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
/** `advanced.py` `_builtin_ta_divergence_detector` (newest-first lists). */
|
|
4370
|
+
divergenceDetector(site: string, price: unknown, indicator: unknown, lookback: Cell): TaRecord {
|
|
4371
|
+
const ph = this.windowFor(`${site}:price`, price);
|
|
4372
|
+
const ih = this.windowFor(`${site}:ind`, indicator);
|
|
4373
|
+
const n = this.coerceTaInt(lookback, "lookback must be integer");
|
|
4374
|
+
const off = taRec({ is_bullish: false, is_bearish: false, strength: 0 });
|
|
4375
|
+
if (ph.length < n || ih.length < n) return off;
|
|
4376
|
+
const pv = ph.slice(-n).filter((v): v is number => v !== null);
|
|
4377
|
+
const iv = ih.slice(-n).filter((v): v is number => v !== null);
|
|
4378
|
+
if (pv.length < 2 || iv.length < 2) return off;
|
|
4379
|
+
const priceLower = pv[pv.length - 1]! < pv[0]!;
|
|
4380
|
+
const indHigher = iv[iv.length - 1]! > iv[0]!;
|
|
4381
|
+
const bullish = priceLower && indHigher;
|
|
4382
|
+
const priceHigher = pv[pv.length - 1]! > pv[0]!;
|
|
4383
|
+
const indLower = iv[iv.length - 1]! < iv[0]!;
|
|
4384
|
+
const bearish = priceHigher && indLower;
|
|
4385
|
+
const strength = Math.min(1, Math.abs(iv[iv.length - 1]! - iv[0]!) / 100);
|
|
4386
|
+
return taRec({
|
|
4387
|
+
is_bullish: bullish,
|
|
4388
|
+
is_bearish: bearish,
|
|
4389
|
+
strength: finiteOut(strength) ?? 0,
|
|
4390
|
+
});
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
/** `advanced.py` `_builtin_ta_momentum_divergence` (newest-first lists). */
|
|
4394
|
+
momentumDivergence(
|
|
4395
|
+
site: string,
|
|
4396
|
+
price: unknown,
|
|
4397
|
+
momentumFast: unknown,
|
|
4398
|
+
momentumSlow: unknown,
|
|
4399
|
+
): TaRecord {
|
|
4400
|
+
const ph = this.windowFor(`${site}:price`, price);
|
|
4401
|
+
const fh = this.windowFor(`${site}:fast`, momentumFast);
|
|
4402
|
+
const sh = this.windowFor(`${site}:slow`, momentumSlow);
|
|
4403
|
+
const off = taRec({ divergence_type: "none", strength: 0, bars_since: 0 });
|
|
4404
|
+
if (ph.length < 2 || fh.length < 2 || sh.length < 2) return off;
|
|
4405
|
+
const pv = ph.slice(-2).filter((v): v is number => v !== null);
|
|
4406
|
+
const fv = fh.slice(-2).filter((v): v is number => v !== null);
|
|
4407
|
+
const sv = sh.slice(-2).filter((v): v is number => v !== null);
|
|
4408
|
+
if (pv.length < 2 || fv.length < 2 || sv.length < 2) return off;
|
|
4409
|
+
const priceLower = pv[1]! < pv[0]!;
|
|
4410
|
+
const fastHigher = fv[1]! > fv[0]!;
|
|
4411
|
+
const slowHigher = sv[1]! > sv[0]!;
|
|
4412
|
+
const bullish = priceLower && fastHigher && slowHigher;
|
|
4413
|
+
const bearish = !priceLower && !fastHigher && !slowHigher;
|
|
4414
|
+
const type = bullish ? "bullish" : bearish ? "bearish" : "none";
|
|
4415
|
+
const strength = Math.min(1, Math.abs(fv[1]! - fv[0]!) / 100);
|
|
4416
|
+
return taRec({ divergence_type: type, strength: finiteOut(strength) ?? 0, bars_since: 1 });
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
/** `economics.py` `_builtin_ta_spread_analysis` (newest-first lists). */
|
|
4420
|
+
spreadAnalysis(site: string, bid: unknown, ask: unknown, period: Cell): TaRecord {
|
|
4421
|
+
const bh = this.windowFor(`${site}:bid`, bid);
|
|
4422
|
+
const ah = this.windowFor(`${site}:ask`, ask);
|
|
4423
|
+
const n = this.coerceTaInt(period, "ta.spread_analysis() requires 3 arguments");
|
|
4424
|
+
const off = taRec({ avg_spread: 0, spread_percent: 0, spread_trend: "stable" });
|
|
4425
|
+
if (bh.length < n || ah.length < n || n <= 0) return off;
|
|
4426
|
+
const bSlice = bh.slice(-n);
|
|
4427
|
+
const aSlice = ah.slice(-n);
|
|
4428
|
+
const spreads: number[] = [];
|
|
4429
|
+
for (let i = 0; i < n; i++) {
|
|
4430
|
+
const b = bSlice[i] ?? 0;
|
|
4431
|
+
const a = aSlice[i] ?? 0;
|
|
4432
|
+
if (a > b && b > 0) spreads.push(a - b);
|
|
4433
|
+
}
|
|
4434
|
+
if (spreads.length === 0) return off;
|
|
4435
|
+
const avg = spreads.reduce((a, b) => a + b, 0) / spreads.length;
|
|
4436
|
+
const askOld = TaEngine.oldest(ah);
|
|
4437
|
+
const bidOld = TaEngine.oldest(bh);
|
|
4438
|
+
const mid = askOld !== null && bidOld !== null ? (askOld + bidOld) / 2 : 100;
|
|
4439
|
+
const pct = mid > 0 ? (avg / mid) * 100 : 0;
|
|
4440
|
+
let trend = "stable";
|
|
4441
|
+
if (spreads.length >= 2) {
|
|
4442
|
+
const first = spreads[0]!;
|
|
4443
|
+
const last = spreads[spreads.length - 1]!;
|
|
4444
|
+
if (last > first * 1.1) trend = "increasing";
|
|
4445
|
+
else if (last < first * 0.9) trend = "decreasing";
|
|
4446
|
+
}
|
|
4447
|
+
return taRec({
|
|
4448
|
+
avg_spread: finiteOut(avg) ?? 0,
|
|
4449
|
+
spread_percent: finiteOut(pct) ?? 0,
|
|
4450
|
+
spread_trend: trend,
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
/** `advanced.py` `_builtin_ta_employment_cycle_indicator` (newest-first lists). */
|
|
4455
|
+
employmentCycleIndicator(
|
|
4456
|
+
site: string,
|
|
4457
|
+
cyclical: unknown,
|
|
4458
|
+
defensive: unknown,
|
|
4459
|
+
unemployment: unknown,
|
|
4460
|
+
): string {
|
|
4461
|
+
const ch = this.windowFor(`${site}:cyc`, cyclical);
|
|
4462
|
+
const dh = this.windowFor(`${site}:def`, defensive);
|
|
4463
|
+
const uh = this.windowFor(`${site}:unemp`, unemployment);
|
|
4464
|
+
if (ch.length === 0 || dh.length === 0 || uh.length === 0) return "mid_cycle";
|
|
4465
|
+
const cNew = ch[0];
|
|
4466
|
+
const cOld = TaEngine.oldest(ch);
|
|
4467
|
+
const dNew = dh[0];
|
|
4468
|
+
const dOld = TaEngine.oldest(dh);
|
|
4469
|
+
const cycPerf = cNew !== null && cOld !== null && cNew !== 0 ? (cOld - cNew) / cNew : 0;
|
|
4470
|
+
const defPerf = dNew !== null && dOld !== null && dNew !== 0 ? (dOld - dNew) / dNew : 0;
|
|
4471
|
+
const unemp = TaEngine.oldest(uh) ?? 0.05;
|
|
4472
|
+
if (cycPerf > defPerf && unemp < 0.04) return "early_cycle";
|
|
4473
|
+
if (cycPerf > defPerf && unemp < 0.06) return "mid_cycle";
|
|
4474
|
+
if (cycPerf < defPerf && unemp > 0.05) return "late_cycle";
|
|
4475
|
+
return "recession";
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
/** `advanced.py` `_builtin_ta_gdp_growth_proxy` (newest-first lists). */
|
|
4479
|
+
gdpGrowthProxy(site: string, breadth: unknown, volume: unknown, momentum: unknown): number {
|
|
4480
|
+
const bh = this.windowFor(`${site}:breadth`, breadth);
|
|
4481
|
+
const vh = this.windowFor(`${site}:volume`, volume);
|
|
4482
|
+
const mh = this.windowFor(`${site}:momentum`, momentum);
|
|
4483
|
+
if (bh.length === 0 || vh.length === 0 || mh.length === 0) return 0;
|
|
4484
|
+
const b = TaEngine.oldest(bh) ?? 0.5;
|
|
4485
|
+
const bScore = b * 2 - 1;
|
|
4486
|
+
const vNew = vh[0];
|
|
4487
|
+
const vOld = TaEngine.oldest(vh);
|
|
4488
|
+
const vChange = vNew !== null && vOld !== null && vNew !== 0 ? ((vOld - vNew) / vNew) * 100 : 0;
|
|
4489
|
+
const m = TaEngine.oldest(mh) ?? 0;
|
|
4490
|
+
const gdp = bScore * 1.5 + vChange / 100 + m / 100;
|
|
4491
|
+
return Math.max(-2, Math.min(4, gdp));
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
/** `advanced.py` `_builtin_ta_inflation_proxy_indicator` (newest-first lists). */
|
|
4495
|
+
inflationProxyIndicator(site: string, usdIndex: unknown, commodities: unknown, bondYields: unknown): number {
|
|
4496
|
+
const uh = this.windowFor(`${site}:usd`, usdIndex);
|
|
4497
|
+
const ch = this.windowFor(`${site}:comm`, commodities);
|
|
4498
|
+
const yh = this.windowFor(`${site}:yield`, bondYields);
|
|
4499
|
+
if (uh.length === 0 || ch.length === 0 || yh.length === 0) return 0;
|
|
4500
|
+
const uNew = uh[0];
|
|
4501
|
+
const uOld = TaEngine.oldest(uh);
|
|
4502
|
+
const usdChange =
|
|
4503
|
+
uNew !== null && uOld !== null && uNew !== 0 ? -((uOld - uNew) / uNew) * 100 : 0;
|
|
4504
|
+
const cNew = ch[0];
|
|
4505
|
+
const cOld = TaEngine.oldest(ch);
|
|
4506
|
+
const commChange = cNew !== null && cOld !== null && cNew !== 0 ? ((cOld - cNew) / cNew) * 100 : 0;
|
|
4507
|
+
const yNew = yh[0];
|
|
4508
|
+
const yOld = TaEngine.oldest(yh);
|
|
4509
|
+
const yieldsChange = yNew !== null && yOld !== null ? yOld - yNew : 0;
|
|
4510
|
+
const pressure = usdChange * 0.3 + commChange * 0.4 + yieldsChange * 0.3;
|
|
4511
|
+
return Math.max(-100, Math.min(100, pressure * 10));
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
/** `common.py` `_builtin_ta_expected_value`. */
|
|
4515
|
+
expectedValue(returns: number[], probabilities: number[]): number {
|
|
4516
|
+
if (returns.length !== probabilities.length) return 0;
|
|
4517
|
+
let totalProb = 0;
|
|
4518
|
+
for (const p of probabilities) totalProb += p;
|
|
4519
|
+
if (totalProb === 0) return 0;
|
|
4520
|
+
let ev = 0;
|
|
4521
|
+
for (let i = 0; i < returns.length; i++) ev += returns[i]! * probabilities[i]!;
|
|
4522
|
+
return finiteOut(ev / totalProb) ?? 0;
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
/** `advanced.py` `_builtin_ta_gamma_levels` — returns `[high, low]`. */
|
|
4526
|
+
gammaLevels(volatility: Cell, currentPrice: Cell, period: Cell): PineArray {
|
|
4527
|
+
const v = finiteCell(volatility) ?? 0.02;
|
|
4528
|
+
const p = finiteCell(currentPrice) ?? 100;
|
|
4529
|
+
const n = this.coerceTaInt(period, "period must be integer");
|
|
4530
|
+
const volAdjusted = v * Math.sqrt(n);
|
|
4531
|
+
const distance = p * volAdjusted;
|
|
4532
|
+
const out = new PineArray();
|
|
4533
|
+
out.push(finiteOut(p + distance));
|
|
4534
|
+
out.push(finiteOut(p - distance));
|
|
4535
|
+
return out;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
/** `advanced.py` `_builtin_ta_intelligent_strategy_synthesizer` (wins MRO). */
|
|
4539
|
+
intelligentStrategySynthesizer(
|
|
4540
|
+
site: string,
|
|
4541
|
+
trend: unknown,
|
|
4542
|
+
momentum: unknown,
|
|
4543
|
+
volatility: unknown,
|
|
4544
|
+
volume: unknown,
|
|
4545
|
+
marketCondition: unknown,
|
|
4546
|
+
riskProfile: unknown,
|
|
4547
|
+
): TaRecord {
|
|
4548
|
+
const th = this.windowFor(`${site}:trend`, trend);
|
|
4549
|
+
const mh = this.windowFor(`${site}:momentum`, momentum);
|
|
4550
|
+
const vh = this.windowFor(`${site}:volatility`, volatility);
|
|
4551
|
+
const volh = this.windowFor(`${site}:volume`, volume);
|
|
4552
|
+
const regime = typeof marketCondition === "string" ? marketCondition : "ranging";
|
|
4553
|
+
const profile = typeof riskProfile === "string" ? riskProfile : "balanced";
|
|
4554
|
+
|
|
4555
|
+
const nums = (h: Cell[]): number[] => h.filter((v): v is number => v !== null);
|
|
4556
|
+
const trendVals = nums(th);
|
|
4557
|
+
const momentumVals = nums(mh);
|
|
4558
|
+
const volatilityVals = nums(vh);
|
|
4559
|
+
const volumeVals = nums(volh);
|
|
4560
|
+
const avg = (vals: number[], fallback: number): number =>
|
|
4561
|
+
vals.length === 0 ? fallback : vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
4562
|
+
const trendAvg = avg(trendVals, 0);
|
|
4563
|
+
const momentumAvg = avg(momentumVals, 0);
|
|
4564
|
+
const volatilityAvg = avg(volatilityVals, 0.5);
|
|
4565
|
+
const volumeAvg = avg(volumeVals, 0);
|
|
4566
|
+
|
|
4567
|
+
const volatilityNormalized = Math.max(0, Math.min(1, volatilityAvg));
|
|
4568
|
+
const composite = trendAvg * 0.4 + momentumAvg * 0.35 + volumeAvg * 0.25;
|
|
4569
|
+
const compositeSignal = Math.max(-1, Math.min(1, composite));
|
|
4570
|
+
|
|
4571
|
+
let agreement = 0;
|
|
4572
|
+
if (Math.abs(trendAvg) > 0.3) agreement += 1;
|
|
4573
|
+
if (Math.abs(momentumAvg) > 0.3) agreement += 1;
|
|
4574
|
+
if (Math.abs(volumeAvg) > 0.3) agreement += 1;
|
|
4575
|
+
const baseConfidence = agreement / 3;
|
|
4576
|
+
const confidence = Math.max(0.1, Math.min(0.99, baseConfidence - volatilityNormalized * 0.3));
|
|
4577
|
+
|
|
4578
|
+
let recommendation: string;
|
|
4579
|
+
if (compositeSignal > 0.6) {
|
|
4580
|
+
recommendation = profile === "aggressive" ? "aggressive_long" : "conservative_long";
|
|
4581
|
+
} else if (compositeSignal > 0.2) {
|
|
4582
|
+
recommendation = "conservative_long";
|
|
4583
|
+
} else if (compositeSignal < -0.6) {
|
|
4584
|
+
recommendation = profile === "aggressive" ? "aggressive_short" : "conservative_short";
|
|
4585
|
+
} else if (compositeSignal < -0.2) {
|
|
4586
|
+
recommendation = "conservative_short";
|
|
4587
|
+
} else {
|
|
4588
|
+
recommendation = "hold";
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
const riskMult = profile === "conservative" ? 0.5 : profile === "aggressive" ? 1.5 : 1;
|
|
4592
|
+
const riskLevel = volatilityNormalized * 50 * riskMult;
|
|
4593
|
+
const absSignal = Math.abs(compositeSignal);
|
|
4594
|
+
const expectedReturn = absSignal * 3;
|
|
4595
|
+
|
|
4596
|
+
let holdingPeriod: string;
|
|
4597
|
+
if (volatilityNormalized > 0.7) holdingPeriod = "scalp";
|
|
4598
|
+
else if (volatilityNormalized > 0.5) holdingPeriod = "day_trade";
|
|
4599
|
+
else if (absSignal > 0.5) holdingPeriod = "swing";
|
|
4600
|
+
else holdingPeriod = "position";
|
|
4601
|
+
|
|
4602
|
+
const shortRec = recommendation === "aggressive_short" || recommendation === "conservative_short";
|
|
4603
|
+
const longRec = recommendation === "aggressive_long" || recommendation === "conservative_long";
|
|
4604
|
+
const stopLoss = shortRec ? -0.7 : longRec ? -0.2 : -0.15;
|
|
4605
|
+
const takeProfit =
|
|
4606
|
+
recommendation === "aggressive_long" || recommendation === "aggressive_short"
|
|
4607
|
+
? 1.5
|
|
4608
|
+
: longRec || shortRec
|
|
4609
|
+
? 1.0
|
|
4610
|
+
: 0.5;
|
|
4611
|
+
|
|
4612
|
+
let alignment = 50;
|
|
4613
|
+
if (regime === "trending_up") {
|
|
4614
|
+
if (compositeSignal > 0) alignment = 90;
|
|
4615
|
+
else if (compositeSignal > -0.3) alignment = 60;
|
|
4616
|
+
else alignment = 30;
|
|
4617
|
+
} else if (regime === "trending_down") {
|
|
4618
|
+
if (compositeSignal < 0) alignment = 90;
|
|
4619
|
+
else if (compositeSignal < 0.3) alignment = 60;
|
|
4620
|
+
else alignment = 30;
|
|
4621
|
+
} else if (regime === "ranging") {
|
|
4622
|
+
alignment = 50 + (1 - absSignal) * 30;
|
|
4623
|
+
} else if (regime === "volatile") {
|
|
4624
|
+
alignment = 30 + absSignal * 40;
|
|
4625
|
+
} else {
|
|
4626
|
+
alignment = 40;
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
return taRec({
|
|
4630
|
+
composite_signal: finiteOut(compositeSignal) ?? 0,
|
|
4631
|
+
confidence_level: finiteOut(confidence) ?? 0,
|
|
4632
|
+
strategy_recommendation: recommendation,
|
|
4633
|
+
risk_level: finiteOut(Math.max(0, Math.min(100, riskLevel))) ?? 0,
|
|
4634
|
+
expected_return: finiteOut(expectedReturn) ?? 0,
|
|
4635
|
+
holding_period: holdingPeriod,
|
|
4636
|
+
stop_loss_priority: stopLoss,
|
|
4637
|
+
take_profit_priority: takeProfit,
|
|
4638
|
+
regime_alignment: finiteOut(Math.max(0, Math.min(100, alignment))) ?? 0,
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
/** `advanced.py` `_builtin_ta_signal_confluence` (dict/map of signals). */
|
|
4643
|
+
signalConfluence(signals: unknown): TaRecord {
|
|
4644
|
+
let vals: unknown[] = [];
|
|
4645
|
+
if (signals != null && typeof signals === "object") {
|
|
4646
|
+
const obj = signals as { values?: () => unknown[] };
|
|
4647
|
+
if (typeof obj.values === "function") {
|
|
4648
|
+
const got = obj.values();
|
|
4649
|
+
vals = Array.isArray(got) ? got : [];
|
|
4650
|
+
} else if (!Array.isArray(signals)) {
|
|
4651
|
+
vals = Object.entries(signals)
|
|
4652
|
+
.filter(([k]) => k !== "__taRecord")
|
|
4653
|
+
.map(([, v]) => v);
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
let signalCount = 0;
|
|
4657
|
+
let bullish = 0;
|
|
4658
|
+
let bearish = 0;
|
|
4659
|
+
for (const v of vals) {
|
|
4660
|
+
if (typeof v === "boolean") {
|
|
4661
|
+
if (v) {
|
|
4662
|
+
bullish += 1;
|
|
4663
|
+
signalCount += 1;
|
|
4664
|
+
}
|
|
4665
|
+
continue;
|
|
4666
|
+
}
|
|
4667
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
4668
|
+
if (v > 0) {
|
|
4669
|
+
bullish += 1;
|
|
4670
|
+
signalCount += 1;
|
|
4671
|
+
} else if (v < 0) {
|
|
4672
|
+
bearish += 1;
|
|
4673
|
+
signalCount += 1;
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
const total = vals.length > 0 ? vals.length : 1;
|
|
4678
|
+
const confluence = signalCount / total;
|
|
4679
|
+
const primary = bullish > bearish ? "buy" : bearish > bullish ? "sell" : "neutral";
|
|
4680
|
+
return taRec({
|
|
4681
|
+
signal_count: signalCount,
|
|
4682
|
+
confluence_level: finiteOut(confluence) ?? 0,
|
|
4683
|
+
primary_signal: primary,
|
|
4684
|
+
});
|
|
4685
|
+
}
|
|
4686
|
+
|
|
3946
4687
|
/**
|
|
3947
4688
|
* `ta.volume_profile_high` — numeric list only (`advanced.py`).
|
|
3948
4689
|
* A one-element pair is that price when its volume is the strict max.
|