finmiti 0.0.1__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.
finmiti/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from importlib.metadata import version
2
+ __version__ = version("finmetry")
3
+
4
+
5
+ from . import clients
6
+ from .stocks_handler import Stock, StockDict
7
+ from . import constants
8
+ from .strategy_handler import StrategyBase, StgDataLoader
9
+ from .portfolio_handler import Portfolio, Account
10
+ from .backtest_handler import Backtester
11
+
12
+ from .utils import *
@@ -0,0 +1,2 @@
1
+
2
+ from .base import Backtester
@@ -0,0 +1,19 @@
1
+ from ..portfolio_handler import Portfolio
2
+ from ..executioners import ExecutionModel
3
+ from ..strategy_handler import StgDataLoader, StrategyBase
4
+
5
+ class Backtester:
6
+ def __init__(self, data_loader: StgDataLoader, strategy: StrategyBase, portfolio: Portfolio):
7
+ self.data_loader = data_loader
8
+ self.strategy = strategy
9
+ self.portfolio = portfolio
10
+
11
+ def run(self):
12
+ for market_data in self.data_loader:
13
+ entry_orders = self.strategy(market_data)
14
+ exit_orders = self.portfolio.get_exit_orders(market_data)
15
+ ### keeping exit_orders first to avoid cash going negative
16
+ all_orders = exit_orders + entry_orders
17
+ for order in all_orders:
18
+ self.portfolio.on_order(order)
19
+ self.portfolio.mark_to_market(market_data)
@@ -0,0 +1,2 @@
1
+
2
+ from . import client_5paisa
@@ -0,0 +1,242 @@
1
+ """
2
+ This module contains the objects related to 5paisa client
3
+
4
+ @author: Rathod Darshan
5
+ """
6
+
7
+ import py5paisa as p5
8
+ import pandas as pd
9
+ import datetime as dtm
10
+ from typing import Union, TypedDict
11
+
12
+ from ..stocks_handler import Stock, StockDict
13
+
14
+ from ..constants import INTERVAL
15
+
16
+
17
+ class ScripMaster:
18
+ """ScripMaster contains all the scipts of 5paisa client.
19
+
20
+ To get the name and symbol of any script, this class needs to be accessed. This class just filters the data from single .csv.
21
+ """
22
+
23
+ def __init__(self, filepath: str = None) -> None:
24
+ """Initializes the ScripMaster class.
25
+
26
+ Loads the .csv file into data attribute.
27
+
28
+ Parameters
29
+ ----------
30
+ filepath : str, optional
31
+ filepath to .csv file. If filepath is given then it reads the file, else it will download the file. by default None
32
+ """
33
+ if filepath is not None:
34
+ self.data = pd.read_pickle(filepath)
35
+ else:
36
+ self.data = pd.read_csv("https://images.5paisa.com/website/scripmaster-csv-format.csv")
37
+
38
+ self.data["Expiry"] = pd.to_datetime(self.data["Expiry"], format="%Y-%m-%d %H:%M:%S")
39
+ self.data["Name"] = self.data["Name"].apply(str.upper)
40
+ self.data["Symbol"] = self.data["Name"]
41
+
42
+ def __repr__(self):
43
+ return f"scrip master data"
44
+
45
+ def __call__(self):
46
+ return self.data
47
+
48
+ def save(self, filepath: str) -> None:
49
+ """saves the scrip master data
50
+
51
+ Parameters
52
+ ----------
53
+ filepath : str
54
+ filepath with filename with .pkl extention
55
+
56
+ Returns
57
+ -------
58
+ _type_
59
+ None
60
+ """
61
+ return self.data.to_pickle(filepath)
62
+
63
+ def get_scrip(self, stock: Stock) -> pd.DataFrame:
64
+ """returns the scrips of the stock
65
+
66
+ Parameters
67
+ ----------
68
+ stock : Stock
69
+ a stock object
70
+
71
+ Returns
72
+ -------
73
+ pd.DataFrame
74
+ Scrip data of a given stock
75
+ """
76
+ try:
77
+ return stock.scrip
78
+ except:
79
+ pass
80
+ d1 = self.data
81
+ f1 = (d1["Exch"] == stock.exchange) & (d1["ExchType"] == stock.exchange_type) & (d1["Symbol"] == stock.symbol)
82
+ f2 = (d1["Series"] == "EQ") | (d1["Series"] == "XX")
83
+ d2 = d1[f1 & f2]
84
+ if d2.empty:
85
+ raise ValueError(f"No Scrip found for {stock.symbol} in scrip_master")
86
+ d2 = d2.set_index("Name")
87
+ ### setting the scrip to stock object
88
+ stock.scrip = d2
89
+ return d2
90
+
91
+
92
+ class Client5paisaCred(TypedDict):
93
+ APP_NAME: str
94
+ APP_SOURCE: str
95
+ USER_ID: str
96
+ PASSWORD: str
97
+ USER_KEY: str
98
+ ENCRYPTION_KEY: str
99
+
100
+
101
+ class Client5paisa(p5.FivePaisaClient):
102
+ def __init__(self, totp: str, mpin: str, client_code: str, cred: Client5paisaCred, scrip_master: ScripMaster = None, **kwargs):
103
+ super().__init__(cred=cred)
104
+ self.get_totp_session(client_code, f"{totp}", mpin)
105
+
106
+ print("downloading the scrip-master")
107
+
108
+ self.scrip_master = ScripMaster() if scrip_master is None else scrip_master
109
+ return
110
+
111
+ def download_historical_data(
112
+ self,
113
+ stock: Stock,
114
+ interval: INTERVAL = INTERVAL.one_day,
115
+ start: Union[str, dtm.datetime] = "2023-01-01",
116
+ end: Union[str, dtm.datetime] = "2023-03-30",
117
+ ) -> pd.DataFrame:
118
+ """Downloads the historical data and saves it to local drive or in Stock.data variable.
119
+
120
+ Parameters
121
+ ----------
122
+ stock : Stock
123
+ Stock object
124
+ interval : str, optional
125
+ time interval of data. it should be within [1m,5m,10m,15m,30m,60m,1d], by default "1d"
126
+ start : Union[str, dtm.datetime], optional
127
+ start date of the data. The data for this date will be downloaded, by default "2023-01-01"
128
+ end : Union[str, dtm.datetime], optional
129
+ end date of the data. The data for this date will be downloaded, by default "2023-03-30"
130
+
131
+ Returns
132
+ ----------
133
+ pd.DataFrame
134
+ A dataframe containing a historical data.
135
+
136
+ """
137
+ # scrip = self.scrip_master.get_scrip(stock)
138
+
139
+ # if isinstance(start, dtm.datetime):
140
+ # start = start.strftime("%Y-%m-%d")
141
+ # if isinstance(end, dtm.datetime):
142
+ # end = end.strftime("%Y-%m-%d")
143
+
144
+ # df = self.historical_data(stock.exchange, stock.exchange_type, scrip.loc[stock.symbol, "Scripcode"], interval.value, start, end)
145
+ # df.columns = ["Datetime", "Open", "High", "Low", "Close", "Volume"]
146
+ # df["Datetime"] = pd.to_datetime(df["Datetime"])
147
+ # df = df.set_index("Datetime")
148
+
149
+ # return df
150
+
151
+ scrip = self.scrip_master.get_scrip(stock)
152
+ if isinstance(start, str):
153
+ start_dt = dtm.datetime.strptime(start, "%Y-%m-%d")
154
+ else:
155
+ start_dt = start
156
+
157
+ if isinstance(end, str):
158
+ end_dt = dtm.datetime.strptime(end, "%Y-%m-%d")
159
+ else:
160
+ end_dt = end
161
+
162
+ def _fetch_chunk(s: dtm.datetime, e: dtm.datetime) -> pd.DataFrame:
163
+ df = self.historical_data(stock.exchange, stock.exchange_type, scrip.loc[stock.symbol, "Scripcode"], interval.value, s.strftime("%Y-%m-%d"), e.strftime("%Y-%m-%d"))
164
+ if df is None or df.empty:
165
+ return pd.DataFrame()
166
+ df.columns = ["Datetime", "Open", "High", "Low", "Close", "Volume"]
167
+ df["Datetime"] = pd.to_datetime(df["Datetime"])
168
+ return df.set_index("Datetime")
169
+
170
+ dfs = []
171
+ curr_start = start_dt
172
+
173
+ while curr_start < end_dt:
174
+ curr_end = min(curr_start + dtm.timedelta(days=90), end_dt)
175
+ chunk = _fetch_chunk(curr_start, curr_end)
176
+ if not chunk.empty:
177
+ dfs.append(chunk)
178
+
179
+ curr_start = curr_end
180
+
181
+ if not dfs:
182
+ return pd.DataFrame()
183
+
184
+ df = pd.concat(dfs).sort_index()
185
+ df = df.loc[~df.index.duplicated(keep="first")]
186
+
187
+ return df
188
+
189
+ def get_market_depth(self, stockdict: Union[list[Stock], StockDict]) -> pd.DataFrame:
190
+ """Gets the market depth for given list of Stocks
191
+
192
+ Parameters
193
+ ----------
194
+ StockDict : list[Stock]|StockDict
195
+ list of Stock class instances or StockDict type object.
196
+
197
+ Returns
198
+ -------
199
+ _pd.DataFrame
200
+ Live Market Depth for all the Stocks in list.
201
+ """
202
+ scrips = pd.concat(self.scrip_master.get_scrip(stock) for stock in stockdict)
203
+ a = scrips.rename(columns={"Exch": "Exchange", "ExchType": "ExchangeType"})[["Exchange", "ExchangeType", "Symbol"]].to_dict(orient="records")
204
+ d1 = pd.DataFrame(self.fetch_market_depth_by_symbol(a)["Data"])
205
+
206
+ d1.set_index("ScripCode", inplace=True)
207
+ d1["Datetime"] = dtm.datetime.now()
208
+ scrips.set_index("Scripcode", inplace=True)
209
+ d1["Symbol"] = scrips["Symbol"]
210
+ d1.set_index("Symbol", inplace=True)
211
+ d1.rename(columns={"Close": "PrevClose"}, inplace=True)
212
+ d1.rename(columns={"LastTradedPrice": "Close"}, inplace=True)
213
+ return d1
214
+
215
+ def update_stock_histdata0_to_ltp(self, stockdict: Union[list[Stock], StockDict]) -> None:
216
+ """Updates the market depth to stock.hist_data0 attribute
217
+
218
+ Parameters
219
+ ----------
220
+ StockDict : list[Stock]|StockDict
221
+ list of Stock class instances or StockDict type object.
222
+
223
+ Returns
224
+ -------
225
+ None
226
+ """
227
+ d1 = self.get_market_depth(stockdict)
228
+ d1 = d1[["Datetime", "Open", "High", "Low", "Close", "Volume"]]
229
+ d1["Datetime"] = d1["Datetime"].dt.normalize()
230
+ ### Convert datatypes of specific columns
231
+ float_cols = ["Open", "High", "Low", "Close"]
232
+ d1[float_cols] = d1[float_cols].astype(float)
233
+ d1["Volume"] = d1["Volume"].astype(int)
234
+
235
+ for s1 in stockdict:
236
+ try:
237
+ d = d1.loc[s1.symbol]
238
+ s1.hist_data0.loc[d.Datetime] = d
239
+ except:
240
+ print(f"Error in {s1.symbol}")
241
+ continue
242
+ return
finmiti/constants.py ADDED
@@ -0,0 +1,133 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass, field, asdict
3
+ from typing import TypedDict, Optional, get_type_hints, Dict, Tuple
4
+ import pandas as pd
5
+ import uuid
6
+ from datetime import datetime
7
+
8
+ import numpy as np
9
+
10
+
11
+ class EXCHANGE(Enum):
12
+ nse = "N"
13
+ bse = "B"
14
+ mcx = "MCX"
15
+
16
+
17
+ class EXCHANGE_TYPE(Enum):
18
+ cash = "C"
19
+ derivative = "D"
20
+ currency = "U"
21
+
22
+
23
+ class INTERVAL(Enum):
24
+ one_day = "1d"
25
+ one_min = "1m"
26
+ five_min = "5m"
27
+ fifteen_min = "15m"
28
+
29
+
30
+ class ORDERTYPE(Enum):
31
+ buy = "buy"
32
+ sell = "sell"
33
+
34
+
35
+ @dataclass
36
+ class Order:
37
+ timestamp: datetime | np.datetime64
38
+ symbol: str
39
+ price: float
40
+ order_type: ORDERTYPE
41
+ ### most of the below attributes are for backtesting and evaluation purpose.
42
+ ### value_frac decides how much fraction of the total cash goes into this order
43
+ value_frac: float = None
44
+ id: Optional[str] = None
45
+ target: Optional[float] = None
46
+ stop_loss: Optional[float] = None
47
+ hold_uptill: Optional[datetime] = None
48
+ remarks: Optional[str] = None
49
+ ### items for handling order fill related noise. These will be handlerd by executioner object. They will fill these values hence they are None initialized.
50
+ fill_price: float = None
51
+ fill_qty: float = None
52
+ fill_timestamp: datetime | np.datetime64 = None
53
+ fill_remarks: Optional[str] = None
54
+ brokerage_cost: float = 0
55
+ total_cost: float = None
56
+ ### the account id is for isolating the cash.
57
+ account_idx: int = 0
58
+
59
+ def __post_init__(self):
60
+ if self.id is None:
61
+ self.id = str(uuid.uuid4())
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class StockData:
66
+ """
67
+ Strategy input data (TorchGeometric-style Data object).
68
+ All arrays must be aligned on the last dimension. Here, the data could be only the last value or it could be the array of historical data. The timestamps must match those values as well.
69
+ """
70
+
71
+ symbol: str
72
+ ### market data entry
73
+ timestamp: datetime | np.datetime64
74
+ open: float
75
+ high: float
76
+ low: float
77
+ close: float
78
+ volume: float
79
+ ### anything else you want
80
+ features: Optional[Dict[str, np.ndarray]] = None
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class DiEdgeData:
85
+ """Directed edge from one stock to another."""
86
+
87
+ start_node_symbol: str
88
+ end_node_symbol: str
89
+ features: Optional[Dict[str, np.ndarray]] = None
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class MarketGraphData:
94
+ """
95
+ Multi-asset market snapshot.
96
+ """
97
+
98
+ ### the time of the data. The StockData could have historical data upto this timestamp.
99
+ timestamp: datetime | np.datetime64
100
+ ### nodes, named after its symbol
101
+ stocks: Dict[str, StockData]
102
+ ### edges: (src, dst) --> edge feature vector. from one node to other.
103
+ edges: Optional[DiEdgeData] = None
104
+ ### optional global features (VIX, index returns, liquidity, etc.)
105
+ global_features: Optional[Dict[str, np.ndarray]] = None
106
+
107
+
108
+ ### Error class
109
+ class StockDataNotAvailableError(Exception):
110
+ """Raised when stock data is missing for a given timestamp."""
111
+ def __init__(self, timestamp:datetime=None, symbol: str=None):
112
+ message = f"Stock data not available. No data for {symbol} on {timestamp}."
113
+ super().__init__(message)
114
+
115
+
116
+ class NegativeCashError(Exception):
117
+ """Raised when the cash of an account goes negative."""
118
+ def __init__(self, account_idx:int=None):
119
+ message=f"The cash in an account cannot go negative. Negative cash registered in account - {account_idx}"
120
+ super().__init__(message)
121
+
122
+ class OrderTypeError(Exception):
123
+ """Raised when the ordertype is out of pre-defined orders"""
124
+ def __init__(self, order: Order=None):
125
+ message=f"The ordertype must be from finmetry.constants.ORDERTYPE. For {order.symbol} on {order.timestamp} got {order.order_type}."
126
+ super().__init__(message)
127
+
128
+ class NotEnoughData(Exception):
129
+ """Raised when there are not enough historical data for feature computation"""
130
+ def __init__(self, timestamp:datetime=None, symbol: str=None):
131
+ message = f"Not enough data available. for {symbol} on {timestamp}."
132
+ super().__init__(message)
133
+
@@ -0,0 +1,49 @@
1
+ from typing import List
2
+ from .constants import Order, ORDERTYPE
3
+
4
+
5
+ class ExecutionModel:
6
+ """ExecutionModel is a simulator of a real market. Slippage, volume limits, liquidity etc. kind of real-market scenarios should be implemented here. This could have also been done in portfolio handler but we apply it here for separating the responsibilities.
7
+
8
+ Thus ExecutionModel introduces real-market noise. And portfolio simply stores the order.
9
+ """
10
+
11
+ def __init__(self, brokerage_perc: float = 0.0):
12
+ self.brokerage = 0.0
13
+
14
+ def fill_buy_order(self, order: Order, available_cash: float) -> Order:
15
+ total_cost = available_cash * order.value_frac
16
+ ### the total cost includes brokerage cost. Thus the amount available to buy is obtained after deducting brokerage from the total cost.
17
+ ### the formula is total_cost = net_cost * (1 + self.brokerage), thus
18
+ net_cost = total_cost / (1 + self.brokerage)
19
+
20
+ order.fill_price = order.price
21
+ order.fill_qty = net_cost / order.fill_price
22
+ order.fill_timestamp = order.timestamp
23
+ order.brokerage_cost = total_cost - net_cost
24
+ order.total_cost = total_cost
25
+ order.fill_remarks = "No added noise"
26
+
27
+ return order
28
+
29
+ def fill_sell_order(self, order: Order) -> Order:
30
+ gross_receivable = order.fill_qty * order.price
31
+ net_receivable = gross_receivable / (1 + self.brokerage)
32
+
33
+ order.fill_price = order.price
34
+ order.fill_timestamp = order.timestamp
35
+ order.brokerage_cost = gross_receivable - net_receivable
36
+ order.total_cost = net_receivable
37
+ order.fill_remarks = "No added noise"
38
+
39
+ return order
40
+
41
+ def fill_order(self, order: Order, available_cash: float) -> Order:
42
+ """
43
+ Naive execution: fill immediately at order.price
44
+ """
45
+ if order.order_type == ORDERTYPE.buy:
46
+ order = self.fill_buy_order(order=order, available_cash=available_cash)
47
+ elif order.order_type == ORDERTYPE.sell:
48
+ order = self.fill_sell_order(order=order)
49
+ return order
@@ -0,0 +1,2 @@
1
+
2
+ from .base import Portfolio, Account
@@ -0,0 +1,231 @@
1
+ from typing import Dict, List, Optional, TypedDict
2
+ import pandas as pd
3
+ from datetime import datetime
4
+ import numpy as np
5
+
6
+ from ..executioners import ExecutionModel
7
+ from ..constants import Order, MarketGraphData, ORDERTYPE, NegativeCashError, OrderTypeError
8
+
9
+
10
+ class PorfolioSnapshot(TypedDict):
11
+ """A snapshot of the portfolio at a given Datetime."""
12
+
13
+ timestamp: str | datetime | np.datetime64
14
+ value: float
15
+
16
+
17
+ class Portfolio:
18
+ def __init__(self, executioner: ExecutionModel, starting_cash: float = 100.0, total_accounts: int = 1):
19
+ self.executioner = executioner
20
+ self.total_accounts = total_accounts
21
+
22
+ self.accounts: List[Account] = [Account(account_idx=i, starting_cash=starting_cash / self.total_accounts, executioner=self.executioner) for i in range(total_accounts)]
23
+ self.history: List[PorfolioSnapshot] = []
24
+
25
+ def mark_to_market(self, market_data: MarketGraphData):
26
+ value = 0.0
27
+ for account in self.accounts:
28
+ account.mark_to_market(market_data=market_data)
29
+ account_snapshot = account.account_history[-1]
30
+ value += account_snapshot["cash"] + account_snapshot["holdings_value"]
31
+ self.history.append(PorfolioSnapshot(timestamp=market_data.timestamp, value=value))
32
+ return
33
+
34
+ def on_order(self, order: Order):
35
+ account = self.accounts[order.account_idx]
36
+ account.on_order(order=order)
37
+ return
38
+
39
+ def get_exit_orders(self, market: MarketGraphData) -> List[Order]:
40
+ exit_orders: List[Order] = []
41
+ for account in self.accounts:
42
+ exit_orders += account.get_exit_orders(market=market)
43
+ return exit_orders
44
+
45
+ @property
46
+ def order_book(self) -> pd.DataFrame:
47
+ d1 = pd.DataFrame()
48
+ for account in self.accounts:
49
+ account_ob = pd.DataFrame(account.order_book)
50
+ d1 = pd.concat([d1, account_ob], ignore_index=True)
51
+ d1["order_type"] = d1["order_type"].apply(lambda x: x.value)
52
+ return d1
53
+
54
+ @property
55
+ def arranged_order_book(self) -> pd.DataFrame:
56
+ keys = ["symbol", "fill_price", "fill_qty", "fill_timestamp", "total_cost", "account_idx", "hold_uptill", "remarks"]
57
+ d1 = self.order_book
58
+ d2 = d1.groupby("order_type").get_group("buy").set_index("id")[keys].add_prefix("buy_")
59
+ d3 = d1.groupby("order_type").get_group("sell").set_index("id")[keys].add_prefix("sell_")
60
+ d4 = pd.concat([d2, d3], axis=1).sort_values(by="buy_fill_timestamp")
61
+ return d4
62
+
63
+ @property
64
+ def account_history(self) -> pd.DataFrame:
65
+ return pd.DataFrame(self.history).set_index("timestamp")
66
+
67
+ @property
68
+ def holdings(self) -> pd.DataFrame:
69
+ d1 = pd.DataFrame()
70
+ for account in self.accounts:
71
+ account_hd = pd.DataFrame(account.holdings.values())
72
+ d1 = pd.concat([d1, account_hd], axis=0, ignore_index=True)
73
+ return d1
74
+
75
+
76
+ class Holding(TypedDict):
77
+ """A holding entry."""
78
+
79
+ order_id: str
80
+ account_idx: int
81
+ symbol: str
82
+ qty: float
83
+ entry_price: float
84
+ ltp: float
85
+ target: Optional[float] = None
86
+ stop_loss: Optional[float] = None
87
+ holding_start_date: Optional[datetime]
88
+ holding_end_date: Optional[datetime]
89
+
90
+
91
+ class AccountSnapshot(TypedDict):
92
+ """A snapshot of the portfolio at a given Datetime."""
93
+
94
+ timestamp: str | datetime | np.datetime64
95
+ holdings_value: float
96
+ cash: float
97
+
98
+
99
+ class Account:
100
+ def __init__(self, account_idx: int, executioner: ExecutionModel, starting_cash: float = 100.0) -> None:
101
+ self.executioner = executioner
102
+ self.idx = account_idx
103
+ self.cash = starting_cash
104
+
105
+ ### holdings are indexed by its holding_id, which is order_id.
106
+ self.holdings: Dict[str, Holding] = {}
107
+ self.order_book: List[Order] = []
108
+ self.account_history: List[AccountSnapshot] = []
109
+
110
+ def mark_to_market(self, market_data: MarketGraphData):
111
+ """
112
+ Update the ltp of holdings from the latest market_data
113
+ Also records portfolio value and cash at this point in time.
114
+ """
115
+ holdings_value = 0.0
116
+ for _, holding in self.holdings.items():
117
+ symbol = holding["symbol"]
118
+ try:
119
+ holding["ltp"] = market_data.stocks[symbol].close
120
+ except KeyError:
121
+ print(f"Cannot update the data for {symbol}, due to missing data on {market_data.timestamp}")
122
+ holdings_value += holding["qty"] * holding["ltp"]
123
+
124
+ self.account_history.append(AccountSnapshot(timestamp=market_data.timestamp, holdings_value=holdings_value, cash=self.cash))
125
+ return
126
+
127
+ def on_order(self, order: Order) -> None:
128
+ order = self.executioner.fill_order(available_cash=self.cash, order=order)
129
+
130
+ qty = order.fill_qty
131
+ price = order.fill_price
132
+ total_cost = order.total_cost
133
+
134
+ if order.order_type == ORDERTYPE.buy:
135
+ qty *= 1
136
+ total_cost *= 1
137
+ elif order.order_type == ORDERTYPE.sell:
138
+ qty *= -1
139
+ total_cost *= -1
140
+ else:
141
+ raise OrderTypeError(order=order)
142
+
143
+ ### change the cash available in the portfolio. If it is sell order then total_cost will be negative and thus that amount will be added to self.cash.
144
+ self.cash = self.cash - total_cost
145
+ if self.cash < 0:
146
+ raise NegativeCashError(account_idx=self.idx)
147
+
148
+ ### add new holding or edit the holding
149
+ holding = self.holdings.get(order.id, None)
150
+ if holding is None:
151
+ holding = Holding(
152
+ order_id=order.id,
153
+ account_idx=self.idx,
154
+ symbol=order.symbol,
155
+ qty=qty,
156
+ entry_price=price,
157
+ ltp=price,
158
+ target=order.target,
159
+ stop_loss=order.stop_loss,
160
+ holding_start_date=order.timestamp,
161
+ holding_end_date=order.hold_uptill,
162
+ )
163
+ self.holdings[order.id] = holding
164
+ else:
165
+ assert (holding["qty"] + qty) == 0, "If you are trying to exit a position then quantity must match with the order_id. Partial fills shall be applied as separate orders."
166
+ self.holdings.pop(order.id, None)
167
+
168
+ ### add order to orderbook
169
+ self.order_book.append(order)
170
+
171
+ return
172
+
173
+ def get_exit_orders(self, market: MarketGraphData) -> List[Order]:
174
+ """
175
+ Check stops, targets, expiry.
176
+ Emits exit Orders if needed.
177
+ """
178
+ exit_orders: List[Order] = []
179
+
180
+ for order_id, holding in self.holdings.items():
181
+ try:
182
+ close = market.stocks[holding["symbol"]].close
183
+ low = market.stocks[holding["symbol"]].low
184
+ high = market.stocks[holding["symbol"]].high
185
+ ### sometimes the data for any particular stock may be missing on a given timestamp. It will thus hault the backtest. To avoid haulting, bypass it.
186
+ except KeyError:
187
+ print(f"Cannot check for exit orders for {holding['symbol']}, due to missing data on {market.timestamp}")
188
+ continue
189
+
190
+ # reason = None
191
+ # if holding["stop_loss"] and close <= holding["stop_loss"]:
192
+ # reason = "stop_loss"
193
+ # # exit_price = holding["stop_loss"]
194
+ # elif holding["target"] and close >= holding["target"]:
195
+ # reason = "target"
196
+ # # exit_price = holding["target"]
197
+ # elif holding["holding_end_date"] and market.timestamp >= holding["holding_end_date"]:
198
+ # reason = "expiry"
199
+ # else:
200
+ # continue
201
+ # exit_price = close
202
+
203
+ reason = None
204
+ if holding["stop_loss"] and low<= holding["stop_loss"]:
205
+ reason = "stop_loss"
206
+ exit_price = holding["stop_loss"]
207
+ elif holding["target"] and high >= holding["target"]:
208
+ reason = "target"
209
+ exit_price = holding["target"]
210
+ elif holding["holding_end_date"] and market.timestamp >= holding["holding_end_date"]:
211
+ reason = "expiry"
212
+ exit_price = close
213
+ else:
214
+ continue
215
+
216
+
217
+ exit_order_type = ORDERTYPE.sell if holding["qty"] > 0 else ORDERTYPE.buy
218
+ exit_orders.append(
219
+ Order(
220
+ timestamp=market.timestamp,
221
+ symbol=holding["symbol"],
222
+ price=exit_price,
223
+ order_type=exit_order_type,
224
+ fill_qty=abs(holding["qty"]),
225
+ remarks=reason,
226
+ account_idx=self.idx,
227
+ id=order_id,
228
+ )
229
+ )
230
+
231
+ return exit_orders
@@ -0,0 +1,3 @@
1
+
2
+ from .stock import Stock
3
+ from .stockdict import StockDict
@@ -0,0 +1,209 @@
1
+ """Module consisting of base class of stock"""
2
+
3
+ import os as _os
4
+ from pathlib import Path
5
+ import datetime as _dtm
6
+ import numpy as _np
7
+ import pandas as _pd
8
+
9
+ from typing import TypeVar, Union, Optional
10
+
11
+ from enum import Enum
12
+
13
+ import duckdb
14
+
15
+ from ..constants import EXCHANGE, EXCHANGE_TYPE, INTERVAL
16
+
17
+
18
+ def append_it(data: _pd.DataFrame, filepath: str) -> None:
19
+ """Appends the data on the given filepath after comparing Indexes of both the data.
20
+
21
+ This compares the data already at the given filepath, and then appends only the data not already present.
22
+
23
+ Parameters
24
+ ----------
25
+ data : _pd.DataFrame
26
+ data frame with Datetime like index
27
+ filepath : str
28
+ filepath, where the dataframe will be appended.
29
+ """
30
+ try:
31
+ df1 = data.combine_first(_pd.read_parquet(filepath)).sort_index()
32
+ df1.to_parquet(filepath)
33
+ except FileNotFoundError as e:
34
+ print(f"Creating the file - {filepath}")
35
+ data.to_parquet(filepath)
36
+ return
37
+
38
+
39
+ class Stock:
40
+ def __init__(
41
+ self,
42
+ symbol: str,
43
+ exchange: EXCHANGE = EXCHANGE.nse,
44
+ exchange_type: EXCHANGE_TYPE = EXCHANGE_TYPE.cash,
45
+ ) -> None:
46
+ """Manages the data for list of stocks.
47
+
48
+ Parameters
49
+ ----------
50
+ Parameters
51
+ ----------
52
+ symbol : str
53
+ Stock symbol as available online.
54
+ exchange : EXCHANGE
55
+ Stock Exchange. can be N, B, M for Nifty, BSE and MCX respectively. By default N
56
+ exchange_type : EXCHANGE_TYPE
57
+ Type of Stock Exchange. can be C, D or U for Cash, Derivative or Currency respectively. By default C.
58
+ """
59
+ self.symbol = symbol
60
+ if isinstance(exchange, EXCHANGE):
61
+ self.exchange = exchange.value
62
+ else:
63
+ raise ValueError("exchange can only be of type EXCHANGE enum")
64
+
65
+ if isinstance(exchange_type, EXCHANGE_TYPE):
66
+ self.exchange_type = exchange_type.value
67
+ else:
68
+ raise ValueError("exchange_type can only be of type EXCHANGE_TYPE enum")
69
+
70
+
71
+ self.hist_data0: Optional[_pd.DataFrame] = None
72
+
73
+ @property
74
+ def foldname(self):
75
+ return self.exchange + "_" + self.exchange_type + "_" + self.symbol
76
+
77
+ def __repr__(self):
78
+ return f"{self.symbol} stock class"
79
+
80
+ def get_filename(self, date: _dtm.datetime, interval: INTERVAL):
81
+ return f"{date.year}{str(date.month).zfill(2)}_{interval.value}.parquet"
82
+
83
+ def save_historical_data(
84
+ self,
85
+ data: _pd.DataFrame,
86
+ interval: INTERVAL,
87
+ local_data_foldpath: str,
88
+ overwrite: bool = False,
89
+ ) -> None:
90
+ """saves the historical stock data.
91
+
92
+ Multiple files, each for saparate month data is created.
93
+
94
+ Parameters
95
+ ----------
96
+ data : _pd.DataFrame
97
+ Data to be saved. It should be indexed with Datetime values.
98
+ interval : str
99
+ time interval of the data
100
+ local_data_foldpath : str
101
+ path to folder where the data will be stored. Inside this folder, multiple folders of individual stocks are created and inside that stock folder, historical data and other data is stored.
102
+ overwrite : bool, False
103
+ wheather to overwrite the existing file or just append the new data. default False. If True then it will overwrite the present data.
104
+ """
105
+ ### creating the folder
106
+ data_foldpath = Path(local_data_foldpath) / self.foldname
107
+ Path(data_foldpath).mkdir(exist_ok=True)
108
+
109
+ ### writing the data
110
+ data["filename"] = data.index.to_series().apply(lambda x: self.get_filename(x, interval))
111
+ for fnm, df in data.groupby("filename"):
112
+ df = df.drop(columns="filename")
113
+ filepath = data_foldpath / fnm
114
+
115
+ if overwrite:
116
+ print("Overwriting:-", filepath)
117
+ df.to_parquet(filepath)
118
+ else:
119
+ append_it(df, filepath)
120
+ pass
121
+ return
122
+
123
+ def load_historical_data(
124
+ self,
125
+ start: Union[str, _dtm.datetime],
126
+ end: Union[str, _dtm.datetime],
127
+ local_data_foldpath: str,
128
+ interval: INTERVAL = INTERVAL.one_day,
129
+ fill_holidays: bool = False,
130
+ remove_weekends: bool = True,
131
+ ) -> _pd.DataFrame:
132
+ """Loads the data from local_directory
133
+
134
+ Parameters
135
+ ----------
136
+ start : Union[str, _dtm.datetime]
137
+ start date of the data. The data for this date will be downloaded
138
+ end : Union[str, _dtm.datetime]
139
+ end date of the data. The data for this date will be downloaded
140
+ local_data_foldpath : str
141
+ path to folder where the data is stored. inside this folder there are multiple sub-folders of individual stock. You only have to give the parent folder path.
142
+ interval : INTERVAL, Optional
143
+ time interval of data. it should be of type INTERVAL enum. Defaults to one day interval
144
+ fill_holidays : bool, Default is False
145
+ The data is not available for the holidays. If this is made True then the previous day data will be filled in as that day's data and that missing holiday row will be inserted.
146
+ remove_weekends : bool, Default is True
147
+ Sometimes the markets are open on saturdays and sundays. These are vary rare and thus are removed from historical data while loading.
148
+
149
+ Returns
150
+ -------
151
+ _pd.DataFrame
152
+ data
153
+
154
+ Raises
155
+ ------
156
+ ValueError
157
+ if no data is found
158
+ """
159
+ data_foldpath = Path(local_data_foldpath) / self.foldname
160
+ Path(data_foldpath).mkdir(exist_ok=True)
161
+
162
+ if isinstance(start, _dtm.datetime):
163
+ start = start.strftime("%Y-%m-%d")
164
+ if isinstance(end, _dtm.datetime):
165
+ end = end.strftime("%Y-%m-%d")
166
+
167
+ # DuckDB SQL
168
+ query = f"""
169
+ SELECT *
170
+ FROM read_parquet('{data_foldpath}/*_{interval.value}.parquet')
171
+ WHERE Datetime BETWEEN TIMESTAMP '{start}' AND TIMESTAMP '{end}'
172
+ ORDER BY Datetime
173
+ """
174
+ with duckdb.connect() as con:
175
+ df = con.execute(query).df()
176
+ d1 = df.set_index("Datetime")
177
+ if fill_holidays:
178
+ # Create full business day range
179
+ full_range = _pd.date_range(start=d1.index.min(), end=d1.index.max(), freq="B")
180
+ d1 = d1.reindex(full_range).ffill()
181
+ d1.index.name = "Datetime"
182
+ if remove_weekends:
183
+ d1 = d1[d1.index.weekday < 5]
184
+ return d1
185
+
186
+ @property
187
+ def scrip(self) -> _pd.DataFrame:
188
+ """scrip for client 5paisa.
189
+
190
+ Returns
191
+ -------
192
+ _pd.DataFrame
193
+ scrip data.
194
+ """
195
+ return self._scrip
196
+
197
+ @scrip.setter
198
+ def scrip(self, data: _pd.DataFrame) -> None:
199
+ """saves the scrip
200
+
201
+ Parameters
202
+ ----------
203
+ data : _pd.DataFrame
204
+ scrip data. This can be optained by ScripMaster.get_scrip() method.
205
+ """
206
+ self._scrip = data
207
+ return
208
+
209
+
@@ -0,0 +1,109 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from concurrent.futures import ThreadPoolExecutor
5
+
6
+ from .stock import Stock
7
+ import duckdb
8
+
9
+
10
+ class StockDict:
11
+ """Class to handle functionalities related to multiple stocks, using symbol as keys."""
12
+
13
+ def __init__(self, stocks: list[Stock]=None) -> None:
14
+ self.stockdict: dict[str, Stock] = {}
15
+ if stocks is not None:
16
+ for stock in stocks:
17
+ self.add(stock)
18
+
19
+
20
+
21
+ def __repr__(self) -> str:
22
+ return f"StockList object with {len(self.stockdict)} Stocks"
23
+
24
+ def __type__(self) -> str:
25
+ return "StockDict"
26
+
27
+ def __getitem__(self, i: str|int):
28
+ if isinstance(i, str):
29
+ return self.stockdict[i]
30
+ elif isinstance(i, int):
31
+ return list(self.stockdict.values())[i]
32
+ else:
33
+ raise ValueError("can only give string or integer values")
34
+
35
+ def __len__(self):
36
+ return len(self.stockdict)
37
+
38
+ def __iter__(self):
39
+ return iter(self.stockdict.values())
40
+
41
+ def __contains__(self, symbol: str) -> bool:
42
+ return symbol in self.stockdict
43
+
44
+ def add(self, stock: Stock) -> None:
45
+ """Adds or replaces a Stock object using its ticker symbol."""
46
+ self.stockdict[stock.symbol] = stock
47
+ return
48
+
49
+ def remove(self, symbol: str) -> None:
50
+ """Removes a stock by its symbol, if it exists."""
51
+ self.stockdict.pop(symbol, None)
52
+ return
53
+
54
+ def get(self, symbol: str) -> Stock | None:
55
+ return self.stockdict.get(symbol)
56
+
57
+ @property
58
+ def symbols(self) -> list[str]:
59
+ return list(self.stockdict.keys())
60
+
61
+ @property
62
+ def stocklist(self) -> list[Stock]:
63
+ return list(self.stockdict.values())
64
+
65
+ def load_multiple_stocks(self, symbols: list[str], *args, **kwargs) -> None:
66
+ """Loads multiple stocks using their symbols and adds them to the stockdict."""
67
+ for symbol in symbols:
68
+ try:
69
+ stock = Stock(symbol, *args, **kwargs)
70
+ self.add(stock)
71
+ except Exception as e:
72
+ print(f"[{symbol}] Error loading stock:\n{e}")
73
+ return
74
+
75
+ def apply(self, method_name: str, *args, **kwargs) -> dict[str, any]:
76
+ """Applies a method to all Stock objects and returns a dict of results."""
77
+ results = {}
78
+ for symbol, stock in self.stockdict.items():
79
+ method = getattr(stock, method_name, None)
80
+ if callable(method):
81
+ results[symbol] = method(*args, **kwargs)
82
+ else:
83
+ results[symbol] = None
84
+ return results
85
+
86
+ def load_historical_data(self, *args, max_workers: int = 5, remove_error_stocks: bool = True, print_info: bool = True, **kwargs) -> None:
87
+ """Calls stock.load_historical_data() for all stocks using multithreading."""
88
+
89
+ def _load(stock: Stock):
90
+ try:
91
+ if print_info:
92
+ print(f"[{stock.symbol}] Loading historical data...")
93
+ stock.hist_data0 = stock.load_historical_data(*args, **kwargs)
94
+ stock.hist_data0 = stock.hist_data0[~stock.hist_data0.index.duplicated(keep="first")] # removing duplicate timestamps
95
+ if print_info:
96
+ print(f"[{stock.symbol}] Data loaded successfully.")
97
+ except Exception as e:
98
+ print(f"[{stock.symbol}] Error loading data:\n{e}")
99
+ errors.append(stock.symbol)
100
+
101
+ errors = []
102
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
103
+ executor.map(_load, self.stockdict.values())
104
+ if remove_error_stocks:
105
+ for symbol in errors:
106
+ print(f"[{symbol}] Removing stock due to loading error.")
107
+ self.remove(symbol)
108
+
109
+ return
@@ -0,0 +1,2 @@
1
+
2
+ from .base import StrategyBase, StgDataLoader
@@ -0,0 +1,36 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Iterator
3
+ import datetime as dtm
4
+
5
+ from ..stocks_handler import StockDict
6
+ from ..constants import Order
7
+ from ..constants import MarketGraphData, StockData
8
+ from ..utils import str_to_dtm
9
+
10
+
11
+ class StgDataLoader(ABC):
12
+ @abstractmethod
13
+ def __getitem__(self, idx: str | dtm.datetime) -> MarketGraphData:
14
+ raise NotImplementedError
15
+
16
+ @abstractmethod
17
+ def __len__(self) -> int:
18
+ raise NotImplementedError
19
+
20
+ @abstractmethod
21
+ def __iter__(self) -> Iterator[MarketGraphData]:
22
+ raise NotImplementedError
23
+
24
+
25
+ class StrategyBase(ABC):
26
+ def __call__(self, data: MarketGraphData) -> List[Order]:
27
+ orders = self.forward(data)
28
+
29
+ if not isinstance(orders, list):
30
+ raise TypeError("Strategy.forward must return List[Order]")
31
+
32
+ return orders
33
+
34
+ @abstractmethod
35
+ def forward(self, data: MarketGraphData) -> List[Order]:
36
+ raise NotImplementedError
finmiti/utils.py ADDED
@@ -0,0 +1,7 @@
1
+ import datetime as dtm
2
+
3
+ def str_to_dtm(timestring:str)->dtm.datetime:
4
+ return dtm.datetime.strptime(timestring, "%Y-%m-%d")
5
+
6
+ def dtm_to_str(timestamp:dtm.datetime)->str:
7
+ return timestamp.strftime("%Y-%m-%d")
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: finmiti
3
+ Version: 0.0.1
4
+ Summary: Persnal project for stock market data analysis
5
+ Author-email: Darshan Rathod <darshan.rathod1994@gmail.com>
6
+ Maintainer-email: Darshan Rathod <darshan.rathod1994@gmail.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://dev-ddr.github.io/finmiti/
9
+ Project-URL: Repository, https://github.com/dev-ddr/finmiti
10
+ Project-URL: Issues, https://github.com/dev-ddr/finmiti/issues
11
+ Keywords: finance,stock-market,quant,research,personal
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: numpy==2.3.5
18
+ Requires-Dist: pandas==2.3.3
19
+ Requires-Dist: pyyaml==6.0.3
20
+ Requires-Dist: duckdb==1.4.2
21
+ Requires-Dist: pyarrow==22.0.0
22
+ Requires-Dist: py5paisa==0.7.21.2
23
+ Requires-Dist: pydantic
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: ruff; extra == "dev"
27
+ Requires-Dist: jupyter; extra == "dev"
28
+ Requires-Dist: notebook; extra == "dev"
29
+ Requires-Dist: sphinx; extra == "dev"
30
+ Requires-Dist: mkdocs; extra == "dev"
31
+ Requires-Dist: mkdocs-material; extra == "dev"
32
+ Requires-Dist: mkdocstrings[python]; extra == "dev"
33
+ Requires-Dist: nbconvert; extra == "dev"
34
+ Requires-Dist: matplotlib; extra == "dev"
35
+ Requires-Dist: plotly; extra == "dev"
36
+
37
+ # Finmiti
38
+
39
+ **This project is developed for my personal use.** I am developing this to keep the documentation, architecture and pipeline consistent so that I can focus more on developing strategies instead of developing pipelines.
40
+
41
+ Visit [Finmiti](https://dev-ddr.github.io/finmetry/) guide for further steps.
42
+
43
+ > This project is solely developed for my personal use. I am publishing this only to keep myself updated and to remove the headache of setting up the framework again and again.
44
+
45
+ ---
46
+
47
+ **Finmiti is a research-first quantitative trading framework** designed to keep
48
+ strategy logic, execution logic, and accounting logic strictly separated.
49
+
50
+ It exists to eliminate repeated reinvention of trading pipelines, so you can
51
+ focus on **researching strategies**, not rebuilding infrastructure.
52
+
53
+
54
+ ## What Finmiti Is (and Is Not)
55
+
56
+ Finmiti is:
57
+
58
+ - a framework for systematic trading research
59
+ - equally suited for backtesting and live trading
60
+ - opinionated by design
61
+ - built around explicit, auditable abstractions
62
+
63
+ Finmiti is **not**:
64
+
65
+ - a strategy library
66
+ - a signal generator
67
+ - a black-box trading system
68
+
69
+ If you want flexibility at the cost of correctness, this framework will feel restrictive.
70
+ That restriction is intentional.
71
+
72
+
73
+ ## The Core Trading Loop
74
+
75
+ Every strategy in finmiti follows the same explicit loop:
76
+
77
+ ```text
78
+ Market Data → Strategy → Orders → Portfolio → Execution → Accounting
79
+ ````
80
+
81
+ Each stage is implemented as a **separate module** with strict responsibilities.
82
+
83
+ This guarantees that:
84
+
85
+ * strategies remain stateless
86
+ * execution assumptions are explicit
87
+ * accounting is consistent
88
+ * backtests can be trusted
89
+ * live trading reuses the same abstractions
90
+
91
+ ## Major Modules
92
+
93
+ Finmiti is organized into the following conceptual modules:
94
+
95
+ ### [Client Handling](https://dev-ddr.github.io/finmetry/concepts/client_handling_module/)
96
+
97
+ Handles interaction with external systems such as broker APIs and live data feeds.
98
+ Keeps the rest of the framework broker-agnostic.
99
+
100
+ ### [Stocks](https://dev-ddr.github.io/finmetry/concepts/stocks_handling_module/)
101
+
102
+ Manages symbols, historical data, and OHLCV storage.
103
+ Acts as the foundation for all market data access.
104
+
105
+ ### [Strategy](https://dev-ddr.github.io/finmetry/concepts/strategy_handling_module/)
106
+
107
+ Consumes immutable market snapshots and emits **order intent only**.
108
+ Strategies never manage cash, positions, or execution details.
109
+
110
+ ### [Orders](https://dev-ddr.github.io/finmetry/concepts/order/)
111
+
112
+ Orders are the contract between strategy, portfolio, and execution.
113
+ They represent intent, not outcome.
114
+
115
+ ### [Executioners](https://dev-ddr.github.io/finmetry/concepts/executioners/)
116
+
117
+ Simulate (or connect to) market reality:
118
+ slippage, brokerage, partial fills, or live execution.
119
+
120
+ ### [Portfolio](https://dev-ddr.github.io/finmetry/concepts/portfolio_handling_module/)
121
+
122
+ The single source of truth for positions, cash, and PnL.
123
+ All state mutation happens here.
124
+
125
+ ### [Backtesting](https://dev-ddr.github.io/finmetry/concepts/backtesting_handling_module/)
126
+
127
+ Pure orchestration.
128
+ Iterates over time and wires everything together without adding logic.
129
+
130
+ ## Final Note
131
+
132
+ > I have built this for my personal use in mind.
133
+
134
+
135
+
136
+
@@ -0,0 +1,19 @@
1
+ finmiti/__init__.py,sha256=yyUQrRvmxjIo9Zc2zAFjv-VddqMDjg5md9zYB12uq4c,349
2
+ finmiti/constants.py,sha256=d7MwCQ1w2FhH_nYfwWu11FrL_urECIkH0MaeRGOMPt8,4143
3
+ finmiti/executioners.py,sha256=NCDlL-YgRIYWkR5OusFD7GWAqWgcn7MFl8ptOP3637c,2088
4
+ finmiti/utils.py,sha256=LhBtCJuPTqXEmz1eARrNhNFUjD8K5TD78-lyAB7mLOA,214
5
+ finmiti/backtest_handler/__init__.py,sha256=9ewjO3EbFqBpOx42fHZgH_w1FFvkXSjsmuFalsQSiKQ,29
6
+ finmiti/backtest_handler/base.py,sha256=bCxMNm-UVuUbUjPtqksiBNuiuFZLQzCyYRsCIZN1ADM,818
7
+ finmiti/clients/__init__.py,sha256=lw_3EbWD7lgWB3_-A88reMb3MNrOQJtCI0CI_W8PwJw,28
8
+ finmiti/clients/client_5paisa.py,sha256=Nzkld-T24qKX0ja5lx3hkh88XEGmCWTJfeC7U5HP8bA,8084
9
+ finmiti/portfolio_handler/__init__.py,sha256=lo09jpUvIurBeKii8KOL3rhRyJBmo3dZC4J-XHzD1D0,38
10
+ finmiti/portfolio_handler/base.py,sha256=MZhesSyPTcH_UVaSqzpL_Nlfoik9xpz6rGn3SJGQtO4,8794
11
+ finmiti/stocks_handler/__init__.py,sha256=XtfClx4hgqQsxNJsgCS4vUC1MhdMfmBfezRZC1IEHeI,58
12
+ finmiti/stocks_handler/stock.py,sha256=5ijFo0NNnp8XlLrnv-2413ek0p30eL-NKqKf6PzBjVI,7087
13
+ finmiti/stocks_handler/stockdict.py,sha256=BhjaT_DRYRLh3OHA9Nga8Ga2-w1gkRDPiit560Tx4mA,3796
14
+ finmiti/strategy_handler/__init__.py,sha256=-xdmRKzfBh0DFk7EshRjSAvv0rCwcfBGXa1KxH52Qys,46
15
+ finmiti/strategy_handler/base.py,sha256=WuPaHSPebtr_JqFNm5MBnv2eB7jy0zQhWvKJs4GDoUI,981
16
+ finmiti-0.0.1.dist-info/METADATA,sha256=_e-ZgOUBh1UVBI2O0589i8fU8-RMcjUuaULB6rl15P8,4528
17
+ finmiti-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ finmiti-0.0.1.dist-info/top_level.txt,sha256=ufoAcQ5Q6CpgwLLsoR_wah292RWfdiIBshfpZBTY_Nc,8
19
+ finmiti-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ finmiti