@hoox-sh/pynets 0.5.0 → 0.7.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 +35 -0
- package/README.md +6 -4
- package/dist/browser.js +68 -68
- package/dist/index.js +2551 -205
- package/package.json +1 -1
- package/src/cli/rich.ts +1 -1
- package/src/generated/PinescriptLexerBase.ts +22 -4
- package/src/parser/LexerBase.ts +22 -4
- package/src/runtime/compile/draw.ts +26 -2
- package/src/runtime/compile/emit_call.ts +485 -6
- package/src/runtime/compile/helpers.ts +2 -1
- package/src/runtime/compile/runtime.ts +20 -5
- package/src/runtime/compile/strategy.ts +5 -2
- package/src/runtime/interpret.ts +680 -41
- package/src/runtime/str.ts +3 -2
- package/src/runtime/strategy.ts +164 -12
- package/src/runtime/ta.ts +1667 -149
- package/src/runtime/time.ts +12 -0
package/src/runtime/str.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Pine `str.*` helpers. `null` is na.
|
|
6
6
|
*/
|
|
7
|
+
import { timestampToMs } from "./time.ts";
|
|
7
8
|
|
|
8
9
|
function asStr(x: unknown): string | null {
|
|
9
10
|
if (x == null) return null;
|
|
@@ -303,8 +304,8 @@ function coerceFormatTimeMs(time: unknown): number | null {
|
|
|
303
304
|
if (raw == null || typeof raw === "boolean" || typeof raw === "string") return null;
|
|
304
305
|
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
305
306
|
let ts = Math.trunc(raw);
|
|
306
|
-
// Python
|
|
307
|
-
|
|
307
|
+
// Python `utility._timestamp_to_ms`: Unix seconds (< 1e11) become ms.
|
|
308
|
+
ts = timestampToMs(ts) ?? ts;
|
|
308
309
|
return ts;
|
|
309
310
|
}
|
|
310
311
|
|
package/src/runtime/strategy.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface BrokerSettings {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface StrategyEvent {
|
|
33
|
-
type: "entry" | "close" | "exit" | "cancel" | "close_all" | "cancel_all" | "fill";
|
|
33
|
+
type: "entry" | "close" | "exit" | "cancel" | "close_all" | "cancel_all" | "fill" | "order";
|
|
34
34
|
id: string;
|
|
35
35
|
direction?: StrategyDirection;
|
|
36
36
|
qty?: number | null;
|
|
@@ -63,11 +63,16 @@ export interface PendingOrder {
|
|
|
63
63
|
oca_name?: string | null;
|
|
64
64
|
oca_type?: OcaType;
|
|
65
65
|
/** Unset on entry orders so existing pending snapshots stay exact. */
|
|
66
|
-
kind?: "entry" | "exit";
|
|
66
|
+
kind?: "entry" | "exit" | "close" | "close_all";
|
|
67
67
|
from_entry?: string | null;
|
|
68
68
|
trail_offset?: number | null;
|
|
69
69
|
trail_activation?: number | null;
|
|
70
70
|
trail_active?: boolean;
|
|
71
|
+
/** Market order queued for the next bar open (PYNE 0.6.29 `placed_bar` gate). */
|
|
72
|
+
deferred?: boolean;
|
|
73
|
+
/** `strategy.entry` target-position semantics at fill time. */
|
|
74
|
+
is_entry?: boolean;
|
|
75
|
+
comment?: string;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
export interface PlaceEntryOpts {
|
|
@@ -79,6 +84,8 @@ export interface PlaceEntryOpts {
|
|
|
79
84
|
time?: number;
|
|
80
85
|
oca_name?: string | null;
|
|
81
86
|
oca_type?: string | null;
|
|
87
|
+
/** Queue a market order for the next bar open instead of filling now. */
|
|
88
|
+
deferMarket?: boolean;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
/** Named `strategy.exit` args (Pine: id, from_entry, qty, qty_percent, …). */
|
|
@@ -99,6 +106,8 @@ export interface PlaceExitOpts {
|
|
|
99
106
|
time?: number;
|
|
100
107
|
/** Current bar OHLC so a newly placed exit can fill same bar. */
|
|
101
108
|
ohlc?: BarOhlc;
|
|
109
|
+
/** Queue a level-less market exit for the next bar open. */
|
|
110
|
+
deferMarket?: boolean;
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
/** One open or closed strategy trade (qty is absolute). */
|
|
@@ -140,6 +149,8 @@ export interface BarOhlc {
|
|
|
140
149
|
high?: number;
|
|
141
150
|
low?: number;
|
|
142
151
|
close?: number;
|
|
152
|
+
/** Bar timestamp used for trade `entry_time` / `exit_time`. */
|
|
153
|
+
time?: number;
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
/** Snapshot of book-level performance scalars (for RuntimeResult). */
|
|
@@ -905,6 +916,64 @@ export class StrategyBook {
|
|
|
905
916
|
});
|
|
906
917
|
}
|
|
907
918
|
|
|
919
|
+
/**
|
|
920
|
+
* `strategy.close` — queue a market cover for the next bar open and emit
|
|
921
|
+
* the placement `close` event (PYNE 0.6.29). A flat book only emits.
|
|
922
|
+
*/
|
|
923
|
+
placeClose(
|
|
924
|
+
bar: number,
|
|
925
|
+
id: string,
|
|
926
|
+
price?: number,
|
|
927
|
+
opts?: { qty?: number | null; time?: number; comment?: string },
|
|
928
|
+
): void {
|
|
929
|
+
const b = isFiniteNumber(bar) ? bar : 0;
|
|
930
|
+
const oid = String(id ?? "");
|
|
931
|
+
this.ensureSanePosition();
|
|
932
|
+
if (this.position.qty !== 0) {
|
|
933
|
+
const absPos = Math.abs(this.position.qty);
|
|
934
|
+
const rawQty = opts?.qty;
|
|
935
|
+
const q =
|
|
936
|
+
isFiniteNumber(rawQty) && rawQty >= 0 ? Math.min(Math.abs(rawQty), absPos) : absPos;
|
|
937
|
+
if (q > 0) {
|
|
938
|
+
this.upsertPending({
|
|
939
|
+
id: `${oid}:close`,
|
|
940
|
+
direction: this.position.qty > 0 ? "short" : "long",
|
|
941
|
+
qty: q,
|
|
942
|
+
limit: null,
|
|
943
|
+
stop: null,
|
|
944
|
+
bar: b,
|
|
945
|
+
kind: "close",
|
|
946
|
+
deferred: true,
|
|
947
|
+
comment: opts?.comment,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
this.close(b, oid);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* `strategy.close_all` — queue a market flatten for the next bar open and
|
|
956
|
+
* emit the placement `close_all` event (PYNE 0.6.29).
|
|
957
|
+
*/
|
|
958
|
+
placeCloseAll(bar: number, price?: number, opts?: { comment?: string }): void {
|
|
959
|
+
const b = isFiniteNumber(bar) ? bar : 0;
|
|
960
|
+
this.ensureSanePosition();
|
|
961
|
+
if (this.position.qty !== 0) {
|
|
962
|
+
this.upsertPending({
|
|
963
|
+
id: "close_all",
|
|
964
|
+
direction: this.position.qty > 0 ? "short" : "long",
|
|
965
|
+
qty: Math.abs(this.position.qty),
|
|
966
|
+
limit: null,
|
|
967
|
+
stop: null,
|
|
968
|
+
bar: b,
|
|
969
|
+
kind: "close_all",
|
|
970
|
+
deferred: true,
|
|
971
|
+
comment: opts?.comment,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
this.events.push({ type: "close_all", id: "", bar: b });
|
|
975
|
+
}
|
|
976
|
+
|
|
908
977
|
exit(bar: number, id: string, qty?: number | null): void {
|
|
909
978
|
const event: StrategyEvent = {
|
|
910
979
|
type: "exit",
|
|
@@ -949,8 +1018,9 @@ export class StrategyBook {
|
|
|
949
1018
|
|
|
950
1019
|
/**
|
|
951
1020
|
* Place an entry. No limit/stop → market via fillEntry (needs `opts.price`).
|
|
952
|
-
*
|
|
953
|
-
* (
|
|
1021
|
+
* With `deferMarket`, a market entry is queued and fills at the next bar
|
|
1022
|
+
* open (PYNE 0.6.29 reference-Pine flow). Otherwise records a pending
|
|
1023
|
+
* limit/stop order. Always emits a placement event.
|
|
954
1024
|
*/
|
|
955
1025
|
placeEntry(
|
|
956
1026
|
bar: number,
|
|
@@ -961,10 +1031,44 @@ export class StrategyBook {
|
|
|
961
1031
|
): void {
|
|
962
1032
|
const limit = optLevel(opts?.limit);
|
|
963
1033
|
const stop = optLevel(opts?.stop);
|
|
1034
|
+
const b = isFiniteNumber(bar) ? bar : 0;
|
|
1035
|
+
const oid = String(id ?? "");
|
|
964
1036
|
if (limit == null && stop == null) {
|
|
1037
|
+
if (opts?.deferMarket === true) {
|
|
1038
|
+
const dir = mapDirection(direction);
|
|
1039
|
+
const absQty = isFiniteNumber(qty) ? Math.abs(qty) : Number.NaN;
|
|
1040
|
+
if (!isFiniteNumber(absQty) || absQty <= 0) return;
|
|
1041
|
+
const t = isFiniteNumber(opts?.time) ? opts.time : b;
|
|
1042
|
+
if (!this.riskAllowsEntry(dir, t)) return;
|
|
1043
|
+
const mark = isFiniteNumber(opts?.price) ? opts.price : 0;
|
|
1044
|
+
const capped = this.capQtyByMaxPosition(absQty, mark);
|
|
1045
|
+
if (!isFiniteNumber(capped) || capped <= 0) return;
|
|
1046
|
+
// Same-direction add gate (PYNE pyramiding cap = pyramiding + 1).
|
|
1047
|
+
const signed = dir === "long" ? capped : -capped;
|
|
1048
|
+
if (this.position.qty !== 0 && Math.sign(this.position.qty) === Math.sign(signed)) {
|
|
1049
|
+
const maxEntries = this.pyramiding !== undefined ? this.pyramiding + 1 : 1;
|
|
1050
|
+
const allowed =
|
|
1051
|
+
this.pyramiding !== undefined && this.pyramiding > 0 && this.opentrades < maxEntries;
|
|
1052
|
+
if (!allowed) return;
|
|
1053
|
+
}
|
|
1054
|
+
this.upsertPending({
|
|
1055
|
+
id: oid,
|
|
1056
|
+
direction: dir,
|
|
1057
|
+
qty: capped,
|
|
1058
|
+
limit: null,
|
|
1059
|
+
stop: null,
|
|
1060
|
+
bar: b,
|
|
1061
|
+
kind: "entry",
|
|
1062
|
+
is_entry: true,
|
|
1063
|
+
deferred: true,
|
|
1064
|
+
comment: opts?.comment,
|
|
1065
|
+
});
|
|
1066
|
+
this.events.push({ type: "order", id: oid, direction: dir, qty: capped, bar: b });
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
965
1069
|
if (isFiniteNumber(opts?.price)) {
|
|
966
1070
|
const dir = mapDirection(direction);
|
|
967
|
-
const t = isFiniteNumber(opts?.time) ? opts.time :
|
|
1071
|
+
const t = isFiniteNumber(opts?.time) ? opts.time : b;
|
|
968
1072
|
this.updateEquityExtremes(opts.price);
|
|
969
1073
|
if (!this.riskAllowsEntry(dir, t)) return;
|
|
970
1074
|
this.fillEntry(bar, id, direction, qty, opts.price, opts);
|
|
@@ -973,8 +1077,6 @@ export class StrategyBook {
|
|
|
973
1077
|
}
|
|
974
1078
|
const dir = mapDirection(direction);
|
|
975
1079
|
const absQty = isFiniteNumber(qty) ? Math.abs(qty) : Number.NaN;
|
|
976
|
-
const b = isFiniteNumber(bar) ? bar : 0;
|
|
977
|
-
const oid = String(id ?? "");
|
|
978
1080
|
this.entry(b, oid, dir, isFiniteNumber(absQty) ? absQty : qty);
|
|
979
1081
|
if (!isFiniteNumber(absQty) || absQty === 0) return;
|
|
980
1082
|
const pending: PendingOrder = { id: oid, direction: dir, qty: absQty, limit, stop, bar: b };
|
|
@@ -1043,6 +1145,23 @@ export class StrategyBook {
|
|
|
1043
1145
|
|
|
1044
1146
|
const t = isFiniteNumber(opts?.time) ? opts.time : b;
|
|
1045
1147
|
if (limitP == null && stopP == null && !hasTrail) {
|
|
1148
|
+
if (opts?.deferMarket === true) {
|
|
1149
|
+
const closeDir: StrategyDirection = isLong ? "short" : "long";
|
|
1150
|
+
this.upsertPending({
|
|
1151
|
+
id: `${oid}:close`,
|
|
1152
|
+
direction: closeDir,
|
|
1153
|
+
qty,
|
|
1154
|
+
limit: null,
|
|
1155
|
+
stop: null,
|
|
1156
|
+
bar: b,
|
|
1157
|
+
kind: "exit",
|
|
1158
|
+
from_entry: fromEntry,
|
|
1159
|
+
deferred: true,
|
|
1160
|
+
comment: opts?.comment,
|
|
1161
|
+
});
|
|
1162
|
+
this.events.push({ type: "order", id: oid, direction: closeDir, qty, bar: b });
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1046
1165
|
if (!isFiniteNumber(opts?.price)) return;
|
|
1047
1166
|
this.reducePosition(b, oid, opts.price, qty, fromEntry, t, opts?.comment);
|
|
1048
1167
|
return;
|
|
@@ -1132,14 +1251,26 @@ export class StrategyBook {
|
|
|
1132
1251
|
const filled: string[] = [];
|
|
1133
1252
|
for (const order of this.pending.slice()) {
|
|
1134
1253
|
if (!this.pending.some((p) => p.id === order.id)) continue;
|
|
1254
|
+
// Reference Pine: an order placed during bar N becomes active for bar
|
|
1255
|
+
// N+1 — never fills on its own placement bar.
|
|
1256
|
+
if (order.bar === b) continue;
|
|
1135
1257
|
if (order.kind === "exit" && isTrailOrder(order)) updateTrailStop(order, high, low);
|
|
1136
|
-
|
|
1258
|
+
let px: number | null;
|
|
1259
|
+
if (order.limit == null && order.stop == null) px = open;
|
|
1260
|
+
else px = triggerPrice(order, open, high, low);
|
|
1137
1261
|
if (px == null) continue;
|
|
1138
1262
|
this.dropPending(order.id);
|
|
1263
|
+
const emitEntry = order.kind === "entry" || order.deferred === true;
|
|
1264
|
+
const barTime = isFiniteNumber(ohlc.time) ? ohlc.time : undefined;
|
|
1139
1265
|
const applied =
|
|
1140
1266
|
order.kind === "exit"
|
|
1141
|
-
? this.applyExitFill(b, order, px)
|
|
1142
|
-
:
|
|
1267
|
+
? this.applyExitFill(b, order, px, barTime)
|
|
1268
|
+
: order.kind === "close" || order.kind === "close_all"
|
|
1269
|
+
? this.applyCloseFill(b, order, px, barTime)
|
|
1270
|
+
: this.applyEntryFill(b, order.id, order.direction, order.qty, px, emitEntry, {
|
|
1271
|
+
comment: order.comment,
|
|
1272
|
+
time: barTime,
|
|
1273
|
+
});
|
|
1143
1274
|
if (applied) {
|
|
1144
1275
|
const q = Math.abs(this.fills.at(-1)?.qty ?? order.qty);
|
|
1145
1276
|
this.ocaAfterFill(order, q, b);
|
|
@@ -1156,6 +1287,27 @@ export class StrategyBook {
|
|
|
1156
1287
|
return filled;
|
|
1157
1288
|
}
|
|
1158
1289
|
|
|
1290
|
+
/** Apply a queued market close/close_all at the next bar open. */
|
|
1291
|
+
private applyCloseFill(
|
|
1292
|
+
bar: number,
|
|
1293
|
+
order: PendingOrder,
|
|
1294
|
+
price: number,
|
|
1295
|
+
time?: number,
|
|
1296
|
+
): boolean {
|
|
1297
|
+
this.ensureSanePosition();
|
|
1298
|
+
if (this.position.qty === 0 || !isFiniteNumber(price)) return false;
|
|
1299
|
+
const absPos = Math.abs(this.position.qty);
|
|
1300
|
+
const rawQty = order.qty;
|
|
1301
|
+
const q = isFiniteNumber(rawQty) && rawQty > 0 ? Math.min(Math.abs(rawQty), absPos) : absPos;
|
|
1302
|
+
if (!(q > 0)) return false;
|
|
1303
|
+
if (q < absPos - 1e-12) {
|
|
1304
|
+
this.reducePosition(bar, order.id, price, q, order.from_entry ?? null, time, order.comment);
|
|
1305
|
+
} else {
|
|
1306
|
+
this.flattenAt(bar, order.id, price, time, order.comment);
|
|
1307
|
+
}
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1159
1311
|
/** Market entry at `price`. Flat/same-dir adds qty; opposite closes then reverses. */
|
|
1160
1312
|
fillEntry(
|
|
1161
1313
|
bar: number,
|
|
@@ -1302,9 +1454,9 @@ export class StrategyBook {
|
|
|
1302
1454
|
return { status: "ok", qty: Math.min(rawQty, target) };
|
|
1303
1455
|
}
|
|
1304
1456
|
|
|
1305
|
-
private applyExitFill(bar: number, order: PendingOrder, price: number): boolean {
|
|
1457
|
+
private applyExitFill(bar: number, order: PendingOrder, price: number, time?: number): boolean {
|
|
1306
1458
|
const t = isFiniteNumber(bar) ? bar : 0;
|
|
1307
|
-
return this.reducePosition(t, order.id, price, order.qty, order.from_entry ?? null,
|
|
1459
|
+
return this.reducePosition(t, order.id, price, order.qty, order.from_entry ?? null, time);
|
|
1308
1460
|
}
|
|
1309
1461
|
|
|
1310
1462
|
private closeLeg(
|