@hoox-sh/pynets 0.1.0 → 0.3.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 +59 -0
- package/README.md +20 -9
- package/dist/browser.js +95 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23002 -12427
- package/package.json +1 -1
- package/src/ast/builder.ts +235 -12
- package/src/ast/nodes.ts +114 -4
- package/src/ast/unparse.ts +192 -17
- package/src/cli/rich.ts +4 -3
- package/src/cli.ts +29 -7
- package/src/generated/PinescriptLexer.interp +5 -1
- package/src/generated/PinescriptLexer.tokens +97 -95
- package/src/generated/PinescriptLexer.ts +512 -502
- package/src/generated/PinescriptParser.interp +8 -1
- package/src/generated/PinescriptParser.tokens +97 -95
- package/src/generated/PinescriptParser.ts +1907 -1287
- package/src/generated/PinescriptParserVisitor.ts +36 -1
- package/src/index.ts +22 -0
- package/src/runtime/array.ts +238 -68
- package/src/runtime/compile/collections.ts +492 -0
- package/src/runtime/compile/draw.ts +181 -0
- package/src/runtime/compile/emit.ts +1421 -0
- package/src/runtime/compile/emit_call.ts +1796 -0
- package/src/runtime/compile/emit_udf.ts +326 -0
- package/src/runtime/compile/engine.ts +265 -0
- package/src/runtime/compile/helpers.ts +140 -0
- package/src/runtime/compile/index.ts +33 -0
- package/src/runtime/compile/runtime.ts +357 -0
- package/src/runtime/compile/strategy.ts +250 -0
- package/src/runtime/compile/types.ts +127 -0
- package/src/runtime/compile/udt_runtime.ts +50 -0
- package/src/runtime/drawings.ts +405 -8
- package/src/runtime/interpret.ts +2387 -281
- package/src/runtime/matrix.ts +371 -42
- package/src/runtime/strategy.ts +695 -33
- package/src/runtime/ta.ts +654 -20
- package/src/runtime/timeframe.ts +199 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Object-mode compile broker. Python analog: CompileStrategyBroker.
|
|
6
|
+
* Injected as `__h.strategy`. Wraps StrategyBook (fills + ledger + risk).
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
StrategyBook,
|
|
10
|
+
type BarOhlc,
|
|
11
|
+
type BrokerSettings,
|
|
12
|
+
type PlaceEntryOpts,
|
|
13
|
+
type PlaceExitOpts,
|
|
14
|
+
type StrategyDirection,
|
|
15
|
+
type StrategyEvent,
|
|
16
|
+
type StrategySummary,
|
|
17
|
+
} from "../strategy.ts";
|
|
18
|
+
|
|
19
|
+
function asDir(v: unknown): StrategyDirection | number | string {
|
|
20
|
+
if (v == null) return "long";
|
|
21
|
+
if (v === 1 || v === "1" || v === "long" || v === "strategy.long" || v === "buy") return "long";
|
|
22
|
+
if (v === -1 || v === "-1" || v === "short" || v === "strategy.short" || v === "sell") return "short";
|
|
23
|
+
return String(v);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function num(v: unknown, fallback: number): number {
|
|
27
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
28
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
29
|
+
if (typeof v === "string" && v !== "") {
|
|
30
|
+
const n = Number(v);
|
|
31
|
+
if (Number.isFinite(n)) return n;
|
|
32
|
+
}
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function str(v: unknown, fallback = ""): string {
|
|
37
|
+
if (v == null) return fallback;
|
|
38
|
+
return String(v);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function optNum(v: unknown): number | null {
|
|
42
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function asExitOpts(v: unknown): PlaceExitOpts {
|
|
47
|
+
if (v == null || typeof v !== "object" || Array.isArray(v)) return {};
|
|
48
|
+
const rec = v as Record<string, unknown>;
|
|
49
|
+
return {
|
|
50
|
+
from_entry: rec.from_entry == null || rec.from_entry === "" ? null : String(rec.from_entry),
|
|
51
|
+
qty: optNum(rec.qty),
|
|
52
|
+
qty_percent: optNum(rec.qty_percent),
|
|
53
|
+
profit: optNum(rec.profit),
|
|
54
|
+
limit: optNum(rec.limit),
|
|
55
|
+
loss: optNum(rec.loss),
|
|
56
|
+
stop: optNum(rec.stop),
|
|
57
|
+
trail_price: optNum(rec.trail_price),
|
|
58
|
+
trail_points: optNum(rec.trail_points),
|
|
59
|
+
trail_offset: optNum(rec.trail_offset),
|
|
60
|
+
comment: rec.comment == null ? undefined : String(rec.comment),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class CompileStrategy {
|
|
65
|
+
readonly book: StrategyBook;
|
|
66
|
+
private bar = 0;
|
|
67
|
+
private mark = 0;
|
|
68
|
+
private time = 0;
|
|
69
|
+
private ohlc: BarOhlc = {};
|
|
70
|
+
|
|
71
|
+
constructor(settings?: BrokerSettings) {
|
|
72
|
+
this.book = new StrategyBook(settings);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Apply `strategy(...)` declaration kwargs (every bar, same as interpret). */
|
|
76
|
+
configure(settings?: unknown): void {
|
|
77
|
+
if (settings == null || typeof settings !== "object" || Array.isArray(settings)) return;
|
|
78
|
+
const rec = settings as Record<string, unknown>;
|
|
79
|
+
const broker: BrokerSettings = {};
|
|
80
|
+
const commission = optNum(rec.commission);
|
|
81
|
+
if (commission != null) broker.commission = commission;
|
|
82
|
+
const slippage = optNum(rec.slippage);
|
|
83
|
+
if (slippage != null) broker.slippage = slippage;
|
|
84
|
+
if (rec.pyramiding !== undefined) {
|
|
85
|
+
const pyr = optNum(rec.pyramiding);
|
|
86
|
+
broker.pyramiding = pyr == null ? undefined : Math.trunc(pyr);
|
|
87
|
+
}
|
|
88
|
+
if (typeof rec.avg_price_model === "string") broker.avg_price_model = rec.avg_price_model;
|
|
89
|
+
const leverage = optNum(rec.leverage);
|
|
90
|
+
if (leverage != null) broker.leverage = leverage;
|
|
91
|
+
const ml = optNum(rec.margin_long);
|
|
92
|
+
if (ml != null) broker.margin_long = ml;
|
|
93
|
+
const ms = optNum(rec.margin_short);
|
|
94
|
+
if (ms != null) broker.margin_short = ms;
|
|
95
|
+
if (typeof rec.default_qty_type === "string") broker.default_qty_type = rec.default_qty_type;
|
|
96
|
+
const dqv = optNum(rec.default_qty_value);
|
|
97
|
+
if (dqv != null) broker.default_qty_value = dqv;
|
|
98
|
+
if (Object.keys(broker).length) this.book.configure(broker);
|
|
99
|
+
const capital = optNum(rec.initial_capital);
|
|
100
|
+
if (capital != null) this.book.initialCapital = capital;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
beginBar(
|
|
104
|
+
bar: unknown,
|
|
105
|
+
open: unknown,
|
|
106
|
+
high: unknown,
|
|
107
|
+
low: unknown,
|
|
108
|
+
close: unknown,
|
|
109
|
+
time?: unknown,
|
|
110
|
+
): void {
|
|
111
|
+
this.bar = num(bar, 0);
|
|
112
|
+
this.mark = num(close, 0);
|
|
113
|
+
this.time = time == null ? this.bar : num(time, this.bar);
|
|
114
|
+
const ohlc = {
|
|
115
|
+
open: num(open, this.mark),
|
|
116
|
+
high: num(high, this.mark),
|
|
117
|
+
low: num(low, this.mark),
|
|
118
|
+
close: this.mark,
|
|
119
|
+
};
|
|
120
|
+
this.ohlc = ohlc;
|
|
121
|
+
this.book.processPending(this.bar, ohlc);
|
|
122
|
+
this.book.markOpenTrades(ohlc.high, ohlc.low, ohlc.close);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
entry(id?: unknown, direction?: unknown, qty?: unknown, opts?: PlaceEntryOpts): void {
|
|
126
|
+
const fillPrice = opts?.price ?? this.mark;
|
|
127
|
+
const q = qty == null ? this.book.resolveDefaultQty(fillPrice) : num(qty, 1);
|
|
128
|
+
this.book.placeEntry(this.bar, str(id, "entry"), asDir(direction), q, {
|
|
129
|
+
price: fillPrice,
|
|
130
|
+
time: opts?.time ?? this.time,
|
|
131
|
+
limit: opts?.limit,
|
|
132
|
+
stop: opts?.stop,
|
|
133
|
+
comment: opts?.comment,
|
|
134
|
+
oca_name: opts?.oca_name,
|
|
135
|
+
oca_type: opts?.oca_type,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
close(id?: unknown, qty?: unknown): void {
|
|
140
|
+
this.book.fillClose(this.bar, str(id, ""), this.mark, {
|
|
141
|
+
time: this.time,
|
|
142
|
+
qty: optNum(qty),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
close_all(_comment?: unknown): void {
|
|
147
|
+
this.book.closeAll(this.bar, this.mark);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
order(id?: unknown, direction?: unknown, qty?: unknown, opts?: PlaceEntryOpts): void {
|
|
151
|
+
this.entry(id, direction, qty, opts);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
cancel(id?: unknown): void {
|
|
155
|
+
this.book.cancel(this.bar, str(id, ""));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
cancel_all(): void {
|
|
159
|
+
this.book.cancelAll(this.bar);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
exit(id?: unknown, opts?: unknown): void {
|
|
163
|
+
const parsed = asExitOpts(opts);
|
|
164
|
+
this.book.placeExit(this.bar, str(id, "exit"), {
|
|
165
|
+
...parsed,
|
|
166
|
+
price: this.mark,
|
|
167
|
+
time: this.time,
|
|
168
|
+
ohlc: this.ohlc,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
position_size(): number {
|
|
173
|
+
return this.book.position.qty;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
netprofit(): number {
|
|
177
|
+
return this.book.realizedPnl;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
equity(): number {
|
|
181
|
+
return this.book.equity(this.mark);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
openprofit(): number {
|
|
185
|
+
const q = this.book.position.qty;
|
|
186
|
+
const avg = this.book.position.avgPrice;
|
|
187
|
+
if (q === 0 || avg == null || !Number.isFinite(avg)) return 0;
|
|
188
|
+
return q * (this.mark - avg);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
opentrades(): number {
|
|
192
|
+
return this.book.opentrades;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
closedtrades(): number {
|
|
196
|
+
return this.book.closedtrades;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
leverage(): number {
|
|
200
|
+
return this.book.leverage;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
margin_liquidation_price(): number | null {
|
|
204
|
+
return this.book.marginLiquidationPrice();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
position_avg_price(): number | null {
|
|
208
|
+
return this.book.position.avgPrice;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
initial_capital(): number {
|
|
212
|
+
return this.book.initialCapital;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
risk_allow_entry_in(value?: unknown): void {
|
|
216
|
+
this.book.riskAllowEntryIn(value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
risk_max_position_size(percent?: unknown): void {
|
|
220
|
+
this.book.riskMaxPositionSize(percent);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
risk_max_drawdown(value?: unknown, type?: unknown): void {
|
|
224
|
+
this.book.riskMaxDrawdown(value, type);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
extras(): CompileStrategyExtras {
|
|
228
|
+
return {
|
|
229
|
+
__events: this.book.events.slice(),
|
|
230
|
+
__fills: this.book.fills.slice(),
|
|
231
|
+
__position_size: this.book.position.qty,
|
|
232
|
+
__netprofit: this.book.realizedPnl,
|
|
233
|
+
__equity: this.book.equity(this.mark),
|
|
234
|
+
__strategy: this.book.summary(this.mark),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface CompileStrategyExtras {
|
|
240
|
+
__events: StrategyEvent[];
|
|
241
|
+
__fills: StrategyBook["fills"];
|
|
242
|
+
__position_size: number;
|
|
243
|
+
__netprofit: number;
|
|
244
|
+
__equity: number;
|
|
245
|
+
__strategy: StrategySummary;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createCompileStrategy(settings?: BrokerSettings): CompileStrategy {
|
|
249
|
+
return new CompileStrategy(settings);
|
|
250
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Compile-path types. TS compile is JS emit (Python object-mode analog).
|
|
6
|
+
* No Numba / nopython backend.
|
|
7
|
+
*/
|
|
8
|
+
import type { Call, FunctionDef, expr, stmt } from "../../ast/nodes.ts";
|
|
9
|
+
|
|
10
|
+
export type CompileBackend = "js";
|
|
11
|
+
|
|
12
|
+
export type RuntimeMode = "interpret" | "compile" | "auto";
|
|
13
|
+
|
|
14
|
+
export interface CompilePlotMeta {
|
|
15
|
+
title: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CompileEligibility {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CompileError extends Error {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "CompileError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CompileEmitError extends CompileError {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "CompileEmitError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class CompileLoadError extends CompileError {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "CompileLoadError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class CompileIneligibleError extends CompileError {
|
|
45
|
+
constructor(message: string) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "CompileIneligibleError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Visitor state while lowering a Script to JS. */
|
|
52
|
+
export interface EmitCtx {
|
|
53
|
+
arrays: Set<string>;
|
|
54
|
+
plots: CompilePlotMeta[];
|
|
55
|
+
varNames: Set<string>;
|
|
56
|
+
functions: string[];
|
|
57
|
+
userFuncs: Set<string>;
|
|
58
|
+
usesStrategy: boolean;
|
|
59
|
+
nextSite: number;
|
|
60
|
+
errors: string[];
|
|
61
|
+
/** `once`-structure fired flags, declared in the bar-loop frame. */
|
|
62
|
+
onceFlags: string[];
|
|
63
|
+
/** Pine UDF name currently being emitted (null at script scope). */
|
|
64
|
+
currentFunc: string | null;
|
|
65
|
+
/** Formal names of the current UDF. */
|
|
66
|
+
currentParamNames: Set<string>;
|
|
67
|
+
/** Current UDF formals that must be passed as full series arrays. */
|
|
68
|
+
currentSeriesParams: Set<string>;
|
|
69
|
+
/** Pine UDF name → series formal names (call-site packing). */
|
|
70
|
+
funcSeriesParams: Map<string, Set<string>>;
|
|
71
|
+
/** Pine UDF name → formal order. */
|
|
72
|
+
funcParamNames: Map<string, string[]>;
|
|
73
|
+
/** Persistent `__st_*` arrays allocated in the bar-loop frame. */
|
|
74
|
+
stArrays: Set<string>;
|
|
75
|
+
/** type name → field order + default JS exprs. */
|
|
76
|
+
udtTypes: Map<string, { fields: string[]; defaults: Record<string, string> }>;
|
|
77
|
+
usesDrawings: boolean;
|
|
78
|
+
/** enum name → member names. */
|
|
79
|
+
enumTypes: Map<string, string[]>;
|
|
80
|
+
/** Method names collected from TypeDef FunctionDefs. */
|
|
81
|
+
udtMethodNames: Set<string>;
|
|
82
|
+
needsHeikinashi: boolean;
|
|
83
|
+
importAliases: Set<string>;
|
|
84
|
+
getLibrary?: (namespace: string, name: string, version: number) => string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface EmitFns {
|
|
88
|
+
visit(node: expr | stmt | null | undefined): string;
|
|
89
|
+
visitStmt(node: stmt): string;
|
|
90
|
+
emitCall(node: Call): string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type VisitFn = (node: expr | stmt | null | undefined) => string;
|
|
94
|
+
|
|
95
|
+
export type EmitCallFn = (ctx: EmitCtx, node: Call, visit: VisitFn) => string;
|
|
96
|
+
|
|
97
|
+
export type EmitUdfFn = (ctx: EmitCtx, node: FunctionDef, visitStmt: (s: stmt) => string) => void;
|
|
98
|
+
|
|
99
|
+
/** Generated module entry: arrays in, titled plot series out. */
|
|
100
|
+
export type ExecuteCompiled = (
|
|
101
|
+
open: Array<number | null>,
|
|
102
|
+
high: Array<number | null>,
|
|
103
|
+
low: Array<number | null>,
|
|
104
|
+
close: Array<number | null>,
|
|
105
|
+
volume: Array<number | null>,
|
|
106
|
+
time: Array<number | null>,
|
|
107
|
+
) => Record<string, Array<number | null>>;
|
|
108
|
+
|
|
109
|
+
export interface CompiledScript {
|
|
110
|
+
readonly source: string;
|
|
111
|
+
readonly plots: CompilePlotMeta[];
|
|
112
|
+
readonly backend: CompileBackend;
|
|
113
|
+
run(
|
|
114
|
+
open: ArrayLike<number | null | undefined>,
|
|
115
|
+
high: ArrayLike<number | null | undefined>,
|
|
116
|
+
low: ArrayLike<number | null | undefined>,
|
|
117
|
+
close: ArrayLike<number | null | undefined>,
|
|
118
|
+
volume?: ArrayLike<number | null | undefined> | null,
|
|
119
|
+
time?: ArrayLike<number | null | undefined> | null,
|
|
120
|
+
extras?: { inputs?: Record<string, number | string | boolean> },
|
|
121
|
+
): Record<string, Array<number | null>>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface CompileCacheStats {
|
|
125
|
+
source_entries: number;
|
|
126
|
+
source_max: number;
|
|
127
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2024-2026 jango_blockchained
|
|
3
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
*
|
|
5
|
+
* Compile-path UDT helpers: dict-like instances (`__type__` + fields).
|
|
6
|
+
*/
|
|
7
|
+
import { UdtInstance, UdtType } from "../udt.ts";
|
|
8
|
+
|
|
9
|
+
const types = new Map<string, UdtType>();
|
|
10
|
+
|
|
11
|
+
export function udtRegister(name: string, fields: string[], defaults?: Record<string, unknown>): void {
|
|
12
|
+
types.set(
|
|
13
|
+
name,
|
|
14
|
+
new UdtType(
|
|
15
|
+
name,
|
|
16
|
+
fields.map((f) => ({ name: f, default: defaults?.[f] ?? null })),
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function udtNew(name: string, overrides?: Record<string, unknown>): UdtInstance | Record<string, unknown> {
|
|
22
|
+
let t = types.get(name);
|
|
23
|
+
if (t == null) {
|
|
24
|
+
const fields = overrides != null ? Object.keys(overrides) : [];
|
|
25
|
+
t = new UdtType(name, fields.map((f) => ({ name: f, default: null })));
|
|
26
|
+
types.set(name, t);
|
|
27
|
+
}
|
|
28
|
+
return t.newInstance(overrides);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function udtGet(obj: unknown, field: string): unknown {
|
|
32
|
+
if (obj instanceof UdtInstance) return obj.get(field);
|
|
33
|
+
if (obj != null && typeof obj === "object") {
|
|
34
|
+
const rec = obj as Record<string, unknown>;
|
|
35
|
+
return rec[field] ?? null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function udtSet(obj: unknown, field: string, value: unknown): unknown {
|
|
41
|
+
if (obj instanceof UdtInstance) {
|
|
42
|
+
obj.set(field, value);
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
if (obj != null && typeof obj === "object") {
|
|
46
|
+
(obj as Record<string, unknown>)[field] = value;
|
|
47
|
+
return obj;
|
|
48
|
+
}
|
|
49
|
+
return obj ?? null;
|
|
50
|
+
}
|