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,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
|
+
)
|