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,244 @@
1
+ """What a strategy case's report was folded under, out of the case's own files.
2
+
3
+ ``conformance.md`` section 3 splits the facts across three files and says why:
4
+ ``instrument.json`` is what the engine read about the instrument,
5
+ ``settings.json`` is what the script's inputs were set to, and ``backtest.json``
6
+ is "the rest of what the host decided, the part the money and the report depend
7
+ on". The declaration is the script's own and arrives in the compiled program's
8
+ meta. Nothing here is a default: a digit count nobody stated is a figure two
9
+ engines round differently, and this module would rather refuse than choose one.
10
+
11
+ **The window is compared against the bars' own times and never against a
12
+ calendar.** Both bounds are inclusive, and a bar outside them executes and is not
13
+ reported: its orders are real and a position opened on it is carried into the
14
+ window. That is section 3's paragraph, and a mark carries the answer per bar so
15
+ that the fold reads it rather than recomputing it.
16
+ """
17
+
18
+ from typing import Any, Dict, Optional, Sequence
19
+
20
+ from ..accounting import (
21
+ BarMark,
22
+ ChargeLine,
23
+ ChargeSchedule,
24
+ Contract,
25
+ schedule_from_declaration,
26
+ schedule_problem,
27
+ )
28
+ from ..diagnostics import Diagnostic, failure
29
+ from .reading import Bar, Case
30
+ from .spellings import Malformed
31
+
32
+ #: Section 3: the file a strategy case is required to carry, and what it holds.
33
+ BACKTEST = "backtest.json"
34
+
35
+ #: What a schedule the host stated is marked with, which is the one of the two
36
+ #: sources that cannot stand beside a commission the declaration states.
37
+ SUPPLIED = "supplied"
38
+
39
+
40
+ def backtest_of(case: Case) -> Dict[str, Any]:
41
+ """``backtest.json``, or the refusal section 3 promises a case without one.
42
+
43
+ Read rather than assumed, and refused rather than defaulted: this engine once
44
+ could not run a strategy case at all, and the first one that ran under a
45
+ digit count nobody stated would have agreed with the engine next door by
46
+ coincidence.
47
+ """
48
+ if case.backtest is None:
49
+ raise Malformed(
50
+ f"{BACKTEST} is missing, and section 3 requires it of a strategy case: a digit count "
51
+ "nobody stated is a figure two engines round differently, so the case is not run "
52
+ "under a default"
53
+ )
54
+ return case.backtest
55
+
56
+
57
+ def contract_for(case: Case) -> Contract:
58
+ """The instrument facts a run was carried out under, plus the run's own digits.
59
+
60
+ Two files and no default, which is why section 3 puts the record in one and
61
+ the digit count in the other: ``host-interface.md`` 4.1 defines the record as
62
+ twelve facts and a rounding count is not among them.
63
+ """
64
+ instrument = case.instrument
65
+ return Contract(
66
+ currency=instrument.get("currency", ""),
67
+ symbol=instrument.get("symbol"),
68
+ exchange=instrument.get("exchange"),
69
+ tick_size=instrument.get("tickSize"),
70
+ lot_size=instrument.get("lotSize"),
71
+ point_value=instrument.get("pointValue", 1.0),
72
+ digits=backtest_of(case)["digits"],
73
+ )
74
+
75
+
76
+ def line_of(stated: Dict[str, Any]) -> ChargeLine:
77
+ """One line of a supplied schedule, as the host stated it.
78
+
79
+ `conformance.md` section 3 says `costs` is the schedule the host supplied
80
+ "whole and as the host stated it", and whole is the word that matters: the
81
+ lines ARE the schedule. Dropping them leaves a currency and a digit count
82
+ charging nothing, which is not a cheaper run, it is a different one, and it
83
+ is wrong in the direction nobody checks because it flatters the strategy.
84
+
85
+ Read by name and not by position, and the optional bounds stay absent when
86
+ the host stated none: a floor of zero and no floor are different rules, and
87
+ a line that invented one would charge a fill the host meant to leave alone.
88
+ """
89
+ return ChargeLine(
90
+ name=stated["name"],
91
+ base=stated["base"],
92
+ side=stated.get("side", "both"),
93
+ rate=stated.get("rate", 0.0),
94
+ min=stated.get("min"),
95
+ max=stated.get("max"),
96
+ of=tuple(stated.get("of", ())),
97
+ )
98
+
99
+
100
+ def schedule_lines(supplied: Dict[str, Any]) -> tuple:
101
+ """Every line of a supplied schedule, in the order the host stated them.
102
+
103
+ Order is part of the result: a charge whose base is `charges` is a fraction
104
+ of the lines named before it, so reordering them changes the money.
105
+ """
106
+ return tuple(line_of(one) for one in supplied.get("lines", ()))
107
+
108
+
109
+ def schedule_for(case: Case, declared: Dict[str, Any]) -> Optional[ChargeSchedule]:
110
+ """The charge schedule the run was carried out under.
111
+
112
+ The host's where it supplied one, and otherwise the declaration's own
113
+ commission and slippage, which an engine derives from the script the same
114
+ way. A supplied schedule's currency and digit count are the contract's,
115
+ because a run under a schedule that disagrees with its contract is refused
116
+ before its first bar, so the two cannot differ inside one case.
117
+ """
118
+ supplied = backtest_of(case)["costs"]
119
+ contract = contract_for(case)
120
+ if supplied is not None:
121
+ return ChargeSchedule(
122
+ currency=supplied["currency"],
123
+ digits=supplied["digits"],
124
+ slippage_ticks=supplied.get("slippageTicks", 0.0),
125
+ lines=schedule_lines(supplied),
126
+ source=SUPPLIED,
127
+ )
128
+ return schedule_from_declaration(
129
+ declared["commission"],
130
+ declared["commissionType"],
131
+ declared["slippage"],
132
+ contract.currency,
133
+ contract.digits,
134
+ )
135
+
136
+
137
+ def settings_problem(
138
+ schedule: Optional[ChargeSchedule], declared: Dict[str, Any], contract: Contract
139
+ ) -> Optional[Diagnostic]:
140
+ """What this run cannot be carried out under, asked before its first bar.
141
+
142
+ Two questions, in this order, and each of them is a figure nobody could
143
+ explain afterwards rather than a tidiness rule.
144
+
145
+ **Two cost models at once, OS6023.** The declaration's commission is the
146
+ script's own statement of what trading costs and a supplied schedule is the
147
+ platform's, and the two describe the same money. Applied together they charge
148
+ it twice; applied one at a time they charge whichever an engine happened to
149
+ prefer, which is a rule nobody wrote down and a figure nobody can explain
150
+ afterwards. So exactly one of the two is stated for a run, and stating both
151
+ is refused here rather than reconciled behind the reader. Asked first,
152
+ because a schedule that is also unusable in some second way would otherwise
153
+ be reported as that, and the reader would correct the wrong half.
154
+
155
+ **A schedule that cannot be evaluated, OS6021**, which ``schedule_problem``
156
+ decides, because the schedule is the money layer's and the rule for it is
157
+ written once, there. Whichever schedule the run will be charged under, the
158
+ declaration's own included: asking only about a supplied one would leave
159
+ every refusal in the money layer unreachable on the path almost every run
160
+ takes.
161
+
162
+ **A quantity in a unit this destination cannot fill, OS6021.** A backtest
163
+ fills in units. A quantity in lots converts when the instrument states a lot
164
+ size and cannot when it does not, and a quantity in any other unit would have
165
+ to be sized against a running equity a backtest works out nothing of. Refused
166
+ rather than guessed, because a guess here is a position size.
167
+
168
+ The first engine asks a fourth question here, about the comparison
169
+ tolerance. This engine asks it in ``matching.tolerance_from``, where the
170
+ tolerance is read, and asks it more strictly: a bound with no reason, a bound
171
+ below zero, and a bound past section 6's cap. Asking again here would be the
172
+ same rule in two places, and the two would drift.
173
+
174
+ Nothing has been computed when this is asked, so a refusal costs one run
175
+ rather than a report a reader has to be told to distrust.
176
+ """
177
+ # A run with no schedule at all still has a quantity to fill, so the sizing
178
+ # question is asked whatever the cost model is. Returning here when there was
179
+ # no schedule is how the check came to be skipped on the path most runs take.
180
+ if schedule is None:
181
+ return _sizing_problem(declared, contract)
182
+ if schedule.source == SUPPLIED and declared["commission"] != 0:
183
+ return failure(
184
+ "OS6023",
185
+ commission=declared["commission"],
186
+ commissionType=declared["commissionType"],
187
+ )
188
+ problem = schedule_problem(schedule, contract)
189
+ if problem is not None:
190
+ setting, reason = problem
191
+ return failure("OS6021", setting=setting, problem=reason)
192
+ return _sizing_problem(declared, contract)
193
+
194
+
195
+ def _sizing_problem(declared: Dict[str, Any], contract: Contract) -> Optional[Diagnostic]:
196
+ """A quantity in a unit this destination cannot fill.
197
+
198
+ A backtest fills in units. Lots convert when the instrument states a lot
199
+ size; without one there is nothing to convert a lot into. Any other unit
200
+ would have to be sized against a running equity a backtest works out none
201
+ of. Both are refused rather than guessed, because the guess is a position
202
+ size and nobody could explain the figure afterwards.
203
+ """
204
+ # No strategy test: this is reached only on the trading path, where the
205
+ # declaration was read, and a study has no declaration to read.
206
+ stated = declared.get("qtyType", "")
207
+ if stated in ("units", ""):
208
+ return None
209
+ if stated == "lots":
210
+ lot = contract.lot_size
211
+ if lot is not None and lot > 0:
212
+ return None
213
+ return failure(
214
+ "OS6021",
215
+ setting="A quantity stated in lots",
216
+ problem="this instrument states no lot size, so there is nothing to convert a lot into",
217
+ )
218
+ return failure(
219
+ "OS6021",
220
+ setting="A quantity stated in " + stated,
221
+ problem=(
222
+ "a backtest fills in units and works out no running equity to size against, so it "
223
+ "cannot convert one. State the quantity in units or in lots"
224
+ ),
225
+ )
226
+
227
+
228
+ def marks_for(case: Case, bars: Sequence[Bar]) -> tuple:
229
+ """One mark per bar supplied, warmup included, with the window's own answer."""
230
+ window = backtest_of(case)["range"]
231
+ lower = window["from"]
232
+ upper = window["to"]
233
+
234
+ def inside(bar: Bar) -> bool:
235
+ if lower is not None and bar.time < lower:
236
+ return False
237
+ if upper is not None and bar.time > upper:
238
+ return False
239
+ return True
240
+
241
+ return tuple(
242
+ BarMark(bar_index=at, time=float(bar.time), close=bar.close, in_report=inside(bar))
243
+ for at, bar in enumerate(bars)
244
+ )