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,168 @@
1
+ """The session facts an engine derives, ``host-interface.md`` 4.3 and ``stdlib.md`` 12.4.
2
+
3
+ **A session is the instrument's, never a window a script invents.** The record
4
+ states a wall clock range, the days it runs on and the zone it is read in, and
5
+ the engine derives the per-bar facts from those and the bar's own time: "an
6
+ engine derives them and never asks a host for them: the facts a host states about
7
+ an execution are the four of ``language.md`` section 7.2 and no others".
8
+
9
+ **A record that states no session leaves every fact absent**, which is 4.1's own
10
+ rule and is exactly what an instrument with no schedule looks like. A record that
11
+ states one badly is refused instead (OS6012), because the difference between a
12
+ fact nobody stated and a fact stated wrongly is the one thing reading the record
13
+ can tell you.
14
+
15
+ **The window arithmetic is not here.** A range of wall clock hours is two things
16
+ in this engine, the record's session and the window a script writes for
17
+ ``session.isIn``, and ``hours.py`` is the one arithmetic both of them reach: a
18
+ second copy would drift over midnight, or over which day a list of days is read
19
+ against, on the dataset nobody tests. What is here is the record: its three
20
+ refusals, and the two facts derived per bar.
21
+
22
+ **Both derived facts are computed here and one of them is in the manifest.**
23
+ ``session.isFirstBar`` looks backwards, which an engine can answer on the bar it
24
+ is executing. ``session.isLastBar`` is "true on the last bar of the schedule even
25
+ if trading stopped early", so it is a bar slot measured against the scheduled
26
+ close and it needs the interval, which ``intervals.py`` now reads;
27
+ ``last_bars`` below is that reading. It is not in ``SESSION_FACTS`` yet, because
28
+ the seam that answers a session fact carries one bar fact to answer it with, and
29
+ a name added to that tuple without a second fact beside it would answer
30
+ ``isLastBar`` with ``isFirstBar``'s value, which is worse than the refusal a
31
+ program gets today (OS6004, naming the function).
32
+
33
+ **The zone is read as the one this engine can read.** A calendar in any other
34
+ zone is a reader the host supplies, so a case whose record names another zone is
35
+ named ``unsupported`` by the caller rather than answered under the wrong
36
+ calendar. ``zones.py`` states which zone that is and why it is one.
37
+ """
38
+
39
+ from typing import Any, Mapping, Optional, Sequence, Tuple
40
+
41
+ from ..hours import Hours, clock_minutes, days_numbered, standing_in
42
+ from ..intervals import bar_minutes_of
43
+ from ..values import ABSENT
44
+ from ..zones import READABLE as READABLE_ZONE, fields_in
45
+ from .spellings import Malformed
46
+
47
+ #: ``stdlib.md`` 12.4: the entries of the namespace this engine serves.
48
+ SESSION_FACTS: Tuple[str, ...] = ("session.isFirstBar",)
49
+
50
+ #: The bar fact both readers of a session boundary take it from: the namespace
51
+ #: entry above, and ``vwap``, which restarts where the session opened.
52
+ SESSION_FIRST = "isSessionFirst"
53
+
54
+ #: The record's session is a range of wall clock hours and nothing else, which is
55
+ #: what lets it and a script's own window be one arithmetic.
56
+ Session = Hours
57
+
58
+
59
+ def _minutes(text: Any, field: str) -> int:
60
+ """``"HH:MM"``, in the one spelling 4.3 fixes, or the refusal it names.
61
+
62
+ ``"9:00"`` is the spelling a host writes first and it is not a time here, so
63
+ it is refused rather than read: a record stating a window in a spelling the
64
+ page does not have is a record whose author believes they stated a session.
65
+ """
66
+ held = clock_minutes(text)
67
+ if held is None:
68
+ raise Malformed(
69
+ f"instrument.json: the session's {field} is {text!r}, and host-interface.md 4.3 "
70
+ 'spells a wall clock time "HH:MM", where the latest time it has is "24:00", '
71
+ "midnight at the end of the day"
72
+ )
73
+ return held
74
+
75
+
76
+ def session_from(instrument: Mapping[str, Any]) -> Optional[Session]:
77
+ """The record's session, or nothing where it states none.
78
+
79
+ The three refusals are 4.3's own table, in its order: a window with no zone
80
+ to read it in, a bound that is not a wall clock time, and a day outside the
81
+ numbering ``date.dayOfWeek`` uses.
82
+ """
83
+ held = instrument.get("session")
84
+ if held is None:
85
+ return None
86
+ if not isinstance(held, dict):
87
+ raise Malformed("instrument.json: the session is not a window of a start, an end and days")
88
+ if not isinstance(instrument.get("timezone"), str):
89
+ raise Malformed(
90
+ "instrument.json states a session and no timezone, and host-interface.md 4.3 refuses "
91
+ "that record at load: a window is wall clock and a wall clock is read in a zone"
92
+ )
93
+ days = held.get("days")
94
+ if not isinstance(days, list) or not days_numbered(days):
95
+ raise Malformed(
96
+ f"instrument.json: the session states the days {days!r}, and 4.3 numbers Monday as 1 "
97
+ "through Sunday as 7, with an empty list not one of them"
98
+ )
99
+ return Session(_minutes(held.get("start"), "start"), _minutes(held.get("end"), "end"), tuple(days))
100
+
101
+
102
+ def opening_day(time: int, session: Session) -> Optional[int]:
103
+ """Which session a bar belongs to, as the day it opened on, or nothing.
104
+
105
+ The day is an identity and not a date: two bars of one session share it, and
106
+ the bar that does not share it with the bar before it is the bar that opened
107
+ a session. A bar outside the window belongs to no session, which is a false
108
+ reading of every fact here rather than an absent one.
109
+ """
110
+ standing = _standing(time, session)
111
+ return None if standing is None or not standing.inside else standing.opening_day
112
+
113
+
114
+ def first_bars(times: Sequence[int], session: Optional[Session]) -> Tuple[Any, ...]:
115
+ """Whether each bar opens its session, absent throughout where there is none.
116
+
117
+ Read forwards and never backwards: a bar opens a session when it is in one
118
+ and the last bar that was in one was in a different one. A bar outside the
119
+ window opens nothing and leaves the answer for the next bar alone, so a
120
+ session with a gap in the middle of it is one session and not two.
121
+ """
122
+ if session is None:
123
+ return tuple(ABSENT for _ in times)
124
+ found = []
125
+ previous: Optional[int] = None
126
+ for time in times:
127
+ opened = opening_day(time, session)
128
+ found.append(opened is not None and opened != previous)
129
+ if opened is not None:
130
+ previous = opened
131
+ return tuple(found)
132
+
133
+
134
+ def last_bars(
135
+ times: Sequence[int], session: Optional[Session], interval: Any
136
+ ) -> Tuple[Any, ...]:
137
+ """Whether each bar is the last of its session's schedule, or absent.
138
+
139
+ The schedule and not the trading: a bar is the last one when the time left
140
+ until the scheduled close is no more than one bar, so it is true on the bar
141
+ that covers the close even if nothing traded in it and even if the feed
142
+ stopped an hour earlier. That is the whole reason 4.3 says a strategy which
143
+ must be flat by the close acts on this rather than on the appearance of a new
144
+ bar, which arrives too late.
145
+
146
+ Absent where there is no session, and absent where the host stated no
147
+ interval this engine can read: without a bar's length there is no slot to
148
+ measure against the close, and answering from the spacing of the bars would
149
+ be an engine promising something the specification does not, which is that
150
+ the bars are evenly spaced.
151
+ """
152
+ minutes = bar_minutes_of(interval)
153
+ if session is None or minutes is None:
154
+ return tuple(ABSENT for _ in times)
155
+ found = []
156
+ for time in times:
157
+ standing = _standing(time, session)
158
+ if standing is None:
159
+ found.append(ABSENT)
160
+ else:
161
+ found.append(standing.inside and standing.remaining <= minutes)
162
+ return tuple(found)
163
+
164
+
165
+ def _standing(time: Any, session: Session) -> Optional[Any]:
166
+ """Where one bar's reading sits in the window, in the zone this engine reads."""
167
+ at = fields_in(time, READABLE_ZONE)
168
+ return None if at is None else standing_in(session, at)
@@ -0,0 +1,184 @@
1
+ """How a value is written in a case file and in a report, and how one is read back.
2
+
3
+ ``conformance.md`` sections 3 and 4 fix both directions and this module is the
4
+ whole of them, so that a reader of a case file and a writer of a report cannot
5
+ part company over a spelling.
6
+
7
+ **A number is read strictly.** The page says a price is "written in the shortest
8
+ form that reads back to the exact binary64 value intended", which is a decimal
9
+ and nothing else. The interpreter's own reader is wider than that: it takes an
10
+ infinity by name, a not-a-number, a hexadecimal fraction, surrounding space and
11
+ an underscore between digits, and each of those would enter the run as a value
12
+ ``language.md`` section 5.1 says never appears. So the text is held to the
13
+ grammar first and converted after, and a case carrying one of those spellings is
14
+ malformed rather than quietly run.
15
+
16
+ **A number is written by the language's own rule.** ``language.md`` section 5.5
17
+ decides when the positional form is used and how an exponent is spelled, and
18
+ ``canonical.py`` next door already implements it for the compiled program's
19
+ encoding. A second rule here would be the one that drifted, and the difference
20
+ would show up as two engines reporting one value two ways in a document a person
21
+ reads to decide which of them is wrong.
22
+
23
+ **A colour is written after the alpha has become a byte**, which is
24
+ ``compiled-program.md`` section 3.1's one way conversion and the reason section 6
25
+ can compare four integers rather than a float. The two spellings of a colour this
26
+ package holds are both accepted here; the note in ``running.py`` says why there
27
+ are two.
28
+ """
29
+
30
+ import re
31
+ from typing import Any, Optional
32
+
33
+ from ..canonical import canonical_number
34
+ from ..library.colour import Colour as LibraryColour, hex_byte
35
+ from ..library.rounding import round_half_away
36
+ from ..values import Colour as MachineColour
37
+ from .page import ABSENT_TEXT
38
+
39
+ #: A decimal, and nothing else: an optional sign, digits, an optional fraction
40
+ #: and an optional exponent. Nothing else the interpreter's reader would accept.
41
+ _DECIMAL = re.compile(r"^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$")
42
+
43
+ #: A whole number of milliseconds, which is what every time in a case file is.
44
+ _WHOLE = re.compile(r"^[+-]?[0-9]+$")
45
+
46
+ #: The eight hexadecimal digits a colour is compared as, with the leading hash.
47
+ _COLOUR = re.compile(r"^#[0-9a-f]{8}$")
48
+
49
+
50
+ class Malformed(Exception):
51
+ """A case file that cannot be read, which section 9 files under ``error``.
52
+
53
+ Carries one sentence naming the file, the row and what was wrong with it. A
54
+ case is input to every engine that meets it, so the cost of a directory this
55
+ one cannot read lands on whoever wrote the directory, and the sentence is
56
+ what tells them which of the two it is.
57
+ """
58
+
59
+
60
+ def read_number(text: str, where: str) -> float:
61
+ """A decimal as the binary64 value it names, or a refusal naming where."""
62
+ if not _DECIMAL.match(text):
63
+ raise Malformed(
64
+ f"{where}: {text!r} is not a decimal number. A case file writes a number in the "
65
+ "shortest form that reads back to the value intended, and every other spelling a "
66
+ "reader might accept, an infinity, a not-a-number or a digit separator, is a value "
67
+ "this language holds none of"
68
+ )
69
+ value = float(text)
70
+ if value != value or value in (float("inf"), float("-inf")):
71
+ raise Malformed(f"{where}: {text!r} is outside the finite range of a binary64 number")
72
+ return value
73
+
74
+
75
+ def read_whole(text: str, where: str) -> int:
76
+ """A whole number, which is what a time and a quantity in a case file are."""
77
+ if not _WHOLE.match(text):
78
+ raise Malformed(f"{where}: {text!r} is not a whole number")
79
+ return int(text)
80
+
81
+
82
+ def read_bool(text: str, where: str) -> bool:
83
+ if text == "true":
84
+ return True
85
+ if text == "false":
86
+ return False
87
+ raise Malformed(f"{where}: {text!r} is neither true nor false")
88
+
89
+
90
+ def read_colour(text: str, where: str) -> str:
91
+ """A colour, kept as the eight digit spelling it is compared as.
92
+
93
+ It is not turned back into channels and an alpha. Section 3.1 says the
94
+ conversion to a byte is one way and not a round trip, so a colour read back
95
+ into the machine's own form and written out again would not always be the
96
+ text the file holds, and the text is what section 6 compares.
97
+ """
98
+ if not _COLOUR.match(text):
99
+ raise Malformed(
100
+ f"{where}: {text!r} is not a colour. A colour is written #rrggbbaa, always eight "
101
+ "hexadecimal digits and always lower case"
102
+ )
103
+ return text
104
+
105
+
106
+ def colour_text(value: Any) -> str:
107
+ """A colour the machine holds, in the one spelling a case file writes.
108
+
109
+ The alpha becomes a byte here and nowhere else: ``round(alpha * 255)`` with
110
+ the language's own rounding, halves away from zero, which is section 3.1's
111
+ rule and the rounding ``stdlib.md`` section 11.2 gives every colour call.
112
+ """
113
+ if isinstance(value, MachineColour):
114
+ red, green, blue, alpha = value.red, value.green, value.blue, value.alpha
115
+ elif isinstance(value, LibraryColour):
116
+ red, green, blue, alpha = value.r, value.g, value.b, value.a
117
+ else:
118
+ raise Malformed(f"{value!r} is not a colour")
119
+ channels = "".join(hex_byte(one) for one in (red, green, blue))
120
+ return f"#{channels}{hex_byte(round_half_away(alpha * 255.0))}"
121
+
122
+
123
+ def written(value: Any) -> str:
124
+ """A value as a report writes it, which is as an expected file writes it.
125
+
126
+ Section 9: "Numbers in a report are written as the shortest round-tripping
127
+ decimal, exactly as in an expected file, so a difference in the last bit is
128
+ visible in the report rather than rounded away by the reporting."
129
+ """
130
+ if value is None:
131
+ return ABSENT_TEXT
132
+ if isinstance(value, bool):
133
+ return "true" if value else "false"
134
+ if isinstance(value, (int, float)):
135
+ return canonical_number(float(value))
136
+ if isinstance(value, str):
137
+ return value
138
+ if isinstance(value, (MachineColour, LibraryColour)):
139
+ return colour_text(value)
140
+ return repr(value)
141
+
142
+
143
+ def as_reported(value: Any) -> Any:
144
+ """One value of an answered channel, in the JSON shapes section 4 gives.
145
+
146
+ Absence is null, a number is a number, a bool is a bool, a string is itself,
147
+ and a colour is its eight digit spelling, because that is what section 6
148
+ compares a colour as and a JSON document has no colour of its own.
149
+ """
150
+ if value is None or isinstance(value, (bool, str)):
151
+ return value
152
+ if isinstance(value, (int, float)):
153
+ return float(value)
154
+ if isinstance(value, (MachineColour, LibraryColour)):
155
+ return colour_text(value)
156
+ return None
157
+
158
+
159
+ def read_cell(text: str, kind: str, where: str) -> Optional[Any]:
160
+ """One field of ``expected.csv``, read as the channel's own type.
161
+
162
+ The type comes from the compiled program's channel table
163
+ (``compiled-program.md`` section 2.7) rather than from the shape of the
164
+ text, so a string channel holding ``none`` is a channel that produced
165
+ nothing and not the four letters.
166
+
167
+ An empty field and ``none`` are both absence here, and that is a narrowing
168
+ of section 4 rather than a reading of it: the page distinguishes "this
169
+ channel produced nothing on this bar" from an absent number, and a channel
170
+ on this machine has one state for both (section 2.7: a channel's value is
171
+ absent unless something wrote it). An engine that answered two different
172
+ values would be inventing a distinction it cannot observe.
173
+ """
174
+ if text == "" or text == ABSENT_TEXT:
175
+ return None
176
+ if kind == "number":
177
+ return read_number(text, where)
178
+ if kind == "bool":
179
+ return read_bool(text, where)
180
+ if kind == "color":
181
+ return read_colour(text, where)
182
+ if kind == "string":
183
+ return text
184
+ raise Malformed(f"{where}: {kind!r} is not a channel type this adapter reads")
openscript/bars.py ADDED
@@ -0,0 +1,157 @@
1
+ """Step 4 of the bar cycle: the host's bar written into every ``"bar"`` register.
2
+
3
+ The derived price fields are written out as expressions rather than as formulas
4
+ because **their order of operations is part of the contract** (section 2.10):
5
+ ``hlc3`` adds high to low, adds close to that, then divides. A different
6
+ association gives a different last bit, and a study that matches a reference
7
+ implementation on one engine and not on another is exactly the failure this
8
+ project exists to prevent.
9
+
10
+ **An absent volume rather than a zero is deliberate.** A script that sizes
11
+ something by volume has to be able to tell "no trades" from "nobody told me",
12
+ which is ``host-interface.md`` section 3.3 and three levels of the same
13
+ distinction.
14
+
15
+ Four of the eight bar facts are the host's and four are the engine's, and a fact
16
+ the engine can derive is never also stated by the host, because two sources for
17
+ one number can disagree and no rule would say which of them wins.
18
+ """
19
+
20
+ from dataclasses import dataclass
21
+ from typing import Any, Tuple
22
+
23
+ from .contracts import Bar, BarState
24
+ from .values import ABSENT, finite, is_number
25
+
26
+ #: The bar fields section 2.10 names.
27
+ #:
28
+ #: ``oi`` is here and is the one field the section's own table does not list. It
29
+ #: is a field of a bar everywhere else: ``host-interface.md`` section 3.1 states
30
+ #: it, and section 2.16.2 folds it into a bucket bar by taking the last rather
31
+ #: than the sum. A register kind the compiler can emit and this engine could not
32
+ #: fill would be absent on every bar with nothing saying why, so it is filled and
33
+ #: the omission is reported rather than resolved here.
34
+ BAR_FIELDS: Tuple[str, ...] = (
35
+ "open",
36
+ "high",
37
+ "low",
38
+ "close",
39
+ "volume",
40
+ "time",
41
+ "hl2",
42
+ "hlc3",
43
+ "ohlc4",
44
+ "hlcc4",
45
+ "oi",
46
+ "bar.index",
47
+ "bar.count",
48
+ "bar.isFirst",
49
+ "bar.isLast",
50
+ "bar.isConfirmed",
51
+ "bar.isRealtime",
52
+ "bar.isNew",
53
+ "bar.updates",
54
+ )
55
+
56
+
57
+ def is_bar_field(name: str) -> bool:
58
+ return name in BAR_FIELDS
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class BarFacts:
63
+ """The eight facts of ``language.md`` section 7.2, for the bar being executed.
64
+
65
+ ``index`` is a position in the supplied data and not a universal address:
66
+ loading more history shifts every index, which is why a script that has to
67
+ remember a bar stores ``time`` instead.
68
+ """
69
+
70
+ index: int
71
+ count: int
72
+ is_first: bool
73
+ is_last: bool
74
+ is_confirmed: bool
75
+ is_realtime: bool
76
+ is_new: bool
77
+ updates: float
78
+
79
+
80
+ def facts_for(index: int, supplied: int, state: BarState) -> BarFacts:
81
+ return BarFacts(
82
+ index=index,
83
+ count=index + 1,
84
+ is_first=index == 0,
85
+ # True when this is the greatest index the host has supplied. A live
86
+ # chart supplies bars one at a time, so its newest bar is always the
87
+ # last; a run over a whole dataset supplies them together, so only its
88
+ # final bar is.
89
+ is_last=index == supplied - 1,
90
+ is_confirmed=state.is_confirmed,
91
+ is_realtime=state.is_realtime,
92
+ is_new=state.is_new,
93
+ updates=float(state.updates),
94
+ )
95
+
96
+
97
+ def _held(value: Any) -> Any:
98
+ return float(value) if is_number(value) else ABSENT
99
+
100
+
101
+ def bar_field(field: str, bar: Bar, facts: BarFacts) -> Any:
102
+ """One bar field's value, by the definitions of section 2.10."""
103
+ if field == "open":
104
+ return _held(bar.open)
105
+ if field == "high":
106
+ return _held(bar.high)
107
+ if field == "low":
108
+ return _held(bar.low)
109
+ if field == "close":
110
+ return _held(bar.close)
111
+ if field == "volume":
112
+ return _held(bar.volume)
113
+ if field == "oi":
114
+ return _held(bar.oi)
115
+ if field == "time":
116
+ return _held(bar.time)
117
+
118
+ high, low, close, opened = (
119
+ _held(bar.high),
120
+ _held(bar.low),
121
+ _held(bar.close),
122
+ _held(bar.open),
123
+ )
124
+ if field == "hl2":
125
+ if high is ABSENT or low is ABSENT:
126
+ return ABSENT
127
+ return finite((high + low) / 2)
128
+ if field == "hlc3":
129
+ if high is ABSENT or low is ABSENT or close is ABSENT:
130
+ return ABSENT
131
+ return finite((high + low + close) / 3)
132
+ if field == "ohlc4":
133
+ if opened is ABSENT or high is ABSENT or low is ABSENT or close is ABSENT:
134
+ return ABSENT
135
+ return finite((opened + high + low + close) / 4)
136
+ if field == "hlcc4":
137
+ if high is ABSENT or low is ABSENT or close is ABSENT:
138
+ return ABSENT
139
+ return finite((high + low + close + close) / 4)
140
+
141
+ if field == "bar.index":
142
+ return float(facts.index)
143
+ if field == "bar.count":
144
+ return float(facts.count)
145
+ if field == "bar.isFirst":
146
+ return facts.is_first
147
+ if field == "bar.isLast":
148
+ return facts.is_last
149
+ if field == "bar.isConfirmed":
150
+ return facts.is_confirmed
151
+ if field == "bar.isRealtime":
152
+ return facts.is_realtime
153
+ if field == "bar.isNew":
154
+ return facts.is_new
155
+ if field == "bar.updates":
156
+ return float(facts.updates)
157
+ return ABSENT