openscript 0.4.0__py3-none-any.whl

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.
Files changed (97) hide show
  1. openscript/__init__.py +40 -0
  2. openscript/__main__.py +62 -0
  3. openscript/accounting/__init__.py +74 -0
  4. openscript/accounting/analysis.py +174 -0
  5. openscript/accounting/charges.py +397 -0
  6. openscript/accounting/equity.py +234 -0
  7. openscript/accounting/report.py +82 -0
  8. openscript/accounting/shapes.py +74 -0
  9. openscript/accounting/statistics.py +300 -0
  10. openscript/accounting/trades.py +294 -0
  11. openscript/adapter/__init__.py +32 -0
  12. openscript/adapter/answers.py +215 -0
  13. openscript/adapter/channels.py +137 -0
  14. openscript/adapter/expectations.py +67 -0
  15. openscript/adapter/facts.py +127 -0
  16. openscript/adapter/matching.py +257 -0
  17. openscript/adapter/ordering.py +187 -0
  18. openscript/adapter/page.py +130 -0
  19. openscript/adapter/reading.py +357 -0
  20. openscript/adapter/reporting.py +244 -0
  21. openscript/adapter/running.py +449 -0
  22. openscript/adapter/serving.py +229 -0
  23. openscript/adapter/sessions.py +168 -0
  24. openscript/adapter/spellings.py +184 -0
  25. openscript/bars.py +157 -0
  26. openscript/budget.py +342 -0
  27. openscript/canonical.py +192 -0
  28. openscript/civil.py +196 -0
  29. openscript/contracts.py +165 -0
  30. openscript/dates.py +302 -0
  31. openscript/diagnostics.py +104 -0
  32. openscript/hours.py +165 -0
  33. openscript/inputs.py +239 -0
  34. openscript/intervals.py +60 -0
  35. openscript/library/__init__.py +76 -0
  36. openscript/library/arithmetic.py +128 -0
  37. openscript/library/averages.py +133 -0
  38. openscript/library/bars.py +60 -0
  39. openscript/library/bookkeeping.py +166 -0
  40. openscript/library/code_points.py +85 -0
  41. openscript/library/colour.py +202 -0
  42. openscript/library/composites.py +208 -0
  43. openscript/library/counting.py +218 -0
  44. openscript/library/deviation.py +155 -0
  45. openscript/library/elementary.py +206 -0
  46. openscript/library/extremes.py +122 -0
  47. openscript/library/flows.py +220 -0
  48. openscript/library/momentum.py +203 -0
  49. openscript/library/number_text.py +223 -0
  50. openscript/library/prices.py +36 -0
  51. openscript/library/ranges.py +105 -0
  52. openscript/library/rounding.py +123 -0
  53. openscript/library/series.py +213 -0
  54. openscript/library/stateful.py +442 -0
  55. openscript/library/stateless.py +261 -0
  56. openscript/library/strength.py +180 -0
  57. openscript/library/strings.py +228 -0
  58. openscript/library/trend.py +260 -0
  59. openscript/library/values.py +91 -0
  60. openscript/logbook.py +119 -0
  61. openscript/machine.py +499 -0
  62. openscript/memory.py +204 -0
  63. openscript/opcodes.py +166 -0
  64. openscript/program.py +146 -0
  65. openscript/run.py +368 -0
  66. openscript/strategy/__init__.py +78 -0
  67. openscript/strategy/calls.py +201 -0
  68. openscript/strategy/closable.py +182 -0
  69. openscript/strategy/fills.py +131 -0
  70. openscript/strategy/holdings.py +277 -0
  71. openscript/strategy/intents.py +162 -0
  72. openscript/strategy/ledger.py +270 -0
  73. openscript/strategy/placing.py +206 -0
  74. openscript/strategy/positions.py +124 -0
  75. openscript/strategy/refusals.py +293 -0
  76. openscript/strategy/rows.py +219 -0
  77. openscript/strategy/sizing.py +229 -0
  78. openscript/strategy/statuses.py +65 -0
  79. openscript/surface/__init__.py +115 -0
  80. openscript/surface/bands.py +103 -0
  81. openscript/surface/levels.py +44 -0
  82. openscript/surface/marks.py +52 -0
  83. openscript/surface/paints.py +58 -0
  84. openscript/surface/plots.py +44 -0
  85. openscript/surface/published.py +119 -0
  86. openscript/values.py +210 -0
  87. openscript/verify.py +301 -0
  88. openscript/verify_code.py +290 -0
  89. openscript/verify_requests.py +271 -0
  90. openscript/verify_shape.py +162 -0
  91. openscript/verify_tables.py +256 -0
  92. openscript/version.py +39 -0
  93. openscript/zones.py +118 -0
  94. openscript-0.4.0.dist-info/METADATA +82 -0
  95. openscript-0.4.0.dist-info/RECORD +97 -0
  96. openscript-0.4.0.dist-info/WHEEL +5 -0
  97. openscript-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,82 @@
