@hoox-sh/pynets 0.3.0 → 0.5.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 +30 -0
- package/README.md +7 -5
- package/dist/browser.js +66 -66
- package/dist/index.js +2009 -47
- 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 +98 -1
- package/src/runtime/compile/emit_call.ts +366 -10
- package/src/runtime/compile/runtime.ts +71 -1
- package/src/runtime/compile/strategy.ts +253 -1
- package/src/runtime/footprint.ts +272 -0
- package/src/runtime/interpret.ts +325 -8
- package/src/runtime/request.ts +19 -4
- package/src/runtime/ta.ts +392 -0
|
@@ -46,6 +46,8 @@ const NS = new Set([
|
|
|
46
46
|
"syminfo",
|
|
47
47
|
"timenow",
|
|
48
48
|
"chart",
|
|
49
|
+
"footprint",
|
|
50
|
+
"volume_row",
|
|
49
51
|
]);
|
|
50
52
|
|
|
51
53
|
/** TaEngine methods we emit. Unknown `ta.*` → `null`. */
|
|
@@ -136,6 +138,23 @@ const TA_METHODS = new Set([
|
|
|
136
138
|
"fractal",
|
|
137
139
|
"atr_stop",
|
|
138
140
|
"zigzag",
|
|
141
|
+
"hammer",
|
|
142
|
+
"gap_detector",
|
|
143
|
+
"macd_signal",
|
|
144
|
+
"acceleration_factor",
|
|
145
|
+
"stoch_smooth",
|
|
146
|
+
"engulfing",
|
|
147
|
+
"inside_bar_pattern",
|
|
148
|
+
"double_top_bottom",
|
|
149
|
+
"voi",
|
|
150
|
+
"bid_ask_imbalance",
|
|
151
|
+
"rsi_oversold_overbought",
|
|
152
|
+
"rsi_divergence",
|
|
153
|
+
"cumulative_delta",
|
|
154
|
+
"volume_momentum",
|
|
155
|
+
"volume_profile_high",
|
|
156
|
+
"volume_profile_low",
|
|
157
|
+
"volume_thrust",
|
|
139
158
|
]);
|
|
140
159
|
|
|
141
160
|
/**
|
|
@@ -367,7 +386,15 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
|
|
|
367
386
|
return `(() => { throw new Error(String(${msg})); })()`;
|
|
368
387
|
}
|
|
369
388
|
|
|
370
|
-
if (name === "
|
|
389
|
+
if (name === "max_bars_back") return "null";
|
|
390
|
+
if (name === "timenow") return "__h.timenow(time_arr)";
|
|
391
|
+
if (name === "alertcondition") {
|
|
392
|
+
ctx.usesDrawings = true;
|
|
393
|
+
const cond = pick(args, 0, ["condition"], "null");
|
|
394
|
+
const title = hasArg(args, 1, ["title"]) ? pick(args, 1, ["title"], `"Alert"`) : `"Alert"`;
|
|
395
|
+
const message = hasArg(args, 2, ["message"]) ? pick(args, 2, ["message"], `"Alert"`) : `"Alert"`;
|
|
396
|
+
return `__h.draw.alertcondition(__bar_idx, ${cond}, ${title}, ${message})`;
|
|
397
|
+
}
|
|
371
398
|
if (name === "str_tostring" || name === "tostring") {
|
|
372
399
|
const x = pick(args, 0, ["value", "source", "x"], "null");
|
|
373
400
|
return `String(${x} ?? "")`;
|
|
@@ -428,6 +455,8 @@ export function emitCall(ctx: EmitCtx, node: Call, visit: VisitFn): string {
|
|
|
428
455
|
|
|
429
456
|
const req = emitRequest(ctx, name, args);
|
|
430
457
|
if (req != null) return req;
|
|
458
|
+
const fp = emitFootprint(name, args);
|
|
459
|
+
if (fp != null) return fp;
|
|
431
460
|
|
|
432
461
|
return "null";
|
|
433
462
|
}
|
|
@@ -525,6 +554,19 @@ function finiteOr(expr: string, fb: string): string {
|
|
|
525
554
|
return `((__v) => (__v != null && Number.isFinite(__v) ? __v : ${fb}))(${expr})`;
|
|
526
555
|
}
|
|
527
556
|
|
|
557
|
+
/** Interpret `unwrap`: bool → 1/0, non-finite → null. */
|
|
558
|
+
function cellOf(expr: string): string {
|
|
559
|
+
return `((__c) => (__c == null ? null : typeof __c === "boolean" ? (__c ? 1 : 0) : (typeof __c === "number" && Number.isFinite(__c) ? __c : null)))(${expr})`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Interpret `wave10bList`: a Pine array becomes its cells; a scalar is a
|
|
564
|
+
* one-element list. Bar-mode series stay one element.
|
|
565
|
+
*/
|
|
566
|
+
function cellList(expr: string): string {
|
|
567
|
+
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)]; })()`;
|
|
568
|
+
}
|
|
569
|
+
|
|
528
570
|
const DRAW_KINDS = new Set(["label", "line", "box", "table", "polyline", "linefill"]);
|
|
529
571
|
|
|
530
572
|
const ARRAY_SIGS: Record<string, string[][]> = {
|
|
@@ -704,6 +746,10 @@ function emitHostCall(ns: string, method: string, args: CallArgs, params: string
|
|
|
704
746
|
function emitArray(name: string, args: CallArgs): string | null {
|
|
705
747
|
const raw = nsMethod(name, "array_");
|
|
706
748
|
if (raw == null) return null;
|
|
749
|
+
if (raw === "from") {
|
|
750
|
+
const parts = args.pos.length > 0 ? args.pos : Object.values(args.kw);
|
|
751
|
+
return `__h.array.from([${parts.join(", ")}])`;
|
|
752
|
+
}
|
|
707
753
|
const method = ctorMethod(raw);
|
|
708
754
|
const params = ARRAY_SIGS[method];
|
|
709
755
|
if (params == null) return null;
|
|
@@ -745,11 +791,24 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
|
745
791
|
label: {
|
|
746
792
|
set_text: [["id"], ["text"]],
|
|
747
793
|
set_xy: [["id"], ["x"], ["y"]],
|
|
794
|
+
set_x: [["id"], ["x"]],
|
|
795
|
+
set_y: [["id"], ["y"]],
|
|
748
796
|
set_color: [["id"], ["color"]],
|
|
749
797
|
set_style: [["id"], ["style"]],
|
|
750
798
|
set_size: [["id"], ["size"]],
|
|
751
799
|
set_tooltip: [["id"], ["tooltip"]],
|
|
752
800
|
set_textalign: [["id"], ["textalign"]],
|
|
801
|
+
set_textcolor: [["id"], ["color", "textcolor"]],
|
|
802
|
+
set_text_font_family: [["id"], ["font_family", "text_font_family"]],
|
|
803
|
+
set_text_halign: [["id"], ["halign", "text_halign"]],
|
|
804
|
+
set_text_valign: [["id"], ["valign", "text_valign"]],
|
|
805
|
+
set_text_size: [["id"], ["size", "text_size"]],
|
|
806
|
+
set_text_formatting: [["id"], ["formatting", "text_formatting"]],
|
|
807
|
+
set_border_color: [["id"], ["color"]],
|
|
808
|
+
set_border_width: [["id"], ["width"]],
|
|
809
|
+
set_border_style: [["id"], ["style"]],
|
|
810
|
+
set_xloc: [["id"], ["xloc"]],
|
|
811
|
+
set_yloc: [["id"], ["yloc"]],
|
|
753
812
|
delete: [["id"]],
|
|
754
813
|
get_text: [["id"]],
|
|
755
814
|
get_x: [["id"]],
|
|
@@ -757,12 +816,17 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
|
757
816
|
},
|
|
758
817
|
line: {
|
|
759
818
|
set_xy: [["id"], ["x1"], ["y1"], ["x2"], ["y2"]],
|
|
760
|
-
set_xy1: [["id"], ["x1"], ["y1"]],
|
|
761
|
-
set_xy2: [["id"], ["x2"], ["y2"]],
|
|
819
|
+
set_xy1: [["id"], ["x1", "x"], ["y1", "y"]],
|
|
820
|
+
set_xy2: [["id"], ["x2", "x"], ["y2", "y"]],
|
|
821
|
+
set_x1: [["id"], ["x1", "x"]],
|
|
822
|
+
set_y1: [["id"], ["y1", "y"]],
|
|
823
|
+
set_x2: [["id"], ["x2", "x"]],
|
|
824
|
+
set_y2: [["id"], ["y2", "y"]],
|
|
762
825
|
set_color: [["id"], ["color"]],
|
|
763
826
|
set_width: [["id"], ["width"]],
|
|
764
827
|
set_style: [["id"], ["style"]],
|
|
765
828
|
set_extend: [["id"], ["extend"]],
|
|
829
|
+
set_xloc: [["id"], ["xloc"]],
|
|
766
830
|
delete: [["id"]],
|
|
767
831
|
get_price: [["id"], ["x"]],
|
|
768
832
|
get_x1: [["id"]],
|
|
@@ -773,44 +837,144 @@ const DRAW_METHODS: Record<string, Record<string, string[][]>> = {
|
|
|
773
837
|
box: {
|
|
774
838
|
set_corners: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
|
|
775
839
|
set_lefttop: [["id"], ["left", "x1"], ["top", "y1"], ["right", "x2"], ["bottom", "y2"]],
|
|
840
|
+
set_left: [["id"], ["left"]],
|
|
841
|
+
set_right: [["id"], ["right"]],
|
|
842
|
+
set_top: [["id"], ["top"]],
|
|
843
|
+
set_bottom: [["id"], ["bottom"]],
|
|
776
844
|
set_bgcolor: [["id"], ["bgcolor", "color"]],
|
|
777
845
|
set_border_color: [["id"], ["color", "border_color"]],
|
|
846
|
+
set_border_width: [["id"], ["width"]],
|
|
847
|
+
set_border_style: [["id"], ["style"]],
|
|
778
848
|
set_text: [["id"], ["text"]],
|
|
849
|
+
set_text_color: [["id"], ["color", "text_color"]],
|
|
850
|
+
set_text_font_family: [["id"], ["font_family"]],
|
|
851
|
+
set_text_halign: [["id"], ["halign"]],
|
|
852
|
+
set_text_valign: [["id"], ["valign"]],
|
|
853
|
+
set_text_size: [["id"], ["size"]],
|
|
854
|
+
set_text_formatting: [["id"], ["formatting"]],
|
|
855
|
+
set_text_wrap: [["id"], ["wrap"]],
|
|
779
856
|
set_extend: [["id"], ["extend"]],
|
|
857
|
+
set_closed: [["id"], ["closed"]],
|
|
858
|
+
set_rightbottom: [["id"], ["right"], ["bottom"]],
|
|
859
|
+
get_left: [["id"]],
|
|
860
|
+
get_right: [["id"]],
|
|
861
|
+
get_top: [["id"]],
|
|
862
|
+
get_bottom: [["id"]],
|
|
780
863
|
delete: [["id"]],
|
|
781
864
|
},
|
|
782
865
|
table: {
|
|
783
866
|
cell: [["id"], ["column", "col"], ["row"], ["text"]],
|
|
784
|
-
|
|
867
|
+
// Interpret positional order is (id, row, column, text), not column-first.
|
|
868
|
+
cell_set_text: [["id"], ["row"], ["column", "col"], ["text"]],
|
|
869
|
+
cell_get_text: [["id"], ["row"], ["column", "col"]],
|
|
870
|
+
cell_set_text_color: [["id"], ["row"], ["column", "col"], ["color", "text"]],
|
|
871
|
+
cell_set_bgcolor: [["id"], ["row"], ["column", "col"], ["color"]],
|
|
872
|
+
cell_set_border_color: [["id"], ["row"], ["column", "col"], ["color"]],
|
|
873
|
+
cell_set_border_width: [["id"], ["row"], ["column", "col"], ["width"]],
|
|
874
|
+
cell_set_tooltip: [["id"], ["row"], ["column", "col"], ["tooltip", "text"]],
|
|
875
|
+
cell_set_width: [["id"], ["column", "col"], ["row"], ["width"]],
|
|
876
|
+
cell_set_height: [["id"], ["column", "col"], ["row"], ["height"]],
|
|
877
|
+
cell_set_text_halign: [["id"], ["column", "col"], ["row"], ["halign"]],
|
|
878
|
+
cell_set_text_valign: [["id"], ["column", "col"], ["row"], ["valign"]],
|
|
879
|
+
cell_set_text_size: [["id"], ["column", "col"], ["row"], ["size"]],
|
|
880
|
+
cell_set_text_font_family: [["id"], ["column", "col"], ["row"], ["font_family"]],
|
|
881
|
+
cell_set_text_formatting: [["id"], ["column", "col"], ["row"], ["formatting"]],
|
|
785
882
|
set_position: [["id"], ["position"]],
|
|
883
|
+
set_bgcolor: [["id"], ["color", "bgcolor"]],
|
|
884
|
+
set_border_color: [["id"], ["color"]],
|
|
885
|
+
set_border_width: [["id"], ["width"]],
|
|
886
|
+
set_frame_color: [["id"], ["color"]],
|
|
887
|
+
set_frame_width: [["id"], ["width"]],
|
|
888
|
+
merge_cells: [["id"], ["start_row"], ["start_column", "start_col"], ["end_row"], ["end_column", "end_col"]],
|
|
786
889
|
delete: [["id"]],
|
|
787
890
|
},
|
|
788
891
|
polyline: {
|
|
789
892
|
delete: [["id"]],
|
|
893
|
+
get_points: [["id"]],
|
|
894
|
+
set_points: [["id"], ["points"]],
|
|
895
|
+
set_line_color: [["id"], ["color"]],
|
|
896
|
+
set_line_width: [["id"], ["width"]],
|
|
897
|
+
set_line_style: [["id"], ["style"]],
|
|
898
|
+
set_fill_color: [["id"], ["fill_color", "color"]],
|
|
899
|
+
set_curved: [["id"], ["curved"]],
|
|
900
|
+
set_force_overlay: [["id"], ["force_overlay"]],
|
|
901
|
+
set_closed: [["id"], ["closed"]],
|
|
902
|
+
set_xloc: [["id"], ["xloc"]],
|
|
790
903
|
},
|
|
791
904
|
linefill: {
|
|
792
905
|
set_color: [["id"], ["color"]],
|
|
793
906
|
delete: [["id"]],
|
|
907
|
+
get_line1: [["id"]],
|
|
908
|
+
get_line2: [["id"]],
|
|
794
909
|
},
|
|
795
910
|
};
|
|
796
911
|
|
|
912
|
+
function emitChartPoint(name: string, args: CallArgs): string | null {
|
|
913
|
+
const meth = name.slice("chart_point_".length);
|
|
914
|
+
if (meth === "new" || meth === "from_time") {
|
|
915
|
+
return `__h.draw.point_new(${pick(args, 0, ["time"], "null")}, ${pick(args, 1, ["price"], "0")})`;
|
|
916
|
+
}
|
|
917
|
+
if (meth === "from_index") {
|
|
918
|
+
return `__h.draw.point_from_index(${pick(args, 0, ["index"], "null")}, ${pick(args, 1, ["price"], "0")})`;
|
|
919
|
+
}
|
|
920
|
+
if (meth === "now") {
|
|
921
|
+
return `__h.draw.point_now(__bar_idx, time, ${pick(args, 0, ["price"], "0")})`;
|
|
922
|
+
}
|
|
923
|
+
if (meth === "copy") {
|
|
924
|
+
return `__h.draw.point_copy(${pick(args, 0, ["point"], "null")})`;
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
|
|
797
929
|
function emitDraw(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallArgs): string | null {
|
|
798
930
|
if (name === "alert" && methodSrc == null) {
|
|
799
931
|
ctx.usesDrawings = true;
|
|
800
932
|
return `__h.draw.alert(__bar_idx, ${pick(args, 0, ["message"], "null")})`;
|
|
801
933
|
}
|
|
934
|
+
if (name.startsWith("chart_point_")) {
|
|
935
|
+
const point = emitChartPoint(name, args);
|
|
936
|
+
if (point != null) ctx.usesDrawings = true;
|
|
937
|
+
return point;
|
|
938
|
+
}
|
|
802
939
|
const parsed = drawKindAndMethod(name, methodSrc);
|
|
803
940
|
if (parsed == null) return null;
|
|
804
941
|
const { kind, method } = parsed;
|
|
805
942
|
let code: string | null = null;
|
|
806
|
-
if (method === "
|
|
943
|
+
if (method === "all") {
|
|
944
|
+
code = `__h.draw.all(${JSON.stringify(kind)})`;
|
|
945
|
+
} else if (method === "copy" && (kind === "line" || kind === "label" || kind === "box" || kind === "polyline")) {
|
|
946
|
+
code = `__h.draw.copy(${pick(args, 0, ["id"], "null")})`;
|
|
947
|
+
} else if (method === "new") {
|
|
807
948
|
if (kind === "label") {
|
|
808
|
-
code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")})`;
|
|
949
|
+
code = `__h.draw.label_new(__bar_idx, ${pick(args, 2, ["text"], "null")}, ${pick(args, 0, ["x"], "0")}, ${pick(args, 1, ["y"], "0")})`;
|
|
950
|
+
} else if (kind === "line") {
|
|
951
|
+
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")})`;
|
|
952
|
+
} else if (kind === "box") {
|
|
953
|
+
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")})`;
|
|
954
|
+
} else if (kind === "polyline") {
|
|
955
|
+
code = `__h.draw.polyline_new(__bar_idx, ${pick(args, 0, ["points"], "null")})`;
|
|
809
956
|
} else if (kind === "linefill") {
|
|
810
|
-
|
|
957
|
+
const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
|
|
958
|
+
code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
|
|
811
959
|
} else if (DRAW_KINDS.has(kind)) {
|
|
812
960
|
code = `__h.draw.${kind}_new(__bar_idx)`;
|
|
813
961
|
}
|
|
962
|
+
} else if (kind === "line" && method === "fill") {
|
|
963
|
+
const color = hasArg(args, 2, ["color"]) ? pick(args, 2, ["color"], "null") : "null";
|
|
964
|
+
code = `__h.draw.linefill_new(__bar_idx, ${pick(args, 0, ["id1", "line1"], "null")}, ${pick(args, 1, ["id2", "line2"], "null")}, ${color})`;
|
|
965
|
+
} else if (
|
|
966
|
+
(kind === "line" && (method === "set_first_point" || method === "set_second_point")) ||
|
|
967
|
+
(kind === "label" && method === "set_point") ||
|
|
968
|
+
(kind === "box" && (method === "set_top_left_point" || method === "set_bottom_right_point"))
|
|
969
|
+
) {
|
|
970
|
+
code = `__h.draw.${kind}_${method}(${pick(args, 0, ["id"], "null")}, ${pick(args, 1, ["point"], "null")})`;
|
|
971
|
+
} else if (kind === "box" && method === "set_xloc") {
|
|
972
|
+
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")})`;
|
|
973
|
+
} else if (kind === "table" && method === "clear") {
|
|
974
|
+
const id = pick(args, 0, ["id", "table_id"], "null");
|
|
975
|
+
code = hasArg(args, 1, ["start_row"])
|
|
976
|
+
? `__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")})`
|
|
977
|
+
: `__h.draw.table_clear(${id})`;
|
|
814
978
|
} else {
|
|
815
979
|
const aliased = kind === "box" && method === "set_lefttop" ? "set_corners" : method;
|
|
816
980
|
const params = DRAW_METHODS[kind]?.[method] ?? DRAW_METHODS[kind]?.[aliased];
|
|
@@ -855,9 +1019,9 @@ function emitUdt(ctx: EmitCtx, name: string, methodSrc: expr | null, args: CallA
|
|
|
855
1019
|
|
|
856
1020
|
const SECURITY_FUNCS = new Set([
|
|
857
1021
|
"security",
|
|
1022
|
+
"security_lower_tf",
|
|
858
1023
|
"request_security",
|
|
859
1024
|
"request_security_lower_tf",
|
|
860
|
-
"request_seed",
|
|
861
1025
|
]);
|
|
862
1026
|
|
|
863
1027
|
const SIMPLE_CHART_EXPR = new Set([
|
|
@@ -903,10 +1067,60 @@ function emitRequest(ctx: EmitCtx, name: string, args: CallArgs): string | null
|
|
|
903
1067
|
if (name === "request_currency_rate" || name === "currency_rate") {
|
|
904
1068
|
return `__h.currencyRate(${pick(args, 0, ["from", "from_currency"], "null")}, ${pick(args, 1, ["to", "to_currency"], "null")})`;
|
|
905
1069
|
}
|
|
1070
|
+
if (name === "request_seed") {
|
|
1071
|
+
return `__h.requestSeed(${pick(args, 0, ["seed"], "0")})`;
|
|
1072
|
+
}
|
|
1073
|
+
if (name === "request_footprint") {
|
|
1074
|
+
return `__h.requestFootprint(${pick(args, 0, ["num_ticks"], "100")}, ${pick(args, 1, ["va_percentage"], "70")})`;
|
|
1075
|
+
}
|
|
906
1076
|
if (name.startsWith("request_")) return "null";
|
|
907
1077
|
return null;
|
|
908
1078
|
}
|
|
909
1079
|
|
|
1080
|
+
/** Python `request.footprint` mock + `footprint.*` / `volume_row.*` accessors. */
|
|
1081
|
+
function emitFootprint(name: string, args: CallArgs): string | null {
|
|
1082
|
+
const fp = pick(args, 0, ["id", "footprint"], "null");
|
|
1083
|
+
const vr = pick(args, 0, ["id", "volume_row"], "null");
|
|
1084
|
+
switch (name) {
|
|
1085
|
+
case "footprint_buy_volume":
|
|
1086
|
+
return `__h.footprint.buy_volume(${fp})`;
|
|
1087
|
+
case "footprint_sell_volume":
|
|
1088
|
+
return `__h.footprint.sell_volume(${fp})`;
|
|
1089
|
+
case "footprint_delta":
|
|
1090
|
+
return `__h.footprint.delta(${fp})`;
|
|
1091
|
+
case "footprint_total_volume":
|
|
1092
|
+
return `__h.footprint.total_volume(${fp})`;
|
|
1093
|
+
case "footprint_vah":
|
|
1094
|
+
return `__h.footprint.vah(${fp})`;
|
|
1095
|
+
case "footprint_val":
|
|
1096
|
+
return `__h.footprint.val(${fp})`;
|
|
1097
|
+
case "footprint_poc":
|
|
1098
|
+
return `__h.footprint.poc(${fp})`;
|
|
1099
|
+
case "footprint_rows":
|
|
1100
|
+
return `__h.footprint.rows(${fp})`;
|
|
1101
|
+
case "footprint_get_row_by_price":
|
|
1102
|
+
return `__h.footprint.get_row_by_price(${fp}, ${pick(args, 1, ["price"], "null")})`;
|
|
1103
|
+
case "volume_row_up_price":
|
|
1104
|
+
return `__h.footprint.volume_row.up_price(${vr})`;
|
|
1105
|
+
case "volume_row_down_price":
|
|
1106
|
+
return `__h.footprint.volume_row.down_price(${vr})`;
|
|
1107
|
+
case "volume_row_buy_volume":
|
|
1108
|
+
return `__h.footprint.volume_row.buy_volume(${vr})`;
|
|
1109
|
+
case "volume_row_sell_volume":
|
|
1110
|
+
return `__h.footprint.volume_row.sell_volume(${vr})`;
|
|
1111
|
+
case "volume_row_delta":
|
|
1112
|
+
return `__h.footprint.volume_row.delta(${vr})`;
|
|
1113
|
+
case "volume_row_total_volume":
|
|
1114
|
+
return `__h.footprint.volume_row.total_volume(${vr})`;
|
|
1115
|
+
case "volume_row_has_buy_imbalance":
|
|
1116
|
+
return `__h.footprint.volume_row.has_buy_imbalance(${vr})`;
|
|
1117
|
+
case "volume_row_has_sell_imbalance":
|
|
1118
|
+
return `__h.footprint.volume_row.has_sell_imbalance(${vr})`;
|
|
1119
|
+
default:
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
910
1124
|
function isSimpleSecurityExpr(expr: string): boolean {
|
|
911
1125
|
const e = expr.trim();
|
|
912
1126
|
if (!e) return false;
|
|
@@ -1024,6 +1238,7 @@ function emitStrategyDecl(ctx: EmitCtx, args: CallArgs): string {
|
|
|
1024
1238
|
add(["default_qty_type"], "default_qty_type");
|
|
1025
1239
|
add(["default_qty_value"], "default_qty_value");
|
|
1026
1240
|
add(["initial_capital"], "initial_capital");
|
|
1241
|
+
add(["currency"], "currency");
|
|
1027
1242
|
return `__h.strategy.configure({${fields.join(", ")}})`;
|
|
1028
1243
|
}
|
|
1029
1244
|
|
|
@@ -1095,8 +1310,50 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
|
|
|
1095
1310
|
case "margin_liquidation_price":
|
|
1096
1311
|
case "position_avg_price":
|
|
1097
1312
|
case "initial_capital":
|
|
1313
|
+
case "cash":
|
|
1314
|
+
case "position_entry_name":
|
|
1315
|
+
case "account_currency":
|
|
1316
|
+
case "max_contracts_held_all":
|
|
1317
|
+
case "max_contracts_held_long":
|
|
1318
|
+
case "max_contracts_held_short":
|
|
1319
|
+
case "wintrades":
|
|
1320
|
+
case "losstrades":
|
|
1321
|
+
case "eventrades":
|
|
1322
|
+
case "grossprofit":
|
|
1323
|
+
case "grossloss":
|
|
1324
|
+
case "avg_trade":
|
|
1325
|
+
case "avg_winning_trade":
|
|
1326
|
+
case "avg_losing_trade":
|
|
1327
|
+
case "netprofit_percent":
|
|
1328
|
+
case "openprofit_percent":
|
|
1329
|
+
case "grossprofit_percent":
|
|
1330
|
+
case "grossloss_percent":
|
|
1331
|
+
case "avg_trade_percent":
|
|
1332
|
+
case "avg_winning_trade_percent":
|
|
1333
|
+
case "avg_losing_trade_percent":
|
|
1334
|
+
case "max_drawdown":
|
|
1335
|
+
case "max_drawdown_percent":
|
|
1336
|
+
case "max_runup":
|
|
1337
|
+
case "max_runup_percent":
|
|
1338
|
+
case "percent_profitable":
|
|
1339
|
+
case "winrate":
|
|
1340
|
+
case "profitfactor":
|
|
1341
|
+
case "profit_factor":
|
|
1342
|
+
case "capital_held":
|
|
1343
|
+
case "closedtrades_first_index":
|
|
1098
1344
|
ctx.usesStrategy = true;
|
|
1099
1345
|
return `__h.strategy.${method}()`;
|
|
1346
|
+
case "commission":
|
|
1347
|
+
return "0";
|
|
1348
|
+
case "default_entry_qty":
|
|
1349
|
+
ctx.usesStrategy = true;
|
|
1350
|
+
return hasArg(args, 0, ["percent_equity"])
|
|
1351
|
+
? `__h.strategy.default_entry_qty(${pick(args, 0, ["percent_equity"], "null")})`
|
|
1352
|
+
: `__h.strategy.default_entry_qty()`;
|
|
1353
|
+
case "convert_to_account":
|
|
1354
|
+
case "convert_to_symbol":
|
|
1355
|
+
ctx.usesStrategy = true;
|
|
1356
|
+
return hasArg(args, 0, ["value"]) ? `__h.strategy.convert_to(${pick(args, 0, ["value"], "null")})` : "1";
|
|
1100
1357
|
case "risk_allow_entry_in":
|
|
1101
1358
|
ctx.usesStrategy = true;
|
|
1102
1359
|
return `__h.strategy.risk_allow_entry_in(${pick(args, 0, ["value"], "null")})`;
|
|
@@ -1106,9 +1363,33 @@ function emitStrategy(ctx: EmitCtx, name: string, args: CallArgs): string {
|
|
|
1106
1363
|
case "risk_max_drawdown":
|
|
1107
1364
|
ctx.usesStrategy = true;
|
|
1108
1365
|
return `__h.strategy.risk_max_drawdown(${pick(args, 0, ["value"], "null")}, ${pick(args, 1, ["type"], "null")})`;
|
|
1109
|
-
default:
|
|
1366
|
+
default: {
|
|
1367
|
+
if (method.startsWith("direction_")) {
|
|
1368
|
+
const tail = method.slice("direction_".length);
|
|
1369
|
+
if (tail === "long" || tail === "short" || tail === "all") return JSON.stringify(tail);
|
|
1370
|
+
}
|
|
1371
|
+
if (method.startsWith("oca_")) {
|
|
1372
|
+
const tail = method.slice("oca_".length);
|
|
1373
|
+
if (tail === "none" || tail === "cancel" || tail === "reduce") return JSON.stringify(tail);
|
|
1374
|
+
}
|
|
1375
|
+
if (method.startsWith("commission_")) {
|
|
1376
|
+
const tail = method.slice("commission_".length);
|
|
1377
|
+
if (tail === "percent" || tail === "cash_per_order" || tail === "cash_per_contract") {
|
|
1378
|
+
return JSON.stringify(tail);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (method.startsWith("closedtrades_") || method.startsWith("opentrades_")) {
|
|
1382
|
+
ctx.usesStrategy = true;
|
|
1383
|
+
if (method === "opentrades_capital_held") return "__h.strategy.capital_held()";
|
|
1384
|
+
if (method === "closedtrades_first_index") return "__h.strategy.closedtrades_first_index()";
|
|
1385
|
+
const idx = hasArg(args, 0, ["trade_num", "trade_index", "index"])
|
|
1386
|
+
? pick(args, 0, ["trade_num", "trade_index", "index"], "0")
|
|
1387
|
+
: "0";
|
|
1388
|
+
return `__h.strategy.${method}(${idx})`;
|
|
1389
|
+
}
|
|
1110
1390
|
if (isStrategyAction(name, method)) ctx.usesStrategy = true;
|
|
1111
1391
|
return "null";
|
|
1392
|
+
}
|
|
1112
1393
|
}
|
|
1113
1394
|
}
|
|
1114
1395
|
|
|
@@ -1167,7 +1448,7 @@ function emitPlot(ctx: EmitCtx, fname: string, args: CallArgs): string {
|
|
|
1167
1448
|
: pick(args, 0, ["series", "source", "price"], "null");
|
|
1168
1449
|
const i = ctx.plots.length;
|
|
1169
1450
|
ctx.plots.push({ title });
|
|
1170
|
-
return `plot_${i}[__bar_idx] = __h.
|
|
1451
|
+
return `plot_${i}[__bar_idx] = __h.plotNum(${series})`;
|
|
1171
1452
|
}
|
|
1172
1453
|
|
|
1173
1454
|
function emitColor(name: string, args: CallArgs): string | null {
|
|
@@ -1792,5 +2073,80 @@ function emitTa(ctx: EmitCtx, method: string, args: CallArgs): string {
|
|
|
1792
2073
|
]);
|
|
1793
2074
|
return `(() => { const __r = ${call}; return [__r.high, __r.low, __r.dir]; })()`;
|
|
1794
2075
|
}
|
|
2076
|
+
if (method === "hammer") {
|
|
2077
|
+
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"))})`;
|
|
2078
|
+
}
|
|
2079
|
+
if (method === "gap_detector") {
|
|
2080
|
+
return `__h.ta.gapDetector(${cellOf(pick(args, 0, ["high"], "null"))}, ${cellOf(pick(args, 1, ["low"], "null"))}, ${cellOf(pick(args, 2, ["prev_close", "prev"], "null"))})`;
|
|
2081
|
+
}
|
|
2082
|
+
if (method === "macd_signal") {
|
|
2083
|
+
return `__h.ta.macdSignal(${cellOf(pick(args, 0, ["macd_line", "macd"], "null"))}, ${cellOf(pick(args, 1, ["signal_line", "signal"], "null"))})`;
|
|
2084
|
+
}
|
|
2085
|
+
if (method === "acceleration_factor") {
|
|
2086
|
+
return taCall("accelerationFactor", site, [
|
|
2087
|
+
cellOf(pick(args, 0, ["momentum_list", "momentum", "source", "series"], "null")),
|
|
2088
|
+
cellOf(pick(args, 1, ["period", "length"], "null")),
|
|
2089
|
+
]);
|
|
2090
|
+
}
|
|
2091
|
+
if (method === "stoch_smooth") {
|
|
2092
|
+
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"))})`;
|
|
2093
|
+
}
|
|
2094
|
+
if (method === "engulfing") {
|
|
2095
|
+
// Bar-mode sentinel. Args are still evaluated (interpret reads all four).
|
|
2096
|
+
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(); })()`;
|
|
2097
|
+
}
|
|
2098
|
+
if (method === "inside_bar_pattern") {
|
|
2099
|
+
return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.insideBarPattern(); })()`;
|
|
2100
|
+
}
|
|
2101
|
+
if (method === "double_top_bottom") {
|
|
2102
|
+
return `(() => { ${cellOf(pick(args, 0, ["high"], "null"))}; ${cellOf(pick(args, 1, ["low"], "null"))}; return __h.ta.doubleTopBottom(${cellOf(pick(args, 2, ["period", "length"], "null"))}); })()`;
|
|
2103
|
+
}
|
|
2104
|
+
if (method === "voi") {
|
|
2105
|
+
return `__h.ta.voi(${cellOf(pick(args, 0, ["buy_volume", "buy"], "null"))}, ${cellOf(pick(args, 1, ["sell_volume", "sell"], "null"))})`;
|
|
2106
|
+
}
|
|
2107
|
+
if (method === "bid_ask_imbalance") {
|
|
2108
|
+
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"))})`;
|
|
2109
|
+
}
|
|
2110
|
+
if (method === "rsi_oversold_overbought") {
|
|
2111
|
+
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"))})`;
|
|
2112
|
+
}
|
|
2113
|
+
if (method === "rsi_divergence") {
|
|
2114
|
+
// Missing period is 0 (interpret `period ?? 0`), not the engine's null → na.
|
|
2115
|
+
const period = `((__n) => (__n == null ? 0 : __n))(${cellOf(pick(args, 1, ["period", "length"], "null"))})`;
|
|
2116
|
+
return `__h.ta.rsiDivergence(${cellList(pick(args, 0, ["rsi_series", "series", "source"], "null"))}, ${period})`;
|
|
2117
|
+
}
|
|
2118
|
+
if (method === "cumulative_delta") {
|
|
2119
|
+
return taCall("cumulativeDelta", site, [
|
|
2120
|
+
cellOf(pick(args, 0, ["close", "source"], "null")),
|
|
2121
|
+
cellOf(pick(args, 1, ["volume"], "null")),
|
|
2122
|
+
cellOf(pick(args, 2, ["period", "length"], "null")),
|
|
2123
|
+
]);
|
|
2124
|
+
}
|
|
2125
|
+
if (method === "volume_momentum") {
|
|
2126
|
+
return taCall("volumeMomentum", site, [
|
|
2127
|
+
cellOf(pick(args, 0, ["volume", "source"], "null")),
|
|
2128
|
+
cellOf(pick(args, 1, ["period", "length"], "null")),
|
|
2129
|
+
]);
|
|
2130
|
+
}
|
|
2131
|
+
if (method === "volume_profile_high") {
|
|
2132
|
+
return `__h.ta.volumeProfileHigh(${cellList(pick(args, 0, ["close", "price", "source", "series"], "null"))}, ${cellList(pick(args, 1, ["volume"], "null"))})`;
|
|
2133
|
+
}
|
|
2134
|
+
if (method === "volume_profile_low") {
|
|
2135
|
+
return taCall("volumeProfileLow", site, [
|
|
2136
|
+
cellOf(pick(args, 0, ["close", "source"], "null")),
|
|
2137
|
+
cellOf(pick(args, 1, ["volume"], "null")),
|
|
2138
|
+
cellOf(pick(args, 2, ["period", "length"], "null")),
|
|
2139
|
+
cellOf(pick(args, 3, ["levels"], "null")),
|
|
2140
|
+
]);
|
|
2141
|
+
}
|
|
2142
|
+
if (method === "volume_thrust") {
|
|
2143
|
+
const sens = cellOf(pick(args, 3, ["sensitivity"], "null"));
|
|
2144
|
+
return taCall("volumeThrust", site, [
|
|
2145
|
+
cellOf(pick(args, 0, ["close", "source"], "null")),
|
|
2146
|
+
cellOf(pick(args, 1, ["volume"], "null")),
|
|
2147
|
+
cellOf(pick(args, 2, ["volume_sma", "vol_sma", "sma"], "null")),
|
|
2148
|
+
`((__n) => (__n == null ? 0.3 : __n))(${sens})`,
|
|
2149
|
+
]);
|
|
2150
|
+
}
|
|
1795
2151
|
return "null";
|
|
1796
2152
|
}
|
|
@@ -56,7 +56,28 @@ import {
|
|
|
56
56
|
strUpper,
|
|
57
57
|
} from "../str.ts";
|
|
58
58
|
import { LogBook, formatLogParts } from "../log.ts";
|
|
59
|
-
import {
|
|
59
|
+
import { PineArray } from "../array.ts";
|
|
60
|
+
import {
|
|
61
|
+
createFootprint,
|
|
62
|
+
footprintBuyVolume,
|
|
63
|
+
footprintDelta,
|
|
64
|
+
footprintGetRowByPrice,
|
|
65
|
+
footprintPoc,
|
|
66
|
+
footprintRows,
|
|
67
|
+
footprintSellVolume,
|
|
68
|
+
footprintTotalVolume,
|
|
69
|
+
footprintVah,
|
|
70
|
+
footprintVal,
|
|
71
|
+
volumeRowBuyVolume,
|
|
72
|
+
volumeRowDelta,
|
|
73
|
+
volumeRowDownPrice,
|
|
74
|
+
volumeRowHasBuyImbalance,
|
|
75
|
+
volumeRowHasSellImbalance,
|
|
76
|
+
volumeRowSellVolume,
|
|
77
|
+
volumeRowTotalVolume,
|
|
78
|
+
volumeRowUpPrice,
|
|
79
|
+
} from "../footprint.ts";
|
|
80
|
+
import { resolveCurrencyRate, resolveSeed } from "../request.ts";
|
|
60
81
|
import { TaEngine } from "../ta.ts";
|
|
61
82
|
import { utcPartsFromMs, timestamp, weekOfYear, timeTradingDay, type UtcParts } from "../time.ts";
|
|
62
83
|
import { TickerId } from "../ticker.ts";
|
|
@@ -83,6 +104,7 @@ import {
|
|
|
83
104
|
safeNeg,
|
|
84
105
|
safeSub,
|
|
85
106
|
} from "./helpers.ts";
|
|
107
|
+
import { isStrategyCashAmount } from "../strategy.ts";
|
|
86
108
|
import { createCompileStrategy } from "./strategy.ts";
|
|
87
109
|
import { udtGet, udtNew, udtRegister, udtSet } from "./udt_runtime.ts";
|
|
88
110
|
|
|
@@ -105,6 +127,8 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
|
|
|
105
127
|
na: NA_FN,
|
|
106
128
|
nz,
|
|
107
129
|
naNum,
|
|
130
|
+
plotNum,
|
|
131
|
+
timenow,
|
|
108
132
|
isNa: isNaCell,
|
|
109
133
|
eq: pineEq,
|
|
110
134
|
ne: pineNe,
|
|
@@ -224,12 +248,41 @@ export function createCompileHelpers(opts?: CompileHelperOpts) {
|
|
|
224
248
|
udtSet,
|
|
225
249
|
udtRegister,
|
|
226
250
|
currencyRate: resolveCurrencyRate,
|
|
251
|
+
requestSeed: resolveSeed,
|
|
252
|
+
requestFootprint: createFootprint,
|
|
253
|
+
footprint: {
|
|
254
|
+
buy_volume: footprintBuyVolume,
|
|
255
|
+
sell_volume: footprintSellVolume,
|
|
256
|
+
delta: footprintDelta,
|
|
257
|
+
total_volume: footprintTotalVolume,
|
|
258
|
+
vah: footprintVah,
|
|
259
|
+
val: footprintVal,
|
|
260
|
+
poc: footprintPoc,
|
|
261
|
+
rows: compileFootprintRows,
|
|
262
|
+
get_row_by_price: footprintGetRowByPrice,
|
|
263
|
+
volume_row: {
|
|
264
|
+
up_price: volumeRowUpPrice,
|
|
265
|
+
down_price: volumeRowDownPrice,
|
|
266
|
+
buy_volume: volumeRowBuyVolume,
|
|
267
|
+
sell_volume: volumeRowSellVolume,
|
|
268
|
+
delta: volumeRowDelta,
|
|
269
|
+
total_volume: volumeRowTotalVolume,
|
|
270
|
+
has_buy_imbalance: volumeRowHasBuyImbalance,
|
|
271
|
+
has_sell_imbalance: volumeRowHasSellImbalance,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
227
274
|
runtimeError(msg: unknown): never {
|
|
228
275
|
throw new Error(String(msg));
|
|
229
276
|
},
|
|
230
277
|
};
|
|
231
278
|
}
|
|
232
279
|
|
|
280
|
+
function compileFootprintRows(fp: unknown): PineArray {
|
|
281
|
+
const arr = new PineArray();
|
|
282
|
+
for (const row of footprintRows(fp)) arr.push(row);
|
|
283
|
+
return arr;
|
|
284
|
+
}
|
|
285
|
+
|
|
233
286
|
/** Pine `color.new(color, transp)` — first arg is a color, not r,g,b. */
|
|
234
287
|
function compileColorNew(color: unknown, transp?: unknown) {
|
|
235
288
|
const parsed = asColor(color);
|
|
@@ -264,6 +317,23 @@ function iff(cond: unknown, thenV: unknown, elseV: unknown): unknown {
|
|
|
264
317
|
return mathIff(cond as number | null, thenV as number | null, elseV as number | null);
|
|
265
318
|
}
|
|
266
319
|
|
|
320
|
+
/** Plot cell: tagged `strategy.cash` unwraps to its number (interpret `unwrap`). */
|
|
321
|
+
function plotNum(v: unknown): number | null {
|
|
322
|
+
if (isStrategyCashAmount(v)) return Number.isFinite(v.value) ? v.value : null;
|
|
323
|
+
return naNum(v);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Dataset last-bar time on every bar. Interpret `beginBar` stores that on
|
|
328
|
+
* `ctx.timenow` (host seed is overwritten). Not wall-clock, not current bar.
|
|
329
|
+
*/
|
|
330
|
+
function timenow(timeArr: unknown): number {
|
|
331
|
+
if (!Array.isArray(timeArr) || timeArr.length === 0) return 0;
|
|
332
|
+
const last = timeArr[timeArr.length - 1];
|
|
333
|
+
if (typeof last === "number" && Number.isFinite(last)) return last;
|
|
334
|
+
return (timeArr.length - 1) * 60_000;
|
|
335
|
+
}
|
|
336
|
+
|
|
267
337
|
function NA_FN(v?: unknown): number | null {
|
|
268
338
|
if (arguments.length === 0) return null;
|
|
269
339
|
return isNaCell(v) ? 1 : 0;
|