@hoox-sh/pynets 0.2.0 → 0.4.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.
@@ -32,6 +32,7 @@ import {
32
32
  mathMin,
33
33
  mathPow,
34
34
  mathRound,
35
+ mathRoundToMintick,
35
36
  mathSign,
36
37
  mathSin,
37
38
  mathSqrt,
@@ -58,7 +59,8 @@ import { LogBook, formatLogParts } from "../log.ts";
58
59
  import { resolveCurrencyRate } from "../request.ts";
59
60
  import { TaEngine } from "../ta.ts";
60
61
  import { utcPartsFromMs, timestamp, weekOfYear, timeTradingDay, type UtcParts } from "../time.ts";
61
- import { timeframeInSeconds } from "../timeframe.ts";
62
+ import { TickerId } from "../ticker.ts";
63
+ import { timeframeInSeconds, timeframePeriodChanged } from "../timeframe.ts";
62
64
  import { compileArray, compileMap, compileMatrix } from "./collections.ts";
63
65
  import { createCompileDraw } from "./draw.ts";
64
66
  import {
@@ -81,6 +83,7 @@ import {
81
83
  safeNeg,
82
84
  safeSub,
83
85
  } from "./helpers.ts";
86
+ import { isStrategyCashAmount } from "../strategy.ts";
84
87
  import { createCompileStrategy } from "./strategy.ts";
85
88
  import { udtGet, udtNew, udtRegister, udtSet } from "./udt_runtime.ts";
86
89
 
@@ -94,10 +97,17 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
94
97
  const ta = new TaEngine();
95
98
  const inputs = opts?.inputs;
96
99
  const book = new LogBook();
100
+ // Python `timeframe_in_seconds` defaults None / "" to "D" (daily, 86400)
101
+ // before parsing — builtins/timeframe.py:189-190. The shared helper in
102
+ // timeframe.ts maps na / empty → null, so compile wraps it here to match.
103
+ const timeframeInSecondsOrDaily = (p: string | null): number | null =>
104
+ timeframeInSeconds(p == null || p === "" ? "D" : p);
97
105
  return {
98
106
  na: NA_FN,
99
107
  nz,
100
108
  naNum,
109
+ plotNum,
110
+ timenow,
101
111
  isNa: isNaCell,
102
112
  eq: pineEq,
103
113
  ne: pineNe,
@@ -174,7 +184,11 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
174
184
  timestamp,
175
185
  weekOfYear,
176
186
  timeTradingDay,
177
- timeframeInSeconds,
187
+ timeframeInSeconds: timeframeInSecondsOrDaily,
188
+ timeframeChange,
189
+ timeClose,
190
+ roundToMintick,
191
+ syminfoPrefix,
178
192
  strLength,
179
193
  strContains,
180
194
  strStartsWith,
@@ -253,6 +267,23 @@ function iff(cond: unknown, thenV: unknown, elseV: unknown): unknown {
253
267
  return mathIff(cond as number | null, thenV as number | null, elseV as number | null);
254
268
  }
255
269
 
270
+ /** Plot cell: tagged `strategy.cash` unwraps to its number (interpret `unwrap`). */
271
+ function plotNum(v: unknown): number | null {
272
+ if (isStrategyCashAmount(v)) return Number.isFinite(v.value) ? v.value : null;
273
+ return naNum(v);
274
+ }
275
+
276
+ /**
277
+ * Dataset last-bar time on every bar. Interpret `beginBar` stores that on
278
+ * `ctx.timenow` (host seed is overwritten). Not wall-clock, not current bar.
279
+ */
280
+ function timenow(timeArr: unknown): number {
281
+ if (!Array.isArray(timeArr) || timeArr.length === 0) return 0;
282
+ const last = timeArr[timeArr.length - 1];
283
+ if (typeof last === "number" && Number.isFinite(last)) return last;
284
+ return (timeArr.length - 1) * 60_000;
285
+ }
286
+
256
287
  function NA_FN(v?: unknown): number | null {
257
288
  if (arguments.length === 0) return null;
258
289
  return isNaCell(v) ? 1 : 0;
@@ -295,3 +326,52 @@ function calendarDayofweek(t: unknown): number | null {
295
326
  function logBar(bar: unknown): number {
296
327
  return typeof bar === "number" && Number.isFinite(bar) ? bar : 0;
297
328
  }
329
+
330
+ /** `timeframe.change`: bar 0 is a new period; empty / unknown tf → false. */
331
+ function timeframeChange(tf: unknown, curr: unknown, prev: unknown, barIndex: unknown): number {
332
+ const s =
333
+ typeof tf === "string"
334
+ ? tf
335
+ : typeof tf === "number" && Number.isFinite(tf)
336
+ ? String(tf)
337
+ : null;
338
+ if (s == null || s.trim() === "") return 0;
339
+ if (typeof curr !== "number" || !Number.isFinite(curr)) return 0;
340
+ const idx = typeof barIndex === "number" && Number.isFinite(barIndex) ? barIndex : null;
341
+ return timeframePeriodChanged(curr, prev, s, idx) ? 1 : 0;
342
+ }
343
+
344
+ /**
345
+ * Next bar open, last bar `time + 86400000`. `0` next-open counts as missing
346
+ * (interpret beginBar). Compile has no per-bar `time_close` column.
347
+ */
348
+ function timeClose(barIdx: unknown, timeArr: unknown): number {
349
+ const i = typeof barIdx === "number" && Number.isFinite(barIdx) ? Math.trunc(barIdx) : 0;
350
+ const arr = Array.isArray(timeArr) ? (timeArr as Array<number | null | undefined>) : [];
351
+ const t = arr[i];
352
+ const tNum = typeof t === "number" && Number.isFinite(t) ? t : 0;
353
+ if (i + 1 < arr.length) {
354
+ const next = arr[i + 1] ?? 0;
355
+ return next !== 0 ? next : tNum;
356
+ }
357
+ return Math.trunc(tNum) + 86_400_000;
358
+ }
359
+
360
+ function roundToMintick(x: unknown): number | null {
361
+ return mathRoundToMintick(naNum(x), 0.01);
362
+ }
363
+
364
+ /** Python `split_symbol` / `extract_prefix`: text before the first `:`, else `""`. */
365
+ function extractPrefix(symbol: string): string {
366
+ const s = symbol.trim();
367
+ const i = s.indexOf(":");
368
+ return i < 0 ? "" : s.slice(0, i);
369
+ }
370
+
371
+ function syminfoPrefix(tickerid?: unknown): string {
372
+ if (arguments.length === 0 || tickerid === undefined) return extractPrefix("SYMBOL");
373
+ if (tickerid instanceof TickerId) return extractPrefix(tickerid.symbol);
374
+ if (typeof tickerid === "string") return extractPrefix(tickerid);
375
+ if (typeof tickerid === "number" && Number.isFinite(tickerid)) return extractPrefix(String(tickerid));
376
+ return "";
377
+ }
@@ -6,13 +6,19 @@
6
6
  * Injected as `__h.strategy`. Wraps StrategyBook (fills + ledger + risk).
7
7
  */
8
8
  import {
9
+ isStrategyCashAmount,
10
+ strategyCashAmount,
9
11
  StrategyBook,
12
+ type BarOhlc,
10
13
  type BrokerSettings,
11
14
  type PlaceEntryOpts,
15
+ type PlaceExitOpts,
16
+ type StrategyCashAmount,
12
17
  type StrategyDirection,
13
18
  type StrategyEvent,
14
19
  type StrategySummary,
15
20
  } from "../strategy.ts";
21
+ import { naNum } from "./helpers.ts";
16
22
 
17
23
  function asDir(v: unknown): StrategyDirection | number | string {
18
24
  if (v == null) return "long";
@@ -36,16 +42,72 @@ function str(v: unknown, fallback = ""): string {
36
42
  return String(v);
37
43
  }
38
44
 
45
+ function optNum(v: unknown): number | null {
46
+ if (typeof v === "number" && Number.isFinite(v)) return v;
47
+ return null;
48
+ }
49
+
50
+ function asExitOpts(v: unknown): PlaceExitOpts {
51
+ if (v == null || typeof v !== "object" || Array.isArray(v)) return {};
52
+ const rec = v as Record<string, unknown>;
53
+ return {
54
+ from_entry: rec.from_entry == null || rec.from_entry === "" ? null : String(rec.from_entry),
55
+ qty: optNum(rec.qty),
56
+ qty_percent: optNum(rec.qty_percent),
57
+ profit: optNum(rec.profit),
58
+ limit: optNum(rec.limit),
59
+ loss: optNum(rec.loss),
60
+ stop: optNum(rec.stop),
61
+ trail_price: optNum(rec.trail_price),
62
+ trail_points: optNum(rec.trail_points),
63
+ trail_offset: optNum(rec.trail_offset),
64
+ comment: rec.comment == null ? undefined : String(rec.comment),
65
+ };
66
+ }
67
+
39
68
  export class CompileStrategy {
40
69
  readonly book: StrategyBook;
41
70
  private bar = 0;
42
71
  private mark = 0;
43
72
  private time = 0;
73
+ private ohlc: BarOhlc = {};
44
74
 
45
75
  constructor(settings?: BrokerSettings) {
46
76
  this.book = new StrategyBook(settings);
47
77
  }
48
78
 
79
+ /** Apply `strategy(...)` declaration kwargs (every bar, same as interpret). */
80
+ configure(settings?: unknown): void {
81
+ if (settings == null || typeof settings !== "object" || Array.isArray(settings)) return;
82
+ const rec = settings as Record<string, unknown>;
83
+ const broker: BrokerSettings = {};
84
+ const commission = optNum(rec.commission);
85
+ if (commission != null) broker.commission = commission;
86
+ const slippage = optNum(rec.slippage);
87
+ if (slippage != null) broker.slippage = slippage;
88
+ if (rec.pyramiding !== undefined) {
89
+ const pyr = optNum(rec.pyramiding);
90
+ broker.pyramiding = pyr == null ? undefined : Math.trunc(pyr);
91
+ }
92
+ if (typeof rec.avg_price_model === "string") broker.avg_price_model = rec.avg_price_model;
93
+ const leverage = optNum(rec.leverage);
94
+ if (leverage != null) broker.leverage = leverage;
95
+ const ml = optNum(rec.margin_long);
96
+ if (ml != null) broker.margin_long = ml;
97
+ const ms = optNum(rec.margin_short);
98
+ if (ms != null) broker.margin_short = ms;
99
+ // String sentinel or tagged free-cash (`strategy.cash`) both select cash sizing.
100
+ if (typeof rec.default_qty_type === "string" || isStrategyCashAmount(rec.default_qty_type)) {
101
+ broker.default_qty_type = rec.default_qty_type;
102
+ }
103
+ const dqv = optNum(rec.default_qty_value);
104
+ if (dqv != null) broker.default_qty_value = dqv;
105
+ if (typeof rec.currency === "string" && rec.currency !== "") broker.currency = rec.currency;
106
+ if (Object.keys(broker).length) this.book.configure(broker);
107
+ const capital = optNum(rec.initial_capital);
108
+ if (capital != null) this.book.initialCapital = capital;
109
+ }
110
+
49
111
  beginBar(
50
112
  bar: unknown,
51
113
  open: unknown,
@@ -63,14 +125,16 @@ export class CompileStrategy {
63
125
  low: num(low, this.mark),
64
126
  close: this.mark,
65
127
  };
128
+ this.ohlc = ohlc;
66
129
  this.book.processPending(this.bar, ohlc);
67
130
  this.book.markOpenTrades(ohlc.high, ohlc.low, ohlc.close);
68
131
  }
69
132
 
70
133
  entry(id?: unknown, direction?: unknown, qty?: unknown, opts?: PlaceEntryOpts): void {
71
- const q = qty == null ? 1 : num(qty, 1);
134
+ const fillPrice = opts?.price ?? this.mark;
135
+ const q = qty == null ? this.book.resolveDefaultQty(fillPrice) : num(qty, 1);
72
136
  this.book.placeEntry(this.bar, str(id, "entry"), asDir(direction), q, {
73
- price: opts?.price ?? this.mark,
137
+ price: fillPrice,
74
138
  time: opts?.time ?? this.time,
75
139
  limit: opts?.limit,
76
140
  stop: opts?.stop,
@@ -80,8 +144,11 @@ export class CompileStrategy {
80
144
  });
81
145
  }
82
146
 
83
- close(id?: unknown, _qty?: unknown): void {
84
- this.book.fillClose(this.bar, str(id, ""), this.mark, { time: this.time });
147
+ close(id?: unknown, qty?: unknown): void {
148
+ this.book.fillClose(this.bar, str(id, ""), this.mark, {
149
+ time: this.time,
150
+ qty: optNum(qty),
151
+ });
85
152
  }
86
153
 
87
154
  close_all(_comment?: unknown): void {
@@ -100,8 +167,14 @@ export class CompileStrategy {
100
167
  this.book.cancelAll(this.bar);
101
168
  }
102
169
 
103
- exit(from_entry?: unknown, _qty?: unknown): void {
104
- this.book.fillClose(this.bar, str(from_entry, ""), this.mark, { time: this.time });
170
+ exit(id?: unknown, opts?: unknown): void {
171
+ const parsed = asExitOpts(opts);
172
+ this.book.placeExit(this.bar, str(id, "exit"), {
173
+ ...parsed,
174
+ price: this.mark,
175
+ time: this.time,
176
+ ohlc: this.ohlc,
177
+ });
105
178
  }
106
179
 
107
180
  position_size(): number {
@@ -131,6 +204,266 @@ export class CompileStrategy {
131
204
  return this.book.closedtrades;
132
205
  }
133
206
 
207
+ leverage(): number {
208
+ return this.book.leverage;
209
+ }
210
+
211
+ margin_liquidation_price(): number | null {
212
+ return this.book.marginLiquidationPrice();
213
+ }
214
+
215
+ position_avg_price(): number | null {
216
+ return this.book.position.avgPrice;
217
+ }
218
+
219
+ initial_capital(): number {
220
+ return this.book.initialCapital;
221
+ }
222
+
223
+ /** Free cash tagged so `default_qty_type=strategy.cash` still selects cash sizing. */
224
+ cash(): StrategyCashAmount {
225
+ return strategyCashAmount(this.book.cash(this.mark));
226
+ }
227
+
228
+ position_entry_name(): string {
229
+ return this.book.position_entry_name;
230
+ }
231
+
232
+ account_currency(): string {
233
+ return this.book.account_currency;
234
+ }
235
+
236
+ /** Zero-arg form is `percent_equity=100` (interpret `strategy.default_entry_qty`). */
237
+ default_entry_qty(percent?: unknown): number | null {
238
+ const pct = arguments.length === 0 || percent === undefined ? 100 : naNum(percent);
239
+ if (pct == null) return null;
240
+ return (this.book.initialCapital * (pct / 100)) / 100;
241
+ }
242
+
243
+ /** Identity FX stub. Missing arg → 1; na / non-finite → na. */
244
+ convert_to(value?: unknown): number | null {
245
+ if (arguments.length === 0 || value === undefined) return 1;
246
+ return naNum(value);
247
+ }
248
+
249
+ max_contracts_held_all(): number {
250
+ return this.book.maxContractsHeldAll();
251
+ }
252
+
253
+ max_contracts_held_long(): number {
254
+ return this.book.maxContractsHeldLong();
255
+ }
256
+
257
+ max_contracts_held_short(): number {
258
+ return this.book.maxContractsHeldShort();
259
+ }
260
+
261
+ capital_held(): number {
262
+ return this.book.capitalHeld();
263
+ }
264
+
265
+ closedtrades_first_index(): number {
266
+ return this.book.closedtrades_first_index;
267
+ }
268
+
269
+ wintrades(): number {
270
+ return this.book.wintrades;
271
+ }
272
+
273
+ losstrades(): number {
274
+ return this.book.losstrades;
275
+ }
276
+
277
+ eventrades(): number {
278
+ return this.book.eventrades;
279
+ }
280
+
281
+ grossprofit(): number {
282
+ return this.book.grossprofit;
283
+ }
284
+
285
+ grossloss(): number {
286
+ return this.book.grossloss;
287
+ }
288
+
289
+ avg_trade(): number {
290
+ return this.book.avgTrade();
291
+ }
292
+
293
+ avg_winning_trade(): number {
294
+ return this.book.avgWinningTrade();
295
+ }
296
+
297
+ avg_losing_trade(): number {
298
+ return this.book.avgLosingTrade();
299
+ }
300
+
301
+ netprofit_percent(): number {
302
+ return this.book.netprofitPercent(this.mark);
303
+ }
304
+
305
+ openprofit_percent(): number {
306
+ return this.book.openprofitPercent(this.mark);
307
+ }
308
+
309
+ grossprofit_percent(): number {
310
+ return this.book.grossprofitPercent();
311
+ }
312
+
313
+ grossloss_percent(): number {
314
+ return this.book.grosslossPercent();
315
+ }
316
+
317
+ avg_trade_percent(): number {
318
+ return this.book.avgTradePercent();
319
+ }
320
+
321
+ avg_winning_trade_percent(): number {
322
+ return this.book.avgWinningTradePercent();
323
+ }
324
+
325
+ avg_losing_trade_percent(): number {
326
+ return this.book.avgLosingTradePercent();
327
+ }
328
+
329
+ max_drawdown(): number {
330
+ return this.book.maxDrawdown();
331
+ }
332
+
333
+ max_drawdown_percent(): number {
334
+ return this.book.maxDrawdownPercent();
335
+ }
336
+
337
+ max_runup(): number {
338
+ return this.book.maxRunup();
339
+ }
340
+
341
+ max_runup_percent(): number {
342
+ return this.book.maxRunupPercent();
343
+ }
344
+
345
+ percent_profitable(): number {
346
+ return this.book.percentProfitable();
347
+ }
348
+
349
+ winrate(): number {
350
+ return this.book.percentProfitable();
351
+ }
352
+
353
+ profitfactor(): number {
354
+ return this.book.profitFactor();
355
+ }
356
+
357
+ profit_factor(): number {
358
+ return this.book.profitFactor();
359
+ }
360
+
361
+ closedtrades_entry_bar_index(i?: unknown): number {
362
+ return this.book.closedEntryBar(this.idx(i)) ?? 0;
363
+ }
364
+
365
+ closedtrades_entry_price(i?: unknown): number {
366
+ return this.book.closedEntryPrice(this.idx(i)) ?? 0;
367
+ }
368
+
369
+ closedtrades_exit_bar_index(i?: unknown): number {
370
+ return this.book.closedExitBar(this.idx(i)) ?? 0;
371
+ }
372
+
373
+ closedtrades_exit_price(i?: unknown): number {
374
+ return this.book.closedExitPrice(this.idx(i)) ?? 0;
375
+ }
376
+
377
+ closedtrades_profit(i?: unknown): number {
378
+ return this.book.closedProfit(this.idx(i)) ?? 0;
379
+ }
380
+
381
+ closedtrades_size(i?: unknown): number {
382
+ return this.book.closedSize(this.idx(i)) ?? 0;
383
+ }
384
+
385
+ closedtrades_entry_id(i?: unknown): string {
386
+ return this.book.closedId(this.idx(i)) ?? "";
387
+ }
388
+
389
+ closedtrades_exit_id(i?: unknown): string {
390
+ return this.book.closedId(this.idx(i)) ?? "";
391
+ }
392
+
393
+ closedtrades_commission(i?: unknown): number {
394
+ return this.book.closedCommission(this.idx(i)) ?? 0;
395
+ }
396
+
397
+ closedtrades_entry_time(i?: unknown): number {
398
+ return this.book.closedEntryTime(this.idx(i));
399
+ }
400
+
401
+ closedtrades_exit_time(i?: unknown): number {
402
+ return this.book.closedExitTime(this.idx(i));
403
+ }
404
+
405
+ closedtrades_entry_comment(i?: unknown): string {
406
+ return this.book.closedEntryComment(this.idx(i));
407
+ }
408
+
409
+ closedtrades_exit_comment(i?: unknown): string {
410
+ return this.book.closedExitComment(this.idx(i));
411
+ }
412
+
413
+ closedtrades_max_drawdown(i?: unknown): number {
414
+ return this.book.closedMaxDrawdown(this.idx(i));
415
+ }
416
+
417
+ closedtrades_max_runup(i?: unknown): number {
418
+ return this.book.closedMaxRunup(this.idx(i));
419
+ }
420
+
421
+ opentrades_entry_bar_index(i?: unknown): number {
422
+ return this.book.openEntryBar(this.idx(i)) ?? 0;
423
+ }
424
+
425
+ opentrades_entry_price(i?: unknown): number {
426
+ return this.book.openEntryPrice(this.idx(i)) ?? 0;
427
+ }
428
+
429
+ opentrades_size(i?: unknown): number {
430
+ return this.book.openSize(this.idx(i)) ?? 0;
431
+ }
432
+
433
+ opentrades_entry_id(i?: unknown): string {
434
+ return this.book.openId(this.idx(i)) ?? "";
435
+ }
436
+
437
+ opentrades_profit(i?: unknown): number {
438
+ return this.book.openProfit(this.idx(i), this.mark) ?? 0;
439
+ }
440
+
441
+ opentrades_entry_time(i?: unknown): number {
442
+ return this.book.openEntryTime(this.idx(i));
443
+ }
444
+
445
+ opentrades_entry_comment(i?: unknown): string {
446
+ return this.book.openEntryComment(this.idx(i));
447
+ }
448
+
449
+ opentrades_commission(i?: unknown): number {
450
+ return this.book.openCommission(this.idx(i));
451
+ }
452
+
453
+ opentrades_max_drawdown(i?: unknown): number {
454
+ return this.book.openMaxDrawdown(this.idx(i));
455
+ }
456
+
457
+ opentrades_max_runup(i?: unknown): number {
458
+ return this.book.openMaxRunup(this.idx(i));
459
+ }
460
+
461
+ private idx(i: unknown): number {
462
+ if (typeof i === "boolean") return i ? 1 : 0;
463
+ if (typeof i === "number" && Number.isFinite(i)) return Math.trunc(i);
464
+ return 0;
465
+ }
466
+
134
467
  risk_allow_entry_in(value?: unknown): void {
135
468
  this.book.riskAllowEntryIn(value);
136
469
  }
@@ -58,6 +58,8 @@ export interface EmitCtx {
58
58
  usesStrategy: boolean;
59
59
  nextSite: number;
60
60
  errors: string[];
61
+ /** `once`-structure fired flags, declared in the bar-loop frame. */
62
+ onceFlags: string[];
61
63
  /** Pine UDF name currently being emitted (null at script scope). */
62
64
  currentFunc: string | null;
63
65
  /** Formal names of the current UDF. */