1
+ """The report: what a run came to, folded from its fills and its bar closes.
2
+
3
+ **The evaluation order is fixed here and nowhere else**, because a report that is
4
+ right to eleven digits and different in the twelfth fails a conformance
5
+ comparison months later on somebody else's engine, and the cause is always a line
6
+ nobody thought was arithmetic. Fills in ``seq`` order, charge lines in declaration
7
+ order, one rounding per fill total, no collection re-summed in another order, no
8
+ dependence on a mapping's iteration order, no clock and no random number anywhere
9
+ in this module.
10
+
11
+ **What the report is not is read by a script.** The money entries of the ``pos``
12
+ namespace stay planned and go on refusing at the call. The moment a script can
13
+ read its own equity mid-run, the money layer joins the execution path and
14
+ therefore joins the conformance surface, and every formula in this module has to
15
+ be agreed by a second engine before a script may branch on it. This module
16
+ computes the money after the fact from a record; whether a script may read it is a
17
+ later decision, and that this module imports no interpreter is the structural half
18
+ of keeping that decision open.
19
+
20
+ **A charge belongs to the fill that incurred it.** One fill's total is rounded
21
+ once, and nothing here rounds it again or re-sums the collection in another order,
22
+ so the trades' charges and the summary's charges are the same money and a test
23
+ asserts it rather than a page claiming it.
24
+
25
+ A run carried out under no schedule at all is charged nothing, which is a study of
26
+ the strategy before costs and is a thing worth being able to ask for. It is not a
27
+ default: the caller states which it wants.
28
+ """
29
+
30
+ from dataclasses import dataclass
31
+ from typing import Optional, Sequence, Tuple
32
+
33
+ from .charges import ChargeSchedule, charge_for
34
+ from .equity import EquityPoint, equity_over
35
+ from .shapes import BarMark, Contract, RecordedFill
36
+ from .analysis import TradeAnalysis, analysis_of
37
+ from .statistics import Summary, summary_of
38
+ from .trades import Trade, trades_of
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Report:
43
+ """Everything a run is reported as, and nothing a chart has to compute."""
44
+
45
+ summary: Summary
46
+ #: The trades by direction, by extreme and by run.
47
+ analysis: TradeAnalysis
48
+ trades: Tuple[Trade, ...]
49
+ equity: Tuple[EquityPoint, ...]
50
+
51
+
52
+ def report_of(
53
+ fills: Sequence[RecordedFill],
54
+ marks: Sequence[BarMark],
55
+ schedule: Optional[ChargeSchedule],
56
+ contract: Contract,
57
+ capital: float,
58
+ ) -> Report:
59
+ """The whole report, folded from the fills, the bar closes and the schedule.
60
+
61
+ **One pass, in one order, and the order is the result.** The fills are put in
62
+ ``seq`` order once, here, and every fold below reads that same list: the
63
+ charges are computed in it, the trades are built from it and the curve is
64
+ marked along it. A second ordering anywhere would be a report that is right to
65
+ eleven digits and different in the twelfth on somebody else's engine.
66
+ """
67
+ ordered = sorted(fills, key=lambda fill: fill.seq)
68
+ charges = [
69
+ 0.0 if schedule is None else charge_for(schedule, fill, contract).total
70
+ for fill in ordered
71
+ ]
72
+
73
+ trades = trades_of(ordered, charges, marks, contract)
74
+ equity = equity_over(trades, marks, contract, capital)
75
+ summary = summary_of(trades, equity, contract, capital)
76
+
77
+ return Report(
78
+ summary=summary,
79
+ analysis=analysis_of(trades),
80
+ trades=trades,
81
+ equity=equity,
82
+ )
@@ -0,0 +1,74 @@
1
+ """The atoms the money is folded from: a fill, a contract and a bar's close.
2
+
3
+ **Everything in this module is portable data.** A shape here is what a JSON
4
+ reader gives back, so a report can be computed here, stored by a platform, sent
5
+ to another process and recomputed there without this implementation being
6
+ present. That is not a convenience. A run record is the conformance case a second
7
+ engine is handed, and a case that can only be read by the engine that wrote it
8
+ proves nothing about either.
9
+
10
+ **A fill is the only thing money is folded from.** Not a position, not a ledger
11
+ row, not a running total the engine happened to be holding: the fills the engine
12
+ settled, in the order it settled them, each naming the position reference it
13
+ moved and the size of that reference either side of the settlement. Every figure
14
+ in a report is a function of that list and of the bars it is marked against,
15
+ which is what makes a report reproducible from a record with no engine in the
16
+ room.
17
+ """
18
+
19
+ from dataclasses import dataclass
20
+ from typing import Optional
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Contract:
25
+ """The instrument facts a run was carried out under, as the host stated them.
26
+
27
+ A snapshot rather than a reference. An instrument's lot size and tick size
28
+ change, and a report recomputed months later under today's facts would be a
29
+ different study wearing the same name, so the facts travel with the run.
30
+ """
31
+
32
+ currency: str = "CUR"
33
+ symbol: Optional[str] = None
34
+ exchange: Optional[str] = None
35
+ tick_size: Optional[float] = None
36
+ lot_size: Optional[float] = None
37
+ #: Money per 1.0 of price per unit; 1 when the host states none.
38
+ point_value: float = 1.0
39
+ #: Money rounding digits, half to even, once per fill total.
40
+ digits: int = 2
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class RecordedFill:
45
+ """One settled fill. Every money figure is folded from these and nothing else."""
46
+
47
+ seq: int
48
+ intent_id: int
49
+ order_ref: str
50
+ tag: str
51
+ position_ref: int
52
+ side: str
53
+ #: Positive, this fill's own quantity.
54
+ units: float
55
+ price: float
56
+ bar_index: int
57
+ bar_time: Optional[float]
58
+ ref_size_before: float
59
+ ref_size_after: float
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class BarMark:
64
+ """One bar as the report marks against it.
65
+
66
+ Close only: ``stdlib.md`` 17.4 marks an open position to this bar's close, and
67
+ an intrabar extreme is a price the strategy could not have acted on.
68
+ """
69
+
70
+ bar_index: int
71
+ time: Optional[float]
72
+ close: Optional[float]
73
+ #: False for a warmup bar, which executes and is not reported.
74
+ in_report: bool = True
@@ -0,0 +1,300 @@
1
+ """The summary, and the two figures in it that decide whether a run means anything.
2
+
3
+ **Win rate is over closed trades, on net profit after charges**, and a trade whose
4
+ net is exactly zero is a scratch counted in neither half. It is absent where
5
+ nothing closed rather than zero, because zero is a number a reader compares
6
+ against and "nothing has closed yet" is not a losing run.
7
+
8
+ **Expectancy is money per closed trade**, and it has two spellings that must
9
+ agree: the win rate against the average win and the average loss, and the net
10
+ profit over the trade count. Two spellings of one figure that disagree is how a
11
+ report loses its reader, so one of them is the computation and the other is a
12
+ test.
13
+
14
+ **And its standard error is what answers the question a comparison asks.** The
15
+ sample standard deviation of per-trade net over the square root of the trade count
16
+ is what turns "this run made more" into "this run made more than the noise", and
17
+ without it a difference of two percent over eleven trades reads like a result.
18
+
19
+ Bar times rather than bar indices, wherever a figure addresses a bar: loading more
20
+ history shifts every index, so a report that addresses a bar by index changes when
21
+ the warmup changes.
22
+
23
+ ## Which trades a figure is counted over, which is three different answers
24
+
25
+ Almost every defect this file can have is a figure counted over the wrong half of
26
+ a list holding closed trades and open ones together.
27
+
28
+ - **Net profit, and everything derived from it**, is over closed trades. An open
29
+ trade's net is its charges so far with no gross against them, so counting it
30
+ would report a run holding a winner as having lost money.
31
+ - **Charges are over every trade**, open ones included, because the money left the
32
+ account whether or not the position came back. This is the figure the equity
33
+ curve's last point carries, and the two are asserted equal.
34
+ - **The drawdown figures are over the curve** and not over the trades at all. A
35
+ drawdown is a thing equity did between two trades as often as during one.
36
+
37
+ ## The cases where a statistic is not a number
38
+
39
+ Every one of them is a division, and every one is answered here rather than left
40
+ to arrive as a value a document turns into nothing. Nothing closed: the win rate
41
+ is absent, which is a different claim from zero, and expectancy and its error are
42
+ zero because the type is money and money is not nullable. One closed trade: a
43
+ sample of one has no spread, so the standard error is zero. Every closed trade a
44
+ scratch: no denominator, so the win rate is absent again. Nothing lost: the profit
45
+ factor is absent rather than an infinity. Everything lost: the profit factor is
46
+ zero, expectancy is negative, the average win is zero, and none of the four is a
47
+ division by zero.
48
+ """
49
+
50
+ import math
51
+ from dataclasses import dataclass
52
+ from typing import List, Optional, Sequence
53
+
54
+ from .equity import EquityPoint, bars_in_market_over, ratio_of
55
+ from .shapes import Contract
56
+ from .trades import Trade
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class Summary:
61
+ """What the whole run came to."""
62
+
63
+ capital: float
64
+ currency: str
65
+ net_profit: float
66
+ #: Sum of winning trades, before charges.
67
+ gross_profit: float
68
+ #: The losing trades' gross, as a magnitude, and it can come out at or below
69
+ #: zero: a trade whose gross was positive and whose charges took it under
70
+ #: lands in the losses carrying a positive gross. ``_tally_of`` says why that
71
+ #: is preferred to counting one trade as a loser in one figure and a winner in
72
+ #: another, and it is written here because a reader dividing by it was handed a
73
+ #: negative profit factor with nothing saying that could happen.
74
+ gross_loss: float
75
+ charges: float
76
+ #: net profit over capital, a fraction and not a figure times a hundred.
77
+ return_percent: float
78
+ #: Closed only.
79
+ trade_count: int
80
+ open_trade_count: int
81
+ wins: int
82
+ losses: int
83
+ #: Exactly zero net.
84
+ scratches: int
85
+ #: wins over wins plus losses, absent when none closed.
86
+ win_rate: Optional[float]
87
+ average_win: float
88
+ #: Positive magnitude.
89
+ average_loss: float
90
+ #: Money per closed trade.
91
+ expectancy: float
92
+ expectancy_standard_error: float
93
+ #: Gross profit over gross loss, absent where there is no ratio to take. A
94
+ #: profit factor is a non-negative ratio everywhere it is used, so a negative
95
+ #: one is not a surprising value, it is a number nobody can act on.
96
+ profit_factor: Optional[float]
97
+ #: Zero or negative, the same sign the curve states it with.
98
+ max_drawdown: float
99
+ #: The deepest point's own fraction, not the worst fraction of any point.
100
+ max_drawdown_percent: float
101
+ #: A bar time, never an index.
102
+ max_drawdown_at: Optional[float]
103
+ longest_drawdown_bars: int
104
+ #: Zero or positive, the mirror of ``max_drawdown`` and read against it. A
105
+ #: run that made ten and gave back nine is a different strategy from one
106
+ #: that made one and kept it, and the two report the same net.
107
+ max_run_up: float
108
+ #: The highest point's own fraction, not the best fraction of any point.
109
+ max_run_up_percent: float
110
+ #: A bar time, never an index, and not the bar ``max_drawdown_at`` names.
111
+ max_run_up_at: Optional[float]
112
+ average_bars_held: Optional[float]
113
+ bars_in_market: int
114
+ bar_count: int
115
+
116
+
117
+ @dataclass
118
+ class _Tally:
119
+ """The closed trades split three ways, and the sums each half contributes."""
120
+
121
+ net_profit: float = 0.0
122
+ gross_profit: float = 0.0
123
+ gross_loss: float = 0.0
124
+ charges: float = 0.0
125
+ trade_count: int = 0
126
+ open_trade_count: int = 0
127
+ wins: int = 0
128
+ losses: int = 0
129
+ scratches: int = 0
130
+ win_total: float = 0.0
131
+ loss_total: float = 0.0
132
+ held_total: float = 0.0
133
+ held_count: int = 0
134
+
135
+
136
+ @dataclass
137
+ class _Depth:
138
+ """The deepest point of the curve, and how long the run stayed under water."""
139
+
140
+ max_drawdown: float = 0.0
141
+ max_drawdown_percent: float = 0.0
142
+ max_drawdown_at: Optional[float] = None
143
+ longest_drawdown_bars: int = 0
144
+ max_run_up: float = 0.0
145
+ max_run_up_percent: float = 0.0
146
+ max_run_up_at: Optional[float] = None
147
+
148
+
149
+ def _tally_of(trades: Sequence[Trade]) -> _Tally:
150
+ """One pass over the trades, in the order they are given.
151
+
152
+ A trade wins or loses on its net after charges, and its gross is what it
153
+ contributes to the gross figures: "the sum of the winning trades, before
154
+ charges" is two statements and this is where they meet. The one arrangement
155
+ that reads oddly is a trade whose gross was positive and whose charges took it
156
+ under, which lands in the losses and takes its positive gross with it. That is
157
+ on purpose: the alternative is a trade counted as a loser in one figure and a
158
+ winner in another, and a profit factor whose two halves are counted over
159
+ different sets is worse than one whose magnitude is odd on a trade that barely
160
+ moved.
161
+ """
162
+ tally = _Tally()
163
+ for trade in trades:
164
+ tally.charges += trade.charges
165
+ if trade.is_open:
166
+ tally.open_trade_count += 1
167
+ continue
168
+ tally.trade_count += 1
169
+ tally.net_profit += trade.net_profit
170
+ if trade.bars_held is not None:
171
+ tally.held_total += trade.bars_held
172
+ tally.held_count += 1
173
+ if trade.net_profit > 0:
174
+ tally.wins += 1
175
+ tally.win_total += trade.net_profit
176
+ tally.gross_profit += trade.gross_profit
177
+ elif trade.net_profit < 0:
178
+ tally.losses += 1
179
+ tally.loss_total += trade.net_profit
180
+ tally.gross_loss -= trade.gross_profit
181
+ else:
182
+ tally.scratches += 1
183
+ return tally
184
+
185
+
186
+ def _depth_of(equity: Sequence[EquityPoint]) -> _Depth:
187
+ """The deepest the curve went, named as one point rather than as three figures.
188
+
189
+ The money, the fraction and the time all come from the same point, and the
190
+ point is the deepest in money with the earliest one winning a tie. Taking the
191
+ worst fraction from one bar and the worst money from another would describe a
192
+ moment the run never had.
193
+
194
+ The longest run is the longest stretch of consecutive bars under a peak, which
195
+ is often the figure that actually stops a trader, and it is not the total
196
+ number of bars spent under water.
197
+ """
198
+ depth = _Depth()
199
+ under = 0
200
+ for point in equity:
201
+ if point.drawdown < depth.max_drawdown:
202
+ depth.max_drawdown = point.drawdown
203
+ depth.max_drawdown_percent = point.drawdown_percent
204
+ depth.max_drawdown_at = point.time
205
+ if point.drawdown < 0:
206
+ under += 1
207
+ if under > depth.longest_drawdown_bars:
208
+ depth.longest_drawdown_bars = under
209
+ else:
210
+ under = 0
211
+ # Strictly greater, so the earliest bar reaching the height wins the
212
+ # tie, which is the rule the depth above is picked by. The two figures
213
+ # name different bars and each names the first that reached it.
214
+ if point.run_up > depth.max_run_up:
215
+ depth.max_run_up = point.run_up
216
+ depth.max_run_up_percent = point.run_up_percent
217
+ depth.max_run_up_at = point.time
218
+ return depth
219
+
220
+
221
+ def _standard_error_of(
222
+ trades: Sequence[Trade], expectancy: float, trade_count: int
223
+ ) -> float:
224
+ """The sample deviation of per-trade net over the root of the count.
225
+
226
+ The sample deviation, with the count less one under it, and not the population
227
+ one. The trades a run took are a sample of the trades the strategy would take,
228
+ which is the whole reason this figure is here, and the population spelling
229
+ understates the spread by exactly the amount that matters on the short runs
230
+ where the question is asked. Fewer than two closed trades has no spread to
231
+ measure and gives zero.
232
+ """
233
+ if trade_count < 2:
234
+ return 0.0
235
+ squares = 0.0
236
+ for trade in trades:
237
+ if trade.is_open:
238
+ continue
239
+ away = trade.net_profit - expectancy
240
+ squares += away * away
241
+ return math.sqrt(squares / (trade_count - 1) / trade_count)
242
+
243
+
244
+ def summary_of(
245
+ trades: Sequence[Trade],
246
+ equity: Sequence[EquityPoint],
247
+ contract: Contract,
248
+ capital: float,
249
+ ) -> Summary:
250
+ """The whole run in one shape, folded from its trades and its own curve.
251
+
252
+ The curve is passed in rather than recomputed, because a summary that folded
253
+ its own would be a second equity curve with a second set of rounding, and the
254
+ first disagreement between them would be a drawdown figure that no point in the
255
+ reported curve ever reached.
256
+ """
257
+ tally = _tally_of(trades)
258
+ depth = _depth_of(equity)
259
+ decided = tally.wins + tally.losses
260
+
261
+ # Net over the closed count, and nothing else, because this is the figure the
262
+ # other spelling is checked against. The win rate spelling divides by the
263
+ # decided trades instead, so the two are the same number exactly when no trade
264
+ # scratched.
265
+ expectancy = 0.0 if tally.trade_count == 0 else tally.net_profit / tally.trade_count
266
+
267
+ return Summary(
268
+ capital=capital,
269
+ currency=contract.currency,
270
+ net_profit=tally.net_profit,
271
+ gross_profit=tally.gross_profit,
272
+ gross_loss=tally.gross_loss,
273
+ charges=tally.charges,
274
+ return_percent=ratio_of(tally.net_profit, capital),
275
+ trade_count=tally.trade_count,
276
+ open_trade_count=tally.open_trade_count,
277
+ wins=tally.wins,
278
+ losses=tally.losses,
279
+ scratches=tally.scratches,
280
+ win_rate=None if decided == 0 else tally.wins / decided,
281
+ average_win=0.0 if tally.wins == 0 else tally.win_total / tally.wins,
282
+ average_loss=0.0 if tally.losses == 0 else -tally.loss_total / tally.losses,
283
+ expectancy=expectancy,
284
+ expectancy_standard_error=_standard_error_of(trades, expectancy, tally.trade_count),
285
+ profit_factor=(
286
+ tally.gross_profit / tally.gross_loss if tally.gross_loss > 0 else None
287
+ ),
288
+ max_drawdown=depth.max_drawdown,
289
+ max_drawdown_percent=depth.max_drawdown_percent,
290
+ max_drawdown_at=depth.max_drawdown_at,
291
+ longest_drawdown_bars=depth.longest_drawdown_bars,
292
+ max_run_up=depth.max_run_up,
293
+ max_run_up_percent=depth.max_run_up_percent,
294
+ max_run_up_at=depth.max_run_up_at,
295
+ average_bars_held=(
296
+ None if tally.held_count == 0 else tally.held_total / tally.held_count
297
+ ),
298
+ bars_in_market=bars_in_market_over(trades, equity),
299
+ bar_count=len(equity),
300
+ )