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.
- openscript/__init__.py +40 -0
- openscript/__main__.py +62 -0
- openscript/accounting/__init__.py +74 -0
- openscript/accounting/analysis.py +174 -0
- openscript/accounting/charges.py +397 -0
- openscript/accounting/equity.py +234 -0
- openscript/accounting/report.py +82 -0
- openscript/accounting/shapes.py +74 -0
- openscript/accounting/statistics.py +300 -0
- openscript/accounting/trades.py +294 -0
- openscript/adapter/__init__.py +32 -0
- openscript/adapter/answers.py +215 -0
- openscript/adapter/channels.py +137 -0
- openscript/adapter/expectations.py +67 -0
- openscript/adapter/facts.py +127 -0
- openscript/adapter/matching.py +257 -0
- openscript/adapter/ordering.py +187 -0
- openscript/adapter/page.py +130 -0
- openscript/adapter/reading.py +357 -0
- openscript/adapter/reporting.py +244 -0
- openscript/adapter/running.py +449 -0
- openscript/adapter/serving.py +229 -0
- openscript/adapter/sessions.py +168 -0
- openscript/adapter/spellings.py +184 -0
- openscript/bars.py +157 -0
- openscript/budget.py +342 -0
- openscript/canonical.py +192 -0
- openscript/civil.py +196 -0
- openscript/contracts.py +165 -0
- openscript/dates.py +302 -0
- openscript/diagnostics.py +104 -0
- openscript/hours.py +165 -0
- openscript/inputs.py +239 -0
- openscript/intervals.py +60 -0
- openscript/library/__init__.py +76 -0
- openscript/library/arithmetic.py +128 -0
- openscript/library/averages.py +133 -0
- openscript/library/bars.py +60 -0
- openscript/library/bookkeeping.py +166 -0
- openscript/library/code_points.py +85 -0
- openscript/library/colour.py +202 -0
- openscript/library/composites.py +208 -0
- openscript/library/counting.py +218 -0
- openscript/library/deviation.py +155 -0
- openscript/library/elementary.py +206 -0
- openscript/library/extremes.py +122 -0
- openscript/library/flows.py +220 -0
- openscript/library/momentum.py +203 -0
- openscript/library/number_text.py +223 -0
- openscript/library/prices.py +36 -0
- openscript/library/ranges.py +105 -0
- openscript/library/rounding.py +123 -0
- openscript/library/series.py +213 -0
- openscript/library/stateful.py +442 -0
- openscript/library/stateless.py +261 -0
- openscript/library/strength.py +180 -0
- openscript/library/strings.py +228 -0
- openscript/library/trend.py +260 -0
- openscript/library/values.py +91 -0
- openscript/logbook.py +119 -0
- openscript/machine.py +499 -0
- openscript/memory.py +204 -0
- openscript/opcodes.py +166 -0
- openscript/program.py +146 -0
- openscript/run.py +368 -0
- openscript/strategy/__init__.py +78 -0
- openscript/strategy/calls.py +201 -0
- openscript/strategy/closable.py +182 -0
- openscript/strategy/fills.py +131 -0
- openscript/strategy/holdings.py +277 -0
- openscript/strategy/intents.py +162 -0
- openscript/strategy/ledger.py +270 -0
- openscript/strategy/placing.py +206 -0
- openscript/strategy/positions.py +124 -0
- openscript/strategy/refusals.py +293 -0
- openscript/strategy/rows.py +219 -0
- openscript/strategy/sizing.py +229 -0
- openscript/strategy/statuses.py +65 -0
- openscript/surface/__init__.py +115 -0
- openscript/surface/bands.py +103 -0
- openscript/surface/levels.py +44 -0
- openscript/surface/marks.py +52 -0
- openscript/surface/paints.py +58 -0
- openscript/surface/plots.py +44 -0
- openscript/surface/published.py +119 -0
- openscript/values.py +210 -0
- openscript/verify.py +301 -0
- openscript/verify_code.py +290 -0
- openscript/verify_requests.py +271 -0
- openscript/verify_shape.py +162 -0
- openscript/verify_tables.py +256 -0
- openscript/version.py +39 -0
- openscript/zones.py +118 -0
- openscript-0.4.0.dist-info/METADATA +82 -0
- openscript-0.4.0.dist-info/RECORD +97 -0
- openscript-0.4.0.dist-info/WHEEL +5 -0
- 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
|
+
)
|