friction-engine 0.1.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.
- friction_engine/__init__.py +48 -0
- friction_engine/fees.py +131 -0
- friction_engine/liquidity.py +41 -0
- friction_engine/mae_mfe.py +184 -0
- friction_engine/models.py +88 -0
- friction_engine/slippage.py +132 -0
- friction_engine-0.1.0.dist-info/METADATA +150 -0
- friction_engine-0.1.0.dist-info/RECORD +11 -0
- friction_engine-0.1.0.dist-info/WHEEL +5 -0
- friction_engine-0.1.0.dist-info/licenses/LICENSE +189 -0
- friction_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""friction-engine: realistic backtest friction modeling.
|
|
2
|
+
|
|
3
|
+
Four orthogonal pieces, composable:
|
|
4
|
+
|
|
5
|
+
- :mod:`friction_engine.slippage` -- volume-aware slippage models
|
|
6
|
+
- :mod:`friction_engine.fees` -- multi-market fee & tax schedules (US/TW/JP)
|
|
7
|
+
- :mod:`friction_engine.liquidity` -- participation-rate liquidity constraints
|
|
8
|
+
- :mod:`friction_engine.mae_mfe` -- MAE/MFE excursion analytics
|
|
9
|
+
|
|
10
|
+
Pure standard library, no third-party dependencies.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .fees import FeeSchedule, Market, fee_for, FEE_SCHEDULES
|
|
14
|
+
from .liquidity import LiquidityConstraint, ParticipationCap
|
|
15
|
+
from .mae_mfe import Excursion, ExcursionStats, TradePath, excursion, excursion_stats, mae, mfe
|
|
16
|
+
from .models import Fill, Order, Side
|
|
17
|
+
from .slippage import (
|
|
18
|
+
FixedSlippage,
|
|
19
|
+
SlippageModel,
|
|
20
|
+
VolumeShareSlippage,
|
|
21
|
+
ZeroSlippage,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"FEE_SCHEDULES",
|
|
28
|
+
"Excursion",
|
|
29
|
+
"ExcursionStats",
|
|
30
|
+
"FeeSchedule",
|
|
31
|
+
"Fill",
|
|
32
|
+
"FixedSlippage",
|
|
33
|
+
"LiquidityConstraint",
|
|
34
|
+
"Market",
|
|
35
|
+
"Order",
|
|
36
|
+
"ParticipationCap",
|
|
37
|
+
"Side",
|
|
38
|
+
"SlippageModel",
|
|
39
|
+
"TradePath",
|
|
40
|
+
"VolumeShareSlippage",
|
|
41
|
+
"ZeroSlippage",
|
|
42
|
+
"excursion",
|
|
43
|
+
"excursion_stats",
|
|
44
|
+
"fee_for",
|
|
45
|
+
"mae",
|
|
46
|
+
"mfe",
|
|
47
|
+
"__version__",
|
|
48
|
+
]
|
friction_engine/fees.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Multi-market fee & tax schedules (US / TW / JP presets, all configurable).
|
|
2
|
+
|
|
3
|
+
Rates below are publicly documented, commonly-cited figures as of mid-2026,
|
|
4
|
+
chosen as *conservative defaults* — they are NOT a substitute for checking
|
|
5
|
+
your broker's current schedule. Every number is a plain constructor argument
|
|
6
|
+
so you can (and should) override them.
|
|
7
|
+
|
|
8
|
+
Design notes:
|
|
9
|
+
- Commissions are per-side (charged on both buy and sell).
|
|
10
|
+
- Transaction taxes are per-market and usually sell-side only.
|
|
11
|
+
- US: no transaction tax on stocks; regulatory fees (SEC fee on sells,
|
|
12
|
+
FINRA TAF on sells) are tiny but included for completeness.
|
|
13
|
+
- TW: broker commission 0.1425% (statutory max, discounts common) +
|
|
14
|
+
securities transaction tax 0.3% on stock sells (0.1% for ETF sells).
|
|
15
|
+
- JP: no transaction tax on shares; commission is broker-negotiated, so the
|
|
16
|
+
preset is a representative flat bps; consumption tax (10%) applies to the
|
|
17
|
+
*commission*, modeled via `commission_tax_bps`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import enum
|
|
23
|
+
from dataclasses import dataclass, replace
|
|
24
|
+
|
|
25
|
+
from .models import Order, Side
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Market(enum.Enum):
|
|
29
|
+
US = "US"
|
|
30
|
+
TW = "TW"
|
|
31
|
+
JP = "JP"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class FeeSchedule:
|
|
36
|
+
"""A complete per-market cost schedule.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
commission_bps: Broker commission in basis points of gross, per side.
|
|
40
|
+
min_commission: Minimum commission per order (account currency).
|
|
41
|
+
sell_tax_bps: Transaction tax on sells, in basis points of gross.
|
|
42
|
+
buy_tax_bps: Transaction tax on buys, in basis points of gross
|
|
43
|
+
(0 for US/TW/JP stocks; present for markets that need it).
|
|
44
|
+
sell_reg_fee_bps: Regulatory fees on sells in basis points
|
|
45
|
+
(e.g. US SEC fee + TAF combined approximation).
|
|
46
|
+
commission_tax_bps: Tax levied on the commission itself
|
|
47
|
+
(e.g. JP consumption tax = 1000 bps = 10%).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
commission_bps: float
|
|
51
|
+
min_commission: float = 0.0
|
|
52
|
+
sell_tax_bps: float = 0.0
|
|
53
|
+
buy_tax_bps: float = 0.0
|
|
54
|
+
sell_reg_fee_bps: float = 0.0
|
|
55
|
+
commission_tax_bps: float = 0.0
|
|
56
|
+
|
|
57
|
+
def fee_and_tax(self, order: Order, gross_amount: float) -> tuple[float, float]:
|
|
58
|
+
"""Return (fee, tax) in account currency for a fill of `gross_amount`."""
|
|
59
|
+
if gross_amount < 0:
|
|
60
|
+
raise ValueError("gross_amount must be >= 0")
|
|
61
|
+
raw_commission = gross_amount * self.commission_bps / 10_000.0
|
|
62
|
+
commission = max(raw_commission, self.min_commission) if gross_amount > 0 else 0.0
|
|
63
|
+
commission_tax = commission * self.commission_tax_bps / 10_000.0
|
|
64
|
+
fee = commission + commission_tax
|
|
65
|
+
|
|
66
|
+
if order.side is Side.SELL:
|
|
67
|
+
tax = gross_amount * (self.sell_tax_bps + self.sell_reg_fee_bps) / 10_000.0
|
|
68
|
+
else:
|
|
69
|
+
tax = gross_amount * self.buy_tax_bps / 10_000.0
|
|
70
|
+
return fee, tax
|
|
71
|
+
|
|
72
|
+
def with_overrides(self, **changes: float) -> "FeeSchedule":
|
|
73
|
+
"""Return a new schedule with selected fields replaced (immutable update)."""
|
|
74
|
+
return replace(self, **changes)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- Presets (publicly documented typical values; verify before live use) ---
|
|
78
|
+
|
|
79
|
+
# US: zero-commission brokers exist; 0 default + tiny sell-side regulatory
|
|
80
|
+
# fees (SEC fee ~0.278 bps of sell gross + FINRA TAF; rounded, mid-2020s).
|
|
81
|
+
_US = FeeSchedule(
|
|
82
|
+
commission_bps=0.0,
|
|
83
|
+
min_commission=0.0,
|
|
84
|
+
sell_tax_bps=0.0,
|
|
85
|
+
sell_reg_fee_bps=0.3,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# TW stocks: commission 0.1425% (14.25 bps, statutory max before discounts),
|
|
89
|
+
# sell-side securities transaction tax 0.3% (30 bps).
|
|
90
|
+
_TW_STOCK = FeeSchedule(
|
|
91
|
+
commission_bps=14.25,
|
|
92
|
+
min_commission=0.0, # many TW brokers have a NT$20 minimum — set it if yours does
|
|
93
|
+
sell_tax_bps=30.0,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# TW ETFs: same commission, reduced 0.1% (10 bps) sell tax.
|
|
97
|
+
_TW_ETF = replace(_TW_STOCK, sell_tax_bps=10.0)
|
|
98
|
+
|
|
99
|
+
# JP: representative commission 10 bps (broker-negotiated), 10% consumption
|
|
100
|
+
# tax on commission, no share transaction tax.
|
|
101
|
+
_JP = FeeSchedule(
|
|
102
|
+
commission_bps=10.0,
|
|
103
|
+
min_commission=0.0,
|
|
104
|
+
sell_tax_bps=0.0,
|
|
105
|
+
commission_tax_bps=1000.0,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
FEE_SCHEDULES: dict[Market, FeeSchedule] = {
|
|
109
|
+
Market.US: _US,
|
|
110
|
+
Market.TW: _TW_STOCK,
|
|
111
|
+
Market.JP: _JP,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
TW_ETF_SCHEDULE = _TW_ETF
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def fee_for(
|
|
118
|
+
order: Order,
|
|
119
|
+
gross_amount: float,
|
|
120
|
+
market: Market | None = None,
|
|
121
|
+
schedule: FeeSchedule | None = None,
|
|
122
|
+
) -> tuple[float, float]:
|
|
123
|
+
"""Compute (fee, tax) for an order.
|
|
124
|
+
|
|
125
|
+
Pass either a `market` (uses the built-in preset) or an explicit
|
|
126
|
+
`schedule` (recommended for anything real). Exactly one is required.
|
|
127
|
+
"""
|
|
128
|
+
if (market is None) == (schedule is None):
|
|
129
|
+
raise ValueError("pass exactly one of market= or schedule=")
|
|
130
|
+
sched = schedule if schedule is not None else FEE_SCHEDULES[market] # type: ignore[index]
|
|
131
|
+
return sched.fee_and_tax(order, gross_amount)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Liquidity constraints: how much of an order can realistically fill.
|
|
2
|
+
|
|
3
|
+
The classic backtest lie is "buy 1,000,000 shares of a stock that trades
|
|
4
|
+
200,000 shares a day, at the close price." These constraints cap executable
|
|
5
|
+
quantity at a fraction of observed market volume (participation rate), so
|
|
6
|
+
your backtest can't trade size the market couldn't absorb.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
|
|
13
|
+
from .models import Order
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LiquidityConstraint(ABC):
|
|
17
|
+
"""Interface: cap an order's quantity given market volume."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def cap(self, order: Order, *, bar_volume: float) -> float:
|
|
21
|
+
"""Return the maximum executable quantity for `order` (>= 0)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ParticipationCap(LiquidityConstraint):
|
|
25
|
+
"""Cap quantity at a fixed fraction of bar volume.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
max_participation: Maximum share of bar volume your order may be,
|
|
29
|
+
e.g. 0.10 = at most 10% of the day's volume. Industry heuristics
|
|
30
|
+
for "low-impact" execution typically live in the 1-20% range.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, max_participation: float = 0.10) -> None:
|
|
34
|
+
if not 0 < max_participation <= 1:
|
|
35
|
+
raise ValueError("max_participation must be in (0, 1]")
|
|
36
|
+
self.max_participation = max_participation
|
|
37
|
+
|
|
38
|
+
def cap(self, order: Order, *, bar_volume: float) -> float:
|
|
39
|
+
if bar_volume < 0:
|
|
40
|
+
raise ValueError("bar_volume must be >= 0")
|
|
41
|
+
return min(order.quantity, self.max_participation * bar_volume)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""MAE / MFE excursion analytics.
|
|
2
|
+
|
|
3
|
+
MAE (Maximum Adverse Excursion): the worst open-loss a trade experienced
|
|
4
|
+
while it was on. MFE (Maximum Favorable Excursion): the best open-profit.
|
|
5
|
+
Together they answer questions a single entry/exit pair cannot:
|
|
6
|
+
|
|
7
|
+
- "My stop is -5%; how often did winners dip below -5% first?" (MAE distribution)
|
|
8
|
+
- "My target is +8%; how often did trades reach +8% but close lower?" (MFE distribution)
|
|
9
|
+
- "Am I exiting too early/late?" (compare exit return to MFE — give-back ratio)
|
|
10
|
+
|
|
11
|
+
These are descriptive statistics over a trade's price path — a measurement
|
|
12
|
+
tool, not a strategy. Everything here is computed from post-entry highs/lows
|
|
13
|
+
the caller supplies, so point-in-time correctness is the caller's job (use
|
|
14
|
+
quant-lint on the strategy that produced the paths).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import statistics
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Sequence
|
|
22
|
+
|
|
23
|
+
from .models import Side
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class TradePath:
|
|
28
|
+
"""One trade's lifecycle for excursion analysis.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
entry_price: Actual fill price at entry.
|
|
32
|
+
side: BUY for long trades, SELL for short trades.
|
|
33
|
+
highs: Post-entry period high prices, in time order (include the
|
|
34
|
+
entry bar's high if the trade was open for the rest of that bar).
|
|
35
|
+
lows: Post-entry period low prices, aligned with `highs`.
|
|
36
|
+
exit_price: Actual fill price at exit. If omitted, the last period's
|
|
37
|
+
low/high is NOT used as a fallback — realized-return fields are
|
|
38
|
+
simply unavailable (None).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
entry_price: float
|
|
42
|
+
side: Side
|
|
43
|
+
highs: Sequence[float]
|
|
44
|
+
lows: Sequence[float]
|
|
45
|
+
exit_price: float | None = None
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if self.entry_price <= 0:
|
|
49
|
+
raise ValueError("entry_price must be positive")
|
|
50
|
+
if len(self.highs) == 0:
|
|
51
|
+
raise ValueError("highs/lows must contain at least one period")
|
|
52
|
+
if len(self.highs) != len(self.lows):
|
|
53
|
+
raise ValueError("highs and lows must have equal length")
|
|
54
|
+
for h, l in zip(self.highs, self.lows):
|
|
55
|
+
if h < l:
|
|
56
|
+
raise ValueError(f"period high {h} below period low {l}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _signed_return(price: float, entry: float, side: Side) -> float:
|
|
60
|
+
"""Return from `entry` to `price`, signed so profit is positive."""
|
|
61
|
+
raw = (price - entry) / entry
|
|
62
|
+
return raw if side is Side.BUY else -raw
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def mae(path: TradePath) -> float:
|
|
66
|
+
"""Maximum Adverse Excursion as a signed return (<= 0 for any losing dip).
|
|
67
|
+
|
|
68
|
+
Long: worst case is the minimum post-entry low.
|
|
69
|
+
Short: worst case is the maximum post-entry high.
|
|
70
|
+
"""
|
|
71
|
+
if path.side is Side.BUY:
|
|
72
|
+
worst = min(path.lows)
|
|
73
|
+
else:
|
|
74
|
+
worst = max(path.highs)
|
|
75
|
+
return min(0.0, _signed_return(worst, path.entry_price, path.side))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def mfe(path: TradePath) -> float:
|
|
79
|
+
"""Maximum Favorable Excursion as a signed return (>= 0 for any gain)."""
|
|
80
|
+
if path.side is Side.BUY:
|
|
81
|
+
best = max(path.highs)
|
|
82
|
+
else:
|
|
83
|
+
best = min(path.lows)
|
|
84
|
+
return max(0.0, _signed_return(best, path.entry_price, path.side))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class Excursion:
|
|
89
|
+
"""Full excursion record for one trade.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
mae: Maximum Adverse Excursion (signed return, <= 0).
|
|
93
|
+
mfe: Maximum Favorable Excursion (signed return, >= 0).
|
|
94
|
+
realized_return: Exit-vs-entry signed return, or None if no exit_price.
|
|
95
|
+
giveback: Fraction of MFE surrendered at exit,
|
|
96
|
+
(mfe - realized) / mfe; None when mfe == 0 or exit unknown.
|
|
97
|
+
0.0 = exited at the peak; 1.0 = gave back everything; >1 = the
|
|
98
|
+
trade round-tripped through zero into a loss.
|
|
99
|
+
end_up_winning: True when realized_return > 0; None if exit unknown.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
mae: float
|
|
103
|
+
mfe: float
|
|
104
|
+
realized_return: float | None
|
|
105
|
+
giveback: float | None
|
|
106
|
+
end_up_winning: bool | None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def excursion(path: TradePath) -> Excursion:
|
|
110
|
+
"""Compute the full excursion record for one trade."""
|
|
111
|
+
a = mae(path)
|
|
112
|
+
f = mfe(path)
|
|
113
|
+
realized: float | None = None
|
|
114
|
+
giveback: float | None = None
|
|
115
|
+
winning: bool | None = None
|
|
116
|
+
if path.exit_price is not None:
|
|
117
|
+
realized = _signed_return(path.exit_price, path.entry_price, path.side)
|
|
118
|
+
winning = realized > 0
|
|
119
|
+
if f > 0:
|
|
120
|
+
giveback = (f - realized) / f
|
|
121
|
+
return Excursion(mae=a, mfe=f, realized_return=realized, giveback=giveback, end_up_winning=winning)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class ExcursionStats:
|
|
126
|
+
"""Aggregate statistics over a set of trades."""
|
|
127
|
+
|
|
128
|
+
n: int
|
|
129
|
+
mae_mean: float
|
|
130
|
+
mae_median: float
|
|
131
|
+
mae_p05: float # 5th percentile (worst tail) of MAE
|
|
132
|
+
mfe_mean: float
|
|
133
|
+
mfe_median: float
|
|
134
|
+
mfe_p95: float # 95th percentile (best tail) of MFE
|
|
135
|
+
giveback_mean: float | None # over trades with mfe > 0 and known exit
|
|
136
|
+
win_rate: float | None # over trades with known exit
|
|
137
|
+
mae_of_winners_mean: float | None # how deep winners dipped first
|
|
138
|
+
mfe_of_losers_mean: float | None # how high losers climbed before failing
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _percentile(sorted_vals: Sequence[float], q: float) -> float:
|
|
142
|
+
"""Linear-interpolation percentile; q in [0, 1]."""
|
|
143
|
+
if not sorted_vals:
|
|
144
|
+
raise ValueError("empty sequence")
|
|
145
|
+
if len(sorted_vals) == 1:
|
|
146
|
+
return float(sorted_vals[0])
|
|
147
|
+
pos = q * (len(sorted_vals) - 1)
|
|
148
|
+
lo = int(pos)
|
|
149
|
+
hi = min(lo + 1, len(sorted_vals) - 1)
|
|
150
|
+
frac = pos - lo
|
|
151
|
+
return float(sorted_vals[lo] * (1 - frac) + sorted_vals[hi] * frac)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def excursion_stats(paths: Sequence[TradePath]) -> ExcursionStats:
|
|
155
|
+
"""Aggregate MAE/MFE statistics over many trades.
|
|
156
|
+
|
|
157
|
+
Use it to sanity-check stops/targets: if `mae_of_winners_mean` is deeper
|
|
158
|
+
than your stop, your stop would have killed trades that later won.
|
|
159
|
+
"""
|
|
160
|
+
if not paths:
|
|
161
|
+
raise ValueError("paths must be non-empty")
|
|
162
|
+
recs = [excursion(p) for p in paths]
|
|
163
|
+
|
|
164
|
+
maes = sorted(r.mae for r in recs)
|
|
165
|
+
mfes = sorted(r.mfe for r in recs)
|
|
166
|
+
|
|
167
|
+
with_exit = [r for r in recs if r.realized_return is not None]
|
|
168
|
+
wins = [r for r in with_exit if r.end_up_winning]
|
|
169
|
+
losses = [r for r in with_exit if not r.end_up_winning]
|
|
170
|
+
givebacks = [r.giveback for r in with_exit if r.giveback is not None]
|
|
171
|
+
|
|
172
|
+
return ExcursionStats(
|
|
173
|
+
n=len(recs),
|
|
174
|
+
mae_mean=statistics.fmean(maes),
|
|
175
|
+
mae_median=statistics.median(maes),
|
|
176
|
+
mae_p05=_percentile(maes, 0.05),
|
|
177
|
+
mfe_mean=statistics.fmean(mfes),
|
|
178
|
+
mfe_median=statistics.median(mfes),
|
|
179
|
+
mfe_p95=_percentile(mfes, 0.95),
|
|
180
|
+
giveback_mean=statistics.fmean(givebacks) if givebacks else None,
|
|
181
|
+
win_rate=len(wins) / len(with_exit) if with_exit else None,
|
|
182
|
+
mae_of_winners_mean=statistics.fmean(r.mae for r in wins) if wins else None,
|
|
183
|
+
mfe_of_losers_mean=statistics.fmean(r.mfe for r in losses) if losses else None,
|
|
184
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Core value types shared across friction-engine modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Side(enum.Enum):
|
|
10
|
+
"""Order/trade direction."""
|
|
11
|
+
|
|
12
|
+
BUY = "buy"
|
|
13
|
+
SELL = "sell"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def sign(self) -> int:
|
|
17
|
+
"""+1 for BUY, -1 for SELL — handy for signed cash-flow math."""
|
|
18
|
+
return 1 if self is Side.BUY else -1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Order:
|
|
23
|
+
"""A desired trade, before friction is applied.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
symbol: Instrument identifier (free-form string).
|
|
27
|
+
side: BUY or SELL.
|
|
28
|
+
quantity: Number of shares/units requested (positive).
|
|
29
|
+
limit_price: Optional limit price; informational only — this library
|
|
30
|
+
models cost, it does not decide whether a limit order fills.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
symbol: str
|
|
34
|
+
side: Side
|
|
35
|
+
quantity: float
|
|
36
|
+
limit_price: float | None = None
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
if self.quantity <= 0:
|
|
40
|
+
raise ValueError(f"quantity must be positive, got {self.quantity}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class Fill:
|
|
45
|
+
"""A trade after friction has been applied.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
symbol: Instrument identifier.
|
|
49
|
+
side: BUY or SELL.
|
|
50
|
+
quantity: Shares/units actually filled (may be capped by liquidity).
|
|
51
|
+
base_price: Reference price before slippage (e.g. bar close).
|
|
52
|
+
fill_price: Effective execution price after slippage.
|
|
53
|
+
slippage_cost: Total slippage cost in account currency (always >= 0;
|
|
54
|
+
for sells the fill price moves *down*, but cost stays positive).
|
|
55
|
+
fee: Commission/fee amount in account currency.
|
|
56
|
+
tax: Transaction tax amount in account currency.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
symbol: str
|
|
60
|
+
side: Side
|
|
61
|
+
quantity: float
|
|
62
|
+
base_price: float
|
|
63
|
+
fill_price: float
|
|
64
|
+
slippage_cost: float
|
|
65
|
+
fee: float
|
|
66
|
+
tax: float
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def gross_amount(self) -> float:
|
|
70
|
+
"""fill_price * quantity — cash before fee/tax."""
|
|
71
|
+
return self.fill_price * self.quantity
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def total_cost(self) -> float:
|
|
75
|
+
"""All friction in account currency: slippage + fee + tax."""
|
|
76
|
+
return self.slippage_cost + self.fee + self.tax
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def net_cash_flow(self) -> float:
|
|
80
|
+
"""Signed cash impact on the account.
|
|
81
|
+
|
|
82
|
+
BUY: negative (cash out) = -(gross + fee + tax).
|
|
83
|
+
SELL: positive (cash in) = gross - fee - tax.
|
|
84
|
+
Slippage is already embedded in fill_price, so it is not added again.
|
|
85
|
+
"""
|
|
86
|
+
if self.side is Side.BUY:
|
|
87
|
+
return -(self.gross_amount + self.fee + self.tax)
|
|
88
|
+
return self.gross_amount - self.fee - self.tax
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Slippage models.
|
|
2
|
+
|
|
3
|
+
Slippage = the gap between the reference price your backtest *assumes* and
|
|
4
|
+
the price you would actually get. Models here are deliberately simple,
|
|
5
|
+
public, and auditable. The volume-aware model is the classic square-root
|
|
6
|
+
market-impact form published widely in the market-microstructure literature
|
|
7
|
+
(e.g. Almgren-style impact): impact grows with the square root of your
|
|
8
|
+
participation rate in the day's volume.
|
|
9
|
+
|
|
10
|
+
Convention: slippage always works *against* the trader — buys fill higher,
|
|
11
|
+
sells fill lower.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
|
|
19
|
+
from .models import Order, Side
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SlippageModel(ABC):
|
|
23
|
+
"""Interface: given an order and market context, return the fill price."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def fill_price(
|
|
27
|
+
self,
|
|
28
|
+
order: Order,
|
|
29
|
+
reference_price: float,
|
|
30
|
+
*,
|
|
31
|
+
bar_volume: float | None = None,
|
|
32
|
+
volatility: float | None = None,
|
|
33
|
+
) -> float:
|
|
34
|
+
"""Return the effective execution price for `order`.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
order: The desired trade.
|
|
38
|
+
reference_price: Price the backtest would naively use (e.g. close).
|
|
39
|
+
bar_volume: Total market volume in the execution bar/day, in the
|
|
40
|
+
same units as order quantity. Required by volume-aware models.
|
|
41
|
+
volatility: Daily return volatility (e.g. 0.02 for 2%). Used by
|
|
42
|
+
impact-style models; ignored by fixed models.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ZeroSlippage(SlippageModel):
|
|
47
|
+
"""No slippage — the naive-backtest default, provided for completeness."""
|
|
48
|
+
|
|
49
|
+
def fill_price(
|
|
50
|
+
self,
|
|
51
|
+
order: Order,
|
|
52
|
+
reference_price: float,
|
|
53
|
+
*,
|
|
54
|
+
bar_volume: float | None = None,
|
|
55
|
+
volatility: float | None = None,
|
|
56
|
+
) -> float:
|
|
57
|
+
return reference_price
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FixedSlippage(SlippageModel):
|
|
61
|
+
"""Constant basis-point slippage per trade.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
bps: Slippage in basis points (1 bps = 0.01%). Applied against the
|
|
65
|
+
trader on every fill.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, bps: float) -> None:
|
|
69
|
+
if bps < 0:
|
|
70
|
+
raise ValueError("bps must be >= 0")
|
|
71
|
+
self.bps = bps
|
|
72
|
+
|
|
73
|
+
def fill_price(
|
|
74
|
+
self,
|
|
75
|
+
order: Order,
|
|
76
|
+
reference_price: float,
|
|
77
|
+
*,
|
|
78
|
+
bar_volume: float | None = None,
|
|
79
|
+
volatility: float | None = None,
|
|
80
|
+
) -> float:
|
|
81
|
+
direction = 1.0 if order.side is Side.BUY else -1.0
|
|
82
|
+
return reference_price * (1.0 + direction * self.bps / 10_000.0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class VolumeShareSlippage(SlippageModel):
|
|
86
|
+
"""Volume-aware slippage: half-spread + square-root temporary impact.
|
|
87
|
+
|
|
88
|
+
fill_price = reference * (1 + dir * (spread/2 + eta * sigma * sqrt(q/V)))
|
|
89
|
+
|
|
90
|
+
where ``q/V`` is your participation rate (order quantity over bar volume),
|
|
91
|
+
``sigma`` is daily volatility, and ``eta`` is an impact coefficient you
|
|
92
|
+
calibrate to your market/execution style (0.1 = gentle, 1.0 = harsh).
|
|
93
|
+
|
|
94
|
+
Why square root: it is the most widely published empirically-supported
|
|
95
|
+
shape for temporary market impact across equities markets. It punishes
|
|
96
|
+
large orders convexly in *rate* terms while keeping small orders near
|
|
97
|
+
the half-spread floor — exactly the behavior naive backtests ignore.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
spread_bps: Assumed bid-ask spread in basis points. You pay half.
|
|
101
|
+
eta: Impact coefficient (dimensionless), >= 0.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, spread_bps: float = 5.0, eta: float = 0.5) -> None:
|
|
105
|
+
if spread_bps < 0:
|
|
106
|
+
raise ValueError("spread_bps must be >= 0")
|
|
107
|
+
if eta < 0:
|
|
108
|
+
raise ValueError("eta must be >= 0")
|
|
109
|
+
self.spread_bps = spread_bps
|
|
110
|
+
self.eta = eta
|
|
111
|
+
|
|
112
|
+
def fill_price(
|
|
113
|
+
self,
|
|
114
|
+
order: Order,
|
|
115
|
+
reference_price: float,
|
|
116
|
+
*,
|
|
117
|
+
bar_volume: float | None = None,
|
|
118
|
+
volatility: float | None = None,
|
|
119
|
+
) -> float:
|
|
120
|
+
if bar_volume is None or bar_volume <= 0:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"VolumeShareSlippage requires positive bar_volume; "
|
|
123
|
+
"use FixedSlippage if volume data is unavailable"
|
|
124
|
+
)
|
|
125
|
+
sigma = volatility if volatility is not None else 0.02
|
|
126
|
+
if sigma < 0:
|
|
127
|
+
raise ValueError("volatility must be >= 0")
|
|
128
|
+
participation = order.quantity / bar_volume
|
|
129
|
+
half_spread = self.spread_bps / 2.0 / 10_000.0
|
|
130
|
+
impact = self.eta * sigma * math.sqrt(participation)
|
|
131
|
+
direction = 1.0 if order.side is Side.BUY else -1.0
|
|
132
|
+
return reference_price * (1.0 + direction * (half_spread + impact))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: friction-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Realistic backtest friction modeling: volume-aware slippage, multi-market fees & taxes (US/TW/JP), liquidity constraints, and MAE/MFE trade analytics.
|
|
5
|
+
Author: friction-engine contributors
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/jalano0i9u8y7-lab/friction-engine
|
|
8
|
+
Keywords: backtest,slippage,transaction-costs,MAE,MFE,trading
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# friction-engine
|
|
26
|
+
|
|
27
|
+
**Your backtest is lying to you — this library measures how much.**
|
|
28
|
+
|
|
29
|
+
`friction-engine` models the four costs naive backtests ignore, as small,
|
|
30
|
+
composable, fully-auditable pieces:
|
|
31
|
+
|
|
32
|
+
| Module | What it fixes | The naive-backtest lie |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `slippage` | Volume-aware slippage (half-spread + square-root impact) | "I filled 200k shares at the close price" |
|
|
35
|
+
| `fees` | Multi-market fees & taxes (US / TW / JP presets, all configurable) | "Commissions are round-off error" |
|
|
36
|
+
| `liquidity` | Participation-rate caps on executable quantity | "The market absorbed my 10× ADV order instantly" |
|
|
37
|
+
| `mae_mfe` | MAE/MFE excursion analytics | "The trade returned +5%" — but it dipped -8% first; would your stop have survived? |
|
|
38
|
+
|
|
39
|
+
Pure Python standard library. **Zero third-party dependencies.** Every model
|
|
40
|
+
is one short file you can read end-to-end before trusting it.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install friction-engine
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or from source:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install .
|
|
52
|
+
pip install ".[dev]" # with pytest for running the test suite
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from friction_engine import (
|
|
59
|
+
Market, Order, ParticipationCap, Side, TradePath,
|
|
60
|
+
VolumeShareSlippage, excursion, fee_for,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
order = Order(symbol="2330.TW", side=Side.BUY, quantity=20_000)
|
|
64
|
+
|
|
65
|
+
# 1. Liquidity: can the market absorb this?
|
|
66
|
+
allowed = ParticipationCap(0.10).cap(order, bar_volume=150_000)
|
|
67
|
+
|
|
68
|
+
# 2. Slippage: volume-aware fill price (not the close you assumed)
|
|
69
|
+
slip = VolumeShareSlippage(spread_bps=5, eta=0.5)
|
|
70
|
+
fill_price = slip.fill_price(order, 100.0, bar_volume=150_000, volatility=0.02)
|
|
71
|
+
|
|
72
|
+
# 3. Fees & taxes: TW stocks = 14.25 bps commission + 30 bps sell tax
|
|
73
|
+
fee, tax = fee_for(order, fill_price * order.quantity, market=Market.TW)
|
|
74
|
+
|
|
75
|
+
# 4. MAE/MFE: what happened inside the trade, not just entry→exit
|
|
76
|
+
path = TradePath(
|
|
77
|
+
entry_price=fill_price, side=Side.BUY,
|
|
78
|
+
highs=[101, 103, 106], lows=[97.5, 99, 102], exit_price=105.0,
|
|
79
|
+
)
|
|
80
|
+
rec = excursion(path)
|
|
81
|
+
print(rec.mae, rec.mfe, rec.giveback) # -0.025 … +0.06 … how much you gave back
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Run the full worked example:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python examples/basic_usage.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## The models, honestly
|
|
91
|
+
|
|
92
|
+
**Volume-aware slippage** — `fill = ref × (1 + dir × (spread/2 + η·σ·√(q/V)))`.
|
|
93
|
+
The square-root temporary-impact shape is the most widely published
|
|
94
|
+
empirical form in the market-microstructure literature. `η` (eta) is a
|
|
95
|
+
calibration knob: 0.1 gentle, 0.5 typical, 1.0 harsh. If you don't know your
|
|
96
|
+
`η`, run all three and treat the spread of outcomes as your uncertainty.
|
|
97
|
+
|
|
98
|
+
**Fee presets** — US (zero commission + ~0.3 bps sell-side regulatory fees),
|
|
99
|
+
TW stocks (14.25 bps commission both sides + 30 bps sell tax), TW ETF
|
|
100
|
+
(10 bps sell tax), JP (10 bps commission + 10% consumption tax *on the
|
|
101
|
+
commission*, no share transaction tax). These are publicly documented
|
|
102
|
+
typical values as of mid-2026 and **will go stale** — brokers discount,
|
|
103
|
+
regulators re-price. Treat presets as starting points:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from friction_engine import FEE_SCHEDULES, Market
|
|
107
|
+
my_tw = FEE_SCHEDULES[Market.TW].with_overrides(commission_bps=6.0, min_commission=20.0)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Liquidity cap** — `min(quantity, max_participation × bar_volume)`. Simple,
|
|
111
|
+
brutal, and the single most common way backtests overstate capacity.
|
|
112
|
+
|
|
113
|
+
**MAE/MFE** — descriptive statistics over a trade's post-entry high/low
|
|
114
|
+
path (Sweeney-style excursion analysis). Aggregates (`excursion_stats`)
|
|
115
|
+
answer: *is my stop inside the noise band of my winners?* (`mae_of_winners_mean`)
|
|
116
|
+
and *how much do my losers climb before failing?* (`mfe_of_losers_mean`).
|
|
117
|
+
|
|
118
|
+
## What this library is NOT
|
|
119
|
+
|
|
120
|
+
- Not a backtester. It computes friction; you bring the loop, the data, and
|
|
121
|
+
the point-in-time discipline. (Pair it with `quant-lint` to audit the
|
|
122
|
+
strategy code feeding it.)
|
|
123
|
+
- Not a broker simulator. Fill *decisions* (partial fills, limit queues,
|
|
124
|
+
halts) are out of scope; it prices the fills you assume.
|
|
125
|
+
- Not financial advice, and presets are not a fee-quote service. Verify
|
|
126
|
+
current rates with your broker/exchange before trading real money.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python -m pytest # run the test suite
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Layout:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
src/friction_engine/
|
|
138
|
+
models.py # Order / Fill / Side value types
|
|
139
|
+
slippage.py # Zero / Fixed / VolumeShare slippage models
|
|
140
|
+
fees.py # FeeSchedule + US/TW/JP presets
|
|
141
|
+
liquidity.py # ParticipationCap
|
|
142
|
+
mae_mfe.py # TradePath, mae/mfe, excursion, excursion_stats
|
|
143
|
+
tests/ # pytest suite, no network, no data files
|
|
144
|
+
examples/ # runnable worked example
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
Apache-2.0 — see [LICENSE](LICENSE). Clean-room provenance: see
|
|
150
|
+
[CLEANROOM.md](CLEANROOM.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
friction_engine/__init__.py,sha256=p2BCX9finMbVrAcZLA5H0ZHkynZ27bzSJPzfNxzL1us,1233
|
|
2
|
+
friction_engine/fees.py,sha256=NZU3VYjqFCZUXyNSMJOdugIip9tngBYutJp-yET6o28,4775
|
|
3
|
+
friction_engine/liquidity.py,sha256=0RscV4VCMork3qVJp_AzvXrZiFyX_Au_tthosNlnuaw,1510
|
|
4
|
+
friction_engine/mae_mfe.py,sha256=EUNoE4aQtFsfHzvqOr5IcT9Y6YVqnFER-DcZfzooC7s,6836
|
|
5
|
+
friction_engine/models.py,sha256=YJtKG5IN6m2mWuM8rNAjLjDrOLUGXQ84piEox7vF-es,2646
|
|
6
|
+
friction_engine/slippage.py,sha256=TZkSq_PX7OOQrnmxt47SFvCWZ7cBGiJfSXm6yvBnJ54,4534
|
|
7
|
+
friction_engine-0.1.0.dist-info/licenses/LICENSE,sha256=v0c51XRi_dXfvYrmUzGU47xaSEMVLNH_w1IeddCzq_k,10550
|
|
8
|
+
friction_engine-0.1.0.dist-info/METADATA,sha256=3zC9dD-dcPwxMeLRYVUXV2PW7am89cyv2TmJOOx6CuU,5832
|
|
9
|
+
friction_engine-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
friction_engine-0.1.0.dist-info/top_level.txt,sha256=8ytBfkfhKRfEqqGhKZKcY_mHHSYyomF_ADq5CWnAIOA,16
|
|
11
|
+
friction_engine-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate as
|
|
87
|
+
of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, provided that You
|
|
91
|
+
meet the following conditions:
|
|
92
|
+
|
|
93
|
+
(a) You must give any other recipients of the Work or
|
|
94
|
+
Derivative Works a copy of this License; and
|
|
95
|
+
|
|
96
|
+
(b) You must cause any modified files to carry prominent notices
|
|
97
|
+
stating that You changed the files; and
|
|
98
|
+
|
|
99
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
100
|
+
that You distribute, all copyright, patent, trademark, and
|
|
101
|
+
attribution notices from the Source form of the Work,
|
|
102
|
+
excluding those notices that do not pertain to any part of
|
|
103
|
+
the Derivative Works; and
|
|
104
|
+
|
|
105
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
106
|
+
distribution, then any Derivative Works that You distribute must
|
|
107
|
+
include a readable copy of the attribution notices contained
|
|
108
|
+
within such NOTICE file, excluding those notices that do not
|
|
109
|
+
pertain to any part of the Derivative Works, in at least one
|
|
110
|
+
of the following places: within a NOTICE text file distributed
|
|
111
|
+
as part of the Derivative Works; within the Source form or
|
|
112
|
+
documentation, if provided along with the Derivative Works; or,
|
|
113
|
+
within a file titled "NOTICE" file in the root directory of
|
|
114
|
+
the Derivative Works; or within a file titled "NOTICE" file in
|
|
115
|
+
the root directory of the Source form. You may copy and
|
|
116
|
+
distribute Derivative Works of the Work only under this License,
|
|
117
|
+
in which the Licensor agrees to place attribution notices
|
|
118
|
+
contained in such NOTICE file. You may reproduce and distribute
|
|
119
|
+
copies of the Work for any purpose, provided that You also meet
|
|
120
|
+
the conditions of the License for Derivative Works as set forth
|
|
121
|
+
in the NOTICE file.
|
|
122
|
+
|
|
123
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
124
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
125
|
+
by You to the Licensor shall be under the terms and conditions of this
|
|
126
|
+
License, without any additional terms or conditions.
|
|
127
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
128
|
+
the terms of any separate license agreement you may have executed
|
|
129
|
+
with Licensor regarding such Contributions.
|
|
130
|
+
|
|
131
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
132
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
133
|
+
except as required for reasonable and customary use in describing the
|
|
134
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
135
|
+
|
|
136
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
137
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
138
|
+
Contributor provides its Work) on an "AS IS" BASIS, WITHOUT
|
|
139
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
|
140
|
+
including, without limitation, any warranties or conditions of
|
|
141
|
+
TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
142
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
143
|
+
appropriateness of using or redistributing the Work and assume any
|
|
144
|
+
risks associated with Your exercise of permissions under this License.
|
|
145
|
+
|
|
146
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
147
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
148
|
+
unless required by applicable law (such as deliberate and grossly
|
|
149
|
+
negligent acts) or agreed to in writing, will any Contributor be
|
|
150
|
+
liable to You for any loss or damage, as a result of any such
|
|
151
|
+
Contributor's exercise of permissions under this License, even if
|
|
152
|
+
such Contributor has been advised of the possibility of such damages.
|
|
153
|
+
|
|
154
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
155
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
156
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
157
|
+
or other liability obligations. However, in accepting such
|
|
158
|
+
obligations, You may act only on Your own behalf and on Your sole
|
|
159
|
+
responsibility, not on behalf of any Contributor, and only if You
|
|
160
|
+
agree to indemnify, defend, and hold each Contributor harmless for
|
|
161
|
+
any liability incurred by each such Contributor in performing its
|
|
162
|
+
obligations under this License.
|
|
163
|
+
|
|
164
|
+
END OF TERMS AND CONDITIONS
|
|
165
|
+
|
|
166
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
167
|
+
|
|
168
|
+
To apply the Apache License to your work, attach the following
|
|
169
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
170
|
+
replaced with your own identifying information. (Don't include
|
|
171
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
172
|
+
comment syntax for the file format. We also recommend that a
|
|
173
|
+
file or class name and description of purpose be included on the
|
|
174
|
+
same "printed page" as the copyright notice for easier
|
|
175
|
+
identification within third-party archives.
|
|
176
|
+
|
|
177
|
+
Keith Infinity
|
|
178
|
+
|
|
179
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
180
|
+
you may not use this file except in compliance with the License.
|
|
181
|
+
You may obtain a copy of the License at
|
|
182
|
+
|
|
183
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
184
|
+
|
|
185
|
+
Unless required by applicable law or agreed to in writing, software
|
|
186
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
187
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
188
|
+
See the License for the specific language governing permissions and
|
|
189
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
friction_engine
|