openscript 0.4.0__tar.gz

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 (149) hide show
  1. openscript-0.4.0/PKG-INFO +82 -0
  2. openscript-0.4.0/README.md +68 -0
  3. openscript-0.4.0/openscript/__init__.py +40 -0
  4. openscript-0.4.0/openscript/__main__.py +62 -0
  5. openscript-0.4.0/openscript/accounting/__init__.py +74 -0
  6. openscript-0.4.0/openscript/accounting/analysis.py +174 -0
  7. openscript-0.4.0/openscript/accounting/charges.py +397 -0
  8. openscript-0.4.0/openscript/accounting/equity.py +234 -0
  9. openscript-0.4.0/openscript/accounting/report.py +82 -0
  10. openscript-0.4.0/openscript/accounting/shapes.py +74 -0
  11. openscript-0.4.0/openscript/accounting/statistics.py +300 -0
  12. openscript-0.4.0/openscript/accounting/trades.py +294 -0
  13. openscript-0.4.0/openscript/adapter/__init__.py +32 -0
  14. openscript-0.4.0/openscript/adapter/answers.py +215 -0
  15. openscript-0.4.0/openscript/adapter/channels.py +137 -0
  16. openscript-0.4.0/openscript/adapter/expectations.py +67 -0
  17. openscript-0.4.0/openscript/adapter/facts.py +127 -0
  18. openscript-0.4.0/openscript/adapter/matching.py +257 -0
  19. openscript-0.4.0/openscript/adapter/ordering.py +187 -0
  20. openscript-0.4.0/openscript/adapter/page.py +130 -0
  21. openscript-0.4.0/openscript/adapter/reading.py +357 -0
  22. openscript-0.4.0/openscript/adapter/reporting.py +244 -0
  23. openscript-0.4.0/openscript/adapter/running.py +449 -0
  24. openscript-0.4.0/openscript/adapter/serving.py +229 -0
  25. openscript-0.4.0/openscript/adapter/sessions.py +168 -0
  26. openscript-0.4.0/openscript/adapter/spellings.py +184 -0
  27. openscript-0.4.0/openscript/bars.py +157 -0
  28. openscript-0.4.0/openscript/budget.py +342 -0
  29. openscript-0.4.0/openscript/canonical.py +192 -0
  30. openscript-0.4.0/openscript/civil.py +196 -0
  31. openscript-0.4.0/openscript/contracts.py +165 -0
  32. openscript-0.4.0/openscript/dates.py +302 -0
  33. openscript-0.4.0/openscript/diagnostics.py +104 -0
  34. openscript-0.4.0/openscript/hours.py +165 -0
  35. openscript-0.4.0/openscript/inputs.py +239 -0
  36. openscript-0.4.0/openscript/intervals.py +60 -0
  37. openscript-0.4.0/openscript/library/__init__.py +76 -0
  38. openscript-0.4.0/openscript/library/arithmetic.py +128 -0
  39. openscript-0.4.0/openscript/library/averages.py +133 -0
  40. openscript-0.4.0/openscript/library/bars.py +60 -0
  41. openscript-0.4.0/openscript/library/bookkeeping.py +166 -0
  42. openscript-0.4.0/openscript/library/code_points.py +85 -0
  43. openscript-0.4.0/openscript/library/colour.py +202 -0
  44. openscript-0.4.0/openscript/library/composites.py +208 -0
  45. openscript-0.4.0/openscript/library/counting.py +218 -0
  46. openscript-0.4.0/openscript/library/deviation.py +155 -0
  47. openscript-0.4.0/openscript/library/elementary.py +206 -0
  48. openscript-0.4.0/openscript/library/extremes.py +122 -0
  49. openscript-0.4.0/openscript/library/flows.py +220 -0
  50. openscript-0.4.0/openscript/library/momentum.py +203 -0
  51. openscript-0.4.0/openscript/library/number_text.py +223 -0
  52. openscript-0.4.0/openscript/library/prices.py +36 -0
  53. openscript-0.4.0/openscript/library/ranges.py +105 -0
  54. openscript-0.4.0/openscript/library/rounding.py +123 -0
  55. openscript-0.4.0/openscript/library/series.py +213 -0
  56. openscript-0.4.0/openscript/library/stateful.py +442 -0
  57. openscript-0.4.0/openscript/library/stateless.py +261 -0
  58. openscript-0.4.0/openscript/library/strength.py +180 -0
  59. openscript-0.4.0/openscript/library/strings.py +228 -0
  60. openscript-0.4.0/openscript/library/trend.py +260 -0
  61. openscript-0.4.0/openscript/library/values.py +91 -0
  62. openscript-0.4.0/openscript/logbook.py +119 -0
  63. openscript-0.4.0/openscript/machine.py +499 -0
  64. openscript-0.4.0/openscript/memory.py +204 -0
  65. openscript-0.4.0/openscript/opcodes.py +166 -0
  66. openscript-0.4.0/openscript/program.py +146 -0
  67. openscript-0.4.0/openscript/run.py +368 -0
  68. openscript-0.4.0/openscript/strategy/__init__.py +78 -0
  69. openscript-0.4.0/openscript/strategy/calls.py +201 -0
  70. openscript-0.4.0/openscript/strategy/closable.py +182 -0
  71. openscript-0.4.0/openscript/strategy/fills.py +131 -0
  72. openscript-0.4.0/openscript/strategy/holdings.py +277 -0
  73. openscript-0.4.0/openscript/strategy/intents.py +162 -0
  74. openscript-0.4.0/openscript/strategy/ledger.py +270 -0
  75. openscript-0.4.0/openscript/strategy/placing.py +206 -0
  76. openscript-0.4.0/openscript/strategy/positions.py +124 -0
  77. openscript-0.4.0/openscript/strategy/refusals.py +293 -0
  78. openscript-0.4.0/openscript/strategy/rows.py +219 -0
  79. openscript-0.4.0/openscript/strategy/sizing.py +229 -0
  80. openscript-0.4.0/openscript/strategy/statuses.py +65 -0
  81. openscript-0.4.0/openscript/surface/__init__.py +115 -0
  82. openscript-0.4.0/openscript/surface/bands.py +103 -0
  83. openscript-0.4.0/openscript/surface/levels.py +44 -0
  84. openscript-0.4.0/openscript/surface/marks.py +52 -0
  85. openscript-0.4.0/openscript/surface/paints.py +58 -0
  86. openscript-0.4.0/openscript/surface/plots.py +44 -0
  87. openscript-0.4.0/openscript/surface/published.py +119 -0
  88. openscript-0.4.0/openscript/values.py +210 -0
  89. openscript-0.4.0/openscript/verify.py +301 -0
  90. openscript-0.4.0/openscript/verify_code.py +290 -0
  91. openscript-0.4.0/openscript/verify_requests.py +271 -0
  92. openscript-0.4.0/openscript/verify_shape.py +162 -0
  93. openscript-0.4.0/openscript/verify_tables.py +256 -0
  94. openscript-0.4.0/openscript/version.py +39 -0
  95. openscript-0.4.0/openscript/zones.py +118 -0
  96. openscript-0.4.0/openscript.egg-info/PKG-INFO +82 -0
  97. openscript-0.4.0/openscript.egg-info/SOURCES.txt +147 -0
  98. openscript-0.4.0/openscript.egg-info/dependency_links.txt +1 -0
  99. openscript-0.4.0/openscript.egg-info/top_level.txt +1 -0
  100. openscript-0.4.0/pyproject.toml +71 -0
  101. openscript-0.4.0/setup.cfg +4 -0
  102. openscript-0.4.0/tests/test_adapter_cases.py +301 -0
  103. openscript-0.4.0/tests/test_adapter_matching.py +190 -0
  104. openscript-0.4.0/tests/test_adapter_page.py +137 -0
  105. openscript-0.4.0/tests/test_adapter_spellings.py +114 -0
  106. openscript-0.4.0/tests/test_analysis_and_runup.py +321 -0
  107. openscript-0.4.0/tests/test_bar_cycle.py +279 -0
  108. openscript-0.4.0/tests/test_bars.py +164 -0
  109. openscript-0.4.0/tests/test_calendar.py +275 -0
  110. openscript-0.4.0/tests/test_calendar_reads.py +275 -0
  111. openscript-0.4.0/tests/test_calls.py +303 -0
  112. openscript-0.4.0/tests/test_canonical.py +204 -0
  113. openscript-0.4.0/tests/test_charges.py +233 -0
  114. openscript-0.4.0/tests/test_colour.py +170 -0
  115. openscript-0.4.0/tests/test_cost_models.py +171 -0
  116. openscript-0.4.0/tests/test_diagnostics.py +149 -0
  117. openscript-0.4.0/tests/test_engine_facts.py +185 -0
  118. openscript-0.4.0/tests/test_frame_fold.py +267 -0
  119. openscript-0.4.0/tests/test_frame_instant.py +73 -0
  120. openscript-0.4.0/tests/test_harness.py +128 -0
  121. openscript-0.4.0/tests/test_host_surface.py +441 -0
  122. openscript-0.4.0/tests/test_ledger_orders.py +393 -0
  123. openscript-0.4.0/tests/test_library_vectors.py +219 -0
  124. openscript-0.4.0/tests/test_logbook.py +123 -0
  125. openscript-0.4.0/tests/test_machine.py +197 -0
  126. openscript-0.4.0/tests/test_maths_rules.py +228 -0
  127. openscript-0.4.0/tests/test_memory.py +201 -0
  128. openscript-0.4.0/tests/test_number_text.py +297 -0
  129. openscript-0.4.0/tests/test_opcodes.py +135 -0
  130. openscript-0.4.0/tests/test_order_page.py +112 -0
  131. openscript-0.4.0/tests/test_order_refusals.py +237 -0
  132. openscript-0.4.0/tests/test_programs.py +170 -0
  133. openscript-0.4.0/tests/test_series_rules.py +461 -0
  134. openscript-0.4.0/tests/test_session_window.py +259 -0
  135. openscript-0.4.0/tests/test_sizing_refusal.py +91 -0
  136. openscript-0.4.0/tests/test_stateful_vectors.py +340 -0
  137. openscript-0.4.0/tests/test_strategy_cases.py +127 -0
  138. openscript-0.4.0/tests/test_strategy_profile.py +351 -0
  139. openscript-0.4.0/tests/test_string_ceiling.py +341 -0
  140. openscript-0.4.0/tests/test_strings.py +182 -0
  141. openscript-0.4.0/tests/test_supplied_schedule.py +85 -0
  142. openscript-0.4.0/tests/test_surface.py +323 -0
  143. openscript-0.4.0/tests/test_surface_bands.py +153 -0
  144. openscript-0.4.0/tests/test_surface_example.py +118 -0
  145. openscript-0.4.0/tests/test_time_programs.py +194 -0
  146. openscript-0.4.0/tests/test_trades_and_summary.py +356 -0
  147. openscript-0.4.0/tests/test_values.py +218 -0
  148. openscript-0.4.0/tests/test_verify.py +456 -0
  149. openscript-0.4.0/tests/test_version.py +121 -0
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: openscript
3
+ Version: 0.4.0
4
+ Summary: An open trading language: the engine that runs a compiled program.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/marketcalls/openscript#readme
7
+ Project-URL: Source, https://github.com/marketcalls/openscript
8
+ Keywords: trading,language,interpreter,indicators,backtesting
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Topic :: Office/Business :: Financial :: Investment
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ # openscript
16
+
17
+ The engine that runs a compiled OpenScript program, in Python.
18
+
19
+ OpenScript is an open trading language. A script is compiled to a **compiled
20
+ program**: plain data, an instruction list, never generated code. This package
21
+ runs one.
22
+
23
+ It exists so that a platform can run a strategy where a JavaScript runtime is
24
+ not available, which for a production trading server is the ordinary case. The
25
+ compiler and the first engine are the `openalgo-script` package on npm; this is
26
+ the second engine, and the two are held to each other by a conformance suite
27
+ where any disagreement is a release blocker.
28
+
29
+ ## What it is
30
+
31
+ - **No compiler.** This package is handed a compiled program and never a script.
32
+ The program arrives as canonical text, which is what a run's hash is taken
33
+ over, so an engine cannot quietly run something other than what was recorded.
34
+ - **Nothing builds code out of text.** No string evaluator, no statement
35
+ executor, no import driven by hand, no object graph loaded out of bytes. That
36
+ is what lets a platform run many people's scripts in one process, and it is
37
+ enforced by a check rather than intended.
38
+ - **Zero dependencies.** The standard library only, and not the parts of it that
39
+ stop a run being reproducible. Measured on every build against the module
40
+ names the running interpreter says are its own.
41
+
42
+ ## Installing
43
+
44
+ ```
45
+ pip install openscript
46
+ ```
47
+
48
+ Python 3.12 or newer. Nothing else.
49
+
50
+ ## Using it
51
+
52
+ ```python
53
+ from openscript.run import load_text
54
+
55
+ loaded = load_text(program_text, settings, library)
56
+ if loaded.ok:
57
+ result = loaded.run.execute_bar(0, bar)
58
+ ```
59
+
60
+ A host loads a program once and pushes bars at it one at a time, keeping a
61
+ checkpoint so a bar that is still moving can be executed again and rolled back.
62
+ The conformance adapter is the other way in:
63
+
64
+ ```
65
+ python -m openscript --describe
66
+ python -m openscript <case-directory>
67
+ ```
68
+
69
+ ## Where the documentation is
70
+
71
+ The specification and the guides live in the repository:
72
+
73
+ - `docs/integrating/the-python-engine.md` for what is in this package and what a
74
+ host needs.
75
+ - `docs/integrating/running-a-strategy.md` for driving it bar by bar: the load,
76
+ the bar cycle, the rollback a moving bar rests on, and the order boundary.
77
+ - `spec/` for the language, the compiled program format, the standard library
78
+ and the conformance suite.
79
+
80
+ ## Licence
81
+
82
+ Apache-2.0.
@@ -0,0 +1,68 @@
1
+ # openscript
2
+
3
+ The engine that runs a compiled OpenScript program, in Python.
4
+
5
+ OpenScript is an open trading language. A script is compiled to a **compiled
6
+ program**: plain data, an instruction list, never generated code. This package
7
+ runs one.
8
+
9
+ It exists so that a platform can run a strategy where a JavaScript runtime is
10
+ not available, which for a production trading server is the ordinary case. The
11
+ compiler and the first engine are the `openalgo-script` package on npm; this is
12
+ the second engine, and the two are held to each other by a conformance suite
13
+ where any disagreement is a release blocker.
14
+
15
+ ## What it is
16
+
17
+ - **No compiler.** This package is handed a compiled program and never a script.
18
+ The program arrives as canonical text, which is what a run's hash is taken
19
+ over, so an engine cannot quietly run something other than what was recorded.
20
+ - **Nothing builds code out of text.** No string evaluator, no statement
21
+ executor, no import driven by hand, no object graph loaded out of bytes. That
22
+ is what lets a platform run many people's scripts in one process, and it is
23
+ enforced by a check rather than intended.
24
+ - **Zero dependencies.** The standard library only, and not the parts of it that
25
+ stop a run being reproducible. Measured on every build against the module
26
+ names the running interpreter says are its own.
27
+
28
+ ## Installing
29
+
30
+ ```
31
+ pip install openscript
32
+ ```
33
+
34
+ Python 3.12 or newer. Nothing else.
35
+
36
+ ## Using it
37
+
38
+ ```python
39
+ from openscript.run import load_text
40
+
41
+ loaded = load_text(program_text, settings, library)
42
+ if loaded.ok:
43
+ result = loaded.run.execute_bar(0, bar)
44
+ ```
45
+
46
+ A host loads a program once and pushes bars at it one at a time, keeping a
47
+ checkpoint so a bar that is still moving can be executed again and rolled back.
48
+ The conformance adapter is the other way in:
49
+
50
+ ```
51
+ python -m openscript --describe
52
+ python -m openscript <case-directory>
53
+ ```
54
+
55
+ ## Where the documentation is
56
+
57
+ The specification and the guides live in the repository:
58
+
59
+ - `docs/integrating/the-python-engine.md` for what is in this package and what a
60
+ host needs.
61
+ - `docs/integrating/running-a-strategy.md` for driving it bar by bar: the load,
62
+ the bar cycle, the rollback a moving bar rests on, and the order boundary.
63
+ - `spec/` for the language, the compiled program format, the standard library
64
+ and the conformance suite.
65
+
66
+ ## Licence
67
+
68
+ Apache-2.0.
@@ -0,0 +1,40 @@
1
+ """The second engine: a compiled program, run in Python.
2
+
3
+ The package is importable as one name, ``openscript``, and this module is its
4
+ door. Nothing is exported through it: every caller, inside this package and
5
+ outside it, names the module it wants, so the import that reaches a rule says
6
+ where that rule lives.
7
+
8
+ What is where, for somebody reading this engine rather than running it:
9
+
10
+ - ``__main__.py`` the entry point the conformance adapter starts, as
11
+ ``python -m openscript``, which is the command line half
12
+ of the three invocations ``spec/conformance.md`` section
13
+ 9 gives an adapter.
14
+ - the modules beside the machine of ``spec/compiled-program.md`` sections 2 to
15
+ this one 11: the program read as data, the instruction set, the
16
+ memory regions and their lifetimes, the values, the bars,
17
+ the inputs, the verification done before the first bar,
18
+ the execution budget, the canonical encoding, the
19
+ diagnostics and the two version numbers.
20
+ - ``library/`` the functions of ``spec/stdlib.md``, each accumulating in
21
+ the order that page fixes, in the two halves that page
22
+ divides them into: the calls that remember nothing and
23
+ the calls that carry state from bar to bar.
24
+ - ``strategy/`` what a strategy decided and what came back: the order
25
+ calls, the intents a bar leaves behind, the frames a host
26
+ folds in and the ledger of ``stdlib.md`` section 17.
27
+ - ``accounting/`` what those fills came to: the charges, the trades, the
28
+ equity a report is marked on, and the summary.
29
+ - ``adapter/`` one conformance case, read from its files, run, and
30
+ answered channel by channel.
31
+
32
+ Two rules that are not this file's to relax, both from ``CLAUDE.md``:
33
+
34
+ Nothing here builds code out of text. No string evaluator, no statement
35
+ executor, no compiler, no import by a name computed at run time, no object
36
+ graph loaded out of bytes. The compiled program is data, and an engine that
37
+ reads data is why a host can run many people's scripts in one process.
38
+
39
+ Nothing here imports anything but the standard library.
40
+ """
@@ -0,0 +1,62 @@
1
+ """The entry point the conformance adapter starts: ``python -m openscript``.
2
+
3
+ ``spec/conformance.md`` section 9 gives an adapter three invocations, each
4
+ writing one JSON object to standard output and exiting 0, and this module is the
5
+ command line half of all three. Everything about what the engine answers is next
6
+ door in ``adapter/``.
7
+
8
+ python -m openscript --describe
9
+ python -m openscript <case-directory>
10
+ python -m openscript --actual <case-directory>
11
+
12
+ **The compiled program arrives on standard input**, as one JSON object holding
13
+ the canonical text of the program under ``program``, or holding ``diagnostics``
14
+ when the case's script did not compile. This engine implements no compiler, which
15
+ section 1 provides for: an implementation that only has an engine reads compiled
16
+ programs produced elsewhere, runs the engine half and says so. The text goes to
17
+ ``load_text`` rather than to a parsed object, because canonical bytes are what a
18
+ host sends in production and the hash it records a run against is taken over
19
+ them.
20
+
21
+ **Why it exits 0 on a failing case.** Section 9 puts the outcome inside the
22
+ object, and the runner holds the clock and the child process, so a non-zero exit
23
+ is the one thing left to mean a crash. An invocation that is not one of the three
24
+ is refused with a non-zero exit, because nothing it could write would be a case
25
+ result.
26
+
27
+ **The output is the canonical encoding**, written by the same writer the compiled
28
+ program's text boundary uses. One writer means a number in a report is spelled
29
+ the way a number in a program is, which is ``language.md`` section 5.5 in both
30
+ places rather than the interpreter's own rendering in one of them.
31
+ """
32
+
33
+ import sys
34
+
35
+ from .adapter.answers import invoke
36
+ from .adapter.spellings import Malformed
37
+ from .canonical import canonicalise
38
+
39
+ _USAGE = (
40
+ "Usage: python -m openscript --describe | <case-directory> | --actual <case-directory>\n"
41
+ "conformance.md section 9 gives an adapter these three invocations and no other.\n"
42
+ "A case invocation is handed the compiled program on standard input, as one JSON\n"
43
+ "object holding the canonical program text under program, because this engine\n"
44
+ "implements no compiler and reads programs produced elsewhere (section 1)."
45
+ )
46
+
47
+
48
+ def main(arguments) -> int:
49
+ try:
50
+ answered = invoke(arguments)
51
+ except Malformed as reason:
52
+ sys.stderr.write(f"{reason}\n")
53
+ return 1
54
+ if answered is None:
55
+ sys.stderr.write(f"{_USAGE}\n")
56
+ return 1
57
+ sys.stdout.write(f"{canonicalise(answered)}\n")
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,74 @@
1
+ """The money: what a run made, what it cost, and what that is worth knowing.
2
+
3
+ **This package imports no interpreter and no ledger, and it never will.** It is
4
+ arithmetic over portable data: a list of settled fills, a list of bar closes, a
5
+ charge schedule and the contract the run was carried out under. Two things follow
6
+ from that and both of them are the reason for it.
7
+
8
+ A stored record can be reported again with no engine present, which is what makes
9
+ a run record a conformance case rather than a souvenir. And the engine can call
10
+ this, when the day comes that a script may read its own equity, without a cycle
11
+ and without a second implementation of any of these formulas sitting inside the
12
+ execution path disagreeing with this one.
13
+
14
+ What is here:
15
+
16
+ - ``shapes`` the atoms: a fill, a contract and a bar's close
17
+ - ``charges`` what one fill cost, in the order the lines are declared
18
+ - ``trades`` the round trips, one per position reference
19
+ - ``equity`` one point per report bar, marked to the close
20
+ - ``statistics`` the summary, and which half of the trade list each figure counts
21
+ - ``report`` the one pass, in the one order, that is the result
22
+
23
+ **What none of these is, is specified.** ``conformance.md`` section 4 makes
24
+ ``performance`` a channel a case asserts and ``stdlib.md`` 17.4 leaves every
25
+ figure in it planned, so the formulas below are read from the first engine rather
26
+ than from a page. That is a defect of the specification and not of either engine,
27
+ and it is recorded where a reader of this package will meet it rather than only in
28
+ a report nobody keeps.
29
+ """
30
+
31
+ from .analysis import SideAnalysis, TradeAnalysis, analysis_of
32
+ from .charges import (
33
+ ChargeBreakdown,
34
+ ChargeLine,
35
+ ChargeSchedule,
36
+ charge_for,
37
+ round_money,
38
+ schedule_from_declaration,
39
+ schedule_problem,
40
+ )
41
+ from .equity import EquityPoint, bars_in_market_over, equity_over, open_on_bar, ratio_of
42
+ from .report import Report, report_of
43
+ from .shapes import BarMark, Contract, RecordedFill
44
+ from .statistics import Summary, summary_of
45
+ from .trades import Trade, closed_by, opened_by, trades_of
46
+
47
+ __all__ = [
48
+ "BarMark",
49
+ "ChargeBreakdown",
50
+ "ChargeLine",
51
+ "ChargeSchedule",
52
+ "Contract",
53
+ "EquityPoint",
54
+ "RecordedFill",
55
+ "Report",
56
+ "SideAnalysis",
57
+ "Summary",
58
+ "Trade",
59
+ "TradeAnalysis",
60
+ "analysis_of",
61
+ "bars_in_market_over",
62
+ "charge_for",
63
+ "closed_by",
64
+ "equity_over",
65
+ "open_on_bar",
66
+ "opened_by",
67
+ "ratio_of",
68
+ "report_of",
69
+ "round_money",
70
+ "schedule_from_declaration",
71
+ "schedule_problem",
72
+ "summary_of",
73
+ "trades_of",
74
+ ]
@@ -0,0 +1,174 @@
1
+ """The trades taken apart: by direction, by extreme, and by run.
2
+
3
+ The summary answers what the whole run did. Three questions it cannot answer are
4
+ asked here, and each of them is a question about whether the summary means what
5
+ it looks like it means.
6
+
7
+ **Which side made the money.** A run whose long trades paid for its short ones
8
+ reports a healthy net and is two strategies, one of which is losing. The summary
9
+ cannot show that, because every figure in it is folded over both sides at once.
10
+ Splitting it is not a refinement of the headline number, it is the first thing
11
+ that can contradict it.
12
+
13
+ **Whether one trade is the result.** A hundred trades and a profit factor of two
14
+ reads as an edge until the largest win is the whole of the net. The expectancy's
15
+ standard error already says how wide the spread is; the largest win and the
16
+ largest loss say where the width came from, which is the part a reader can act
17
+ on.
18
+
19
+ **What the run of losses was.** The deepest drawdown is a money figure and the
20
+ longest one is a bar count, and neither is the number that actually stops
21
+ somebody trading a strategy. That number is how many times in a row it was
22
+ wrong, and it is not derivable from anything in the summary: the same win rate
23
+ over the same trades gives a streak of two or a streak of eleven depending on an
24
+ ordering the summary folds away.
25
+
26
+ **Every figure here is over closed trades, and that is the whole rule.** An open
27
+ trade has no net to win or lose by, so it is in none of these counts, in no
28
+ streak and in no extreme. The consequence worth stating: ``long.count`` plus
29
+ ``short.count`` is the summary's ``trade_count`` and not the length of the list
30
+ folded, and the summary's ``open_trade_count`` is where the difference goes.
31
+
32
+ This is the first engine's ``src/core/accounting/analysis.ts`` in this language's
33
+ spellings. The two are one algorithm and the conformance suite is what holds
34
+ them to it.
35
+ """
36
+
37
+ from dataclasses import dataclass
38
+ from typing import List, Optional, Sequence
39
+
40
+ from .trades import Trade
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class SideAnalysis:
45
+ """One direction's own figures, folded over that side's closed trades."""
46
+
47
+ count: int
48
+ wins: int
49
+ losses: int
50
+ #: Exactly zero net, counted in neither half, as in the summary.
51
+ scratches: int
52
+ #: Net after charges, which is the figure the sides are compared on.
53
+ net_profit: float
54
+ #: ``wins / (wins + losses)``, ``None`` where this side decided nothing.
55
+ win_rate: Optional[float]
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class TradeAnalysis:
60
+ """The trades by direction, by extreme and by run.
61
+
62
+ ``long`` and ``short`` partition the closed trades, so their counts sum to
63
+ the summary's ``trade_count`` and their nets sum to its ``net_profit``.
64
+ """
65
+
66
+ long: SideAnalysis
67
+ short: SideAnalysis
68
+ #: The best closed trade's net, zero where none closed and zero where every
69
+ #: closed trade lost. Zero rather than ``None``, because the figure is read
70
+ #: beside ``largest_loss`` and against the net, and a ``None`` in a column
71
+ #: of money makes every reader handle a case that means "nothing won",
72
+ #: which is what zero already means in this column.
73
+ largest_win: float
74
+ #: The worst closed trade's net as a positive magnitude, zero where none lost.
75
+ largest_loss: float
76
+ #: The longest run of consecutive winning closed trades, in the order they
77
+ #: closed, and not the order they opened: that is the order the account
78
+ #: experienced them in. The two differ whenever a trade is held across
79
+ #: another one's whole life, which is every scaling strategy.
80
+ max_consecutive_wins: int
81
+ #: The same, for losses.
82
+ max_consecutive_losses: int
83
+
84
+
85
+ @dataclass
86
+ class _SideTally:
87
+ """A side's figures under construction, before the rates are taken."""
88
+
89
+ count: int = 0
90
+ wins: int = 0
91
+ losses: int = 0
92
+ scratches: int = 0
93
+ net_profit: float = 0.0
94
+
95
+
96
+ def _side_of(tally: _SideTally) -> SideAnalysis:
97
+ decided = tally.wins + tally.losses
98
+ return SideAnalysis(
99
+ count=tally.count,
100
+ wins=tally.wins,
101
+ losses=tally.losses,
102
+ scratches=tally.scratches,
103
+ net_profit=tally.net_profit,
104
+ win_rate=None if decided == 0 else tally.wins / decided,
105
+ )
106
+
107
+
108
+ def analysis_of(trades: Sequence[Trade]) -> TradeAnalysis:
109
+ """One pass over the closed trades, in closing order.
110
+
111
+ The list arrives in opening order, which is what the equity fold needs. The
112
+ streaks need closing order, so the closed trades are ordered here rather
113
+ than anywhere else: reordering the list the caller holds would change the
114
+ equity curve.
115
+
116
+ A trade whose net is exactly zero is a scratch, which is the summary's rule.
117
+ A scratch **breaks** a streak without extending either one: a strategy that
118
+ went right, flat, right was not right twice running, and counting the flat
119
+ trade as either would make the streak a figure that depends on a rounding at
120
+ the last digit.
121
+ """
122
+ long = _SideTally()
123
+ short = _SideTally()
124
+ largest_win = 0.0
125
+ largest_loss = 0.0
126
+ max_consecutive_wins = 0
127
+ max_consecutive_losses = 0
128
+ win_streak = 0
129
+ loss_streak = 0
130
+
131
+ closed: List[Trade] = [trade for trade in trades if not trade.is_open]
132
+ # A trade closes on a bar, and two can close on the same one. The opening
133
+ # order breaks the tie, because it is the order the list arrived in and the
134
+ # only other fact available: a sort that is not total gives two engines two
135
+ # different streaks from one list of trades.
136
+ closed.sort(key=lambda trade: (
137
+ 0 if trade.closed_on_bar is None else trade.closed_on_bar,
138
+ trade.index,
139
+ ))
140
+
141
+ for trade in closed:
142
+ side = long if trade.side == "long" else short
143
+ side.count += 1
144
+ side.net_profit += trade.net_profit
145
+
146
+ if trade.net_profit > 0:
147
+ side.wins += 1
148
+ if trade.net_profit > largest_win:
149
+ largest_win = trade.net_profit
150
+ win_streak += 1
151
+ loss_streak = 0
152
+ if win_streak > max_consecutive_wins:
153
+ max_consecutive_wins = win_streak
154
+ elif trade.net_profit < 0:
155
+ side.losses += 1
156
+ if -trade.net_profit > largest_loss:
157
+ largest_loss = -trade.net_profit
158
+ loss_streak += 1
159
+ win_streak = 0
160
+ if loss_streak > max_consecutive_losses:
161
+ max_consecutive_losses = loss_streak
162
+ else:
163
+ side.scratches += 1
164
+ win_streak = 0
165
+ loss_streak = 0
166
+
167
+ return TradeAnalysis(
168
+ long=_side_of(long),
169
+ short=_side_of(short),
170
+ largest_win=largest_win,
171
+ largest_loss=largest_loss,
172
+ max_consecutive_wins=max_consecutive_wins,
173
+ max_consecutive_losses=max_consecutive_losses,
174
+ )