@hoox-sh/pynets 0.3.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.
- package/CHANGELOG.md +17 -0
- package/README.md +5 -5
- package/dist/browser.js +66 -66
- package/dist/index.js +1532 -38
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/runtime/compile/collections.ts +4 -0
- package/src/runtime/compile/draw.ts +609 -19
- package/src/runtime/compile/emit.ts +81 -1
- package/src/runtime/compile/emit_call.ts +312 -9
- package/src/runtime/compile/runtime.ts +20 -0
- package/src/runtime/compile/strategy.ts +253 -1
- package/src/runtime/interpret.ts +151 -0
- package/src/runtime/ta.ts +392 -0
|
@@ -418,6 +418,8 @@ function emitName(state: State, node: Name): string {
|
|
|
418
418
|
if (CHART_LOCALS.has(id)) return id;
|
|
419
419
|
if (state.ctx.userFuncs.has(id)) return id;
|
|
420
420
|
if (state.ctx.enumTypes.has(id)) return JSON.stringify(id);
|
|
421
|
+
// Dataset last-bar time on every bar (interpret ctx.timenow / lastBarTime).
|
|
422
|
+
if (id === "timenow") return "__h.timenow(time_arr)";
|
|
421
423
|
return safeIdent(id);
|
|
422
424
|
}
|
|
423
425
|
|
|
@@ -540,8 +542,57 @@ const STRATEGY_QUERY_ATTRS = new Set([
|
|
|
540
542
|
"margin_liquidation_price",
|
|
541
543
|
"position_avg_price",
|
|
542
544
|
"initial_capital",
|
|
545
|
+
"cash",
|
|
546
|
+
"position_entry_name",
|
|
547
|
+
"account_currency",
|
|
548
|
+
"default_entry_qty",
|
|
549
|
+
"max_contracts_held_all",
|
|
550
|
+
"max_contracts_held_long",
|
|
551
|
+
"max_contracts_held_short",
|
|
552
|
+
"wintrades",
|
|
553
|
+
"losstrades",
|
|
554
|
+
"eventrades",
|
|
555
|
+
"grossprofit",
|
|
556
|
+
"grossloss",
|
|
557
|
+
"avg_trade",
|
|
558
|
+
"avg_winning_trade",
|
|
559
|
+
"avg_losing_trade",
|
|
560
|
+
"netprofit_percent",
|
|
561
|
+
"openprofit_percent",
|
|
562
|
+
"grossprofit_percent",
|
|
563
|
+
"grossloss_percent",
|
|
564
|
+
"avg_trade_percent",
|
|
565
|
+
"avg_winning_trade_percent",
|
|
566
|
+
"avg_losing_trade_percent",
|
|
567
|
+
"max_drawdown",
|
|
568
|
+
"max_drawdown_percent",
|
|
569
|
+
"max_runup",
|
|
570
|
+
"max_runup_percent",
|
|
571
|
+
"percent_profitable",
|
|
572
|
+
"winrate",
|
|
573
|
+
"profitfactor",
|
|
574
|
+
"profit_factor",
|
|
543
575
|
]);
|
|
544
576
|
|
|
577
|
+
/** Nested `strategy.*` constants (interpret `strategySeriesValue`). */
|
|
578
|
+
const STRATEGY_NESTED_CONSTS: Record<string, string> = {
|
|
579
|
+
"strategy.direction.long": "long",
|
|
580
|
+
"strategy.direction.short": "short",
|
|
581
|
+
"strategy.direction.all": "all",
|
|
582
|
+
"strategy.oca.none": "none",
|
|
583
|
+
"strategy.oca.cancel": "cancel",
|
|
584
|
+
"strategy.oca.reduce": "reduce",
|
|
585
|
+
"strategy.commission.percent": "percent",
|
|
586
|
+
"strategy.commission.cash_per_order": "cash_per_order",
|
|
587
|
+
"strategy.commission.cash_per_contract": "cash_per_contract",
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
/** Nested `strategy.*` series that are not a single identifier. */
|
|
591
|
+
const STRATEGY_NESTED_CALLS: Record<string, string> = {
|
|
592
|
+
"strategy.closedtrades.first_index": "closedtrades_first_index",
|
|
593
|
+
"strategy.opentrades.capital_held": "capital_held",
|
|
594
|
+
};
|
|
595
|
+
|
|
545
596
|
/** Namespaces used as call receivers or dotted constants — not UDT field access. */
|
|
546
597
|
const ATTR_NAMESPACES = new Set([
|
|
547
598
|
"ta",
|
|
@@ -652,6 +703,10 @@ const ATTR_CONSTANTS: Record<string, string | number | boolean> = {
|
|
|
652
703
|
"hline.style_solid": "solid",
|
|
653
704
|
"hline.style_dashed": "dashed",
|
|
654
705
|
"hline.style_dotted": "dotted",
|
|
706
|
+
// Python plotting.py PlotStyle — longer than hline's "solid".
|
|
707
|
+
"plot.linestyle_solid": "linestyle_solid",
|
|
708
|
+
"plot.linestyle_dashed": "linestyle_dashed",
|
|
709
|
+
"plot.linestyle_dotted": "linestyle_dotted",
|
|
655
710
|
"dayofweek.sunday": 1,
|
|
656
711
|
"dayofweek.monday": 2,
|
|
657
712
|
"dayofweek.tuesday": 3,
|
|
@@ -688,6 +743,7 @@ const ATTR_CONST_NS = new Set([
|
|
|
688
743
|
"display",
|
|
689
744
|
"position",
|
|
690
745
|
"hline",
|
|
746
|
+
"plot",
|
|
691
747
|
"dayofweek",
|
|
692
748
|
"month",
|
|
693
749
|
]);
|
|
@@ -847,13 +903,29 @@ function emitAttribute(state: State, node: Attribute): string {
|
|
|
847
903
|
if (attr === "avg_price_stock") return `"stock"`;
|
|
848
904
|
if (attr === "avg_price_futures") return `"futures"`;
|
|
849
905
|
if (attr === "avg_price_inverse") return `"inverse"`;
|
|
850
|
-
if (attr === "cash") return `"cash"`;
|
|
851
906
|
if (attr === "fixed") return `"fixed"`;
|
|
852
907
|
if (attr === "percent_of_equity") return `"percent_of_equity"`;
|
|
908
|
+
// `strategy.commission` the series is 0; `strategy.commission.percent` is nested.
|
|
909
|
+
if (attr === "commission") return "0";
|
|
853
910
|
if (STRATEGY_QUERY_ATTRS.has(attr)) {
|
|
854
911
|
state.ctx.usesStrategy = true;
|
|
855
912
|
return `__h.strategy.${attr}()`;
|
|
856
913
|
}
|
|
914
|
+
return "null";
|
|
915
|
+
}
|
|
916
|
+
if (node.attr === "all" && node.value.kind === "Name") {
|
|
917
|
+
const kind = node.value.id;
|
|
918
|
+
if (
|
|
919
|
+
kind === "line" ||
|
|
920
|
+
kind === "label" ||
|
|
921
|
+
kind === "box" ||
|
|
922
|
+
kind === "table" ||
|
|
923
|
+
kind === "polyline" ||
|
|
924
|
+
kind === "linefill"
|
|
925
|
+
) {
|
|
926
|
+
state.ctx.usesDrawings = true;
|
|
927
|
+
return `__h.draw.all(${JSON.stringify(kind)})`;
|
|
928
|
+
}
|
|
857
929
|
}
|
|
858
930
|
if (node.value.kind === "Name") {
|
|
859
931
|
const members = state.ctx.enumTypes.get(node.value.id);
|
|
@@ -863,6 +935,14 @@ function emitAttribute(state: State, node: Attribute): string {
|
|
|
863
935
|
}
|
|
864
936
|
const qn = attrQualifiedName(node);
|
|
865
937
|
if (qn != null) {
|
|
938
|
+
const nested = STRATEGY_NESTED_CONSTS[qn];
|
|
939
|
+
if (nested !== undefined) return JSON.stringify(nested);
|
|
940
|
+
const nestedCall = STRATEGY_NESTED_CALLS[qn];
|
|
941
|
+
if (nestedCall != null) {
|
|
942
|
+
state.ctx.usesStrategy = true;
|
|
943
|
+
return `__h.strategy.${nestedCall}()`;
|
|
944
|
+
}
|
|
945
|
+
if (qn.startsWith("strategy.") && qn.split(".").length > 2) return "null";
|
|
866
946
|
const dot = qn.indexOf(".");
|
|
867
947
|
const root = dot < 0 ? qn : qn.slice(0, dot);
|
|
868
948
|
if (ATTR_CONST_NS.has(root)) {
|
|
@@ -136,6 +136,23 @@ const TA_METHODS = new Set([
|
|
|
136
136
|
"fractal",
|
|
137
137
|
"atr_stop",
|
|
138
138
|
"zigzag",
|
|
139
|
+
"hammer",
|
|
140
|
+
"gap_detector",
|
|
141
|
+
"macd_signal",
|
|
142
|
+
"acceleration_factor",
|
|
143
|
+
"stoch_smooth",
|
|
144
|
+
"engulfing",
|
|
145
|
+
"inside_bar_pattern",
|
|
146
|
+
"double_top_bottom",
|
|
147
|
+
"voi",
|
|
148
|
+
"bid_ask_imbalance",
|
|
149
|
+
"rsi_oversold_overbought",
|
|
150
|
+
"rsi_divergence",
|
|
151
|
+
"cumulative_delta",
|
|
152
|
+
"volume_momentum",
|
|
153
|
+
"volume_profile_high",
|
|
154
|
+
"volume_profile_low",
|
|
155
|
+
"volume_thrust",
|
|
139
156
|
]);
|
|
140
157
|
|
|
141
158
|
/**
|
|
@@ -367,7 +384,15 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
|
|
|
367
384
|
return `(() => { throw new Error(String(${msg})); })()`;
|
|
368
385
|
}
|
|
369
386
|
|
|
370
|
-
if (name === "
|
|
387
|
+
if (name === "max_bars_back") return "null";
|
|
388
|
+
if (name === "timenow") return "__h.timenow(time_arr)";
|
|
389
|
+
if (name === "alertcondition") {
|
|
390
|
+
ctx.usesDrawings = true;
|
|
391
|
+
const cond = pick(args, 0, ["condition"], "null");
|
|
392
|
+
const title = hasArg(args, 1, ["title"]) ? pick(args, 1, ["title"], `"Alert"`) : `"Alert"`;
|
|
393
|
+
const message = hasArg(args, 2, ["message"]) ? pick(args, 2, ["message"], `"Alert"`) : `"Alert"`;
|
|
394
|
+
return `__h.draw.alertcondition(__bar_idx, ${cond}, ${title}, ${message})`;
|
|
395
|
+
}
|
|
371
396
|
if (name === "str_tostring" || name === "tostring") {
|
|
372
397
|
const x = pick(args, 0, ["value", "source", "x"], "null");
|
|
373
398
|
return `String(${x} ?? "")`;
|
|
@@ -525,6 +550,19 @@ function finiteOr(expr: string, fb: string): string {
|
|
|
525
550
|
return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
|
|
526
551
|
}
|
|
527
552
|
|
|
553
|
+
/** Interpret `unwrap`: bool → 1/0, non-finite → null. */
|
|
554
|
+
function cellOf(expr: string): string {
|
|
555
|
+
return `((__c) => (__c == null ? null : typeof __c === "boolean" ? (__c ? 1 : 0) : (typeof __c === "number" && Number.isFinite(__c) ? __c : null)))(${expr})`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Interpret `wave10bList`: a Pine array becomes its cells; a scalar is a
|
|
560
|
+
* one-element list. Bar-mode series stay one element.
|
|
561
|
+
*/
|
|
562
|
+
function cellList(expr: string): string {
|
|
563
|
+
return `(() => { const __v = ${expr}; const __cell = (e) => (e == null ? null : typeof e === "boolean" ? (e ? 1 : 0) : (typeof e === "number" && Number.isFinite(e) ? e : null)); if (__v != null && typeof __v.size === "function" && typeof __v.get === "function") { const __n = __v.size(); const __o = []; for (let __i = 0; __i < __n; __i++) __o.push(__cell(__v.get(__i))); return __o; } return [__cell(__v)]; })()`;
|
|
564
|
+
}
|
|
565
|
+
|
|
528
566
|
const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
|
|
529
567
|
|
|
530
568
|
const ARRAY_SIGS: Record<string, string[][]> = {
|
|
@@ -704,6 +742,10 @@ function emitHostCall(ns: string, method: string, args: CallArgs, params: string
|
|
|
704
742
|
function emitArray(name: string, args: CallArgs): string | null {
|
|
705
743
|
const raw = nsMethod(name, "array_");
|
|
706
744
|
if (raw == null) return null;
|
|
745
|
+
if (raw === "from") {
|
|
746
|
+
const parts = args.pos.length > 0 ? args.pos : Object.values(args.kw);
|
|
747
|
+
return `__h.array.from([${parts.join(", ")}])`;
|
|
748
|
+
}
|
|
707
749
|
const method = ctorMethod(raw);
|
|
708
750
|
const params = ARRAY_SIGS[method];
|
|
709
751
|
if (params == null) return null;
|
|
@@ -745,11 +787,24 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
|
745
787
|
label: {
|
|
746
788
|
set_text: [["id"], ["text"]],
|
|
747
789
|
set_xy: [["id"], ["x"], ["y"]],
|
|
790
|
+
set_x: [["id"], ["x"]],
|
|
791
|
+
set_y: [["id"], ["y"]],
|
|
748
792
|
set_color: [["id"], ["color"]],
|
|
749
793
|
set_style: [["id"], ["style"]],
|
|
750
794
|
set_size: [["id"], ["size"]],
|
|
751
795
|
set_tooltip: [["id"], ["tooltip"]],
|
|
752
796
|
set_textalign: [["id"], ["textalign"]],
|
|
797
|
+
set_textcolor: [["id"], ["color", "textcolor"]],
|
|
798
|
+
set_text_font_family: [["id"], ["font_family", "text_font_family"]],
|
|
799
|
+
set_text_halign: [["id"], ["halign", "text_halign"]],
|
|
800
|
+
set_text_valign: [["id"], ["valign", "text_valign"]],
|
|
801
|
+
set_text_size: [["id"], ["size", "text_size"]],
|
|
802
|
+
set_text_formatting: [["id"], ["formatting", "text_formatting"]],
|
|
803
|
+
set_border_color: [["id"], ["color"]],
|
|
804
|
+
set_border_width: [["id"], ["width"]],
|
|
805
|
+
set_border_style: [["id"], ["style"]],
|
|
806
|
+
set_xloc: [["id"], ["xloc"]],
|
|
807
|
+
set_yloc: [["id"], ["yloc"]],
|
|
753
808
|
delete: [["id"]],
|
|
754
809
|
get_text: [["id"]],
|
|
755
810
|
get_x: [["id"]],
|
|
@@ -757,12 +812,17 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
|
757
812
|
},
|
|
758
813
|
line: {
|
|
759
814
|
set_xy: [["id"], ["x1"], ["y1"], ["x2"], ["y2"]],
|
|
760
|
-
set_xy1: [["id"], ["x1"], ["y1"]],
|
|
761
|
-
set_xy2: [["id"], ["x2"], ["y2"]],
|
|
815
|
+
set_xy1: [["id"], ["x1", "x"], ["y1", "y"]],
|
|
816
|
+
set_xy2: [["id"], ["x2", "x"], ["y2", "y"]],
|
|
817
|
+
set_x1: [["id"], ["x1", "x"]],
|
|
818
|
+
set_y1: [["id"], ["y1", "y"]],
|
|
819
|
+
set_x2: [["id"], ["x2", "x"]],
|
|
820
|
+
set_y2: [["id"], ["y2", "y"]],
|
|
762
821
|
set_color: [["id"], ["color"]],
|
|
763
822
|
set_width: [["id"], ["width"]],
|
|
764
823
|
set_style: [["id"], ["style"]],
|
|
765
824
|
set_extend: [["id"], ["extend"]],
|
|
825
|
+
set_xloc: [["id"], ["xloc"]],
|
|
766
826
|
delete: [["id"]],
|
|
767
827
|
get_price: [["id"], ["x"]],
|
|
768
828
|
get_x1: [["id"]],
|
|
@@ -773,44 +833,144 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
|
773
833
|
box: {
|
|
774
834
|
set_corners: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
|
|
775
835
|
set_lefttop: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
|
|
836
|
+
set_left: [["id"], ["left"]],
|
|
837
|
+
set_right: [["id"], ["right"]],
|
|
838
|
+
set_top: [["id"], ["top"]],
|
|
839
|
+
set_bottom: [["id"], ["bottom"]],
|
|
776
840
|
set_bgcolor: [["id"], ["bgcolor", "color"]],
|
|
777
841
|
set_border_color: [["id"], ["color", "border_color"]],
|
|
842
|
+
set_border_width: [["id"], ["width"]],
|
|
843
|
+
set_border_style: [["id"], ["style"]],
|
|
778
844
|
set_text: [["id"], ["text"]],
|
|
845
|
+
set_text_color: [["id"], ["color", "text_color"]],
|
|
846
|
+
set_text_font_family: [["id"], ["font_family"]],
|
|
847
|
+
set_text_halign: [["id"], ["halign"]],
|
|
848
|
+
set_text_valign: [["id"], ["valign"]],
|
|
849
|
+
set_text_size: [["id"], ["size"]],
|
|
850
|
+
set_text_formatting: [["id"], ["formatting"]],
|
|
851
|
+
set_text_wrap: [["id"], ["wrap"]],
|
|
779
852
|
set_extend: [["id"], ["extend"]],
|
|
853
|
+
set_closed: [["id"], ["closed"]],
|
|
854
|
+
set_rightbottom: [["id"], ["right"], ["bottom"]],
|
|
855
|
+
get_left: [["id"]],
|
|
856
|
+
get_right: [["id"]],
|
|
857
|
+
get_top: [["id"]],
|
|
858
|
+
get_bottom: [["id"]],
|
|
780
859
|
delete: [["id"]],
|
|
781
860
|
},
|
|
782
861
|
table: {
|
|
783
862
|
cell: [["id"], ["column", "col"], ["row"], ["text"]],
|
|
784
|
-
|
|
863
|
+
// Interpret positional order is (id, row, column, text), not column-first.
|
|
864
|
+
cell_set_text: [["id"], ["row"], ["column", "col"], ["text"]],
|
|
865
|
+
cell_get_text: [["id"], ["row"], ["column", "col"]],
|
|
866
|
+
cell_set_text_color: [["id"], ["row"], ["column", "col"], ["color", "text"]],
|
|
867
|
+
cell_set_bgcolor: [["id"], ["row"], ["column", "col"], ["color"]],
|
|
868
|
+
cell_set_border_color: [["id"], ["row"], ["column", "col"], ["color"]],
|
|
869
|
+
cell_set_border_width: [["id"], ["row"], ["column", "col"], ["width"]],
|
|
870
|
+
cell_set_tooltip: [["id"], ["row"], ["column", "col"], ["tooltip", "text"]],
|
|
871
|
+
cell_set_width: [["id"], ["column", "col"], ["row"], ["width"]],
|
|
872
|
+
cell_set_height: [["id"], ["column", "col"], ["row"], ["height"]],
|
|
873
|
+
cell_set_text_halign: [["id"], ["column", "col"], ["row"], ["halign"]],
|
|
874
|
+
cell_set_text_valign: [["id"], ["column", "col"], ["row"], ["valign"]],
|
|
875
|
+
cell_set_text_size: [["id"], ["column", "col"], ["row"], ["size"]],
|
|
876
|
+
cell_set_text_font_family: [["id"], ["column", "col"], ["row"], ["font_family"]],
|
|
877
|
+
cell_set_text_formatting: [["id"], ["column", "col"], ["row"], ["formatting"]],
|
|
785
878
|
set_position: [["id"], ["position"]],
|
|
879
|
+
set_bgcolor: [["id"], ["color", "bgcolor"]],
|
|
880
|
+
set_border_color: [["id"], ["color"]],
|
|
881
|
+
set_border_width: [["id"], ["width"]],
|
|
882
|
+
set_frame_color: [["id"], ["color"]],
|
|
883
|
+
set_frame_width: [["id"], ["width"]],
|
|
884
|
+
merge_cells: [["id"], ["start_row"], ["start_column", "start_col"], ["end_row"], ["end_column", "end_col"]],
|
|
786
885
|
delete: [["id"]],
|
|
787
886
|
},
|
|
788
887
|
polyline: {
|
|
789
888
|
delete: [["id"]],
|
|
889
|
+
get_points: [["id"]],
|
|
890
|
+
set_points: [["id"], ["points"]],
|
|
891
|
+
set_line_color: [["id"], ["color"]],
|
|
892
|
+
set_line_width: [["id"], ["width"]],
|
|
893
|
+
set_line_style: [["id"], ["style"]],
|
|
894
|
+
set_fill_color: [["id"], ["fill_color", "color"]],
|
|
895
|
+
set_curved: [["id"], ["curved"]],
|
|
896
|
+
set_force_overlay: [["id"], ["force_overlay"]],
|
|
897
|
+
set_closed: [["id"], ["closed"]],
|
|
898
|
+
set_xloc: [["id"], ["xloc"]],
|
|
790
899
|
},
|
|
791
900
|
linefill: {
|
|
792
901
|
set_color: [["id"], ["color"]],
|
|
793
902
|
delete: [["id"]],
|
|
903
|
+
get_line1: [["id"]],
|
|
904
|
+
get_line2: [["id"]],
|
|
794
905
|
},
|
|
795
906
|
};
|
|
796
907
|
|
|
908
|
+
function emitChartPoint(name: string, args: CallArgs): string | null {
|
|
909
|
+
const meth = name.slice("chart_point_".length);
|
|
910
|
+
if (meth === "new" || meth === "from_time") {
|
|
911
|
+
return `__h.draw.point_new(${pick(args, 0, ["time"], "null")}, ${pick(args, 1, ["price"], "0")})`;
|
|
912
|
+
}
|
|
913
|
+
if (meth === "from_index") {
|
|
914
|
+
return `__h.draw.point_from_index(${pick(args, 0, ["index"], "null")}, ${pick(args, 1, ["price"], "0")})`;
|
|
915
|
+
}
|
|
916
|
+
if (meth === "now") {
|
|
917
|
+
return `__h.draw.point_now(__bar_idx, time, ${pick(args, 0, ["price"], "0")})`;
|
|
918
|
+
}
|
|
919
|
+
if (meth === "copy") {
|
|
920
|
+
return `__h.draw.point_copy(${pick(args, 0, ["point"], "null")})`;
|
|
921
|
+
}
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
|
|
797
925
|
function emitDraw(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
|
|
798
926
|
if (name === "alert" && methodSrc == null) {
|
|
799
927
|
ctx.usesDrawings = true;
|
|
800
928
|
return `__h.draw.alert(__bar_idx, ${pick(args, 0, ["message"], "null")})`;
|
|
801
929
|
}
|
|
930
|
+
if (name.startsWith("chart_point_")) {
|
|
931
|
+
const point = emitChartPoint(name, args);
|
|
932
|
+
if (point != null) ctx.usesDrawings = true;
|
|
933
|
+
return point;
|
|
934
|
+
}
|
|
802
935
|
const parsed = drawKindAndMethod(name, methodSrc);
|
|
803
936
|
if (parsed == null) return null;
|
|
804
937
|
const { kind, method } = parsed;
|
|
805
938
|
let code: string | null = null;
|
|
806
|
-
if (method === "
|
|
939
|
+
if (method === "all") {
|
|
940
|
+
code = `__h.draw.all(${JSON.stringify(kind)})`;
|
|
941
|
+
} else if (method === "copy" && (kind === "line" || kind === "label" || kind === "box" || kind === "polyline")) {
|
|
942
|
+
code = `__h.draw.copy(${pick(args, 0, ["id"], "null")})`;
|
|
943
|
+
} else if (method === "new") {
|
|
807
944
|
if (kind === "label") {
|
|
808
|
-
code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")})`;
|
|
945
|
+
code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")}, ${pick(args, 0, ["x"], "0")}, ${pick(args, 1, ["y"], "0")})`;
|
|
946
|
+
} else if (kind === "line") {
|
|
947
|
+
code = `__h.draw.line_new(__bar_idx, ${pick(args, 0, ["x1"], "0")}, ${pick(args, 1, ["y1"], "0")}, ${pick(args, 2, ["x2"], "0")}, ${pick(args, 3, ["y2"], "0")})`;
|
|
948
|
+
} else if (kind === "box") {
|
|
949
|
+
code = `__h.draw.box_new(__bar_idx, ${pick(args, 0, ["left"], "0")}, ${pick(args, 1, ["top"], "0")}, ${pick(args, 2, ["right"], "0")}, ${pick(args, 3, ["bottom"], "0")})`;
|
|
950
|
+
} else if (kind === "polyline") {
|
|
951
|
+
code = `__h.draw.polyline_new(__bar_idx, ${pick(args, 0, ["points"], "null")})`;
|
|
809
952
|
} else if (kind === "linefill") {
|
|
810
|
-
|
|
953
|
+
const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
|
|
954
|
+
code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
|
|
811
955
|
} else if (DRAW_KINDS.has(kind)) {
|
|
812
956
|
code = `__h.draw.${kind}_new(__bar_idx)`;
|
|
813
957
|
}
|
|
958
|
+
} else if (kind === "line" && method === "fill") {
|
|
959
|
+
const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
|
|
960
|
+
code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
|
|
961
|
+
} else if (
|
|
962
|
+
(kind === "line" && (method === "set_first_point" || method === "set_second_point")) ||
|
|
963
|
+
(kind === "label" && method === "set_point") ||
|
|
964
|
+
(kind === "box" && (method === "set_top_left_point" || method === "set_bottom_right_point"))
|
|
965
|
+
) {
|
|
966
|
+
code = `__h.draw.${kind}_${method}(${pick(args, 0, ["id"], "null")}, ${pick(args, 1, ["point"], "null")})`;
|
|
967
|
+
} else if (kind === "box" && method === "set_xloc") {
|
|
968
|
+
code = `__h.draw.box_set_xloc(${pick(args, 0, ["id"], "null")}, ${pick(args, 1, ["left"], "null")}, ${pick(args, 2, ["right"], "null")}, ${pick(args, 3, ["xloc"], "null")})`;
|
|
969
|
+
} else if (kind === "table" && method === "clear") {
|
|
970
|
+
const id = pick(args, 0, ["id", "table_id"], "null");
|
|
971
|
+
code = hasArg(args, 1, ["start_row"])
|
|
972
|
+
? `__h.draw.table_clear(${id}, ${pick(args, 1, ["start_row"], "null")}, ${pick(args, 2, ["start_column", "start_col"], "null")}, ${pick(args, 3, ["end_row"], "null")}, ${pick(args, 4, ["end_column", "end_col"], "null")})`
|
|
973
|
+
: `__h.draw.table_clear(${id})`;
|
|
814
974
|
} else {
|
|
815
975
|
const aliased = kind === "box" && method === "set_lefttop" ? "set_corners" : method;
|
|
816
976
|
const params = DRAW_METHODS[kind]?.[method] ?? DRAW_METHODS[kind]?.[aliased];
|
|
@@ -855,6 +1015,7 @@ function emitUdt(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallA
|
|
|
855
1015
|
|
|
856
1016
|
const SECURITY_FUNCS = new Set([
|
|
857
1017
|
"security",
|
|
1018
|
+
"security_lower_tf",
|
|
858
1019
|
"request_security",
|
|
859
1020
|
"request_security_lower_tf",
|
|
860
1021
|
"request_seed",
|
|
@@ -1024,6 +1185,7 @@ function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
|
|
|
1024
1185
|
add(["default_qty_type"], "default_qty_type");
|
|
1025
1186
|
add(["default_qty_value"], "default_qty_value");
|
|
1026
1187
|
add(["initial_capital"], "initial_capital");
|
|
1188
|
+
add(["currency"], "currency");
|
|
1027
1189
|
return `__h.strategy.configure({${fields.join(", ")}})`;
|
|
1028
1190
|
}
|
|
1029
1191
|
|
|
@@ -1095,8 +1257,50 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
|
|
|
1095
1257
|
case "margin_liquidation_price":
|
|
1096
1258
|
case "position_avg_price":
|
|
1097
1259
|
case "initial_capital":
|
|
1260
|
+
case "cash":
|
|
1261
|
+
case "position_entry_name":
|
|
1262
|
+
case "account_currency":
|
|
1263
|
+
case "max_contracts_held_all":
|
|
1264
|
+
case "max_contracts_held_long":
|
|
1265
|
+
case "max_contracts_held_short":
|
|
1266
|
+
case "wintrades":
|
|
1267
|
+
case "losstrades":
|
|
1268
|
+
case "eventrades":
|
|
1269
|
+
case "grossprofit":
|
|
1270
|
+
case "grossloss":
|
|
1271
|
+
case "avg_trade":
|
|
1272
|
+
case "avg_winning_trade":
|
|
1273
|
+
case "avg_losing_trade":
|
|
1274
|
+
case "netprofit_percent":
|
|
1275
|
+
case "openprofit_percent":
|
|
1276
|
+
case "grossprofit_percent":
|
|
1277
|
+
case "grossloss_percent":
|
|
1278
|
+
case "avg_trade_percent":
|
|
1279
|
+
case "avg_winning_trade_percent":
|
|
1280
|
+
case "avg_losing_trade_percent":
|
|
1281
|
+
case "max_drawdown":
|
|
1282
|
+
case "max_drawdown_percent":
|
|
1283
|
+
case "max_runup":
|
|
1284
|
+
case "max_runup_percent":
|
|
1285
|
+
case "percent_profitable":
|
|
1286
|
+
case "winrate":
|
|
1287
|
+
case "profitfactor":
|
|
1288
|
+
case "profit_factor":
|
|
1289
|
+
case "capital_held":
|
|
1290
|
+
case "closedtrades_first_index":
|
|
1098
1291
|
ctx.usesStrategy = true;
|
|
1099
1292
|
return `__h.strategy.${method}()`;
|
|
1293
|
+
case "commission":
|
|
1294
|
+
return "0";
|
|
1295
|
+
case "default_entry_qty":
|
|
1296
|
+
ctx.usesStrategy = true;
|
|
1297
|
+
return hasArg(args, 0, ["percent_equity"])
|
|
1298
|
+
? `__h.strategy.default_entry_qty(${pick(args, 0, ["percent_equity"], "null")})`
|
|
1299
|
+
: `__h.strategy.default_entry_qty()`;
|
|
1300
|
+
case "convert_to_account":
|
|
1301
|
+
case "convert_to_symbol":
|
|
1302
|
+
ctx.usesStrategy = true;
|
|
1303
|
+
return hasArg(args, 0, ["value"]) ? `__h.strategy.convert_to(${pick(args, 0, ["value"], "null")})` : "1";
|
|
1100
1304
|
case "risk_allow_entry_in":
|
|
1101
1305
|
ctx.usesStrategy = true;
|
|
1102
1306
|
return `__h.strategy.risk_allow_entry_in(${pick(args, 0, ["value"], "null")})`;
|
|
@@ -1106,9 +1310,33 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
|
|
|
1106
1310
|
case "risk_max_drawdown":
|
|
1107
1311
|
ctx.usesStrategy = true;
|
|
1108
1312
|
return `__h.strategy.risk_max_drawdown(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["type"], "null")})`;
|
|
1109
|
-
default:
|
|
1313
|
+
default: {
|
|
1314
|
+
if (method.startsWith("direction_")) {
|
|
1315
|
+
const tail = method.slice("direction_".length);
|
|
1316
|
+
if (tail === "long" || tail === "short" || tail === "all") return JSON.stringify(tail);
|
|
1317
|
+
}
|
|
1318
|
+
if (method.startsWith("oca_")) {
|
|
1319
|
+
const tail = method.slice("oca_".length);
|
|
1320
|
+
if (tail === "none" || tail === "cancel" || tail === "reduce") return JSON.stringify(tail);
|
|
1321
|
+
}
|
|
1322
|
+
if (method.startsWith("commission_")) {
|
|
1323
|
+
const tail = method.slice("commission_".length);
|
|
1324
|
+
if (tail === "percent" || tail === "cash_per_order" || tail === "cash_per_contract") {
|
|
1325
|
+
return JSON.stringify(tail);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (method.startsWith("closedtrades_") || method.startsWith("opentrades_")) {
|
|
1329
|
+
ctx.usesStrategy = true;
|
|
1330
|
+
if (method === "opentrades_capital_held") return "__h.strategy.capital_held()";
|
|
1331
|
+
if (method === "closedtrades_first_index") return "__h.strategy.closedtrades_first_index()";
|
|
1332
|
+
const idx = hasArg(args, 0, ["trade_num", "trade_index", "index"])
|
|
1333
|
+
? pick(args, 0, ["trade_num", "trade_index", "index"], "0")
|
|
1334
|
+
: "0";
|
|
1335
|
+
return `__h.strategy.${method}(${idx})`;
|
|
1336
|
+
}
|
|
1110
1337
|
if (isStrategyAction(name, method)) ctx.usesStrategy = true;
|
|
1111
1338
|
return "null";
|
|
1339
|
+
}
|
|
1112
1340
|
}
|
|
1113
1341
|
}
|
|
1114
1342
|
|
|
@@ -1167,7 +1395,7 @@ function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
|
|
|
1167
1395
|
: pick(args, 0, ["series", "source", "price"], "null");
|
|
1168
1396
|
const i = ctx.plots.length;
|
|
1169
1397
|
ctx.plots.push({ title });
|
|
1170
|
-
return `plot_${i}[__bar_idx] = __h.
|
|
1398
|
+
return `plot_${i}[__bar_idx] = __h.plotNum(${series})`;
|
|
1171
1399
|
}
|
|
1172
1400
|
|
|
1173
1401
|
function emitColor(name: string, args: CallArgs): string | null {
|
|
@@ -1792,5 +2020,80 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
|
|
|
1792
2020
|
]);
|
|
1793
2021
|
return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
|
|
1794
2022
|
}
|
|
2023
|
+
if (method === "hammer") {
|
|
2024
|
+
return `__h.ta.hammer(${cellOf(pick(args, 0, ["open"], "null"))}, ${cellOf(pick(args, 1, ["high"], "null"))}, ${cellOf(pick(args, 2, ["low"], "null"))}, ${cellOf(pick(args, 3, ["close"], "null"))})`;
|
|
2025
|
+
}
|
|
2026
|
+
if (method === "gap_detector") {
|
|
2027
|
+
return `__h.ta.gapDetector(${cellOf(pick(args, 0, ["high"], "null"))}, ${cellOf(pick(args, 1, ["low"], "null"))}, ${cellOf(pick(args, 2, ["prev_close", "prev"], "null"))})`;
|
|
2028
|
+
}
|
|
2029
|
+
if (method === "macd_signal") {
|
|
2030
|
+
return `__h.ta.macdSignal(${cellOf(pick(args, 0, ["macd_line", "macd"], "null"))}, ${cellOf(pick(args, 1, ["signal_line", "signal"], "null"))})`;
|
|
2031
|
+
}
|
|
2032
|
+
if (method === "acceleration_factor") {
|
|
2033
|
+
return taCall("accelerationFactor", site, [
|
|
2034
|
+
cellOf(pick(args, 0, ["momentum_list", "momentum", "source", "series"], "null")),
|
|
2035
|
+
cellOf(pick(args, 1, ["period", "length"], "null")),
|
|
2036
|
+
]);
|
|
2037
|
+
}
|
|
2038
|
+
if (method === "stoch_smooth") {
|
|
2039
|
+
return `__h.ta.stochSmooth(${cellOf(pick(args, 0, ["high"], "null"))}, ${cellOf(pick(args, 1, ["low"], "null"))}, ${cellOf(pick(args, 2, ["close"], "null"))}, ${cellOf(pick(args, 3, ["period", "length"], "null"))}, ${cellOf(pick(args, 4, ["smooth_k", "k"], "null"))}, ${cellOf(pick(args, 5, ["smooth_d", "d"], "null"))})`;
|
|
2040
|
+
}
|
|
2041
|
+
if (method === "engulfing") {
|
|
2042
|
+
// Bar-mode sentinel. Args are still evaluated (interpret reads all four).
|
|
2043
|
+
return `(() => { ${cellOf(pick(args, 0, ["open"], "null"))}; ${cellOf(pick(args, 1, ["high"], "null"))}; ${cellOf(pick(args, 2, ["low"], "null"))}; ${cellOf(pick(args, 3, ["close"], "null"))}; return __h.ta.engulfing(); })()`;
|
|
2044
|
+
}
|
|
2045
|
+
if (method === "inside_bar_pattern") {
|
|
2046
|
+
return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.insideBarPattern(); })()`;
|
|
2047
|
+
}
|
|
2048
|
+
if (method === "double_top_bottom") {
|
|
2049
|
+
return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.doubleTopBottom(${cellOf(pick(args, 2, ["period", "length"], "null"))}); })()`;
|
|
2050
|
+
}
|
|
2051
|
+
if (method === "voi") {
|
|
2052
|
+
return `__h.ta.voi(${cellOf(pick(args, 0, ["buy_volume", "buy"], "null"))}, ${cellOf(pick(args, 1, ["sell_volume", "sell"], "null"))})`;
|
|
2053
|
+
}
|
|
2054
|
+
if (method === "bid_ask_imbalance") {
|
|
2055
|
+
return `__h.ta.bidAskImbalance(${cellOf(pick(args, 0, ["bid_size", "bid"], "null"))}, ${cellOf(pick(args, 1, ["ask_size", "ask"], "null"))}, ${cellOf(pick(args, 2, ["bid_price"], "null"))}, ${cellOf(pick(args, 3, ["ask_price"], "null"))})`;
|
|
2056
|
+
}
|
|
2057
|
+
if (method === "rsi_oversold_overbought") {
|
|
2058
|
+
return `__h.ta.rsiOversoldOverbought(${cellOf(pick(args, 0, ["rsi_series", "rsi", "source"], "null"))}, ${cellOf(pick(args, 1, ["oversold_level", "oversold"], "null"))}, ${cellOf(pick(args, 2, ["overbought_level", "overbought"], "null"))})`;
|
|
2059
|
+
}
|
|
2060
|
+
if (method === "rsi_divergence") {
|
|
2061
|
+
// Missing period is 0 (interpret `period ?? 0`), not the engine's null → na.
|
|
2062
|
+
const period = `((__n) => (__n == null ? 0 : __n))(${cellOf(pick(args, 1, ["period", "length"], "null"))})`;
|
|
2063
|
+
return `__h.ta.rsiDivergence(${cellList(pick(args, 0, ["rsi_series", "series", "source"], "null"))}, ${period})`;
|
|
2064
|
+
}
|
|
2065
|
+
if (method === "cumulative_delta") {
|
|
2066
|
+
return taCall("cumulativeDelta", site, [
|
|
2067
|
+
cellOf(pick(args, 0, ["close", "source"], "null")),
|
|
2068
|
+
cellOf(pick(args, 1, ["volume"], "null")),
|
|
2069
|
+
cellOf(pick(args, 2, ["period", "length"], "null")),
|
|
2070
|
+
]);
|
|
2071
|
+
}
|
|
2072
|
+
if (method === "volume_momentum") {
|
|
2073
|
+
return taCall("volumeMomentum", site, [
|
|
2074
|
+
cellOf(pick(args, 0, ["volume", "source"], "null")),
|
|
2075
|
+
cellOf(pick(args, 1, ["period", "length"], "null")),
|
|
2076
|
+
]);
|
|
2077
|
+
}
|
|
2078
|
+
if (method === "volume_profile_high") {
|
|
2079
|
+
return `__h.ta.volumeProfileHigh(${cellList(pick(args, 0, ["close", "price", "source", "series"], "null"))}, ${cellList(pick(args, 1, ["volume"], "null"))})`;
|
|
2080
|
+
}
|
|
2081
|
+
if (method === "volume_profile_low") {
|
|
2082
|
+
return taCall("volumeProfileLow", site, [
|
|
2083
|
+
cellOf(pick(args, 0, ["close", "source"], "null")),
|
|
2084
|
+
cellOf(pick(args, 1, ["volume"], "null")),
|
|
2085
|
+
cellOf(pick(args, 2, ["period", "length"], "null")),
|
|
2086
|
+
cellOf(pick(args, 3, ["levels"], "null")),
|
|
2087
|
+
]);
|
|
2088
|
+
}
|
|
2089
|
+
if (method === "volume_thrust") {
|
|
2090
|
+
const sens = cellOf(pick(args, 3, ["sensitivity"], "null"));
|
|
2091
|
+
return taCall("volumeThrust", site, [
|
|
2092
|
+
cellOf(pick(args, 0, ["close", "source"], "null")),
|
|
2093
|
+
cellOf(pick(args, 1, ["volume"], "null")),
|
|
2094
|
+
cellOf(pick(args, 2, ["volume_sma", "vol_sma", "sma"], "null")),
|
|
2095
|
+
`((__n) => (__n == null ? 0.3 : __n))(${sens})`,
|
|
2096
|
+
]);
|
|
2097
|
+
}
|
|
1795
2098
|
return "null";
|
|
1796
2099
|
}
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
safeNeg,
|
|
84
84
|
safeSub,
|
|
85
85
|
} from "./helpers.ts";
|
|
86
|
+
import { isStrategyCashAmount } from "../strategy.ts";
|
|
86
87
|
import { createCompileStrategy } from "./strategy.ts";
|
|
87
88
|
import { udtGet, udtNew, udtRegister, udtSet } from "./udt_runtime.ts";
|
|
88
89
|
|
|
@@ -105,6 +106,8 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
|
|
|
105
106
|
na: NA_FN,
|
|
106
107
|
nz,
|
|
107
108
|
naNum,
|
|
109
|
+
plotNum,
|
|
110
|
+
timenow,
|
|
108
111
|
isNa: isNaCell,
|
|
109
112
|
eq: pineEq,
|
|
110
113
|
ne: pineNe,
|
|
@@ -264,6 +267,23 @@ function iff(cond: unknown, thenV: unknown, elseV: unknown): unknown {
|
|
|
264
267
|
return mathIff(cond as number | null, thenV as number | null, elseV as number | null);
|
|
265
268
|
}
|
|
266
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
|
+
|
|
267
287
|
function NA_FN(v?: unknown): number | null {
|
|
268
288
|
if (arguments.length === 0) return null;
|
|
269
289
|
return isNaCell(v) ? 1 : 0;
|